feat(260729-d3k): multi-base LDAP sync scope + re-keyed no-op guard
- parseBaseDns() splits the newline-separated baseDn field into a list - syncUsersForTenant no-op guard re-keyed on empty parsed base-DN list (was empty groupFilterDns) — the sole condition that skips search + the deactivation loop, preventing mass-deactivation on an unconfigured config - collectSearchEntries/listGroups/searchUsers loop every base DN and merge/dedupe results by entry dn - empty groupFilterDns is no longer a no-op: it now performs a normal multi-base search with no memberOf restriction - groupFilterDns ou= entries stay additional search bases; group DNs become an optional memberOf constraint applied to every base search - spec: replaced empty-groupFilterDns no-op test with empty-base-DN no-op test, added multi-base merge/dedup test Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -34,9 +34,10 @@ describe('LdapService.syncUsersForTenant — per-user exclude list', () => {
|
||||
serverUrl: 'ldap://example',
|
||||
baseDn: 'dc=example,dc=com',
|
||||
searchFilter: '(objectClass=person)',
|
||||
// Non-empty selection: an empty groupFilterDns is now a no-op (see the
|
||||
// dedicated "empty selection no-op" describe block below), so these
|
||||
// exclude-list tests need a real selection to exercise the search path.
|
||||
// The Base-DN is the sync scope (an empty parsed base-DN list is the
|
||||
// sole no-op path — see the dedicated "empty base DN no-op" describe
|
||||
// block below). groupFilterDns here is an optional extra restriction;
|
||||
// set to exercise the memberOf-restricted search path.
|
||||
groupFilterDns: ['ou=people,dc=example,dc=com'] as string[],
|
||||
userExcludeList: [] as string[],
|
||||
fieldMappings: [
|
||||
@@ -118,16 +119,16 @@ describe('LdapService.syncUsersForTenant — per-user exclude list', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('LdapService.syncUsersForTenant — empty selection no-op', () => {
|
||||
describe('LdapService.syncUsersForTenant — empty base DN no-op', () => {
|
||||
let service: LdapService;
|
||||
let prisma: any;
|
||||
let userService: any;
|
||||
|
||||
const emptySelectionConfig = {
|
||||
const emptyBaseDnConfig = {
|
||||
id: 'cfg1',
|
||||
tenantId: 't1',
|
||||
serverUrl: 'ldap://example',
|
||||
baseDn: 'dc=example,dc=com',
|
||||
baseDn: '',
|
||||
searchFilter: '(objectClass=person)',
|
||||
groupFilterDns: [] as string[],
|
||||
userExcludeList: [] as string[],
|
||||
@@ -152,9 +153,9 @@ describe('LdapService.syncUsersForTenant — empty selection no-op', () => {
|
||||
service = new LdapService(prisma, userService);
|
||||
});
|
||||
|
||||
it('creates nobody and deactivates nobody when groupFilterDns is empty', async () => {
|
||||
it('creates nobody and deactivates nobody when the base DN is empty (whitespace-only)', async () => {
|
||||
const result = await service.syncUsersForTenant(
|
||||
emptySelectionConfig as any,
|
||||
{ ...emptyBaseDnConfig, baseDn: ' \n \n' } as any,
|
||||
't1',
|
||||
);
|
||||
|
||||
@@ -165,6 +166,94 @@ describe('LdapService.syncUsersForTenant — empty selection no-op', () => {
|
||||
expect(prisma.user.update).not.toHaveBeenCalled();
|
||||
expect(prisma.ldapConfig.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('is NOT a no-op when baseDn is set but groupFilterDns is empty (normal multi-base search)', async () => {
|
||||
mockSearch.mockResolvedValue({
|
||||
searchEntries: [{ dn: 'cn=alice', sAMAccountName: 'alice' }],
|
||||
});
|
||||
|
||||
const result = await service.syncUsersForTenant(
|
||||
{ ...emptyBaseDnConfig, baseDn: 'dc=example,dc=com' } as any,
|
||||
't1',
|
||||
);
|
||||
|
||||
expect(mockBind).toHaveBeenCalled();
|
||||
expect(mockSearch).toHaveBeenCalled();
|
||||
expect(result.created).toBe(1);
|
||||
expect(userService.create).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('LdapService.syncUsersForTenant — multi base DN scope', () => {
|
||||
let service: LdapService;
|
||||
let prisma: any;
|
||||
let userService: any;
|
||||
|
||||
const multiBaseConfig = {
|
||||
id: 'cfg1',
|
||||
tenantId: 't1',
|
||||
serverUrl: 'ldap://example',
|
||||
baseDn: 'dc=a,dc=com\ndc=b,dc=com',
|
||||
searchFilter: '(objectClass=person)',
|
||||
groupFilterDns: [] as string[],
|
||||
userExcludeList: [] as string[],
|
||||
fieldMappings: [
|
||||
{ ldapField: 'sAMAccountName', tesseraField: 'username' },
|
||||
],
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockBind.mockResolvedValue(undefined);
|
||||
mockUnbind.mockResolvedValue(undefined);
|
||||
prisma = {
|
||||
user: {
|
||||
findFirst: vi.fn().mockResolvedValue(null),
|
||||
findMany: vi.fn().mockResolvedValue([]),
|
||||
update: vi.fn().mockResolvedValue({}),
|
||||
},
|
||||
ldapConfig: { update: vi.fn().mockResolvedValue({}) },
|
||||
};
|
||||
userService = { create: vi.fn().mockResolvedValue({}) };
|
||||
service = new LdapService(prisma, userService);
|
||||
});
|
||||
|
||||
it('searches every configured base DN and merges/dedupes results by dn', async () => {
|
||||
mockSearch
|
||||
.mockResolvedValueOnce({
|
||||
searchEntries: [
|
||||
{ dn: 'cn=shared,dc=a,dc=com', sAMAccountName: 'shared' },
|
||||
{ dn: 'cn=alice,dc=a,dc=com', sAMAccountName: 'alice' },
|
||||
],
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
searchEntries: [
|
||||
{ dn: 'cn=shared,dc=a,dc=com', sAMAccountName: 'shared' },
|
||||
{ dn: 'cn=bob,dc=b,dc=com', sAMAccountName: 'bob' },
|
||||
],
|
||||
});
|
||||
|
||||
const result = await service.syncUsersForTenant(
|
||||
multiBaseConfig as any,
|
||||
't1',
|
||||
);
|
||||
|
||||
expect(mockSearch).toHaveBeenCalledTimes(2);
|
||||
expect(mockSearch).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'dc=a,dc=com',
|
||||
expect.objectContaining({ filter: '(objectClass=person)' }),
|
||||
);
|
||||
expect(mockSearch).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'dc=b,dc=com',
|
||||
expect.objectContaining({ filter: '(objectClass=person)' }),
|
||||
);
|
||||
// 3 distinct dns (shared, alice, bob) — the duplicate "shared" dn from
|
||||
// the second base is deduped, not double-created.
|
||||
expect(result.created).toBe(3);
|
||||
expect(userService.create).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('LdapService — individual user search & import (dedup)', () => {
|
||||
|
||||
Reference in New Issue
Block a user