Skip to content

Commit

Permalink
feat: allow defining initial admin user as env variable (#4927)
Browse files Browse the repository at this point in the history
Closes #4560
  • Loading branch information
jonasws committed Oct 6, 2023
1 parent 3634362 commit 80c4a82
Show file tree
Hide file tree
Showing 8 changed files with 167 additions and 17 deletions.
4 changes: 4 additions & 0 deletions src/lib/__snapshots__/create-config.test.ts.snap
Expand Up @@ -19,6 +19,10 @@ exports[`should create default config 1`] = `
"customAuthHandler": [Function],
"enableApiToken": true,
"initApiTokens": [],
"initialAdminUser": {
"password": "unleash4all",
"username": "admin",
},
"type": "open-source",
},
"clientFeatureCaching": {
Expand Down
4 changes: 4 additions & 0 deletions src/lib/create-config.ts
Expand Up @@ -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: [],
};

Expand Down
96 changes: 94 additions & 2 deletions src/lib/services/user-service.test.ts
Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand Down
48 changes: 37 additions & 11 deletions src/lib/services/user-service.ts
Expand Up @@ -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';
Expand Down Expand Up @@ -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 || '';
Expand All @@ -122,27 +128,47 @@ class UserService {
}
}

async initAdminUser(): Promise<void> {
async initAdminUser(
initialAdminUserConfig: Pick<
IAuthOption,
'createAdminUser' | 'initialAdminUser'
>,
): Promise<void> {
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}'`,
);
}
}
}
Expand Down Expand Up @@ -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,
Expand Down
6 changes: 5 additions & 1 deletion src/lib/types/option.ts
Expand Up @@ -57,7 +57,11 @@ export interface IAuthOption {
enableApiToken: boolean;
type: IAuthType;
customAuthHandler?: Function;
createAdminUser: boolean;
createAdminUser?: boolean;
initialAdminUser?: {
username: string;
password: string;
};
initApiTokens: ILegacyApiTokenCreate[];
}

Expand Down
16 changes: 14 additions & 2 deletions src/test/e2e/services/user-service.e2e.test.ts
Expand Up @@ -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);
Expand All @@ -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');
Expand Down
2 changes: 1 addition & 1 deletion website/docs/reference/deploy/configuring-unleash.md
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions website/docs/reference/deploy/getting-started.md
Expand Up @@ -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:**
Expand Down

0 comments on commit 80c4a82

Please sign in to comment.