1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-02-23 00:22:19 +01:00

feat: Disable password based login (#1046)

This commit will introduce a new setting used to disbaled
simple password based authention.

The setting itself is an enterprise setting.
This commit is contained in:
Ivar Conradi Østhus 2021-10-29 10:25:42 +02:00 committed by GitHub
parent 9e73ed8f47
commit 4fb1bcb524
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 176 additions and 15 deletions

View File

@ -3,15 +3,31 @@ import { IUnleashServices } from '../../types/services';
import { IUnleashConfig } from '../../types/option'; import { IUnleashConfig } from '../../types/option';
import version from '../../util/version'; import version from '../../util/version';
const Controller = require('../controller'); import Controller from '../controller';
import VersionService from '../../services/version-service';
import SettingService from '../../services/setting-service';
import {
simpleAuthKey,
SimpleAuthSettings,
} from '../../types/settings/simple-auth-settings';
class ConfigController extends Controller { class ConfigController extends Controller {
private versionService: VersionService;
private settingService: SettingService;
private uiConfig: any;
constructor( constructor(
config: IUnleashConfig, config: IUnleashConfig,
{ versionService }: Pick<IUnleashServices, 'versionService'>, {
versionService,
settingService,
}: Pick<IUnleashServices, 'versionService' | 'settingService'>,
) { ) {
super(config); super(config);
this.versionService = versionService; this.versionService = versionService;
this.settingService = settingService;
const authenticationType = const authenticationType =
config.authentication && config.authentication.type; config.authentication && config.authentication.type;
this.uiConfig = { this.uiConfig = {
@ -26,13 +42,12 @@ class ConfigController extends Controller {
async getUIConfig(req: Request, res: Response): Promise<void> { async getUIConfig(req: Request, res: Response): Promise<void> {
const config = this.uiConfig; const config = this.uiConfig;
if (this.versionService) { const simpleAuthSettings =
const versionInfo = this.versionService.getVersionInfo(); await this.settingService.get<SimpleAuthSettings>(simpleAuthKey);
res.json({ ...config, versionInfo });
} else { const versionInfo = this.versionService.getVersionInfo();
res.json(config); const disablePasswordAuth = simpleAuthSettings?.disabled;
} res.json({ ...config, versionInfo, disablePasswordAuth });
} }
} }
export default ConfigController; export default ConfigController;
module.exports = ConfigController;

View File

@ -24,6 +24,7 @@ import { IAuthRequest } from './routes/unleash-types';
import * as permissions from './types/permissions'; import * as permissions from './types/permissions';
import * as eventType from './types/events'; import * as eventType from './types/events';
import { RoleName } from './types/model'; import { RoleName } from './types/model';
import { SimpleAuthSettings } from './types/settings/simple-auth-settings';
async function createApp( async function createApp(
config: IUnleashConfig, config: IUnleashConfig,
@ -177,4 +178,5 @@ export type {
IUser, IUser,
IUnleashServices, IUnleashServices,
IAuthRequest, IAuthRequest,
SimpleAuthSettings,
}; };

View File

@ -47,15 +47,16 @@ export const createServices = (
const tagTypeService = new TagTypeService(stores, config); const tagTypeService = new TagTypeService(stores, config);
const addonService = new AddonService(stores, config, tagTypeService); const addonService = new AddonService(stores, config, tagTypeService);
const sessionService = new SessionService(stores, config); const sessionService = new SessionService(stores, config);
const settingService = new SettingService(stores, config);
const userService = new UserService(stores, config, { const userService = new UserService(stores, config, {
accessService, accessService,
resetTokenService, resetTokenService,
emailService, emailService,
sessionService, sessionService,
settingService,
}); });
const versionService = new VersionService(stores, config); const versionService = new VersionService(stores, config);
const healthService = new HealthService(stores, config); const healthService = new HealthService(stores, config);
const settingService = new SettingService(stores, config);
const userFeedbackService = new UserFeedbackService(stores, config); const userFeedbackService = new UserFeedbackService(stores, config);
const featureToggleServiceV2 = new FeatureToggleServiceV2(stores, config); const featureToggleServiceV2 = new FeatureToggleServiceV2(stores, config);
const environmentService = new EnvironmentService(stores, config); const environmentService = new EnvironmentService(stores, config);

View File

@ -16,7 +16,7 @@ export default class SettingService {
this.settingStore = settingStore; this.settingStore = settingStore;
} }
async get(id: string): Promise<object> { async get<T>(id: string): Promise<T> {
return this.settingStore.get(id); return this.settingStore.get(id);
} }

View File

@ -11,6 +11,8 @@ import SessionService from './session-service';
import FakeSessionStore from '../../test/fixtures/fake-session-store'; import FakeSessionStore from '../../test/fixtures/fake-session-store';
import User from '../types/user'; import User from '../types/user';
import FakeResetTokenStore from '../../test/fixtures/fake-reset-token-store'; import FakeResetTokenStore from '../../test/fixtures/fake-reset-token-store';
import SettingService from './setting-service';
import FakeSettingStore from '../../test/fixtures/fake-setting-store';
const config: IUnleashConfig = createTestConfig(); const config: IUnleashConfig = createTestConfig();
@ -28,12 +30,17 @@ test('Should create new user', async () => {
const sessionStore = new FakeSessionStore(); const sessionStore = new FakeSessionStore();
const sessionService = new SessionService({ sessionStore }, config); const sessionService = new SessionService({ sessionStore }, config);
const emailService = new EmailService(config.email, config.getLogger); const emailService = new EmailService(config.email, config.getLogger);
const settingService = new SettingService(
{ settingStore: new FakeSettingStore() },
config,
);
const service = new UserService({ userStore, eventStore }, config, { const service = new UserService({ userStore, eventStore }, config, {
accessService, accessService,
resetTokenService, resetTokenService,
emailService, emailService,
sessionService, sessionService,
settingService,
}); });
const user = await service.createUser( const user = await service.createUser(
{ {
@ -63,12 +70,17 @@ test('Should create default user', async () => {
const emailService = new EmailService(config.email, config.getLogger); const emailService = new EmailService(config.email, config.getLogger);
const sessionStore = new FakeSessionStore(); const sessionStore = new FakeSessionStore();
const sessionService = new SessionService({ sessionStore }, config); const sessionService = new SessionService({ sessionStore }, config);
const settingService = new SettingService(
{ settingStore: new FakeSettingStore() },
config,
);
const service = new UserService({ userStore, eventStore }, config, { const service = new UserService({ userStore, eventStore }, config, {
accessService, accessService,
resetTokenService, resetTokenService,
emailService, emailService,
sessionService, sessionService,
settingService,
}); });
await service.initAdminUser(); await service.initAdminUser();
@ -90,12 +102,17 @@ test('Should be a valid password', async () => {
const emailService = new EmailService(config.email, config.getLogger); const emailService = new EmailService(config.email, config.getLogger);
const sessionStore = new FakeSessionStore(); const sessionStore = new FakeSessionStore();
const sessionService = new SessionService({ sessionStore }, config); const sessionService = new SessionService({ sessionStore }, config);
const settingService = new SettingService(
{ settingStore: new FakeSettingStore() },
config,
);
const service = new UserService({ userStore, eventStore }, config, { const service = new UserService({ userStore, eventStore }, config, {
accessService, accessService,
resetTokenService, resetTokenService,
emailService, emailService,
sessionService, sessionService,
settingService,
}); });
const valid = service.validatePassword('this is a strong password!'); const valid = service.validatePassword('this is a strong password!');
@ -115,12 +132,17 @@ test('Password must be at least 10 chars', async () => {
const emailService = new EmailService(config.email, config.getLogger); const emailService = new EmailService(config.email, config.getLogger);
const sessionStore = new FakeSessionStore(); const sessionStore = new FakeSessionStore();
const sessionService = new SessionService({ sessionStore }, config); const sessionService = new SessionService({ sessionStore }, config);
const settingService = new SettingService(
{ settingStore: new FakeSettingStore() },
config,
);
const service = new UserService({ userStore, eventStore }, config, { const service = new UserService({ userStore, eventStore }, config, {
accessService, accessService,
resetTokenService, resetTokenService,
emailService, emailService,
sessionService, sessionService,
settingService,
}); });
expect(() => service.validatePassword('admin')).toThrow( expect(() => service.validatePassword('admin')).toThrow(
'The password must be at least 10 characters long.', 'The password must be at least 10 characters long.',
@ -142,12 +164,17 @@ test('The password must contain at least one uppercase letter.', async () => {
const emailService = new EmailService(config.email, config.getLogger); const emailService = new EmailService(config.email, config.getLogger);
const sessionStore = new FakeSessionStore(); const sessionStore = new FakeSessionStore();
const sessionService = new SessionService({ sessionStore }, config); const sessionService = new SessionService({ sessionStore }, config);
const settingService = new SettingService(
{ settingStore: new FakeSettingStore() },
config,
);
const service = new UserService({ userStore, eventStore }, config, { const service = new UserService({ userStore, eventStore }, config, {
accessService, accessService,
resetTokenService, resetTokenService,
emailService, emailService,
sessionService, sessionService,
settingService,
}); });
expect(() => service.validatePassword('qwertyabcde')).toThrowError( expect(() => service.validatePassword('qwertyabcde')).toThrowError(
@ -171,12 +198,17 @@ test('The password must contain at least one number', async () => {
const emailService = new EmailService(config.email, config.getLogger); const emailService = new EmailService(config.email, config.getLogger);
const sessionStore = new FakeSessionStore(); const sessionStore = new FakeSessionStore();
const sessionService = new SessionService({ sessionStore }, config); const sessionService = new SessionService({ sessionStore }, config);
const settingService = new SettingService(
{ settingStore: new FakeSettingStore() },
config,
);
const service = new UserService({ userStore, eventStore }, config, { const service = new UserService({ userStore, eventStore }, config, {
accessService, accessService,
resetTokenService, resetTokenService,
emailService, emailService,
sessionService, sessionService,
settingService,
}); });
expect(() => service.validatePassword('qwertyabcdE')).toThrowError( expect(() => service.validatePassword('qwertyabcdE')).toThrowError(
@ -199,12 +231,17 @@ test('The password must contain at least one special character', async () => {
const emailService = new EmailService(config.email, config.getLogger); const emailService = new EmailService(config.email, config.getLogger);
const sessionStore = new FakeSessionStore(); const sessionStore = new FakeSessionStore();
const sessionService = new SessionService({ sessionStore }, config); const sessionService = new SessionService({ sessionStore }, config);
const settingService = new SettingService(
{ settingStore: new FakeSettingStore() },
config,
);
const service = new UserService({ userStore, eventStore }, config, { const service = new UserService({ userStore, eventStore }, config, {
accessService, accessService,
resetTokenService, resetTokenService,
emailService, emailService,
sessionService, sessionService,
settingService,
}); });
expect(() => service.validatePassword('qwertyabcdE2')).toThrowError( expect(() => service.validatePassword('qwertyabcdE2')).toThrowError(
@ -227,12 +264,17 @@ test('Should be a valid password with special chars', async () => {
const emailService = new EmailService(config.email, config.getLogger); const emailService = new EmailService(config.email, config.getLogger);
const sessionStore = new FakeSessionStore(); const sessionStore = new FakeSessionStore();
const sessionService = new SessionService({ sessionStore }, config); const sessionService = new SessionService({ sessionStore }, config);
const settingService = new SettingService(
{ settingStore: new FakeSettingStore() },
config,
);
const service = new UserService({ userStore, eventStore }, config, { const service = new UserService({ userStore, eventStore }, config, {
accessService, accessService,
resetTokenService, resetTokenService,
emailService, emailService,
sessionService, sessionService,
settingService,
}); });
const valid = service.validatePassword('this is a strong password!'); const valid = service.validatePassword('this is a strong password!');

View File

@ -21,6 +21,9 @@ import { USER_UPDATED, USER_CREATED, USER_DELETED } from '../types/events';
import { IEventStore } from '../types/stores/event-store'; import { IEventStore } from '../types/stores/event-store';
import { IUserSearch, IUserStore } from '../types/stores/user-store'; import { IUserSearch, IUserStore } from '../types/stores/user-store';
import { RoleName } from '../types/model'; import { RoleName } from '../types/model';
import SettingService from './setting-service';
import { SimpleAuthSettings } from '../server-impl';
import { simpleAuthKey } from '../types/settings/simple-auth-settings';
const systemUser = new User({ id: -1, username: 'system' }); const systemUser = new User({ id: -1, username: 'system' });
@ -77,6 +80,8 @@ class UserService {
private emailService: EmailService; private emailService: EmailService;
private settingService: SettingService;
constructor( constructor(
stores: Pick<IUnleashStores, 'userStore' | 'eventStore'>, stores: Pick<IUnleashStores, 'userStore' | 'eventStore'>,
{ {
@ -88,6 +93,7 @@ class UserService {
resetTokenService: ResetTokenService; resetTokenService: ResetTokenService;
emailService: EmailService; emailService: EmailService;
sessionService: SessionService; sessionService: SessionService;
settingService: SettingService;
}, },
) { ) {
this.logger = getLogger('service/user-service.js'); this.logger = getLogger('service/user-service.js');
@ -97,6 +103,7 @@ class UserService {
this.resetTokenService = services.resetTokenService; this.resetTokenService = services.resetTokenService;
this.emailService = services.emailService; this.emailService = services.emailService;
this.sessionService = services.sessionService; this.sessionService = services.sessionService;
this.settingService = services.settingService;
if (authentication && authentication.createAdminUser) { if (authentication && authentication.createAdminUser) {
process.nextTick(() => this.initAdminUser()); process.nextTick(() => this.initAdminUser());
} }
@ -241,6 +248,16 @@ class UserService {
} }
async loginUser(usernameOrEmail: string, password: string): Promise<IUser> { async loginUser(usernameOrEmail: string, password: string): Promise<IUser> {
const settings = await this.settingService.get<SimpleAuthSettings>(
simpleAuthKey,
);
if (settings && settings.disabled) {
throw new Error(
'Logging in with username/password has been disabled.',
);
}
const idQuery = isEmail(usernameOrEmail) const idQuery = isEmail(usernameOrEmail)
? { email: usernameOrEmail } ? { email: usernameOrEmail }
: { username: usernameOrEmail }; : { username: usernameOrEmail };

View File

@ -2,6 +2,7 @@ interface IBaseOptions {
type: string; type: string;
path: string; path: string;
message: string; message: string;
defaultHidden?: boolean;
} }
interface IOptions extends IBaseOptions { interface IOptions extends IBaseOptions {
@ -15,13 +16,22 @@ class AuthenticationRequired {
private message: string; private message: string;
private defaultHidden: boolean;
private options?: IBaseOptions[]; private options?: IBaseOptions[];
constructor({ type, path, message, options }: IOptions) { constructor({
type,
path,
message,
options,
defaultHidden = false,
}: IOptions) {
this.type = type; this.type = type;
this.path = path; this.path = path;
this.message = message; this.message = message;
this.options = options; this.options = options;
this.defaultHidden = defaultHidden;
} }
} }

View File

@ -0,0 +1,4 @@
export const simpleAuthKey = 'unleash.auth.simple';
export interface SimpleAuthSettings {
disabled: boolean;
}

View File

@ -6,6 +6,6 @@ export interface ISettingInsert {
} }
export interface ISettingStore extends Store<any, string> { export interface ISettingStore extends Store<any, string> {
insert(name: string, content: any): Promise<void>; insert<T>(name: string, content: T): Promise<void>;
updateRow(name: string, content: any): Promise<void>; updateRow(name: string, content: any): Promise<void>;
} }

View File

@ -0,0 +1,37 @@
import dbInit, { ITestDb } from '../../helpers/database-init';
import { setupApp } from '../../helpers/test-helper';
import getLogger from '../../../fixtures/no-logger';
import { simpleAuthKey } from '../../../../lib/types/settings/simple-auth-settings';
let db: ITestDb;
let app;
beforeAll(async () => {
db = await dbInit('config_api_serial', getLogger);
app = await setupApp(db.stores);
});
afterAll(async () => {
await app.destroy();
await db.destroy();
});
test('gets ui config', async () => {
const { body } = await app.request
.get('/api/admin/ui-config')
.expect('Content-Type', /json/)
.expect(200);
expect(body.unleashUrl).toBe('http://localhost:4242');
expect(body.version).toBeDefined();
});
test('gets ui config with disablePasswordAuth', async () => {
await db.stores.settingStore.insert(simpleAuthKey, { disabled: true });
const { body } = await app.request
.get('/api/admin/ui-config')
.expect('Content-Type', /json/)
.expect(200);
expect(body.disablePasswordAuth).toBe(true);
});

View File

@ -13,6 +13,8 @@ import { EmailService } from '../../../../lib/services/email-service';
import SessionStore from '../../../../lib/db/session-store'; import SessionStore from '../../../../lib/db/session-store';
import SessionService from '../../../../lib/services/session-service'; import SessionService from '../../../../lib/services/session-service';
import { RoleName } from '../../../../lib/types/model'; import { RoleName } from '../../../../lib/types/model';
import SettingService from '../../../../lib/services/setting-service';
import FakeSettingStore from '../../../fixtures/fake-setting-store';
let app; let app;
let stores; let stores;
@ -53,11 +55,16 @@ beforeAll(async () => {
config.getLogger, config.getLogger,
); );
const sessionService = new SessionService({ sessionStore }, config); const sessionService = new SessionService({ sessionStore }, config);
const settingService = new SettingService(
{ settingStore: new FakeSettingStore() },
config,
);
userService = new UserService(stores, config, { userService = new UserService(stores, config, {
accessService, accessService,
resetTokenService, resetTokenService,
emailService, emailService,
sessionService, sessionService,
settingService,
}); });
resetTokenService = new ResetTokenService(stores, config); resetTokenService = new ResetTokenService(stores, config);
const adminRole = await accessService.getRootRole(RoleName.ADMIN); const adminRole = await accessService.getRootRole(RoleName.ADMIN);

View File

@ -9,6 +9,8 @@ import { createTestConfig } from '../../config/test-config';
import SessionService from '../../../lib/services/session-service'; import SessionService from '../../../lib/services/session-service';
import InvalidTokenError from '../../../lib/error/invalid-token-error'; import InvalidTokenError from '../../../lib/error/invalid-token-error';
import { IUser } from '../../../lib/types/user'; import { IUser } from '../../../lib/types/user';
import SettingService from '../../../lib/services/setting-service';
import FakeSettingStore from '../../fixtures/fake-setting-store';
const config: IUnleashConfig = createTestConfig(); const config: IUnleashConfig = createTestConfig();
@ -28,12 +30,17 @@ beforeAll(async () => {
resetTokenService = new ResetTokenService(stores, config); resetTokenService = new ResetTokenService(stores, config);
sessionService = new SessionService(stores, config); sessionService = new SessionService(stores, config);
const emailService = new EmailService(undefined, config.getLogger); const emailService = new EmailService(undefined, config.getLogger);
const settingService = new SettingService(
{ settingStore: new FakeSettingStore() },
config,
);
userService = new UserService(stores, config, { userService = new UserService(stores, config, {
accessService, accessService,
resetTokenService, resetTokenService,
emailService, emailService,
sessionService, sessionService,
settingService,
}); });
adminUser = await userService.createUser({ adminUser = await userService.createUser({

View File

@ -10,6 +10,8 @@ import SessionService from '../../../lib/services/session-service';
import NotFoundError from '../../../lib/error/notfound-error'; import NotFoundError from '../../../lib/error/notfound-error';
import { IRole } from '../../../lib/types/stores/access-store'; import { IRole } from '../../../lib/types/stores/access-store';
import { RoleName } from '../../../lib/types/model'; import { RoleName } from '../../../lib/types/model';
import SettingService from '../../../lib/services/setting-service';
import { simpleAuthKey } from '../../../lib/types/settings/simple-auth-settings';
let db; let db;
let stores; let stores;
@ -27,12 +29,14 @@ beforeAll(async () => {
const resetTokenService = new ResetTokenService(stores, config); const resetTokenService = new ResetTokenService(stores, config);
const emailService = new EmailService(undefined, config.getLogger); const emailService = new EmailService(undefined, config.getLogger);
sessionService = new SessionService(stores, config); sessionService = new SessionService(stores, config);
const settingService = new SettingService(stores, config);
userService = new UserService(stores, config, { userService = new UserService(stores, config, {
accessService, accessService,
resetTokenService, resetTokenService,
emailService, emailService,
sessionService, sessionService,
settingService,
}); });
userStore = stores.userStore; userStore = stores.userStore;
const rootRoles = await accessService.getRootRoles(); const rootRoles = await accessService.getRootRoles();
@ -93,6 +97,22 @@ test('should create user with password', async () => {
expect(user.username).toBe('test'); expect(user.username).toBe('test');
}); });
test('should not login user if simple auth is disabled', async () => {
await db.stores.settingStore.insert(simpleAuthKey, { disabled: true });
await userService.createUser({
username: 'test_no_pass',
password: 'A very strange P4ssw0rd_',
rootRole: adminRole.id,
});
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 () => { test('should login for user _without_ password', async () => {
const email = 'some@test.com'; const email = 'some@test.com';
await userService.createUser({ await userService.createUser({

View File

@ -1,5 +1,4 @@
import { ISettingStore } from '../../lib/types/stores/settings-store'; import { ISettingStore } from '../../lib/types/stores/settings-store';
import NotFoundError from '../../lib/error/notfound-error';
export default class FakeSettingStore implements ISettingStore { export default class FakeSettingStore implements ISettingStore {
settings: Map<string, any> = new Map(); settings: Map<string, any> = new Map();
@ -23,7 +22,7 @@ export default class FakeSettingStore implements ISettingStore {
if (setting) { if (setting) {
return setting; return setting;
} }
throw new NotFoundError(`Could not find setting with key ${key}`); return undefined;
} }
async getAll(): Promise<any[]> { async getAll(): Promise<any[]> {