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 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 {
private versionService: VersionService;
private settingService: SettingService;
private uiConfig: any;
constructor(
config: IUnleashConfig,
{ versionService }: Pick<IUnleashServices, 'versionService'>,
{
versionService,
settingService,
}: Pick<IUnleashServices, 'versionService' | 'settingService'>,
) {
super(config);
this.versionService = versionService;
this.settingService = settingService;
const authenticationType =
config.authentication && config.authentication.type;
this.uiConfig = {
@ -26,13 +42,12 @@ class ConfigController extends Controller {
async getUIConfig(req: Request, res: Response): Promise<void> {
const config = this.uiConfig;
if (this.versionService) {
const versionInfo = this.versionService.getVersionInfo();
res.json({ ...config, versionInfo });
} else {
res.json(config);
}
const simpleAuthSettings =
await this.settingService.get<SimpleAuthSettings>(simpleAuthKey);
const versionInfo = this.versionService.getVersionInfo();
const disablePasswordAuth = simpleAuthSettings?.disabled;
res.json({ ...config, versionInfo, disablePasswordAuth });
}
}
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 eventType from './types/events';
import { RoleName } from './types/model';
import { SimpleAuthSettings } from './types/settings/simple-auth-settings';
async function createApp(
config: IUnleashConfig,
@ -177,4 +178,5 @@ export type {
IUser,
IUnleashServices,
IAuthRequest,
SimpleAuthSettings,
};

View File

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

View File

@ -16,7 +16,7 @@ export default class SettingService {
this.settingStore = settingStore;
}
async get(id: string): Promise<object> {
async get<T>(id: string): Promise<T> {
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 User from '../types/user';
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();
@ -28,12 +30,17 @@ test('Should create new user', async () => {
const sessionStore = new FakeSessionStore();
const sessionService = new SessionService({ sessionStore }, config);
const emailService = new EmailService(config.email, config.getLogger);
const settingService = new SettingService(
{ settingStore: new FakeSettingStore() },
config,
);
const service = new UserService({ userStore, eventStore }, config, {
accessService,
resetTokenService,
emailService,
sessionService,
settingService,
});
const user = await service.createUser(
{
@ -63,12 +70,17 @@ test('Should create default user', async () => {
const emailService = new EmailService(config.email, config.getLogger);
const sessionStore = new FakeSessionStore();
const sessionService = new SessionService({ sessionStore }, config);
const settingService = new SettingService(
{ settingStore: new FakeSettingStore() },
config,
);
const service = new UserService({ userStore, eventStore }, config, {
accessService,
resetTokenService,
emailService,
sessionService,
settingService,
});
await service.initAdminUser();
@ -90,12 +102,17 @@ test('Should be a valid password', async () => {
const emailService = new EmailService(config.email, config.getLogger);
const sessionStore = new FakeSessionStore();
const sessionService = new SessionService({ sessionStore }, config);
const settingService = new SettingService(
{ settingStore: new FakeSettingStore() },
config,
);
const service = new UserService({ userStore, eventStore }, config, {
accessService,
resetTokenService,
emailService,
sessionService,
settingService,
});
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 sessionStore = new FakeSessionStore();
const sessionService = new SessionService({ sessionStore }, config);
const settingService = new SettingService(
{ settingStore: new FakeSettingStore() },
config,
);
const service = new UserService({ userStore, eventStore }, config, {
accessService,
resetTokenService,
emailService,
sessionService,
settingService,
});
expect(() => service.validatePassword('admin')).toThrow(
'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 sessionStore = new FakeSessionStore();
const sessionService = new SessionService({ sessionStore }, config);
const settingService = new SettingService(
{ settingStore: new FakeSettingStore() },
config,
);
const service = new UserService({ userStore, eventStore }, config, {
accessService,
resetTokenService,
emailService,
sessionService,
settingService,
});
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 sessionStore = new FakeSessionStore();
const sessionService = new SessionService({ sessionStore }, config);
const settingService = new SettingService(
{ settingStore: new FakeSettingStore() },
config,
);
const service = new UserService({ userStore, eventStore }, config, {
accessService,
resetTokenService,
emailService,
sessionService,
settingService,
});
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 sessionStore = new FakeSessionStore();
const sessionService = new SessionService({ sessionStore }, config);
const settingService = new SettingService(
{ settingStore: new FakeSettingStore() },
config,
);
const service = new UserService({ userStore, eventStore }, config, {
accessService,
resetTokenService,
emailService,
sessionService,
settingService,
});
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 sessionStore = new FakeSessionStore();
const sessionService = new SessionService({ sessionStore }, config);
const settingService = new SettingService(
{ settingStore: new FakeSettingStore() },
config,
);
const service = new UserService({ userStore, eventStore }, config, {
accessService,
resetTokenService,
emailService,
sessionService,
settingService,
});
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 { IUserSearch, IUserStore } from '../types/stores/user-store';
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' });
@ -77,6 +80,8 @@ class UserService {
private emailService: EmailService;
private settingService: SettingService;
constructor(
stores: Pick<IUnleashStores, 'userStore' | 'eventStore'>,
{
@ -88,6 +93,7 @@ class UserService {
resetTokenService: ResetTokenService;
emailService: EmailService;
sessionService: SessionService;
settingService: SettingService;
},
) {
this.logger = getLogger('service/user-service.js');
@ -97,6 +103,7 @@ class UserService {
this.resetTokenService = services.resetTokenService;
this.emailService = services.emailService;
this.sessionService = services.sessionService;
this.settingService = services.settingService;
if (authentication && authentication.createAdminUser) {
process.nextTick(() => this.initAdminUser());
}
@ -241,6 +248,16 @@ class UserService {
}
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)
? { email: usernameOrEmail }
: { username: usernameOrEmail };

View File

@ -2,6 +2,7 @@ interface IBaseOptions {
type: string;
path: string;
message: string;
defaultHidden?: boolean;
}
interface IOptions extends IBaseOptions {
@ -15,13 +16,22 @@ class AuthenticationRequired {
private message: string;
private defaultHidden: boolean;
private options?: IBaseOptions[];
constructor({ type, path, message, options }: IOptions) {
constructor({
type,
path,
message,
options,
defaultHidden = false,
}: IOptions) {
this.type = type;
this.path = path;
this.message = message;
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> {
insert(name: string, content: any): Promise<void>;
insert<T>(name: string, content: T): 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 SessionService from '../../../../lib/services/session-service';
import { RoleName } from '../../../../lib/types/model';
import SettingService from '../../../../lib/services/setting-service';
import FakeSettingStore from '../../../fixtures/fake-setting-store';
let app;
let stores;
@ -53,11 +55,16 @@ beforeAll(async () => {
config.getLogger,
);
const sessionService = new SessionService({ sessionStore }, config);
const settingService = new SettingService(
{ settingStore: new FakeSettingStore() },
config,
);
userService = new UserService(stores, config, {
accessService,
resetTokenService,
emailService,
sessionService,
settingService,
});
resetTokenService = new ResetTokenService(stores, config);
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 InvalidTokenError from '../../../lib/error/invalid-token-error';
import { IUser } from '../../../lib/types/user';
import SettingService from '../../../lib/services/setting-service';
import FakeSettingStore from '../../fixtures/fake-setting-store';
const config: IUnleashConfig = createTestConfig();
@ -28,12 +30,17 @@ beforeAll(async () => {
resetTokenService = new ResetTokenService(stores, config);
sessionService = new SessionService(stores, config);
const emailService = new EmailService(undefined, config.getLogger);
const settingService = new SettingService(
{ settingStore: new FakeSettingStore() },
config,
);
userService = new UserService(stores, config, {
accessService,
resetTokenService,
emailService,
sessionService,
settingService,
});
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 { IRole } from '../../../lib/types/stores/access-store';
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 stores;
@ -27,12 +29,14 @@ beforeAll(async () => {
const resetTokenService = new ResetTokenService(stores, config);
const emailService = new EmailService(undefined, config.getLogger);
sessionService = new SessionService(stores, config);
const settingService = new SettingService(stores, config);
userService = new UserService(stores, config, {
accessService,
resetTokenService,
emailService,
sessionService,
settingService,
});
userStore = stores.userStore;
const rootRoles = await accessService.getRootRoles();
@ -93,6 +97,22 @@ test('should create user with password', async () => {
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 () => {
const email = 'some@test.com';
await userService.createUser({

View File

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