Skip to content

Commit

Permalink
feat: allow admin login using demo auth (#6808)
Browse files Browse the repository at this point in the history
This PR introduces a configuration option (`authentication.demoAllowAdminLogin`) that allows you to log in as admin when using demo authentication. To do this, use the username `admin`. 

## About the changes
The `admin` user currently cannot be accessed in `demo` authentication
mode, as the auth mode requires only an email to log in, and the admin
user is not created with an email. This change allows for logging in as
the admin user only if an `AUTH_DEMO_ALLOW_ADMIN_LOGIN` is set to `true`
(or the corresponding `authDemoAllowAdminLogin` config is enabled).

<!-- Does it close an issue? Multiple? -->
Closes #6398 

### Important files

[demo-authentication.ts](https://github.com/Unleash/unleash/compare/main...00Chaotic:unleash:feat/allow_admin_login_using_demo_auth?expand=1#diff-c166f00f0a8ca4425236b3bcba40a8a3bd07a98d067495a0a092eec26866c9f1R25)


## Discussion points
Can continue discussion of [this
comment](#6447 (comment))
in this PR.

---------

Co-authored-by: Thomas Heartman <thomasheartman+github@gmail.com>
  • Loading branch information
00Chaotic and thomasheartman committed Apr 23, 2024
1 parent 9ba6be6 commit 13aa58e
Show file tree
Hide file tree
Showing 8 changed files with 105 additions and 10 deletions.
2 changes: 1 addition & 1 deletion frontend/src/component/user/DemoAuth/DemoAuth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ const DemoAuth: VFC<IDemoAuthProps> = ({ authDetails, redirect }) => {
id='email'
data-testid={LOGIN_EMAIL_ID}
required
type='email'
type={email === 'admin' ? 'text' : 'email'}
/>

<Button
Expand Down
1 change: 1 addition & 0 deletions src/lib/__snapshots__/create-config.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ exports[`should create default config 1`] = `
"authentication": {
"createAdminUser": true,
"customAuthHandler": [Function],
"demoAllowAdminLogin": false,
"enableApiToken": true,
"initApiTokens": [],
"initialAdminUser": {
Expand Down
5 changes: 5 additions & 0 deletions src/lib/create-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,11 @@ test('should handle cases where no env var specified for tokens', async () => {
expect(config.authentication.initApiTokens).toHaveLength(1);
});

test('should default demo admin login to false', async () => {
const config = createConfig({});
expect(config.authentication.demoAllowAdminLogin).toBeFalsy();
});

test('should load environment overrides from env var', async () => {
process.env.ENABLED_ENVIRONMENTS = 'default,production';

Expand Down
4 changes: 4 additions & 0 deletions src/lib/create-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,10 @@ const defaultVersionOption: IVersionOption = {
};

const defaultAuthentication: IAuthOption = {
demoAllowAdminLogin: parseEnvVarBoolean(
process.env.AUTH_DEMO_ALLOW_ADMIN_LOGIN,
false,
),
enableApiToken: parseEnvVarBoolean(process.env.AUTH_ENABLE_API_TOKEN, true),
type: authTypeFromString(process.env.AUTH_TYPE),
customAuthHandler: defaultCustomAuthDenyAll,
Expand Down
73 changes: 73 additions & 0 deletions src/lib/middleware/demo-authentication.e2e.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import dbInit from '../../test/e2e/helpers/database-init';
import { IAuthType } from '../server-impl';
import { setupAppWithCustomAuth } from '../../test/e2e/helpers/test-helper';
import type { ITestDb } from '../../test/e2e/helpers/database-init';
import type { IUnleashStores } from '../types';

let db: ITestDb;
let stores: IUnleashStores;

beforeAll(async () => {
db = await dbInit('demo_auth_serial');
stores = db.stores;
});

afterAll(async () => {
await db?.destroy();
});

const getApp = (adminLoginEnabled: boolean) =>
setupAppWithCustomAuth(stores, () => {}, {
authentication: {
demoAllowAdminLogin: adminLoginEnabled,
type: IAuthType.DEMO,
createAdminUser: true,
},
});

test('the demoAllowAdminLogin flag should not affect regular user login/creation', async () => {
const app = await getApp(true);
return app.request
.post(`/auth/demo/login`)
.send({ email: 'test@example.com' })
.expect(200)
.expect((res) => {
expect(res.body.email).toBe('test@example.com');
expect(res.body.id).not.toBe(1);
});
});

test('if the demoAllowAdminLogin flag is disabled, using `admin` should have the same result as any other invalid email', async () => {
const app = await getApp(false);

const nonAdminUsername = 'not-an-email';
const adminUsername = 'admin';

const nonAdminUser = await app.request
.post(`/auth/demo/login`)
.send({ email: nonAdminUsername });

const adminUser = await app.request
.post(`/auth/demo/login`)
.send({ email: adminUsername });

expect(nonAdminUser.status).toBe(adminUser.status);

for (const user of [nonAdminUser, adminUser]) {
expect(user.body).toMatchObject({
error: expect.stringMatching(/^Could not sign in with /),
});
}
});

test('should allow you to login as admin if the demoAllowAdminLogin flag enabled', async () => {
const app = await getApp(true);
return app.request
.post(`/auth/demo/login`)
.send({ email: 'admin' })
.expect(200)
.expect((res) => {
expect(res.body.id).toBe(1);
expect(res.body.username).toBe('admin');
});
});
23 changes: 14 additions & 9 deletions src/lib/middleware/demo-authentication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { IUnleashServices } from '../types/services';
import type { IUnleashConfig } from '../types/option';
import ApiUser from '../types/api-user';
import { ApiTokenType } from '../types/models/api-token';
import type { IAuthRequest } from '../server-impl';
import type { IAuthRequest, IUser } from '../server-impl';
import type { IApiRequest } from '../routes/unleash-types';
import { encrypt } from '../util';

Expand All @@ -19,14 +19,19 @@ function demoAuthentication(
): void {
app.post(`${basePath}/auth/demo/login`, async (req: IAuthRequest, res) => {
let { email } = req.body;
email = flagResolver.isEnabled('encryptEmails', { email })
? encrypt(email)
: email;
let user: IUser;

try {
const user = await userService.loginUserWithoutPassword(
email,
true,
);
if (authentication.demoAllowAdminLogin && email === 'admin') {
user = await userService.loginDemoAuthDefaultAdmin();
} else {
email = flagResolver.isEnabled('encryptEmails', { email })
? encrypt(email)
: email;

user = await userService.loginUserWithoutPassword(email, true);
}

req.session.user = user;
return res.status(200).json(user);
} catch (e) {
Expand All @@ -37,7 +42,7 @@ function demoAuthentication(
});

app.use(`${basePath}/api/admin/`, (req: IAuthRequest, res, next) => {
if (req.session.user?.email) {
if (req.session.user?.email || req.session.user?.username === 'admin') {
req.user = req.session.user;
}
next();
Expand Down
6 changes: 6 additions & 0 deletions src/lib/services/user-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,12 @@ class UserService {
return user;
}

async loginDemoAuthDefaultAdmin(): Promise<IUser> {
const user = await this.store.getByQuery({ id: 1 });
await this.store.successfullyLogin(user);
return user;
}

async changePassword(userId: number, password: string): Promise<void> {
this.validatePassword(password);
const passwordHash = await bcrypt.hash(password, saltRounds);
Expand Down
1 change: 1 addition & 0 deletions src/lib/types/option.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export type CustomAuthHandler = (
) => void;

export interface IAuthOption {
demoAllowAdminLogin?: boolean;
enableApiToken: boolean;
type: IAuthType;
customAuthHandler?: CustomAuthHandler;
Expand Down

0 comments on commit 13aa58e

Please sign in to comment.