fix(16): WR-04 normalize the rename-vs-unchanged comparison
syncBoundGroupsForTenant() compared cn/dn for byte equality, so any casing difference AD returns between two runs (e.g. after a domain-controller switch) would look like a rename and re-write name/ldapDn every single sync — violating the 'sync twice over an unchanged AD state = no-op' idempotency guarantee. The comparison used to DECIDE 'is this a rename' is now case-insensitive; the value written on an actual rename is still stored byte-for-byte as the directory reports it, per D-03.
This commit is contained in:
@@ -1627,6 +1627,36 @@ describe('LdapService.syncBoundGroupsForTenant — Rekonziliation gegen das Verz
|
|||||||
expect(second.groupsRenamed).toBe(0);
|
expect(second.groupsRenamed).toBe(0);
|
||||||
expect(second.groupsDeleted).toBe(0);
|
expect(second.groupsDeleted).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('a hit whose cn/dn differ only in casing from the stored values is NOT a rename — no write, no groupsRenamed increment (WR-04, 16-REVIEW.md)', async () => {
|
||||||
|
// Simulates a domain-controller switch returning different casing for
|
||||||
|
// the same, unchanged AD group between two syncs — Priority Check 7
|
||||||
|
// ("sync twice over an unchanged AD state = no-op") must hold even
|
||||||
|
// then.
|
||||||
|
groups = [
|
||||||
|
{
|
||||||
|
id: 'g1',
|
||||||
|
tenantId: 't1',
|
||||||
|
name: 'Sales',
|
||||||
|
ldapDn: 'CN=Sales,DC=example,DC=com',
|
||||||
|
ldapObjectGuid: guidHex,
|
||||||
|
isDefault: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
mockSearch.mockResolvedValue({
|
||||||
|
searchEntries: [{ dn: 'cn=sales,dc=example,dc=com', cn: 'sales' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = makeResult();
|
||||||
|
await run(result);
|
||||||
|
|
||||||
|
expect(prisma.group.update).not.toHaveBeenCalled();
|
||||||
|
expect(result.groupsRenamed).toBe(0);
|
||||||
|
// The stored value is untouched — it is NOT rewritten to the
|
||||||
|
// differently-cased AD response either.
|
||||||
|
expect(groups[0].name).toBe('Sales');
|
||||||
|
expect(groups[0].ldapDn).toBe('CN=Sales,DC=example,DC=com');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('LdapService — AD group import (SC-1/SC-2, D-01/D-02)', () => {
|
describe('LdapService — AD group import (SC-1/SC-2, D-01/D-02)', () => {
|
||||||
|
|||||||
@@ -1308,8 +1308,27 @@ export class LdapService {
|
|||||||
: hit.dn;
|
: hit.dn;
|
||||||
const dn = hit.dn;
|
const dn = hit.dn;
|
||||||
|
|
||||||
if (name !== group.name || dn !== group.ldapDn) {
|
// WR-04 (16-REVIEW.md): the CHANGE CHECK is case-insensitive —
|
||||||
|
// only the DECISION "is this a rename" is normalized, never the
|
||||||
|
// value written below. AD returning the same cn/dn with different
|
||||||
|
// casing between two runs (e.g. after a domain-controller switch)
|
||||||
|
// must not look like a rename: that would break the idempotency
|
||||||
|
// guarantee (Priority Check 7 — sync twice over an unchanged AD
|
||||||
|
// state = no-op) and increment groupsRenamed / issue an update on
|
||||||
|
// every subsequent run. This is a plain lowercase compare, not
|
||||||
|
// full RFC 4514 DN canonicalization (per-attribute-type
|
||||||
|
// case-sensitivity rules, escaped-character normalization, etc.)
|
||||||
|
// — a pragmatic simplification, same uncertainty class as A1/A2 in
|
||||||
|
// RESEARCH.md, not verified against a real AD.
|
||||||
|
const nameChanged = name.toLowerCase() !== (group.name ?? '').toLowerCase();
|
||||||
|
const dnChanged =
|
||||||
|
dn.toLowerCase() !== (group.ldapDn ?? '').toLowerCase();
|
||||||
|
|
||||||
|
if (nameChanged || dnChanged) {
|
||||||
try {
|
try {
|
||||||
|
// The write below stores name/dn EXACTLY as the directory
|
||||||
|
// reports them, byte-for-byte — never the lowercased
|
||||||
|
// comparison values above. Group.name stays AD's, per D-03.
|
||||||
await tenantPrisma.group.update({
|
await tenantPrisma.group.update({
|
||||||
where: { id: group.id },
|
where: { id: group.id },
|
||||||
data: { name, ldapDn: dn },
|
data: { name, ldapDn: dn },
|
||||||
|
|||||||
Reference in New Issue
Block a user