1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-25 00:07:47 +01:00

feat: allow admin login using demo auth (#6808)

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](https://github.com/Unleash/unleash/pull/6447#issuecomment-2042405647)
in this PR.

---------

Co-authored-by: Thomas Heartman <thomasheartman+github@gmail.com>
This commit is contained in:
00Chaotic 2024-04-23 16:39:33 +10:00 committed by GitHub
parent 9ba6be6000
commit 13aa58e0e9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 105 additions and 10 deletions

View File

@ -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

View File

@ -17,6 +17,7 @@ exports[`should create default config 1`] = `
"authentication": {
"createAdminUser": true,
"customAuthHandler": [Function],
"demoAllowAdminLogin": false,
"enableApiToken": true,
"initApiTokens": [],
"initialAdminUser": {

View File

@ -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';

View File

@ -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,

View File

@ -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');
});
});

View File

@ -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';
@ -19,14 +19,19 @@ function demoAuthentication(
): void {
app.post(`${basePath}/auth/demo/login`, async (req: IAuthRequest, res) => {
let { email } = req.body;
let user: IUser;
try {
if (authentication.demoAllowAdminLogin && email === 'admin') {
user = await userService.loginDemoAuthDefaultAdmin();
} else {
email = flagResolver.isEnabled('encryptEmails', { email })
? encrypt(email)
: email;
try {
const user = await userService.loginUserWithoutPassword(
email,
true,
);
user = await userService.loginUserWithoutPassword(email, true);
}
req.session.user = user;
return res.status(200).json(user);
} catch (e) {
@ -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();

View File

@ -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);

View File

@ -67,6 +67,7 @@ export type CustomAuthHandler = (
) => void;
export interface IAuthOption {
demoAllowAdminLogin?: boolean;
enableApiToken: boolean;
type: IAuthType;
customAuthHandler?: CustomAuthHandler;