unleash-server
Version:
Unleash is an enterprise ready feature flag service. It provides different strategies for handling feature flags.
447 lines • 18.4 kB
JavaScript
import dbInit from '../helpers/database-init.js';
import getLogger from '../../fixtures/no-logger.js';
import UserService from '../../../lib/services/user-service.js';
import { AccessService } from '../../../lib/services/access-service.js';
import ResetTokenService from '../../../lib/services/reset-token-service.js';
import { EmailService } from '../../../lib/services/email-service.js';
import { createTestConfig } from '../../config/test-config.js';
import SessionService from '../../../lib/services/session-service.js';
import NotFoundError from '../../../lib/error/notfound-error.js';
import { RoleName } from '../../../lib/types/model.js';
import SettingService from '../../../lib/services/setting-service.js';
import { simpleAuthSettingsKey } from '../../../lib/types/settings/simple-auth-settings.js';
import { addDays, minutesToMilliseconds } from 'date-fns';
import { GroupService } from '../../../lib/services/group-service.js';
import { BadDataError } from '../../../lib/error/index.js';
import PasswordMismatch from '../../../lib/error/password-mismatch.js';
import { CREATE_ADDON, SYSTEM_USER_AUDIT, TEST_AUDIT_USER, } from '../../../lib/types/index.js';
import { USER_CREATED, USER_DELETED, USER_UPDATED, } from '../../../lib/events/index.js';
import { CUSTOM_ROOT_ROLE_TYPE } from '../../../lib/util/index.js';
import { PasswordPreviouslyUsedError } from '../../../lib/error/password-previously-used.js';
import { createEventsService } from '../../../lib/features/index.js';
import { USER_LOGIN } from '../../../lib/metric-events.js';
import { beforeAll, afterAll, beforeEach, test, describe, expect, } from 'vitest';
let db;
let stores;
let userService;
let userStore;
let adminRole;
let viewerRole;
let customRole;
let sessionService;
let settingService;
let eventService;
let accessService;
let eventBus;
const allowedSessions = 2;
beforeAll(async () => {
db = await dbInit('user_service_serial', getLogger);
stores = db.stores;
const config = createTestConfig({
session: { maxParallelSessions: allowedSessions },
});
eventBus = config.eventBus;
eventService = createEventsService(db.rawDatabase, config);
const groupService = new GroupService(stores, config, eventService);
accessService = new AccessService(stores, config, groupService, eventService);
const resetTokenService = new ResetTokenService(stores, config);
const emailService = new EmailService(config);
sessionService = new SessionService(stores, config);
settingService = new SettingService(stores, config, eventService);
userService = new UserService(stores, config, {
accessService,
resetTokenService,
emailService,
eventService,
sessionService,
settingService,
});
userStore = stores.userStore;
const rootRoles = await accessService.getRootRoles();
adminRole = rootRoles.find((r) => r.name === RoleName.ADMIN);
viewerRole = rootRoles.find((r) => r.name === RoleName.VIEWER);
customRole = await accessService.createRole({
name: 'Custom role',
type: CUSTOM_ROOT_ROLE_TYPE,
description: 'A custom role',
permissions: [
{
name: CREATE_ADDON,
},
],
createdByUserId: 1,
}, SYSTEM_USER_AUDIT);
});
afterAll(async () => {
await db.destroy();
});
beforeEach(async () => {
await userStore.deleteAll();
await settingService.deleteAll();
});
test('should create initial admin user', async () => {
await userService.initAdminUser({
createAdminUser: true,
});
await expect(async () => userService.loginUser('admin', 'wrong-password')).rejects.toThrow(Error);
await expect(async () => userService.loginUser('admin', 'unleash4all')).toBeTruthy();
});
test('should not init default user if we already have users', async () => {
await userService.createUser({
username: 'test',
password: 'A very strange P4ssw0rd_',
rootRole: adminRole.id,
}, SYSTEM_USER_AUDIT);
await userService.initAdminUser({
createAdminUser: true,
});
const users = await userService.getAll();
expect(users).toHaveLength(1);
expect(users[0].username).toBe('test');
await expect(async () => userService.loginUser('admin', 'unleash4all')).rejects.toThrow(Error);
});
test('should not be allowed to create existing user', async () => {
await userStore.insert({ username: 'test', name: 'Hans Mola' });
await expect(async () => userService.createUser({ username: 'test', rootRole: adminRole.id }, TEST_AUDIT_USER)).rejects.toThrow(Error);
});
test('should create user with password', async () => {
const recordedEvents = [];
eventBus.on(USER_LOGIN, (data) => {
recordedEvents.push(data);
});
await userService.createUser({
username: 'test',
password: 'A very strange P4ssw0rd_',
rootRole: adminRole.id,
}, TEST_AUDIT_USER);
const user = await userService.loginUser('test', 'A very strange P4ssw0rd_');
expect(user.username).toBe('test');
expect(recordedEvents).toEqual([{ loginOrder: 0 }]);
});
test('should create user with rootRole in audit-log', async () => {
const user = await userService.createUser({
username: 'test',
rootRole: viewerRole.id,
}, TEST_AUDIT_USER);
const { events } = await eventService.getEvents();
expect(events[0].type).toBe(USER_CREATED);
expect(events[0].data.id).toBe(user.id);
expect(events[0].data.username).toBe('test');
expect(events[0].data.rootRole).toBe(viewerRole.id);
});
test('should update user with rootRole in audit-log', async () => {
const user = await userService.createUser({
username: 'test',
rootRole: viewerRole.id,
}, TEST_AUDIT_USER);
await userService.updateUser({ id: user.id, rootRole: adminRole.id }, TEST_AUDIT_USER);
const { events } = await eventService.getEvents();
expect(events[0].type).toBe(USER_UPDATED);
expect(events[0].data.id).toBe(user.id);
expect(events[0].data.username).toBe('test');
expect(events[0].data.rootRole).toBe(adminRole.id);
});
test('should remove user with rootRole in audit-log', async () => {
const user = await userService.createUser({
username: 'test',
rootRole: viewerRole.id,
}, TEST_AUDIT_USER);
await userService.deleteUser(user.id, TEST_AUDIT_USER);
const { events } = await eventService.getEvents();
expect(events[0].type).toBe(USER_DELETED);
expect(events[0].preData.id).toBe(user.id);
expect(events[0].preData.username).toBe('test');
expect(events[0].preData.rootRole).toBe(viewerRole.id);
});
test('should not be able to login with deleted user', async () => {
const user = await userService.createUser({
username: 'deleted_user',
password: 'unleash4all',
rootRole: adminRole.id,
}, TEST_AUDIT_USER);
await userService.deleteUser(user.id, TEST_AUDIT_USER);
await expect(userService.loginUser('deleted_user', 'unleash4all')).rejects.errorWithMessage(new PasswordMismatch('The combination of password and username you provided is invalid. If you have forgotten your password, visit /forgotten-password or get in touch with your instance administrator.'));
});
test('should not be able to login without password_hash on user', async () => {
const user = await userService.createUser({
username: 'deleted_user',
password: 'unleash4all',
rootRole: adminRole.id,
}, TEST_AUDIT_USER);
/*@ts-ignore: we are testing for null on purpose! */
await userStore.setPasswordHash(user.id, null);
await expect(userService.loginUser('deleted_user', 'anything-should-fail')).rejects.errorWithMessage(new PasswordMismatch('The combination of password and username you provided is invalid. If you have forgotten your password, visit /forgotten-password or get in touch with your instance administrator.'));
});
test('should not login user if simple auth is disabled', async () => {
await settingService.insert(simpleAuthSettingsKey, { disabled: true }, TEST_AUDIT_USER, true);
await userService.createUser({
username: 'test_no_pass',
password: 'A very strange P4ssw0rd_',
rootRole: adminRole.id,
}, TEST_AUDIT_USER);
await expect(async () => {
await userService.loginUser('test_no_pass', 'A very strange P4ssw0rd_');
}).rejects.toThrowError('Logging in with username/password has been disabled.');
});
test('should login for user _without_ password', async () => {
const email = 'some@test.com';
await userService.createUser({
email,
password: 'A very strange P4ssw0rd_',
rootRole: adminRole.id,
}, TEST_AUDIT_USER);
const user = await userService.loginUserWithoutPassword(email);
expect(user.email).toBe(email);
});
test('should get user with root role', async () => {
const email = 'some@test.com';
const u = await userService.createUser({
email,
password: 'A very strange P4ssw0rd_',
rootRole: adminRole.id,
}, TEST_AUDIT_USER);
const user = await userService.getUser(u.id);
expect(user.email).toBe(email);
expect(user.id).toBe(u.id);
expect(user.rootRole).toBe(adminRole.id);
});
test('should get user with root role by name', async () => {
const email = 'some2@test.com';
const u = await userService.createUser({
email,
password: 'A very strange P4ssw0rd_',
rootRole: RoleName.ADMIN,
}, TEST_AUDIT_USER);
const user = await userService.getUser(u.id);
expect(user.email).toBe(email);
expect(user.id).toBe(u.id);
expect(user.rootRole).toBe(adminRole.id);
});
test("deleting a user should delete the user's sessions", async () => {
const email = 'some@test.com';
const user = await userService.createUser({
email,
password: 'A very strange P4ssw0rd_',
rootRole: adminRole.id,
}, TEST_AUDIT_USER);
const testComSession = {
sid: 'xyz321',
sess: {
cookie: {
originalMaxAge: minutesToMilliseconds(48),
expires: addDays(Date.now(), 1).toDateString(),
secure: false,
httpOnly: true,
path: '/',
},
user,
},
};
await sessionService.insertSession(testComSession);
const userSessions = await sessionService.getSessionsForUser(user.id);
expect(userSessions.length).toBe(1);
await userService.deleteUser(user.id, TEST_AUDIT_USER);
const noSessions = await sessionService.getSessionsForUser(user.id);
expect(noSessions.length).toBe(0);
});
test('user login should remove stale sessions', async () => {
const email = 'some@test.com';
const user = await userService.createUser({
email,
password: 'A very strange P4ssw0rd_',
rootRole: adminRole.id,
}, TEST_AUDIT_USER);
const userSession = (index) => ({
sid: `sid${index}`,
sess: {
cookie: {
originalMaxAge: minutesToMilliseconds(48),
expires: addDays(Date.now(), 1).toDateString(),
secure: false,
httpOnly: true,
path: '/',
},
user,
},
});
for (let i = 0; i < allowedSessions; i++) {
await sessionService.insertSession(userSession(i));
}
const loggedInUser = await userService.loginUser(email, 'A very strange P4ssw0rd_');
expect(loggedInUser.deletedSessions).toBe(1);
expect(loggedInUser.activeSessions).toBe(allowedSessions);
});
test('updating a user without an email should not strip the email', async () => {
const email = 'some@test.com';
const user = await userService.createUser({
email,
password: 'A very strange P4ssw0rd_',
rootRole: adminRole.id,
}, TEST_AUDIT_USER);
await userService.updateUser({
id: user.id,
name: 'some',
}, TEST_AUDIT_USER);
const updatedUser = await userService.getUser(user.id);
expect(updatedUser.email).toBe(email);
});
test('should login and create user via SSO', async () => {
const recordedEvents = [];
eventBus.on(USER_LOGIN, (data) => {
recordedEvents.push(data);
});
const email = 'some@test.com';
const user = await userService.loginUserSSO({
email,
rootRole: RoleName.VIEWER,
name: 'some',
autoCreate: true,
});
const userWithRole = await userService.getUser(user.id);
expect(user.email).toBe(email);
expect(user.name).toBe('some');
expect(userWithRole.name).toBe('some');
expect(userWithRole.rootRole).toBe(viewerRole.id);
expect(recordedEvents).toEqual([{ loginOrder: 0 }]);
});
test('should throw if rootRole is wrong via SSO', async () => {
expect.assertions(1);
await expect(userService.loginUserSSO({
email: 'some@test.com',
rootRole: RoleName.MEMBER,
name: 'some',
autoCreate: true,
})).rejects.errorWithMessage(new BadDataError('Could not find rootRole=Member'));
});
test('should update user name when signing in via SSO', async () => {
const email = 'some@test.com';
const originalUser = await userService.createUser({
email,
rootRole: RoleName.VIEWER,
name: 'some',
}, TEST_AUDIT_USER);
await userService.loginUserSSO({
email,
rootRole: RoleName.ADMIN,
name: 'New name!',
autoCreate: true,
});
const actualUser = await userService.getUser(originalUser.id);
expect(actualUser.email).toBe(email);
expect(actualUser.name).toBe('New name!');
expect(actualUser.rootRole).toBe(viewerRole.id);
});
test('should update name if it is different via SSO', async () => {
const email = 'some@test.com';
const originalUser = await userService.createUser({
email,
rootRole: RoleName.VIEWER,
name: 'some',
}, TEST_AUDIT_USER);
await userService.loginUserSSO({
email,
rootRole: RoleName.ADMIN,
name: 'New name!',
autoCreate: false,
});
const actualUser = await userService.getUser(originalUser.id);
expect(actualUser.email).toBe(email);
expect(actualUser.name).toBe('New name!');
expect(actualUser.rootRole).toBe(viewerRole.id);
});
test('should throw if autoCreate is false via SSO', async () => {
expect.assertions(1);
await expect(userService.loginUserSSO({
email: 'some@test.com',
rootRole: RoleName.MEMBER,
name: 'some',
autoCreate: false,
})).rejects.errorWithMessage(new NotFoundError('No user found'));
});
test('should support a root role id when logging in and creating user via SSO', async () => {
const name = 'root-role-id';
const email = `${name}@test.com`;
const user = await userService.loginUserSSO({
email,
rootRole: viewerRole.id,
name,
autoCreate: true,
});
const userWithRole = await userService.getUser(user.id);
expect(user.email).toBe(email);
expect(user.name).toBe(name);
expect(userWithRole.name).toBe(name);
expect(userWithRole.rootRole).toBe(viewerRole.id);
});
test('should support a custom root role id when logging in and creating user via SSO', async () => {
const name = 'custom-root-role-id';
const email = `${name}@test.com`;
const user = await userService.loginUserSSO({
email,
rootRole: customRole.id,
name,
autoCreate: true,
});
const userWithRole = await userService.getUser(user.id);
expect(user.email).toBe(email);
expect(user.name).toBe(name);
expect(userWithRole.name).toBe(name);
expect(userWithRole.rootRole).toBe(customRole.id);
const permissions = await accessService.getPermissionsForUser(user);
expect(permissions).toHaveLength(1);
expect(permissions[0].permission).toBe(CREATE_ADDON);
});
describe('Should not be able to use any of previous 5 passwords', () => {
test('throws exception when trying to use a previously used password', async () => {
const name = 'same-password-is-not-allowed';
const email = `${name}@test.com`;
const password = 'externalScreaming$123';
const user = await userService.createUser({
email,
rootRole: customRole.id,
name,
password,
});
await expect(userService.changePasswordWithPreviouslyUsedPasswordCheck(user.id, password)).rejects.errorWithMessage(new PasswordPreviouslyUsedError());
});
test('Is still able to change password to one not used', async () => {
const name = 'new-password-is-allowed';
const email = `${name}@test.com`;
const password = 'externalScreaming$123';
const user = await userService.createUser({
email,
rootRole: customRole.id,
name,
password,
});
await expect(userService.changePasswordWithPreviouslyUsedPasswordCheck(user.id, 'internalScreaming$123')).resolves.not.toThrow();
});
test('Remembers 5 passwords', async () => {
const name = 'remembers-5-passwords-like-a-boss';
const email = `${name}@test.com`;
const password = 'externalScreaming$123';
const user = await userService.createUser({
email,
rootRole: customRole.id,
name,
password,
});
for (let i = 0; i < 5; i++) {
await userService.changePasswordWithPreviouslyUsedPasswordCheck(user.id, `${password}${i}`);
}
await expect(userService.changePasswordWithPreviouslyUsedPasswordCheck(user.id, `${password}`)).resolves.not.toThrow(); // We've added 5 new passwords, so the original should work again
});
test('Can bypass check by directly calling the changePassword method', async () => {
const name = 'can-bypass-check-like-a-boss';
const email = `${name}@test.com`;
const password = 'externalScreaming$123';
const user = await userService.createUser({
email,
rootRole: customRole.id,
name,
password,
});
await expect(userService.changePassword(user.id, `${password}`)).resolves.not.toThrow(); // By bypassing the check, we can still set the same password as currently set
});
});
//# sourceMappingURL=user-service.e2e.test.js.map