From 80c4a8277cf2d5ed7b02560a90601640facbdd64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Str=C3=B8msodd?= Date: Fri, 6 Oct 2023 09:07:06 +0200 Subject: [PATCH] feat: allow defining initial admin user as env variable (#4927) Closes #4560 --- .../__snapshots__/create-config.test.ts.snap | 4 + src/lib/create-config.ts | 4 + src/lib/services/user-service.test.ts | 96 ++++++++++++++++++- src/lib/services/user-service.ts | 48 +++++++--- src/lib/types/option.ts | 6 +- .../e2e/services/user-service.e2e.test.ts | 16 +++- .../reference/deploy/configuring-unleash.md | 2 +- .../docs/reference/deploy/getting-started.md | 8 ++ 8 files changed, 167 insertions(+), 17 deletions(-) diff --git a/src/lib/__snapshots__/create-config.test.ts.snap b/src/lib/__snapshots__/create-config.test.ts.snap index 2a1727fcd715..7370414379c4 100644 --- a/src/lib/__snapshots__/create-config.test.ts.snap +++ b/src/lib/__snapshots__/create-config.test.ts.snap @@ -19,6 +19,10 @@ exports[`should create default config 1`] = ` "customAuthHandler": [Function], "enableApiToken": true, "initApiTokens": [], + "initialAdminUser": { + "password": "unleash4all", + "username": "admin", + }, "type": "open-source", }, "clientFeatureCaching": { diff --git a/src/lib/create-config.ts b/src/lib/create-config.ts index 65640ab14e50..85a2b328b039 100644 --- a/src/lib/create-config.ts +++ b/src/lib/create-config.ts @@ -195,6 +195,10 @@ const defaultAuthentication: IAuthOption = { type: authTypeFromString(process.env.AUTH_TYPE), customAuthHandler: defaultCustomAuthDenyAll, createAdminUser: true, + initialAdminUser: { + username: process.env.UNLEASH_DEFAULT_ADMIN_USERNAME ?? 'admin', + password: process.env.UNLEASH_DEFAULT_ADMIN_PASSWORD ?? 'unleash4all', + }, initApiTokens: [], }; diff --git a/src/lib/services/user-service.test.ts b/src/lib/services/user-service.test.ts index 39ec7b4ecfb5..d7275f812654 100644 --- a/src/lib/services/user-service.test.ts +++ b/src/lib/services/user-service.test.ts @@ -69,7 +69,7 @@ test('Should create new user', async () => { expect(storedUser.username).toBe('test'); }); -test('Should create default user', async () => { +test('Should create default user - with defaults', async () => { const userStore = new UserStoreMock(); const eventStore = new EventStoreMock(); const accessService = new AccessServiceMock(); @@ -102,12 +102,104 @@ test('Should create default user', async () => { settingService, }); - await service.initAdminUser(); + await service.initAdminUser({}); const user = await service.loginUser('admin', 'unleash4all'); expect(user.username).toBe('admin'); }); +test('Should create default user - with provided username and password', async () => { + const userStore = new UserStoreMock(); + const eventStore = new EventStoreMock(); + const accessService = new AccessServiceMock(); + const resetTokenStore = new FakeResetTokenStore(); + const resetTokenService = new ResetTokenService( + { resetTokenStore }, + config, + ); + const emailService = new EmailService(config.email, config.getLogger); + const sessionStore = new FakeSessionStore(); + const sessionService = new SessionService({ sessionStore }, config); + const eventService = new EventService( + { eventStore, featureTagStore: new FakeFeatureTagStore() }, + config, + ); + const settingService = new SettingService( + { + settingStore: new FakeSettingStore(), + }, + config, + eventService, + ); + + const service = new UserService({ userStore }, config, { + accessService, + resetTokenService, + emailService, + eventService, + sessionService, + settingService, + }); + + await service.initAdminUser({ + initialAdminUser: { + username: 'admin', + password: 'unleash4all!', + }, + }); + + const user = await service.loginUser('admin', 'unleash4all!'); + expect(user.username).toBe('admin'); +}); + +test('Should not create default user - with `createAdminUser` === false', async () => { + const userStore = new UserStoreMock(); + const eventStore = new EventStoreMock(); + const accessService = new AccessServiceMock(); + const resetTokenStore = new FakeResetTokenStore(); + const resetTokenService = new ResetTokenService( + { resetTokenStore }, + config, + ); + const emailService = new EmailService(config.email, config.getLogger); + const sessionStore = new FakeSessionStore(); + const sessionService = new SessionService({ sessionStore }, config); + const eventService = new EventService( + { eventStore, featureTagStore: new FakeFeatureTagStore() }, + config, + ); + const settingService = new SettingService( + { + settingStore: new FakeSettingStore(), + }, + config, + eventService, + ); + + const service = new UserService({ userStore }, config, { + accessService, + resetTokenService, + emailService, + eventService, + sessionService, + settingService, + }); + + await service.initAdminUser({ + createAdminUser: false, + initialAdminUser: { + username: 'admin', + password: 'unleash4all!', + }, + }); + + await expect( + service.loginUser('admin', 'unleash4all!'), + ).rejects.toThrowError( + 'The combination of password and username you provided is invalid', + ); +}); + test('Should be a valid password', async () => { const userStore = new UserStoreMock(); const eventStore = new EventStoreMock(); diff --git a/src/lib/services/user-service.ts b/src/lib/services/user-service.ts index c8be6bbf52e9..f2e4eb40e029 100644 --- a/src/lib/services/user-service.ts +++ b/src/lib/services/user-service.ts @@ -12,7 +12,7 @@ import InvalidTokenError from '../error/invalid-token-error'; import NotFoundError from '../error/notfound-error'; import OwaspValidationError from '../error/owasp-validation-error'; import { EmailService } from './email-service'; -import { IUnleashConfig } from '../types/option'; +import { IAuthOption, IUnleashConfig } from '../types/option'; import SessionService from './session-service'; import { IUnleashStores } from '../types/stores'; import PasswordUndefinedError from '../error/password-undefined'; @@ -104,8 +104,14 @@ class UserService { this.emailService = services.emailService; this.sessionService = services.sessionService; this.settingService = services.settingService; - if (authentication?.createAdminUser) { - process.nextTick(() => this.initAdminUser()); + + if (authentication.createAdminUser !== false) { + process.nextTick(() => + this.initAdminUser({ + createAdminUser: authentication.createAdminUser, + initialAdminUser: authentication.initialAdminUser, + }), + ); } this.baseUriPath = server.baseUriPath || ''; @@ -122,27 +128,47 @@ class UserService { } } - async initAdminUser(): Promise { + async initAdminUser( + initialAdminUserConfig: Pick< + IAuthOption, + 'createAdminUser' | 'initialAdminUser' + >, + ): Promise { + let username: string; + let password: string; + + if ( + initialAdminUserConfig.createAdminUser !== false && + initialAdminUserConfig.initialAdminUser + ) { + username = initialAdminUserConfig.initialAdminUser.username; + password = initialAdminUserConfig.initialAdminUser.password; + } else { + username = 'admin'; + password = 'unleash4all'; + } + const userCount = await this.store.count(); - if (userCount === 0) { + if (userCount === 0 && username && password) { // create default admin user try { - const pwd = 'unleash4all'; this.logger.info( - `Creating default user "admin" with password "${pwd}"`, + `Creating default user '${username}' with password '${password}'`, ); const user = await this.store.insert({ - username: 'admin', + username, }); - const passwordHash = await bcrypt.hash(pwd, saltRounds); + const passwordHash = await bcrypt.hash(password, saltRounds); await this.store.setPasswordHash(user.id, passwordHash); await this.accessService.setUserRootRole( user.id, RoleName.ADMIN, ); } catch (e) { - this.logger.error('Unable to create default user "admin"'); + this.logger.error( + `Unable to create default user '${username}'`, + ); } } } @@ -344,7 +370,7 @@ class UserService { user = await this.store.update(user.id, { name, email }); } } catch (e) { - // User does not exists. Create if "autoCreate" is enabled + // User does not exists. Create if 'autoCreate' is enabled if (autoCreate) { user = await this.createUser({ email, diff --git a/src/lib/types/option.ts b/src/lib/types/option.ts index a948c7e80095..07cd7b79fabc 100644 --- a/src/lib/types/option.ts +++ b/src/lib/types/option.ts @@ -57,7 +57,11 @@ export interface IAuthOption { enableApiToken: boolean; type: IAuthType; customAuthHandler?: Function; - createAdminUser: boolean; + createAdminUser?: boolean; + initialAdminUser?: { + username: string; + password: string; + }; initApiTokens: ILegacyApiTokenCreate[]; } diff --git a/src/test/e2e/services/user-service.e2e.test.ts b/src/test/e2e/services/user-service.e2e.test.ts index 95f1b3c7a507..1e0b552684da 100644 --- a/src/test/e2e/services/user-service.e2e.test.ts +++ b/src/test/e2e/services/user-service.e2e.test.ts @@ -63,7 +63,13 @@ afterEach(async () => { }); test('should create initial admin user', async () => { - await userService.initAdminUser(); + await userService.initAdminUser({ + createAdminUser: true, + initialAdminUser: { + username: 'admin', + password: 'unleash4all', + }, + }); await expect(async () => userService.loginUser('admin', 'wrong-password'), ).rejects.toThrow(Error); @@ -78,7 +84,13 @@ test('should not init default user if we already have users', async () => { password: 'A very strange P4ssw0rd_', rootRole: adminRole.id, }); - await userService.initAdminUser(); + await userService.initAdminUser({ + createAdminUser: true, + initialAdminUser: { + username: 'admin', + password: 'unleash4all', + }, + }); const users = await userService.getAll(); expect(users).toHaveLength(1); expect(users[0].username).toBe('test'); diff --git a/website/docs/reference/deploy/configuring-unleash.md b/website/docs/reference/deploy/configuring-unleash.md index fa3028b2c6d1..4a5b1eb7acd0 100644 --- a/website/docs/reference/deploy/configuring-unleash.md +++ b/website/docs/reference/deploy/configuring-unleash.md @@ -66,7 +66,7 @@ unleash.start(unleashOptions); - `none` - Turn off authentication all together - `demo` - Only requires an email to sign in (was default in v3) - `customAuthHandler`: function `(app: any, config: IUnleashConfig): void` — custom express middleware handling authentication. Used when type is set to `custom`. Can not be set via environment variables. - - `createAdminUser`: `boolean` — whether to create an admin user with default password - Defaults to `true`. Can not be set via environment variables. Can not be set via environment variables. + - `initialAdminUser`: `{ username: string, password: string} | null` — whether to create an admin user with default password - Defaults to using `admin` and `unleash4all` as the username and password. Can not be overridden by setting the `UNLEASH_DEFAULT_ADMIN_USERNAME` and `UNLEASH_DEFAULT_ADMIN_PASSWORD` environment variables. - `initApiTokens` / `INIT_ADMIN_API_TOKENS` and `INIT_CLIENT_API_TOKENS` (see below): `ApiTokens[]` — Array of API tokens to create on startup. The tokens will only be created if the database doesn't already contain any API tokens. Example: ```ts diff --git a/website/docs/reference/deploy/getting-started.md b/website/docs/reference/deploy/getting-started.md index 16fbdc4cab90..7061abe9a372 100644 --- a/website/docs/reference/deploy/getting-started.md +++ b/website/docs/reference/deploy/getting-started.md @@ -31,6 +31,14 @@ To run multiple replicas of Unleash simply point all instances to the same datab - username: `admin` - password: `unleash4all` +If you'd like the default admin user to be created with a different username and password, you may define the following environment variables when running Unleash: + +- `UNLEASH_DEFAULT_ADMIN_USERNAME` +- UNLEASH_DEFAULT_ADMIN_PASSWORD + +The way of defining these variables may vary depending on how you run Unleash. + + ### Option 1 - use Docker {#option-one---use-docker} **Useful links:**