first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,712 @@
|
||||
import {
|
||||
randomEmail,
|
||||
randomName,
|
||||
uniqueId,
|
||||
getPersonalProject,
|
||||
testDb,
|
||||
} from '@n8n/backend-test-utils';
|
||||
import { LDAP_DEFAULT_CONFIGURATION } from '@n8n/constants';
|
||||
import type { User } from '@n8n/db';
|
||||
import {
|
||||
AuthProviderSyncHistoryRepository,
|
||||
GLOBAL_MEMBER_ROLE,
|
||||
GLOBAL_OWNER_ROLE,
|
||||
UserRepository,
|
||||
} from '@n8n/db';
|
||||
import { Container } from '@n8n/di';
|
||||
import { Not } from '@n8n/typeorm';
|
||||
import type { Entry as LdapUser } from 'ldapts';
|
||||
import { Cipher } from 'n8n-core';
|
||||
|
||||
import config from '@/config';
|
||||
import { saveLdapSynchronization } from '@/modules/ldap.ee/helpers.ee';
|
||||
import { LdapService } from '@/modules/ldap.ee/ldap.service.ee';
|
||||
import {
|
||||
getCurrentAuthenticationMethod,
|
||||
setCurrentAuthenticationMethod,
|
||||
} from '@/sso.ee/sso-helpers';
|
||||
|
||||
import { createLdapUser, createUser, getAllUsers, getLdapIdentities } from '../shared/db/users';
|
||||
import { createLdapConfig, defaultLdapConfig } from '../shared/ldap';
|
||||
import type { SuperAgentTest } from '../shared/types';
|
||||
import * as utils from '../shared/utils/';
|
||||
|
||||
jest.mock('@/telemetry');
|
||||
|
||||
let owner: User;
|
||||
let authOwnerAgent: SuperAgentTest;
|
||||
|
||||
const testServer = utils.setupTestServer({
|
||||
endpointGroups: ['auth', 'ldap'],
|
||||
enabledFeatures: ['feat:ldap'],
|
||||
modules: ['ldap'],
|
||||
});
|
||||
|
||||
beforeAll(async () => {
|
||||
owner = await createUser({ role: GLOBAL_OWNER_ROLE });
|
||||
authOwnerAgent = testServer.authAgentFor(owner);
|
||||
|
||||
defaultLdapConfig.bindingAdminPassword = Container.get(Cipher).encrypt(
|
||||
defaultLdapConfig.bindingAdminPassword,
|
||||
);
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await testDb.truncate([
|
||||
'AuthIdentity',
|
||||
'AuthProviderSyncHistory',
|
||||
'SharedCredentials',
|
||||
'CredentialsEntity',
|
||||
'SharedWorkflow',
|
||||
'WorkflowEntity',
|
||||
]);
|
||||
|
||||
await Container.get(UserRepository).delete({ id: Not(owner.id) });
|
||||
|
||||
jest.mock('@/telemetry');
|
||||
|
||||
await setCurrentAuthenticationMethod('email');
|
||||
});
|
||||
|
||||
test('Member role should not be able to access ldap routes', async () => {
|
||||
const member = await createUser({ role: { slug: 'global:member' } });
|
||||
const authAgent = testServer.authAgentFor(member);
|
||||
await authAgent.get('/ldap/config').expect(403);
|
||||
await authAgent.put('/ldap/config').expect(403);
|
||||
await authAgent.post('/ldap/test-connection').expect(403);
|
||||
await authAgent.post('/ldap/sync').expect(403);
|
||||
await authAgent.get('/ldap/sync').expect(403);
|
||||
});
|
||||
|
||||
test('Chat user role should not be able to access ldap routes', async () => {
|
||||
const chatUser = await createUser({ role: { slug: 'global:chatUser' } });
|
||||
const authAgent = testServer.authAgentFor(chatUser);
|
||||
await authAgent.get('/ldap/config').expect(403);
|
||||
await authAgent.put('/ldap/config').expect(403);
|
||||
await authAgent.post('/ldap/test-connection').expect(403);
|
||||
await authAgent.post('/ldap/sync').expect(403);
|
||||
await authAgent.get('/ldap/sync').expect(403);
|
||||
});
|
||||
|
||||
describe('PUT /ldap/config', () => {
|
||||
test('should not allow access without license', async () => {
|
||||
testServer.license.disable('feat:ldap');
|
||||
await authOwnerAgent.put('/ldap/config').expect(403);
|
||||
});
|
||||
|
||||
test('route should validate payload', async () => {
|
||||
const invalidValuePayload = {
|
||||
...LDAP_DEFAULT_CONFIGURATION,
|
||||
loginEnabled: '', // enabled property only allows boolean
|
||||
loginLabel: '',
|
||||
};
|
||||
|
||||
const invalidExtraPropertyPayload = {
|
||||
...LDAP_DEFAULT_CONFIGURATION,
|
||||
example: true, // property not defined in the validation schema
|
||||
};
|
||||
|
||||
const missingPropertyPayload = {
|
||||
loginEnabled: true,
|
||||
loginLabel: '',
|
||||
// missing all other properties defined in the schema
|
||||
};
|
||||
|
||||
const invalidPayloads = [
|
||||
invalidValuePayload,
|
||||
invalidExtraPropertyPayload,
|
||||
missingPropertyPayload,
|
||||
];
|
||||
|
||||
for (const invalidPayload of invalidPayloads) {
|
||||
const response = await authOwnerAgent.put('/ldap/config').send(invalidPayload);
|
||||
expect(response.statusCode).toBe(400);
|
||||
expect(response.body).toHaveProperty('message');
|
||||
}
|
||||
});
|
||||
|
||||
test('route should update model', async () => {
|
||||
const validPayload = {
|
||||
...LDAP_DEFAULT_CONFIGURATION,
|
||||
loginEnabled: true,
|
||||
loginLabel: '',
|
||||
};
|
||||
|
||||
const response = await authOwnerAgent.put('/ldap/config').send(validPayload);
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.body.data.loginEnabled).toBe(true);
|
||||
expect(response.body.data.loginLabel).toBe('');
|
||||
});
|
||||
|
||||
test('route should fail due to trying to enable LDAP login with SSO as current authentication method', async () => {
|
||||
const validPayload = {
|
||||
...LDAP_DEFAULT_CONFIGURATION,
|
||||
loginEnabled: true,
|
||||
};
|
||||
|
||||
config.set('userManagement.authenticationMethod', 'saml');
|
||||
|
||||
const response = await authOwnerAgent.put('/ldap/config').send(validPayload);
|
||||
|
||||
expect(response.statusCode).toBe(400);
|
||||
});
|
||||
|
||||
test('should apply "Convert all LDAP users to email users" strategy when LDAP login disabled', async () => {
|
||||
const ldapConfig = await createLdapConfig();
|
||||
Container.get(LdapService).setConfig(ldapConfig);
|
||||
|
||||
const member = await createLdapUser({ role: { slug: 'global:member' } }, uniqueId());
|
||||
|
||||
const configuration = ldapConfig;
|
||||
|
||||
// disable the login, so the strategy is applied
|
||||
await authOwnerAgent.put('/ldap/config').send({ ...configuration, loginEnabled: false });
|
||||
|
||||
const emailUser = await Container.get(UserRepository).findOneByOrFail({ id: member.id });
|
||||
const localLdapIdentities = await getLdapIdentities();
|
||||
|
||||
expect(getCurrentAuthenticationMethod()).toBe('email');
|
||||
expect(emailUser.email).toBe(member.email);
|
||||
expect(emailUser.lastName).toBe(member.lastName);
|
||||
expect(emailUser.firstName).toBe(member.firstName);
|
||||
expect(localLdapIdentities.length).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /ldap/config', () => {
|
||||
test('should not allow access without license', async () => {
|
||||
testServer.license.disable('feat:ldap');
|
||||
await authOwnerAgent.get('/ldap/config').expect(403);
|
||||
});
|
||||
|
||||
test('route should retrieve current configuration', async () => {
|
||||
const validPayload = {
|
||||
...LDAP_DEFAULT_CONFIGURATION,
|
||||
loginEnabled: true,
|
||||
loginLabel: '',
|
||||
};
|
||||
|
||||
let response = await authOwnerAgent.put('/ldap/config').send(validPayload);
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(getCurrentAuthenticationMethod()).toBe('ldap');
|
||||
|
||||
response = await authOwnerAgent.get('/ldap/config');
|
||||
|
||||
expect(response.body.data).toMatchObject(validPayload);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /ldap/test-connection', () => {
|
||||
test('should not allow access without license', async () => {
|
||||
testServer.license.disable('feat:ldap');
|
||||
await authOwnerAgent.post('/ldap/test-connection').expect(403);
|
||||
});
|
||||
|
||||
test('route should success', async () => {
|
||||
jest.spyOn(LdapService.prototype, 'testConnection').mockResolvedValue();
|
||||
|
||||
await authOwnerAgent.post('/ldap/test-connection').expect(200);
|
||||
});
|
||||
|
||||
test('route should fail', async () => {
|
||||
const errorMessage = 'Invalid connection';
|
||||
|
||||
jest.spyOn(LdapService.prototype, 'testConnection').mockRejectedValue(new Error(errorMessage));
|
||||
|
||||
const response = await authOwnerAgent.post('/ldap/test-connection');
|
||||
expect(response.statusCode).toBe(400);
|
||||
expect(response.body).toHaveProperty('message');
|
||||
expect(response.body.message).toStrictEqual(errorMessage);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /ldap/sync', () => {
|
||||
test('should not allow access without license', async () => {
|
||||
testServer.license.disable('feat:ldap');
|
||||
await authOwnerAgent.post('/ldap/sync').expect(403);
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
const ldapConfig = await createLdapConfig({
|
||||
ldapIdAttribute: 'uid',
|
||||
firstNameAttribute: 'givenName',
|
||||
lastNameAttribute: 'sn',
|
||||
emailAttribute: 'mail',
|
||||
});
|
||||
Container.get(LdapService).setConfig(ldapConfig);
|
||||
});
|
||||
|
||||
describe('dry mode', () => {
|
||||
const runTest = async (ldapUsers: LdapUser[]) => {
|
||||
jest.spyOn(LdapService.prototype, 'searchWithAdminBinding').mockResolvedValue(ldapUsers);
|
||||
|
||||
await authOwnerAgent.post('/ldap/sync').send({ type: 'dry' }).expect(200);
|
||||
|
||||
const synchronization = await Container.get(
|
||||
AuthProviderSyncHistoryRepository,
|
||||
).findOneByOrFail({});
|
||||
|
||||
expect(synchronization.id).toBeDefined();
|
||||
expect(synchronization.startedAt).toBeDefined();
|
||||
expect(synchronization.endedAt).toBeDefined();
|
||||
expect(synchronization.created).toBeDefined();
|
||||
expect(synchronization.updated).toBeDefined();
|
||||
expect(synchronization.disabled).toBeDefined();
|
||||
expect(synchronization.status).toBeDefined();
|
||||
expect(synchronization.scanned).toBeDefined();
|
||||
expect(synchronization.error).toBeDefined();
|
||||
expect(synchronization.runMode).toBeDefined();
|
||||
expect(synchronization.runMode).toBe('dry');
|
||||
expect(synchronization.scanned).toBe(ldapUsers.length);
|
||||
return synchronization;
|
||||
};
|
||||
|
||||
test('should detect new user but not persist change in model', async () => {
|
||||
const synchronization = await runTest([
|
||||
{
|
||||
dn: '',
|
||||
mail: randomEmail(),
|
||||
sn: randomName(),
|
||||
givenName: randomName(),
|
||||
uid: uniqueId(),
|
||||
},
|
||||
]);
|
||||
|
||||
expect(synchronization.created).toBe(1);
|
||||
|
||||
// Make sure only the instance owner is on the DB
|
||||
const localDbUsers = await Container.get(UserRepository).find();
|
||||
expect(localDbUsers.length).toBe(1);
|
||||
expect(localDbUsers[0].id).toBe(owner.id);
|
||||
});
|
||||
|
||||
test('should detect updated user but not persist change in model', async () => {
|
||||
const ldapUserEmail = randomEmail();
|
||||
const ldapUserId = uniqueId();
|
||||
|
||||
const member = await createLdapUser(
|
||||
{ role: { slug: 'global:member' }, email: ldapUserEmail },
|
||||
ldapUserId,
|
||||
);
|
||||
|
||||
const synchronization = await runTest([
|
||||
{
|
||||
dn: '',
|
||||
mail: ldapUserEmail,
|
||||
sn: randomName(),
|
||||
givenName: 'updated',
|
||||
uid: ldapUserId,
|
||||
},
|
||||
]);
|
||||
|
||||
expect(synchronization.updated).toBe(1);
|
||||
|
||||
// Make sure the changes in the "LDAP server" were not persisted in the database
|
||||
const localLdapIdentities = await getLdapIdentities();
|
||||
const localLdapUsers = localLdapIdentities.map(({ user }) => user);
|
||||
expect(localLdapUsers.length).toBe(1);
|
||||
expect(localLdapUsers[0].id).toBe(member.id);
|
||||
expect(localLdapUsers[0].lastName).toBe(member.lastName);
|
||||
});
|
||||
|
||||
test('should detect disabled user but not persist change in model', async () => {
|
||||
const ldapUserEmail = randomEmail();
|
||||
const ldapUserId = uniqueId();
|
||||
|
||||
const member = await createLdapUser(
|
||||
{ role: { slug: 'global:member' }, email: ldapUserEmail },
|
||||
ldapUserId,
|
||||
);
|
||||
|
||||
const synchronization = await runTest([]);
|
||||
|
||||
expect(synchronization.disabled).toBe(1);
|
||||
|
||||
// Make sure the changes in the "LDAP server" were not persisted in the database
|
||||
const localLdapIdentities = await getLdapIdentities();
|
||||
const localLdapUsers = localLdapIdentities.map(({ user }) => user);
|
||||
expect(localLdapUsers.length).toBe(1);
|
||||
expect(localLdapUsers[0].id).toBe(member.id);
|
||||
expect(localLdapUsers[0].disabled).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('live mode', () => {
|
||||
const runTest = async (ldapUsers: LdapUser[]) => {
|
||||
jest.spyOn(LdapService.prototype, 'searchWithAdminBinding').mockResolvedValue(ldapUsers);
|
||||
|
||||
await authOwnerAgent.post('/ldap/sync').send({ type: 'live' }).expect(200);
|
||||
|
||||
const synchronization = await Container.get(
|
||||
AuthProviderSyncHistoryRepository,
|
||||
).findOneByOrFail({});
|
||||
|
||||
expect(synchronization.id).toBeDefined();
|
||||
expect(synchronization.startedAt).toBeDefined();
|
||||
expect(synchronization.endedAt).toBeDefined();
|
||||
expect(synchronization.created).toBeDefined();
|
||||
expect(synchronization.updated).toBeDefined();
|
||||
expect(synchronization.disabled).toBeDefined();
|
||||
expect(synchronization.status).toBeDefined();
|
||||
expect(synchronization.scanned).toBeDefined();
|
||||
expect(synchronization.error).toBeDefined();
|
||||
expect(synchronization.runMode).toBeDefined();
|
||||
expect(synchronization.runMode).toBe('live');
|
||||
expect(synchronization.scanned).toBe(ldapUsers.length);
|
||||
return synchronization;
|
||||
};
|
||||
|
||||
test('should detect new user and persist change in model', async () => {
|
||||
const ldapUser = {
|
||||
mail: randomEmail(),
|
||||
dn: '',
|
||||
sn: randomName(),
|
||||
givenName: randomName(),
|
||||
uid: uniqueId(),
|
||||
};
|
||||
|
||||
const synchronization = await runTest([ldapUser]);
|
||||
expect(synchronization.created).toBe(1);
|
||||
|
||||
// Make sure the changes in the "LDAP server" were persisted in the database
|
||||
const allUsers = await getAllUsers();
|
||||
expect(allUsers.length).toBe(2);
|
||||
|
||||
const ownerUser = allUsers.find((u) => u.email === owner.email)!;
|
||||
expect(ownerUser.email).toBe(owner.email);
|
||||
|
||||
const memberUser = allUsers.find((u) => u.email !== owner.email)!;
|
||||
expect(memberUser.email).toBe(ldapUser.mail);
|
||||
expect(memberUser.lastName).toBe(ldapUser.sn);
|
||||
expect(memberUser.firstName).toBe(ldapUser.givenName);
|
||||
const memberProject = getPersonalProject(memberUser);
|
||||
expect(memberProject).toBeDefined();
|
||||
|
||||
const authIdentities = await getLdapIdentities();
|
||||
expect(authIdentities.length).toBe(1);
|
||||
expect(authIdentities[0].providerId).toBe(ldapUser.uid);
|
||||
expect(authIdentities[0].providerType).toBe('ldap');
|
||||
});
|
||||
|
||||
test('should detect updated user and persist change in model', async () => {
|
||||
const ldapUser = {
|
||||
mail: randomEmail(),
|
||||
dn: '',
|
||||
sn: 'updated',
|
||||
givenName: randomName(),
|
||||
uid: uniqueId(),
|
||||
};
|
||||
|
||||
await createLdapUser(
|
||||
{
|
||||
role: { slug: 'global:member' },
|
||||
email: ldapUser.mail,
|
||||
firstName: ldapUser.givenName,
|
||||
lastName: randomName(),
|
||||
},
|
||||
ldapUser.uid,
|
||||
);
|
||||
|
||||
const synchronization = await runTest([ldapUser]);
|
||||
expect(synchronization.updated).toBe(1);
|
||||
|
||||
// Make sure the changes in the "LDAP server" were persisted in the database
|
||||
const localLdapIdentities = await getLdapIdentities();
|
||||
const localLdapUsers = localLdapIdentities.map(({ user }) => user);
|
||||
|
||||
expect(localLdapUsers.length).toBe(1);
|
||||
expect(localLdapUsers[0].email).toBe(ldapUser.mail);
|
||||
expect(localLdapUsers[0].lastName).toBe(ldapUser.sn);
|
||||
expect(localLdapUsers[0].firstName).toBe(ldapUser.givenName);
|
||||
expect(localLdapIdentities[0].providerId).toBe(ldapUser.uid);
|
||||
});
|
||||
|
||||
test('should detect disabled user and persist change in model', async () => {
|
||||
const ldapUser = {
|
||||
mail: randomEmail(),
|
||||
dn: '',
|
||||
sn: 'updated',
|
||||
givenName: randomName(),
|
||||
uid: uniqueId(),
|
||||
};
|
||||
|
||||
await createLdapUser(
|
||||
{
|
||||
role: { slug: 'global:member' },
|
||||
email: ldapUser.mail,
|
||||
firstName: ldapUser.givenName,
|
||||
lastName: ldapUser.sn,
|
||||
},
|
||||
ldapUser.uid,
|
||||
);
|
||||
|
||||
const synchronization = await runTest([]);
|
||||
expect(synchronization.disabled).toBe(1);
|
||||
|
||||
// Make sure the changes in the "LDAP server" were persisted in the database
|
||||
const allUsers = await getAllUsers();
|
||||
expect(allUsers.length).toBe(2);
|
||||
|
||||
const ownerUser = allUsers.find((u) => u.email === owner.email)!;
|
||||
expect(ownerUser.email).toBe(owner.email);
|
||||
|
||||
const memberUser = allUsers.find((u) => u.email !== owner.email)!;
|
||||
expect(memberUser.email).toBe(ldapUser.mail);
|
||||
expect(memberUser.lastName).toBe(ldapUser.sn);
|
||||
expect(memberUser.firstName).toBe(ldapUser.givenName);
|
||||
expect(memberUser.disabled).toBe(true);
|
||||
|
||||
const authIdentities = await getLdapIdentities();
|
||||
expect(authIdentities.length).toBe(0);
|
||||
});
|
||||
|
||||
test('should remove user instance access once the user is disabled during synchronization', async () => {
|
||||
const member = await createLdapUser({ role: { slug: 'global:member' } }, uniqueId());
|
||||
|
||||
jest.spyOn(LdapService.prototype, 'searchWithAdminBinding').mockResolvedValue([]);
|
||||
|
||||
await authOwnerAgent.post('/ldap/sync').send({ type: 'live' });
|
||||
|
||||
const response = await testServer.authAgentFor(member).get('/login');
|
||||
expect(response.status).toBe(401);
|
||||
});
|
||||
|
||||
test('should filter out users with invalid email addresses during sync', async () => {
|
||||
const validLdapUser = {
|
||||
mail: randomEmail(),
|
||||
dn: '',
|
||||
sn: randomName(),
|
||||
givenName: randomName(),
|
||||
uid: uniqueId(),
|
||||
};
|
||||
|
||||
const invalidLdapUser = {
|
||||
mail: 'invalid-email',
|
||||
dn: '',
|
||||
sn: randomName(),
|
||||
givenName: randomName(),
|
||||
uid: uniqueId(),
|
||||
};
|
||||
|
||||
const ldapUsers = [validLdapUser, invalidLdapUser];
|
||||
|
||||
const loggerSpy = jest.spyOn(Container.get(LdapService)['logger'], 'warn');
|
||||
|
||||
const synchronization = await runTest(ldapUsers);
|
||||
|
||||
// Should only create 1 user (the valid one)
|
||||
expect(synchronization.created).toBe(1);
|
||||
|
||||
// Should log error for invalid email
|
||||
expect(loggerSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining(`LDAP - Invalid email format for user ${invalidLdapUser.uid}`),
|
||||
);
|
||||
|
||||
loggerSpy.mockReset();
|
||||
loggerSpy.mockRestore();
|
||||
|
||||
// Verify only valid user was created
|
||||
const allUsers = await getAllUsers();
|
||||
expect(allUsers.length).toBe(2); // owner + valid user
|
||||
|
||||
const memberUser = allUsers.find((u) => u.email !== owner.email)!;
|
||||
expect(memberUser.email).toBe(validLdapUser.mail);
|
||||
});
|
||||
|
||||
test('should filter out users with invalid email addresses during update', async () => {
|
||||
const originalEmail = randomEmail();
|
||||
const originalUserId = uniqueId();
|
||||
|
||||
// Create user with valid email first
|
||||
await createLdapUser(
|
||||
{
|
||||
role: GLOBAL_MEMBER_ROLE,
|
||||
email: originalEmail,
|
||||
firstName: randomName(),
|
||||
lastName: randomName(),
|
||||
},
|
||||
originalUserId,
|
||||
);
|
||||
|
||||
// Now try to update with invalid email
|
||||
const invalidLdapUser = {
|
||||
mail: 'not-an-email',
|
||||
dn: '',
|
||||
sn: randomName(),
|
||||
givenName: randomName(),
|
||||
uid: originalUserId,
|
||||
};
|
||||
|
||||
const loggerSpy = jest.spyOn(Container.get(LdapService)['logger'], 'warn');
|
||||
|
||||
const synchronization = await runTest([invalidLdapUser]);
|
||||
|
||||
// Should not update any users
|
||||
expect(synchronization.updated).toBe(0);
|
||||
|
||||
// Should log error for invalid email
|
||||
expect(loggerSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining(`LDAP - Invalid email format for user ${originalUserId}`),
|
||||
);
|
||||
|
||||
loggerSpy.mockReset();
|
||||
loggerSpy.mockRestore();
|
||||
|
||||
// Verify user still has original email
|
||||
const localLdapIdentities = await getLdapIdentities();
|
||||
const localLdapUsers = localLdapIdentities.map(({ user }) => user);
|
||||
expect(localLdapUsers.length).toBe(1);
|
||||
expect(localLdapUsers[0].email).toBe(originalEmail);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /ldap/sync', () => {
|
||||
test('should not allow access without license', async () => {
|
||||
testServer.license.disable('feat:ldap');
|
||||
await authOwnerAgent.get('/ldap/sync').expect(403);
|
||||
});
|
||||
|
||||
test('should return paginated synchronizations', async () => {
|
||||
for (let i = 0; i < 2; i++) {
|
||||
await saveLdapSynchronization({
|
||||
created: 0,
|
||||
scanned: 0,
|
||||
updated: 0,
|
||||
disabled: 0,
|
||||
startedAt: new Date(),
|
||||
endedAt: new Date(),
|
||||
status: 'success',
|
||||
error: '',
|
||||
runMode: 'dry',
|
||||
});
|
||||
}
|
||||
|
||||
let response = await authOwnerAgent.get('/ldap/sync?perPage=1&page=0');
|
||||
expect(response.body.data.length).toBe(1);
|
||||
|
||||
response = await authOwnerAgent.get('/ldap/sync?perPage=1&page=1');
|
||||
expect(response.body.data.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /login', () => {
|
||||
const runTest = async (ldapUser: LdapUser) => {
|
||||
const ldapConfig = await createLdapConfig();
|
||||
Container.get(LdapService).setConfig(ldapConfig);
|
||||
|
||||
await setCurrentAuthenticationMethod('ldap');
|
||||
|
||||
jest.spyOn(LdapService.prototype, 'searchWithAdminBinding').mockResolvedValue([ldapUser]);
|
||||
|
||||
jest.spyOn(LdapService.prototype, 'validUser').mockResolvedValue();
|
||||
|
||||
const response = await testServer.authlessAgent
|
||||
.post('/login')
|
||||
.send({ emailOrLdapLoginId: ldapUser.mail, password: 'password' });
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.headers['set-cookie']).toBeDefined();
|
||||
expect(response.headers['set-cookie'][0]).toContain('n8n-auth=');
|
||||
|
||||
// Make sure the changes in the "LDAP server" were persisted in the database
|
||||
const localLdapIdentities = await getLdapIdentities();
|
||||
const localLdapUsers = localLdapIdentities.map(({ user }) => user);
|
||||
|
||||
expect(localLdapUsers.length).toBe(1);
|
||||
expect(localLdapUsers[0].email).toBe(ldapUser.mail);
|
||||
expect(localLdapUsers[0].lastName).toBe(ldapUser.sn);
|
||||
expect(localLdapUsers[0].firstName).toBe(ldapUser.givenName);
|
||||
expect(localLdapIdentities[0].providerId).toBe(ldapUser.uid);
|
||||
expect(localLdapUsers[0].disabled).toBe(false);
|
||||
|
||||
await expect(getPersonalProject(localLdapUsers[0])).resolves.toBeDefined();
|
||||
};
|
||||
|
||||
test('should allow new LDAP user to login and synchronize data', async () => {
|
||||
const ldapUser = {
|
||||
mail: randomEmail(),
|
||||
dn: '',
|
||||
sn: '',
|
||||
givenName: randomName(),
|
||||
uid: uniqueId(),
|
||||
};
|
||||
await runTest(ldapUser);
|
||||
});
|
||||
|
||||
test('should allow existing LDAP user to login and synchronize data', async () => {
|
||||
const ldapUser = {
|
||||
mail: randomEmail(),
|
||||
dn: '',
|
||||
sn: 'updated',
|
||||
givenName: 'updated',
|
||||
uid: uniqueId(),
|
||||
};
|
||||
|
||||
await createLdapUser(
|
||||
{
|
||||
role: { slug: 'global:member' },
|
||||
email: ldapUser.mail,
|
||||
firstName: 'firstname',
|
||||
lastName: 'lastname',
|
||||
},
|
||||
ldapUser.uid,
|
||||
);
|
||||
|
||||
await runTest(ldapUser);
|
||||
});
|
||||
|
||||
test('should allow instance owner to sign in with email/password when LDAP is enabled', async () => {
|
||||
const ldapConfig = await createLdapConfig();
|
||||
Container.get(LdapService).setConfig(ldapConfig);
|
||||
|
||||
const response = await testServer.authlessAgent
|
||||
.post('/login')
|
||||
.send({ emailOrLdapLoginId: owner.email, password: 'password' });
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.data?.signInType).toBeDefined();
|
||||
expect(response.body.data?.signInType).toBe('email');
|
||||
});
|
||||
|
||||
test('should transform email user into LDAP user when match found', async () => {
|
||||
const ldapUser = {
|
||||
mail: randomEmail(),
|
||||
dn: '',
|
||||
sn: randomName(),
|
||||
givenName: randomName(),
|
||||
uid: uniqueId(),
|
||||
};
|
||||
|
||||
await createUser({
|
||||
role: GLOBAL_MEMBER_ROLE,
|
||||
email: ldapUser.mail,
|
||||
firstName: ldapUser.givenName,
|
||||
lastName: 'lastname',
|
||||
});
|
||||
|
||||
await runTest(ldapUser);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Instance owner should able to delete LDAP users', () => {
|
||||
test("don't transfer workflows", async () => {
|
||||
const ldapConfig = await createLdapConfig();
|
||||
Container.get(LdapService).setConfig(ldapConfig);
|
||||
|
||||
const member = await createLdapUser({ role: { slug: 'global:member' } }, uniqueId());
|
||||
|
||||
await authOwnerAgent.post(`/users/${member.id}`);
|
||||
});
|
||||
|
||||
test('transfer workflows and credentials', async () => {
|
||||
const ldapConfig = await createLdapConfig();
|
||||
Container.get(LdapService).setConfig(ldapConfig);
|
||||
|
||||
const member = await createLdapUser({ role: { slug: 'global:member' } }, uniqueId());
|
||||
|
||||
// delete the LDAP member and transfer its workflows/credentials to instance owner
|
||||
await authOwnerAgent.post(`/users/${member.id}?transferId=${owner.id}`);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user