1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-05-31 01:16:01 +02:00

feat: Add username/password authentication (#777)

This commit is contained in:
Ivar Conradi Østhus 2021-04-09 13:46:53 +02:00 committed by GitHub
parent b7b19de442
commit 9bd425c193
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 1755 additions and 127 deletions

View File

@ -67,6 +67,7 @@
"dependencies": {
"async": "^3.1.0",
"basic-auth": "^2.0.1",
"bcrypt": "^5.0.1",
"compression": "^1.7.3",
"connect-session-knex": "^2.0.0",
"cookie-parser": "^1.4.4",
@ -92,13 +93,14 @@
"mustache": "^4.1.0",
"node-fetch": "^2.6.1",
"nodemailer": "^6.5.0",
"owasp-password-strength-test": "^1.3.0",
"parse-database-url": "^0.3.0",
"pg": "^8.0.3",
"pkginfo": "^0.4.1",
"prom-client": "^13.1.0",
"response-time": "^2.3.2",
"serve-favicon": "^2.5.0",
"unleash-frontend": "4.0.0-alpha.1",
"unleash-frontend": "4.0.0-alpha.2",
"uuid": "^8.3.2",
"yargs": "^16.0.3"
},
@ -111,6 +113,8 @@
"@types/nodemailer": "^6.4.1",
"@typescript-eslint/eslint-plugin": "^4.15.2",
"@typescript-eslint/parser": "^4.15.2",
"@types/bcrypt": "^3.0.0",
"@types/owasp-password-strength-test": "^1.3.0",
"ava": "^3.7.0",
"copyfiles": "^2.4.1",
"coveralls": "^3.1.0",

View File

@ -15,6 +15,7 @@ const unleashDbSession = require('./middleware/session-db');
const requestLogger = require('./middleware/request-logger');
const simpleAuthentication = require('./middleware/simple-authentication');
const ossAuthentication = require('./middleware/oss-authentication');
const noAuthentication = require('./middleware/no-authentication');
const secureHeaders = require('./middleware/secure-headers');
@ -57,7 +58,12 @@ module.exports = function(config, services = {}) {
// Deprecated. Will go away in v4.
if (config.adminAuthentication === AuthenticationType.unsecure) {
app.use(baseUriPath, apiTokenMiddleware(config, services));
simpleAuthentication(baseUriPath, app);
simpleAuthentication(app, config, services);
}
if (config.adminAuthentication === AuthenticationType.openSource) {
app.use(baseUriPath, apiTokenMiddleware(config, services));
ossAuthentication(app, config, services);
}
if (config.adminAuthentication === AuthenticationType.enterprise) {

View File

@ -22,6 +22,11 @@ export interface IRole {
project?: string;
}
export interface IUserRole {
roleId: number;
userId: number;
}
export class AccessStore {
private logger: Function;
@ -82,6 +87,13 @@ export class AccessStore {
.andWhere('type', 'project');
}
async getRootRoles(): Promise<IRole[]> {
return this.db
.select(['id', 'name', 'type', 'project', 'description'])
.from<IRole>(T.ROLES)
.andWhere('type', 'root');
}
async removeRolesForProject(projectId: string): Promise<void> {
return this.db(T.ROLES)
.where({
@ -122,6 +134,20 @@ export class AccessStore {
.delete();
}
async removeRolesOfTypeForUser(
userId: number,
roleType: string,
): Promise<void> {
const rolesToRemove = this.db(T.ROLES)
.select('id')
.where({ type: roleType });
return this.db(T.ROLE_USER)
.where({ user_id: userId })
.whereIn('role_id', rolesToRemove)
.delete();
}
async createRole(
name: string,
type: string,
@ -160,4 +186,18 @@ export class AccessStore {
})
.delete();
}
async getRootRoleForAllUsers(): Promise<IUserRole[]> {
const rows = await this.db
.select('id', 'user_id')
.distinctOn('user_id')
.from(`${T.ROLES} AS r`)
.leftJoin(`${T.ROLE_USER} AS ru`, 'r.id', 'ru.role_id')
.where('r.type', '=', 'root');
return rows.map(row => ({
roleId: +row.id,
userId: +row.user_id,
}));
}
}

View File

@ -1,9 +1,10 @@
/* eslint camelcase: "off" */
'use strict';
import { Knex } from 'knex';
import { Logger, LogProvider } from '../logger';
import User from '../user';
const NotFoundError = require('../error/notfound-error');
const User = require('../user');
const TABLE = 'users';
@ -13,7 +14,7 @@ const USER_COLUMNS = [
'username',
'email',
'image_url',
'permissions',
'permissions', // TODO: remove in v4
'login_attempts',
'seen_at',
'created_at',
@ -53,27 +54,57 @@ const rowToUser = row => {
});
};
export interface IUserLookup {
id?: number;
username?: string;
email?: string;
}
export interface IUserSearch {
name?: string;
username?: string;
email: string;
}
export interface IUserUpdateFields {
name?: string;
email?: string;
}
class UserStore {
constructor(db, getLogger) {
private db: Knex;
private logger: Logger;
constructor(db: Knex, getLogger: LogProvider) {
this.db = db;
this.logger = getLogger('user-store.js');
}
async update(id, user) {
async update(id: number, fields: IUserUpdateFields): Promise<User> {
await this.db(TABLE)
.where('id', id)
.update(mapUserToColumns(user));
.update(mapUserToColumns(fields));
return this.get({ id });
}
async insert(user) {
const [id] = await this.db(TABLE)
async insert(user: User): Promise<User> {
const rows = await this.db(TABLE)
.insert(mapUserToColumns(user))
.returning('id');
return this.get({ id });
.returning(USER_COLUMNS);
return rowToUser(rows[0]);
}
buildSelectUser(q) {
async upsert(user: User): Promise<User> {
const id = await this.hasUser(user);
if (id) {
return this.update(id, user);
}
return this.insert(user);
}
buildSelectUser(q: IUserLookup): any {
const query = this.db(TABLE);
if (q.id) {
return query.where('id', q.id);
@ -87,27 +118,18 @@ class UserStore {
throw new Error('Can only find users with id, username or email.');
}
async hasUser(idQuery) {
async hasUser(idQuery: IUserLookup): Promise<number | undefined> {
const query = this.buildSelectUser(idQuery);
const item = await query.first('id');
return item ? item.id : undefined;
}
async upsert(user) {
const id = await this.hasUser(user);
if (id) {
return this.update(id, user);
}
return this.insert(user);
}
async getAll() {
async getAll(): Promise<User[]> {
const users = await this.db.select(USER_COLUMNS).from(TABLE);
return users.map(rowToUser);
}
async search(query) {
async search(query: IUserSearch): Promise<User[]> {
const users = await this.db
.select(USER_COLUMNS_PUBLIC)
.from(TABLE)
@ -117,7 +139,7 @@ class UserStore {
return users.map(rowToUser);
}
async getAllWithId(userIdList) {
async getAllWithId(userIdList: number[]): Promise<User[]> {
const users = await this.db
.select(USER_COLUMNS_PUBLIC)
.from(TABLE)
@ -125,18 +147,18 @@ class UserStore {
return users.map(rowToUser);
}
async get(idQuery) {
async get(idQuery: IUserLookup): Promise<User> {
const row = await this.buildSelectUser(idQuery).first(USER_COLUMNS);
return rowToUser(row);
}
async delete(id) {
async delete(id: number): Promise<void> {
return this.db(TABLE)
.where({ id })
.del();
}
async getPasswordHash(userId) {
async getPasswordHash(userId: number): Promise<string> {
const item = await this.db(TABLE)
.where('id', userId)
.first('password_hash');
@ -148,7 +170,7 @@ class UserStore {
return item.password_hash;
}
async setPasswordHash(userId, passwordHash) {
async setPasswordHash(userId: number, passwordHash: string): Promise<void> {
return this.db(TABLE)
.where('id', userId)
.update({
@ -156,13 +178,11 @@ class UserStore {
});
}
async incLoginAttempts(user) {
return this.buildSelectUser(user).increment({
login_attempts: 1,
});
async incLoginAttempts(user: User): Promise<void> {
return this.buildSelectUser(user).increment('login_attempts', 1);
}
async succesfullLogin(user) {
async successfullyLogin(user: User): Promise<void> {
return this.buildSelectUser(user).update({
login_attempts: 0,
seen_at: new Date(),
@ -171,3 +191,4 @@ class UserStore {
}
module.exports = UserStore;
export default UserStore;

View File

@ -96,8 +96,10 @@ test('should not add user if disabled', async t => {
const disabledConfig = {
getLogger,
baseUriPath: '',
authentication: {
enableApiToken: false,
createAdminUser: false,
},
};

View File

@ -0,0 +1,32 @@
const AuthenticationRequired = require('../authentication-required');
function ossAuthHook(app, config) {
const { baseUriPath } = config;
const generateAuthResponse = async () => {
return new AuthenticationRequired({
type: 'password',
path: `${baseUriPath}/auth/simple/login`,
message: 'You must sign in order to use Unleash',
});
};
app.use(`${baseUriPath}/api`, async (req, res, next) => {
if (req.session && req.session.user) {
req.user = req.session.user;
return next();
}
if (req.user) {
return next();
}
if (req.header('authorization')) {
// API clients should get 401 without body
return res.sendStatus(401);
}
// Admin UI users should get auth-response
const authRequired = await generateAuthResponse();
return res.status(401).json(authRequired);
});
}
module.exports = ossAuthHook;

View File

@ -0,0 +1,58 @@
'use strict';
const test = require('ava');
const supertest = require('supertest');
const { EventEmitter } = require('events');
const store = require('../../test/fixtures/store');
const ossAuth = require('./oss-authentication');
const getApp = require('../app');
const getLogger = require('../../test/fixtures/no-logger');
const { User } = require('../server-impl');
const eventBus = new EventEmitter();
function getSetup(preRouterHook) {
const base = `/random${Math.round(Math.random() * 1000)}`;
const stores = store.createStores();
const app = getApp({
baseUriPath: base,
stores,
eventBus,
getLogger,
preRouterHook(_app) {
preRouterHook(_app);
ossAuth(_app, { baseUriPath: base });
_app.get(`${base}/api/protectedResource`, (req, res) => {
res.status(200)
.json({ message: 'OK' })
.end();
});
},
});
return {
base,
request: supertest(app),
};
}
test('should return 401 when missing user', t => {
t.plan(0);
const { base, request } = getSetup(() => {});
return request.get(`${base}/api/protectedResource`).expect(401);
});
test('should return 200 when user exists', t => {
t.plan(0);
const user = new User({ id: 1, email: 'some@mail.com' });
const { base, request } = getSetup(app =>
app.use((req, res, next) => {
req.user = user;
next();
}),
);
return request.get(`${base}/api/protectedResource`).expect(200);
});

View File

@ -2,10 +2,11 @@ const auth = require('basic-auth');
const User = require('../user');
const AuthenticationRequired = require('../authentication-required');
function insecureAuthentication(basePath = '', app) {
app.post(`${basePath}/api/admin/login`, (req, res) => {
const user = req.body;
req.session.user = new User({ email: user.email });
function insecureAuthentication(app, { basePath = '' }, { userService }) {
app.post(`${basePath}/api/admin/login`, async (req, res) => {
const { email } = req.body;
const user = await userService.loginUserWithoutPassword(email, true);
req.session.user = user;
res.status(200)
.json(req.session.user)
.end();

View File

@ -81,6 +81,7 @@ function defaultOptions() {
enableApiToken: process.env.AUTH_ENABLE_API_TOKEN || true,
type: process.env.AUTH_TYPE || 'open-source',
customHook: () => {},
createAdminUser: true,
},
ui: {},
importFile: process.env.IMPORT_FILE,

View File

@ -14,16 +14,7 @@ import { AccessService } from '../../services/access-service';
import { IAuthRequest } from '../unleash-types';
import { isRbacEnabled } from '../../util/feature-enabled';
import User from '../../user';
interface IExperimentalFlags {
[key: string]: boolean;
}
interface IConfig {
getLogger: LogProvider;
extendedPermissions: boolean;
experimental: IExperimentalFlags;
}
import { IUnleashConfig } from '../../types/core';
interface IServices {
apiTokenService: ApiTokenService;
@ -41,7 +32,7 @@ class ApiTokenController extends Controller {
private logger: Logger;
constructor(config: IConfig, services: IServices) {
constructor(config: IUnleashConfig, services: IServices) {
super(config);
this.apiTokenService = services.apiTokenService;
this.accessService = services.accessService;

View File

@ -16,6 +16,7 @@ const TagTypeController = require('./tag-type');
const AddonController = require('./addon');
const ApiTokenController = require('./api-token-controller');
const EmailController = require('./email');
const UserAdminController = require('./user-admin');
const apiDef = require('./api-def.json');
class AdminApi extends Controller {
@ -65,6 +66,10 @@ class AdminApi extends Controller {
new ApiTokenController(config, services).router,
);
this.app.use('/email', new EmailController(config, services).router);
this.app.use(
'/user-admin',
new UserAdminController(config, services).router,
);
}
index(req, res) {

View File

@ -0,0 +1,125 @@
import Controller from '../controller';
import { ADMIN } from '../../permissions';
import { IUnleashConfig } from '../../types/core';
import UserService from '../../services/user-service';
import { AccessService, RoleName } from '../../services/access-service';
import { Logger } from '../../logger';
class UserAdminController extends Controller {
private userService: UserService;
private accessService: AccessService;
private logger: Logger;
constructor(config: IUnleashConfig, { userService, accessService }) {
super(config);
this.userService = userService;
this.accessService = accessService;
this.logger = config.getLogger('routes/user-controller.js');
this.get('/', this.getUsers);
this.get('/search', this.search);
this.post('/', this.createUser, ADMIN);
this.post('/validate-password', this.validatePassword);
this.put('/:id', this.updateUser, ADMIN);
this.post('/:id/change-password', this.changePassword, ADMIN);
this.delete('/:id', this.deleteUser, ADMIN);
}
async getUsers(req, res) {
try {
const users = await this.userService.getAll();
const rootRoles = await this.accessService.getRootRoles();
res.json({ users, rootRoles });
} catch (error) {
this.logger.error(error);
res.status(500).send({ msg: 'server errors' });
}
}
async search(req, res) {
const { q } = req.query;
try {
const users =
q && q.length > 1 ? await this.userService.search(q) : [];
res.json(users);
} catch (error) {
this.logger.error(error);
res.status(500).send({ msg: 'server errors' });
}
}
async createUser(req, res) {
const { username, email, name, rootRole } = req.body;
try {
const user = await this.userService.createUser({
username,
email,
name,
rootRole: Number(rootRole),
});
res.status(201).send({ ...user, rootRole });
} catch (e) {
this.logger.warn(e.message);
res.status(400).send([{ msg: e.message }]);
}
}
async updateUser(req, res) {
const { id } = req.params;
const { name, email, rootRole } = req.body;
try {
const user = await this.userService.updateUser({
id: Number(id),
name,
email,
rootRole: Number(rootRole),
});
res.status(200).send({ ...user, rootRole });
} catch (e) {
this.logger.warn(e.message);
res.status(400).send([{ msg: e.message }]);
}
}
async deleteUser(req, res) {
const { id } = req.params;
try {
await this.userService.deleteUser(+id);
res.status(200).send();
} catch (error) {
this.logger.warn(error);
res.status(500).send();
}
}
async validatePassword(req, res) {
const { password } = req.body;
try {
this.userService.validatePassword(password);
res.status(200).send();
} catch (e) {
res.status(400).send([{ msg: e.message }]);
}
}
async changePassword(req, res) {
const { id } = req.params;
const { password } = req.body;
try {
await this.userService.changePassword(+id, password);
res.status(200).send();
} catch (e) {
res.status(400).send([{ msg: e.message }]);
}
}
}
module.exports = UserAdminController;

View File

@ -5,6 +5,7 @@ const Controller = require('../controller');
class UserController extends Controller {
constructor(config) {
super(config);
this.get('/', this.getUser);
this.get('/logout', this.logout);
}
@ -25,7 +26,7 @@ class UserController extends Controller {
return res.status(404).end();
}
// Depcreated, use "/logout" instead. Will be removed in later release.
// Deprecated, use "/logout" instead. Will be removed in v4.
logout(req, res) {
if (req.session) {
req.session = null;

View File

@ -0,0 +1,31 @@
const Controller = require('../controller');
class PasswordProvider extends Controller {
constructor({ getLogger }, { userService }) {
super();
this.logger = getLogger('/auth/password-provider.js');
this.userService = userService;
this.post('/login', this.login);
}
async login(req, res) {
const { username, password } = req.body;
if (!username || !password) {
return res.status(400).json({
message: 'You must provide username and password',
});
}
try {
const user = await this.userService.loginUser(username, password);
req.session.user = user;
return res.status(200).json(user);
} catch (e) {
return res.status(401).json({ message: e.message });
}
}
}
module.exports = PasswordProvider;

View File

@ -0,0 +1,83 @@
const test = require('ava');
const request = require('supertest');
const express = require('express');
const User = require('../../user');
const PasswordProvider = require('./simple-password-provider');
const getLogger = () => ({ info: () => {}, error: () => {} });
test('Should require password', async t => {
const app = express();
app.use(express.json());
const userService = () => {};
const ctr = new PasswordProvider({ getLogger }, { userService });
app.use('/auth/simple', ctr.router);
const res = await request(app)
.post('/auth/simple/login')
.send({ name: 'john' });
t.is(400, res.status);
});
test('Should login user', async t => {
const username = 'ola';
const password = 'simplepass';
const user = new User({ username, permissions: ['ADMIN'] });
const app = express();
app.use(express.json());
app.use((req, res, next) => {
req.session = {};
next();
});
const userService = {
loginUser: (u, p) => {
if (u === username && p === password) {
return user;
}
throw new Error('Wrong password');
},
};
const ctr = new PasswordProvider({ getLogger }, { userService });
app.use('/auth/simple', ctr.router);
const res = await request(app)
.post('/auth/simple/login')
.send({ username, password });
t.is(200, res.status);
t.is(user.username, res.body.username);
});
test('Should not login user with wrong password', async t => {
const username = 'ola';
const password = 'simplepass';
const user = new User({ username, permissions: ['ADMIN'] });
const app = express();
app.use(express.json());
app.use((req, res, next) => {
req.session = {};
next();
});
const userService = {
loginUser: (u, p) => {
if (u === username && p === password) {
return user;
}
throw new Error('Wrong password');
},
};
const ctr = new PasswordProvider({ getLogger }, { userService });
app.use('/auth/simple', ctr.router);
const res = await request(app)
.post('/auth/simple/login')
.send({ username, password: 'not-correct' });
t.is(401, res.status);
});

View File

@ -8,6 +8,7 @@ const Controller = require('./controller');
const HealthCheckController = require('./health-check');
const LogoutController = require('./logout');
const api = require('./api-def');
const SimplePasswordProvider = require('./auth/simple-password-provider');
class IndexRouter extends Controller {
constructor(config, services) {
@ -15,6 +16,10 @@ class IndexRouter extends Controller {
this.use('/health', new HealthCheckController(config).router);
this.use('/internal-backstage', new BackstageController(config).router);
this.use('/logout', new LogoutController(config).router);
this.use(
'/auth/simple',
new SimplePasswordProvider(config, services).router,
);
this.get(api.uri, this.index);
this.use(api.links.admin.uri, new AdminApi(config, services).router);
this.use(api.links.client.uri, new ClientApi(config, services).router);

View File

@ -1,4 +1,4 @@
import { AccessStore, IRole, IUserPermission } from '../db/access-store';
import { AccessStore, IRole, IUserPermission, IUserRole } from '../db/access-store';
import p from '../permissions';
import User from '../user';
@ -37,13 +37,13 @@ export interface IUserWithRole {
imageUrl?: string;
}
interface IRoleData {
export interface IRoleData {
role: IRole;
users: User[];
permissions: IUserPermission[];
}
interface IPermission {
export interface IPermission {
name: string;
type: PermissionType;
}
@ -64,6 +64,11 @@ export enum RoleType {
PROJECT = 'project',
}
export interface IRoleIdentifier {
roleId?: number;
roleName?: RoleName;
}
export class AccessService {
public RoleName = RoleName;
private store: AccessStore;
@ -101,6 +106,10 @@ export class AccessService {
.some(p => p.permission === permission || p.permission === ADMIN);
}
async getPermissionsForUser(user: User) {
return this.store.getPermissionsForUser(user.id);
}
getPermissions(): IPermission[] {
return this.permissions;
}
@ -109,22 +118,27 @@ export class AccessService {
return this.store.addUserToRole(userId, roleId);
}
async setUserRootRole(userId: number, roleName: RoleName ) {
const userRoles = await this.store.getRolesForUserId(userId);
const currentRootRoles = userRoles.filter(r => r.type === RoleType.ROOT);
const roles = await this.getRoles();
const role = roles.find(r => r.type === RoleType.ROOT && r.name === roleName);
if(role) {
async setUserRootRole(userId: number, roleId: number) {
const roles = await this.getRootRoles();
const newRootRole = roles.find(r => r.id === roleId);
if(newRootRole) {
try {
await Promise.all(currentRootRoles.map(r => this.store.removeUserFromRole(userId, r.id)));
await this.store.addUserToRole(userId, role.id);
await this.store.removeRolesOfTypeForUser(userId, RoleType.ROOT);
await this.store.addUserToRole(userId, newRootRole.id);
} catch (error) {
this.logger.warn('Could not add role=${roleName} to userId=${userId}');
throw new Error('Could not add role=${roleName} to userId=${userId}');
}
} else {
throw new Error(`Could not find rootRole with id=${roleId}`);
}
}
async getUserRootRoles(userId: number) {
const userRoles = await this.store.getRolesForUserId(userId);
return userRoles.filter(r => r.type === RoleType.ROOT);
}
async removeUserFromRole(userId: number, roleId: number) {
return this.store.removeUserFromRole(userId, roleId);
}
@ -220,4 +234,17 @@ export class AccessService {
this.logger.info(`Removing project roles for ${projectId}`);
return this.store.removeRolesForProject(projectId);
}
async getRootRoleForAllUsers(): Promise<IUserRole[]> {
return this.store.getRootRoleForAllUsers();
}
async getRootRoles(): Promise<IRole[]> {
return this.store.getRootRoles();
}
async getRootRole(roleName: RoleName): Promise<IRole> {
const roles = await this.store.getRootRoles();
return roles.find(r => r.name === roleName);
}
}

View File

@ -11,6 +11,7 @@ const VersionService = require('./version-service');
const { EmailService } = require('./email-service');
const { AccessService } = require('./access-service');
const { ApiTokenService } = require('./api-token-service');
const UserService = require('./user-service');
module.exports.createServices = (stores, config) => {
const accessService = new AccessService(stores, config);
@ -30,6 +31,7 @@ module.exports.createServices = (stores, config) => {
const versionService = new VersionService(stores, config);
const apiTokenService = new ApiTokenService(stores, config);
const emailService = new EmailService(config.email, config.getLogger);
const userService = new UserService(stores, config, accessService);
return {
accessService,
@ -45,5 +47,6 @@ module.exports.createServices = (stores, config) => {
versionService,
apiTokenService,
emailService,
userService,
};
};

View File

@ -0,0 +1,102 @@
import test from 'ava';
import UserService from './user-service';
import UserStoreMock from '../../test/fixtures/fake-user-store';
import AccessServiceMock from '../../test/fixtures/access-service-mock';
import noLogger from '../../test/fixtures/no-logger';
import { RoleName } from './access-service';
import { IUnleashConfig } from '../types/core';
const config: IUnleashConfig = {
getLogger: noLogger,
baseUriPath: '',
authentication: { enableApiToken: true, createAdminUser: false },
};
test('Should create new user', async t => {
const userStore = new UserStoreMock();
const accessService = new AccessServiceMock();
const service = new UserService({ userStore }, config, accessService);
const user = await service.createUser({
username: 'test',
rootRole: 1,
});
const storedUser = await userStore.get(user);
const allUsers = await userStore.getAll();
t.truthy(user.id);
t.is(user.username, 'test');
t.is(allUsers.length, 1);
t.is(storedUser.username, 'test');
});
test('Should create default user', async t => {
const userStore = new UserStoreMock();
const accessService = new AccessServiceMock();
const service = new UserService({ userStore }, config, accessService);
await service.initAdminUser();
const user = await service.loginUser('admin', 'admin');
t.is(user.username, 'admin');
});
test('Should be a valid password', async t => {
const userStore = new UserStoreMock();
const accessService = new AccessServiceMock();
const service = new UserService({ userStore }, config, accessService);
const valid = service.validatePassword('this is a strong password!');
t.true(valid);
});
test('Password must be at least 10 chars', async t => {
const userStore = new UserStoreMock();
const accessService = new AccessServiceMock();
const service = new UserService({ userStore }, config, accessService);
t.throws(() => service.validatePassword('admin'), {
message: 'The password must be at least 10 characters long.',
});
});
test('The password must contain at least one uppercase letter.', async t => {
const userStore = new UserStoreMock();
const accessService = new AccessServiceMock();
const service = new UserService({ userStore }, config, accessService);
t.throws(() => service.validatePassword('qwertyabcde'), {
message: 'The password must contain at least one uppercase letter.',
});
});
test('The password must contain at least one number', async t => {
const userStore = new UserStoreMock();
const accessService = new AccessServiceMock();
const service = new UserService({ userStore }, config, accessService);
t.throws(() => service.validatePassword('qwertyabcdE'), {
message: 'The password must contain at least one number.',
});
});
test('The password must contain at least one special character', async t => {
const userStore = new UserStoreMock();
const accessService = new AccessServiceMock();
const service = new UserService({ userStore }, config, accessService);
t.throws(() => service.validatePassword('qwertyabcdE2'), {
message: 'The password must contain at least one special character.',
});
});
test('Should be a valid password with special chars', async t => {
const userStore = new UserStoreMock();
const accessService = new AccessServiceMock();
const service = new UserService({ userStore }, config, accessService);
const valid = service.validatePassword('this is a strong password!');
t.true(valid);
});

View File

@ -0,0 +1,237 @@
import assert from 'assert';
import bcrypt from 'bcrypt';
import owasp from 'owasp-password-strength-test';
import Joi from 'joi';
import UserStore, { IUserSearch } from '../db/user-store';
import { Logger } from '../logger';
import { IUnleashConfig } from '../types/core';
import User, { IUser } from '../user';
import isEmail from '../util/is-email';
import { AccessService, RoleName } from './access-service';
import { ADMIN } from '../permissions';
export interface ICreateUser {
name?: string;
email?: string;
username?: string;
password?: string;
rootRole: number;
}
export interface IUpdateUser {
id: number;
name?: string;
email?: string;
rootRole?: number;
}
interface IUserWithRole extends IUser {
rootRole: number;
}
interface IStores {
userStore: UserStore;
}
const saltRounds = 10;
class UserService {
private logger: Logger;
private store: UserStore;
private accessService: AccessService;
constructor(
stores: IStores,
config: IUnleashConfig,
accessService: AccessService,
) {
this.logger = config.getLogger('service/user-service.js');
this.store = stores.userStore;
this.accessService = accessService;
if (config.authentication && config.authentication.createAdminUser) {
process.nextTick(() => this.initAdminUser());
}
}
validatePassword(password: string): boolean {
const result = owasp.test(password);
if (!result.strong) {
throw new Error(result.errors[0]);
} else return true;
}
async initAdminUser(): Promise<void> {
const hasAdminUser = await this.store.hasUser({ username: 'admin' });
if (!hasAdminUser) {
// create default admin user
try {
this.logger.info(
'Creating default user "admin" with password "admin"',
);
const user = await this.store.insert(
new User({
username: 'admin',
permissions: [ADMIN], // TODO: remove in v4
}),
);
const passwordHash = await bcrypt.hash('admin', saltRounds);
await this.store.setPasswordHash(user.id, passwordHash);
const rootRoles = await this.accessService.getRootRoles();
const adminRole = rootRoles.find(
r => r.name === RoleName.ADMIN,
);
await this.accessService.setUserRootRole(user.id, adminRole.id);
} catch (e) {
this.logger.error('Unable to create default user "admin"');
}
}
}
async getAll(): Promise<IUserWithRole[]> {
const users = await this.store.getAll();
const defaultRole = await this.accessService.getRootRole(RoleName.READ);
const userRoles = await this.accessService.getRootRoleForAllUsers();
const usersWithRootRole = users.map(u => {
const rootRole = userRoles.find(r => r.userId === u.id);
const roleId = rootRole ? rootRole.roleId : defaultRole.id;
return { ...u, rootRole: roleId };
});
return usersWithRootRole;
}
async getUser(id: number): Promise<IUserWithRole> {
const roles = await this.accessService.getUserRootRoles(id);
const defaultRole = await this.accessService.getRootRole(RoleName.READ);
const roleId = roles.length > 0 ? roles[0].id : defaultRole.id;
const user = await this.store.get({ id });
return { ...user, rootRole: roleId };
}
async search(query: IUserSearch): Promise<User[]> {
return this.store.search(query);
}
async createUser({
username,
email,
name,
password,
rootRole,
}: ICreateUser): Promise<User> {
assert.ok(username || email, 'You must specify username or email');
if (email) {
Joi.assert(email, Joi.string().email(), 'Email');
}
const exists = await this.store.hasUser({ username, email });
if (exists) {
throw new Error('User already exists');
}
const user = await this.store.insert(
// TODO: remove permission in v4.
new User({ username, email, name, permissions: [ADMIN] }),
);
await this.accessService.setUserRootRole(user.id, rootRole);
if (password) {
const passwordHash = await bcrypt.hash(password, saltRounds);
await this.store.setPasswordHash(user.id, passwordHash);
}
return user;
}
async updateUser({
id,
name,
email,
rootRole,
}: IUpdateUser): Promise<User> {
if (email) {
Joi.assert(email, Joi.string().email(), 'Email');
}
if (rootRole) {
await this.accessService.setUserRootRole(id, rootRole);
}
return this.store.update(id, { name, email });
}
async loginUser(usernameOrEmail: string, password: string): Promise<User> {
const idQuery = isEmail(usernameOrEmail)
? { email: usernameOrEmail }
: { username: usernameOrEmail };
const user = await this.store.get(idQuery);
const passwordHash = await this.store.getPasswordHash(user.id);
const match = await bcrypt.compare(password, passwordHash);
if (match) {
await this.store.successfullyLogin(user);
return user;
}
throw new Error('Wrong password, try again.');
}
/**
* Used to login users without specifying password. Used when integrating
* with external identity providers.
*
* @param usernameOrEmail
* @param autoCreateUser
* @returns
*/
async loginUserWithoutPassword(
email: string,
autoCreateUser: boolean = false,
): Promise<User> {
let user: User;
try {
user = await this.store.get({ email });
} catch (e) {
if (autoCreateUser) {
const defaultRole = await this.accessService.getRootRole(
RoleName.REGULAR,
);
user = await this.createUser({
email,
rootRole: defaultRole.id,
});
} else {
throw e;
}
}
this.store.successfullyLogin(user);
return user;
}
async changePassword(userId: number, password: string): Promise<void> {
this.validatePassword(password);
const passwordHash = await bcrypt.hash(password, saltRounds);
return this.store.setPasswordHash(userId, passwordHash);
}
async deleteUser(userId: number): Promise<void> {
const roles = await this.accessService.getRolesForUser(userId);
await Promise.all(
roles.map(role =>
this.accessService.removeUserFromRole(userId, role.id),
),
);
await this.store.delete(userId);
}
}
module.exports = UserService;
export default UserService;

View File

@ -1,9 +1,17 @@
import { LogProvider } from '../logger';
interface IExperimentalFlags {
[key: string]: boolean;
}
export interface IUnleashConfig {
getLogger: LogProvider;
baseUriPath: string;
extendedPermissions?: boolean;
experimental?: IExperimentalFlags;
authentication: {
enableApiToken: boolean;
createAdminUser: boolean;
};
}

View File

@ -14,7 +14,15 @@ export interface UserData {
createdAt?: Date;
}
export default class User {
export interface IUser {
id: number;
name?: string;
username?: string;
email?: string;
createdAt: Date;
}
export default class User implements IUser {
id: number;
isAPI: boolean;

View File

@ -1,12 +1,6 @@
interface IExperimentalFlags {
[key: string]: boolean;
}
import { IUnleashConfig } from '../types/core';
interface IConfig {
experimental: IExperimentalFlags;
}
export const isRbacEnabled = (config: IConfig): boolean => {
export const isRbacEnabled = (config: IUnleashConfig): boolean => {
return config && config.experimental && config.experimental.rbac;
};

19
src/lib/util/is-email.ts Normal file
View File

@ -0,0 +1,19 @@
// Email address matcher.
// eslint-disable-next-line no-useless-escape
const matcher = /.+\@.+\..+/;
/**
* Loosely validate an email address.
*
* @param {string} string
* @return {boolean}
*/
function isEmail(value: string): boolean {
return matcher.test(value);
}
/*
* Exports.
*/
export default isEmail;

View File

@ -1,5 +1,3 @@
'use strict';
import test from 'ava';
import { setupApp, setupAppWithCustomAuth } from '../../helpers/test-helper';
import dbInit from '../../helpers/database-init';

View File

@ -0,0 +1,221 @@
import test from 'ava';
import { setupApp } from '../../helpers/test-helper';
import dbInit from '../../helpers/database-init';
import getLogger from '../../../fixtures/no-logger';
import User from '../../../../lib/user';
import UserStore from '../../../../lib/db/user-store';
import { AccessStore, IRole } from '../../../../lib/db/access-store';
import { RoleName } from '../../../../lib/services/access-service';
let stores;
let db;
let userStore: UserStore;
let accessStore: AccessStore;
let regularRole: IRole;
let adminRole: IRole;
test.before(async () => {
db = await dbInit('user_admin_api_serial', getLogger);
stores = db.stores;
userStore = stores.userStore;
accessStore = stores.accessStore;
const roles = await accessStore.getRootRoles();
regularRole = roles.find(r => r.name === RoleName.REGULAR);
adminRole = roles.find(r => r.name === RoleName.ADMIN);
});
test.after(async () => {
await db.destroy();
});
test.afterEach.always(async () => {
const users = await userStore.getAll();
const deleteAll = users.map((u: User) => userStore.delete(u.id));
await Promise.all(deleteAll);
});
test.serial('returns empty list of users', async t => {
t.plan(1);
const request = await setupApp(stores);
return request
.get('/api/admin/user-admin')
.expect('Content-Type', /json/)
.expect(200)
.expect(res => {
t.is(res.body.users.length, 0);
});
});
test.serial('creates and returns all users', async t => {
t.plan(2);
const request = await setupApp(stores);
const createUserRequests = [...Array(20).keys()].map(i =>
request
.post('/api/admin/user-admin')
.send({
email: `some${i}@getunleash.ai`,
name: `Some Name ${i}`,
rootRole: regularRole.id,
})
.set('Content-Type', 'application/json'),
);
await Promise.all(createUserRequests);
return request
.get('/api/admin/user-admin')
.expect('Content-Type', /json/)
.expect(200)
.expect(res => {
t.is(res.body.users.length, 20);
t.is(res.body.users[2].rootRole, regularRole.id);
});
});
test.serial('creates regular-user without password', async t => {
t.plan(3);
const request = await setupApp(stores);
return request
.post('/api/admin/user-admin')
.send({
email: 'some@getunelash.ai',
name: 'Some Name',
rootRole: regularRole.id,
})
.set('Content-Type', 'application/json')
.expect(201)
.expect(res => {
t.is(res.body.email, 'some@getunelash.ai');
t.is(res.body.rootRole, regularRole.id);
t.truthy(res.body.id);
});
});
test.serial('creates admin-user with password', async t => {
t.plan(6);
const request = await setupApp(stores);
const { body } = await request
.post('/api/admin/user-admin')
.send({
email: 'some@getunelash.ai',
name: 'Some Name',
password: 'some-strange-pass-123-GH',
rootRole: adminRole.id,
})
.set('Content-Type', 'application/json')
.expect(201);
t.is(body.rootRole, adminRole.id);
const user = await userStore.get({ id: body.id });
t.is(user.email, 'some@getunelash.ai');
t.is(user.name, 'Some Name');
const passwordHash = userStore.getPasswordHash(body.id);
t.truthy(passwordHash);
const roles = await stores.accessStore.getRolesForUserId(body.id);
t.is(roles.length, 1);
t.is(roles[0].name, RoleName.ADMIN);
});
test.serial('requires known root role', async t => {
t.plan(0);
const request = await setupApp(stores);
return request
.post('/api/admin/user-admin')
.send({
email: 'some@getunelash.ai',
name: 'Some Name',
rootRole: 'Unknown',
})
.set('Content-Type', 'application/json')
.expect(400);
});
test.serial('update user name', async t => {
t.plan(3);
const request = await setupApp(stores);
const { body } = await request
.post('/api/admin/user-admin')
.send({
email: 'some@getunelash.ai',
name: 'Some Name',
rootRole: regularRole.id,
})
.set('Content-Type', 'application/json');
return request
.put(`/api/admin/user-admin/${body.id}`)
.send({
name: 'New name',
})
.set('Content-Type', 'application/json')
.expect(200)
.expect(res => {
t.is(res.body.email, 'some@getunelash.ai');
t.is(res.body.name, 'New name');
// t.is(res.body.rootRole, 'Regular');
t.is(res.body.id, body.id);
});
});
test.serial('should delete user', async t => {
t.plan(0);
const user = await userStore.insert(new User({ email: 'some@mail.com' }));
const request = await setupApp(stores);
return request.delete(`/api/admin/user-admin/${user.id}`).expect(200);
});
test.serial('validator should require strong password', async t => {
t.plan(0);
const request = await setupApp(stores);
return request
.post('/api/admin/user-admin/validate-password')
.send({ password: 'simple' })
.expect(400);
});
test.serial('validator should accept strong password', async t => {
t.plan(0);
const request = await setupApp(stores);
return request
.post('/api/admin/user-admin/validate-password')
.send({ password: 'simple123-_ASsad' })
.expect(200);
});
test.serial('should change password', async t => {
t.plan(0);
const user = await userStore.insert(new User({ email: 'some@mail.com' }));
const request = await setupApp(stores);
return request
.post(`/api/admin/user-admin/${user.id}/change-password`)
.send({ password: 'simple123-_ASsad' })
.expect(200);
});
test.serial('should search for users', async t => {
t.plan(2);
await userStore.insert(new User({ email: 'some@mail.com' }));
await userStore.insert(new User({ email: 'another@mail.com' }));
await userStore.insert(new User({ email: 'another2@mail.com' }));
const request = await setupApp(stores);
return request
.get('/api/admin/user-admin/search?q=another')
.expect(200)
.expect(res => {
t.is(res.body.length, 2);
t.true(res.body.some(u => u.email === 'another@mail.com'));
});
});

View File

@ -17,12 +17,13 @@ let accessService;
let regularUser;
let superUser;
let regularRole;
let adminRole;
let readRole;
const createUserWithRegularAccess = async (name, email) => {
const { userStore } = stores;
const user = await userStore.insert(new User({ name, email }));
const roles = await accessService.getRoles();
const regularRole = roles.find(r => r.name === 'Regular');
await accessService.addUserToRole(user.id, regularRole.id);
return user;
};
@ -32,9 +33,7 @@ const createSuperUser = async () => {
const user = await userStore.insert(
new User({ name: 'Alice Admin', email: 'admin@getunleash.io' }),
);
const roles = await accessService.getRoles();
const superRole = roles.find(r => r.name === 'Admin');
await accessService.addUserToRole(user.id, superRole.id);
await accessService.addUserToRole(user.id, adminRole.id);
return user;
};
@ -43,6 +42,11 @@ test.before(async () => {
stores = db.stores;
// projectStore = stores.projectStore;
accessService = new AccessService(stores, { getLogger });
const roles = await accessService.getRootRoles();
regularRole = roles.find(r => r.name === RoleName.REGULAR);
adminRole = roles.find(r => r.name === RoleName.ADMIN);
readRole = roles.find(r => r.name === RoleName.READ);
regularUser = await createUserWithRegularAccess(
'Bob Test',
'bob@getunleash.io',
@ -120,11 +124,6 @@ test.serial('should grant regular CREATE_FEATURE on all projects', async t => {
const { CREATE_FEATURE } = permissions;
const user = regularUser;
const roles = await accessService.getRoles();
const regularRole = roles.find(
r => r.name === 'Regular' && r.type === 'root',
);
await accessService.addPermissionToRole(
regularRole.id,
permissions.CREATE_FEATURE,
@ -137,11 +136,6 @@ test.serial('should grant regular CREATE_FEATURE on all projects', async t => {
});
test.serial('cannot add CREATE_FEATURE without defining project', async t => {
const roles = await accessService.getRoles();
const regularRole = roles.find(
r => r.name === 'Regular' && r.type === 'root',
);
await t.throwsAsync(
async () => {
await accessService.addPermissionToRole(
@ -159,11 +153,6 @@ test.serial('cannot add CREATE_FEATURE without defining project', async t => {
test.serial(
'cannot remove CREATE_FEATURE without defining project',
async t => {
const roles = await accessService.getRoles();
const regularRole = roles.find(
r => r.name === 'Regular' && r.type === 'root',
);
await t.throwsAsync(
async () => {
await accessService.removePermissionFromRole(
@ -184,11 +173,6 @@ test.serial('should remove CREATE_FEATURE on all projects', async t => {
const { CREATE_FEATURE } = permissions;
const user = regularUser;
const roles = await accessService.getRoles();
const regularRole = roles.find(
r => r.name === 'Regular' && r.type === 'root',
);
await accessService.addPermissionToRole(
regularRole.id,
permissions.CREATE_FEATURE,
@ -272,10 +256,10 @@ test.serial('should grant user access to project', async t => {
const roles = await accessService.getRolesForProject(project);
const regularRole = roles.find(
const projectRole = roles.find(
r => r.name === 'Regular' && r.project === project,
);
await accessService.addUserToRole(sUser.id, regularRole.id);
await accessService.addUserToRole(sUser.id, projectRole.id);
// Should be able to update feature toggles inside the project
t.true(await accessService.hasPermission(sUser, CREATE_FEATURE, project));
@ -299,10 +283,10 @@ test.serial('should not get access if not specifying project', async t => {
const roles = await accessService.getRolesForProject(project);
const regularRole = roles.find(
const projectRole = roles.find(
r => r.name === 'Regular' && r.project === project,
);
await accessService.addUserToRole(sUser.id, regularRole.id);
await accessService.addUserToRole(sUser.id, projectRole.id);
// Should not be able to update feature toggles outside project
t.false(await accessService.hasPermission(sUser, CREATE_FEATURE));
@ -316,8 +300,6 @@ test.serial('should remove user from role', async t => {
new User({ name: 'Some User', email: 'random123@getunleash.io' }),
);
const roles = await accessService.getRoles();
const regularRole = roles.find(r => r.name === 'Regular');
await accessService.addUserToRole(user.id, regularRole.id);
// check user has one role
@ -336,8 +318,6 @@ test.serial('should return role with users', async t => {
new User({ name: 'Some User', email: 'random2223@getunleash.io' }),
);
const roles = await accessService.getRoles();
const regularRole = roles.find(r => r.name === 'Regular');
await accessService.addUserToRole(user.id, regularRole.id);
const roleWithUsers = await accessService.getRole(regularRole.id);
@ -354,8 +334,6 @@ test.serial('should return role with permissions and users', async t => {
new User({ name: 'Some User', email: 'random2244@getunleash.io' }),
);
const roles = await accessService.getRoles();
const regularRole = roles.find(r => r.name === 'Regular');
await accessService.addUserToRole(user.id, regularRole.id);
const roleWithPermission = await accessService.getRole(regularRole.id);
@ -397,7 +375,7 @@ test.serial('should set root role for user', async t => {
new User({ name: 'Some User', email: 'random2255@getunleash.io' }),
);
await accessService.setUserRootRole(user.id, RoleName.REGULAR);
await accessService.setUserRootRole(user.id, regularRole.id);
const roles = await accessService.getRolesForUser(user.id);
@ -411,8 +389,8 @@ test.serial('should switch root role for user', async t => {
new User({ name: 'Some User', email: 'random22Read@getunleash.io' }),
);
await accessService.setUserRootRole(user.id, RoleName.REGULAR);
await accessService.setUserRootRole(user.id, RoleName.READ);
await accessService.setUserRootRole(user.id, regularRole.id);
await accessService.setUserRootRole(user.id, readRole.id);
const roles = await accessService.getRolesForUser(user.id);

View File

@ -9,7 +9,7 @@ let stores;
let apiTokenService: ApiTokenService;
test.before(async () => {
db = await dbInit('api_tokens_serial', getLogger);
db = await dbInit('api_token_service_serial', getLogger);
stores = db.stores;
// projectStore = stores.projectStore;
apiTokenService = new ApiTokenService(stores, {

View File

@ -0,0 +1,93 @@
import test from 'ava';
import dbInit from '../helpers/database-init';
import getLogger from '../../fixtures/no-logger';
import UserService from '../../../lib/services/user-service';
import { AccessService, RoleName } from '../../../lib/services/access-service';
import UserStore from '../../../lib/db/user-store';
import User from '../../../lib/user';
import { IUnleashConfig } from '../../../lib/types/core';
import { IRole } from '../../../lib/db/access-store';
let db;
let stores;
let userService: UserService;
let userStore: UserStore;
let adminRole: IRole;
test.before(async () => {
db = await dbInit('user_service_serial', getLogger);
stores = db.stores;
const config: IUnleashConfig = {
getLogger,
baseUriPath: '/test',
authentication: {
enableApiToken: false,
createAdminUser: false,
},
};
const accessService = new AccessService(stores, config);
userService = new UserService(stores, config, accessService);
userStore = stores.userStore;
const rootRoles = await accessService.getRootRoles();
adminRole = rootRoles.find(r => r.name === RoleName.ADMIN);
});
test.after(async () => {
await db.destroy();
});
test.afterEach(async () => {
const users = await userStore.getAll();
const deleteAll = users.map((u: User) => userStore.delete(u.id));
await Promise.all(deleteAll);
});
test.serial('should create initial admin user', async t => {
await userService.initAdminUser();
await t.notThrowsAsync(userService.loginUser('admin', 'admin'));
await t.throwsAsync(userService.loginUser('admin', 'wrong-password'));
});
test.serial('should not be allowed to create existing user', async t => {
await userStore.insert(new User({ username: 'test', name: 'Hans Mola' }));
await t.throwsAsync(
userService.createUser({ username: 'test', rootRole: adminRole.id }),
);
});
test.serial('should create user with password', async t => {
await userService.createUser({
username: 'test',
password: 'A very strange P4ssw0rd_',
rootRole: adminRole.id,
});
const user = await userService.loginUser(
'test',
'A very strange P4ssw0rd_',
);
t.is(user.username, 'test');
});
test.serial('should login for user _without_ password', async t => {
const email = 'some@test.com';
await userService.createUser({
email,
password: 'A very strange P4ssw0rd_',
rootRole: adminRole.id,
});
const user = await userService.loginUserWithoutPassword(email);
t.is(user.email, email);
});
test.serial('should get user with root role', async t => {
const email = 'some@test.com';
const u = await userService.createUser({
email,
password: 'A very strange P4ssw0rd_',
rootRole: adminRole.id,
});
const user = await userService.getUser(u.id);
t.is(user.email, email);
t.is(user.id, u.id);
t.is(user.rootRole, adminRole.id);
});

View File

@ -127,14 +127,14 @@ test.serial('should reset user after successful login', async t => {
await store.incLoginAttempts(user);
await store.incLoginAttempts(user);
await store.succesfullLogin(user);
await store.successfullyLogin(user);
const storedUser = await store.get(user);
t.is(storedUser.loginAttempts, 0);
t.true(storedUser.seenAt >= user.seenAt);
});
test.serial('should store and get permsissions', async t => {
test.serial('should store and get permissions', async t => {
const store = stores.userStore;
const email = 'userWithPermissions@mail.com';
const user = new User({

View File

@ -0,0 +1,99 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
/* eslint-disable @typescript-eslint/no-unused-vars */
import { IRole } from '../../lib/db/access-store';
import {
AccessService,
IUserWithRole,
RoleName,
IPermission,
IRoleData,
} from '../../lib/services/access-service';
import User from '../../lib/user';
import noLoggerProvider from './no-logger';
class AccessServiceMock extends AccessService {
public roleName: RoleName;
constructor() {
super(
{ accessStore: undefined, userStore: undefined },
{ getLogger: noLoggerProvider },
);
}
hasPermission(
user: User,
permission: string,
projectId?: string,
): Promise<boolean> {
throw new Error('Method not implemented.');
}
getPermissions(): IPermission[] {
throw new Error('Method not implemented.');
}
addUserToRole(userId: number, roleId: number): Promise<void> {
throw new Error('Method not implemented.');
}
setUserRootRole(userId: number, roleId: number): Promise<void> {
return Promise.resolve();
}
removeUserFromRole(userId: number, roleId: number): Promise<void> {
throw new Error('Method not implemented.');
}
addPermissionToRole(
roleId: number,
permission: string,
projectId?: string,
): Promise<void> {
throw new Error('Method not implemented.');
}
removePermissionFromRole(
roleId: number,
permission: string,
projectId?: string,
): Promise<void> {
throw new Error('Method not implemented.');
}
getRoles(): Promise<IRole[]> {
throw new Error('Method not implemented.');
}
getRole(roleId: number): Promise<IRoleData> {
throw new Error('Method not implemented.');
}
getRolesForProject(projectId: string): Promise<IRole[]> {
throw new Error('Method not implemented.');
}
getRolesForUser(userId: number): Promise<IRole[]> {
throw new Error('Method not implemented.');
}
getUsersForRole(roleId: any): Promise<User[]> {
throw new Error('Method not implemented.');
}
getProjectRoleUsers(
projectId: string,
): Promise<[IRole[], IUserWithRole[]]> {
throw new Error('Method not implemented.');
}
createDefaultProjectRoles(owner: User, projectId: string): Promise<void> {
throw new Error('Method not implemented.');
}
removeDefaultProjectRoles(owner: User, projectId: string): Promise<void> {
throw new Error('Method not implemented.');
}
}
export default AccessServiceMock;

87
src/test/fixtures/fake-access-store.ts vendored Normal file
View File

@ -0,0 +1,87 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import {
AccessStore,
IRole,
IUserRole,
IUserPermission,
} from '../../lib/db/access-store';
import noLoggerProvider from './no-logger';
class AccessStoreMock extends AccessStore {
constructor() {
super(undefined, undefined, noLoggerProvider);
}
getPermissionsForUser(userId: Number): Promise<IUserPermission[]> {
throw new Error('Method not implemented.');
}
getPermissionsForRole(roleId: number): Promise<IUserPermission[]> {
throw new Error('Method not implemented.');
}
getRoles(): Promise<IRole[]> {
return Promise.resolve([]);
}
getRoleWithId(id: number): Promise<IRole> {
throw new Error('Method not implemented.');
}
getRolesForProject(projectId: string): Promise<IRole[]> {
throw new Error('Method not implemented.');
}
removeRolesForProject(projectId: string): Promise<void> {
throw new Error('Method not implemented.');
}
getRolesForUserId(userId: number): Promise<IRole[]> {
return Promise.resolve([]);
}
getUserIdsForRole(roleId: number): Promise<IRole[]> {
throw new Error('Method not implemented.');
}
addUserToRole(userId: number, roleId: number): Promise<void> {
throw new Error('Method not implemented.');
}
removeUserFromRole(userId: number, roleId: number): Promise<void> {
throw new Error('Method not implemented.');
}
createRole(
name: string,
type: string,
project?: string,
description?: string,
): Promise<IRole> {
throw new Error('Method not implemented.');
}
addPermissionsToRole(
role_id: number,
permissions: string[],
projectId?: string,
): Promise<void> {
throw new Error('Method not implemented.');
}
removePermissionFromRole(
roleId: number,
permission: string,
projectId?: string,
): Promise<void> {
throw new Error('Method not implemented.');
}
getRootRoleForAllUsers(): Promise<IUserRole[]> {
throw new Error('Method not implemented.');
}
}
module.exports = AccessStoreMock;
export default AccessStoreMock;

106
src/test/fixtures/fake-user-store.ts vendored Normal file
View File

@ -0,0 +1,106 @@
import UserStore, { IUserLookup } from '../../lib/db/user-store';
import User from '../../lib/user';
import noLoggerProvider from './no-logger';
class UserStoreMock extends UserStore {
data: any[];
idSeq: number;
constructor() {
super(undefined, noLoggerProvider);
this.idSeq = 1;
this.data = [];
}
async hasUser({
id,
username,
email,
}: IUserLookup): Promise<number | undefined> {
const user = this.data.find(i => {
if (id && i.id === id) return true;
if (username && i.username === username) return true;
if (email && i.email === email) return true;
return false;
});
return user;
}
async insert(user: User): Promise<User> {
// eslint-disable-next-line no-param-reassign
user.id = this.idSeq;
this.idSeq += 1;
this.data.push(user);
return Promise.resolve(user);
}
async update(id: number, user: User): Promise<User> {
// eslint-disable-next-line no-param-reassign
this.data = this.data.map(o => {
if (o.id === id) return { ...o, name: user.name };
return o;
});
return Promise.resolve(user);
}
async get({ id, username, email }: IUserLookup): Promise<User> {
const user = this.data.find(i => {
if (i.id && i.id === id) return true;
if (i.username && i.username === username) return true;
if (i.email && i.email === email) return true;
return false;
});
if (user) {
return user;
}
throw new Error('Could not find user');
}
async getAll(): Promise<User[]> {
return Promise.resolve(this.data);
}
async setPasswordHash(userId: number, passwordHash: string): Promise<void> {
const u = this.data.find(a => a.id === userId);
u.passwordHash = passwordHash;
return Promise.resolve();
}
async getPasswordHash(id: number): Promise<string> {
const u = this.data.find(i => i.id === id);
return Promise.resolve(u.passwordHash);
}
async delete(id: number): Promise<void> {
this.data = this.data.filter(item => item.id !== id);
return Promise.resolve();
}
async successfullyLogin(user: User): Promise<void> {
const u = this.data.find(i => i.id === user.id);
u.login_attempts = 0;
u.seen_at = new Date();
return Promise.resolve();
}
buildSelectUser(): any {
throw new Error('Not implemented');
}
async search(): Promise<User[]> {
throw new Error('Not implemented');
}
async getAllWithId(): Promise<User[]> {
throw new Error('Not implemented');
}
async incLoginAttempts(): Promise<void> {
throw new Error('Not implemented');
}
}
module.exports = UserStoreMock;
export default UserStoreMock;

View File

@ -12,6 +12,8 @@ const contextFieldStore = require('./fake-context-store');
const settingStore = require('./fake-setting-store');
const addonStore = require('./fake-addon-store');
const projectStore = require('./fake-project-store');
const UserStore = require('./fake-user-store');
const AccessStore = require('./fake-access-store');
module.exports = {
createStores: (databaseIsUp = true) => {
@ -35,6 +37,8 @@ module.exports = {
settingStore: settingStore(databaseIsUp),
addonStore: addonStore(databaseIsUp),
projectStore: projectStore(databaseIsUp),
userStore: new UserStore(),
accessStore: new AccessStore(),
};
},
};

260
yarn.lock
View File

@ -390,6 +390,21 @@
resolved "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.2.tgz"
integrity sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw==
"@mapbox/node-pre-gyp@^1.0.0":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.3.tgz#c740c23ec1007b9278d4c28f767b6e843a88c3d3"
integrity sha512-9dTIfQW8HVCxLku5QrJ/ysS/b2MdYngs9+/oPrOTLvp3TrggdANYVW2h8FGJGDf0J7MYfp44W+c90cVJx+ASuA==
dependencies:
detect-libc "^1.0.3"
https-proxy-agent "^5.0.0"
make-dir "^3.1.0"
node-fetch "^2.6.1"
nopt "^5.0.0"
npmlog "^4.1.2"
rimraf "^3.0.2"
semver "^7.3.4"
tar "^6.1.0"
"@nodelib/fs.scandir@2.1.3":
version "2.1.3"
resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz"
@ -503,6 +518,11 @@
dependencies:
defer-to-connect "^1.0.1"
"@types/bcrypt@^3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@types/bcrypt/-/bcrypt-3.0.0.tgz#851489a9065a067cb7f3c9cbe4ce9bed8bba0876"
integrity sha512-nohgNyv+1ViVcubKBh0+XiNJ3dO8nYu///9aJ4cgSqv70gBL+94SNy/iC2NLzKPT2Zt/QavrOkBVbZRLZmw6NQ==
"@types/body-parser@*":
version "1.19.0"
resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.0.tgz#0685b3c47eb3006ffed117cdd55164b61f80538f"
@ -587,6 +607,11 @@
resolved "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz"
integrity sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==
"@types/owasp-password-strength-test@^1.3.0":
version "1.3.0"
resolved "https://registry.yarnpkg.com/@types/owasp-password-strength-test/-/owasp-password-strength-test-1.3.0.tgz#f639e38847eeb0db14bf7b70896cecd4342ac571"
integrity sha512-eKYl6svyRua5OVUFm+AXSxdBrKo7snzrCcFv0KoqKNvNgB3fJzRq3s/xphf+jNTllqYxgsx1uWLeAcL4MjLWQQ==
"@types/parse-json@^4.0.0":
version "4.0.0"
resolved "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz"
@ -680,6 +705,11 @@
"@typescript-eslint/types" "4.15.2"
eslint-visitor-keys "^2.0.0"
abbrev@1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==
abort-controller@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz"
@ -710,6 +740,13 @@ acorn@^7.1.1, acorn@^7.3.1:
resolved "https://registry.npmjs.org/acorn/-/acorn-7.4.0.tgz"
integrity sha512-+G7P8jJmCHr+S+cLfQxygbWhXy+8YTVGzAkpEbcLo2mLoL7tij/VG41QSHACSf5QgYRhMZYHuNc6drJaO0Da+w==
agent-base@6:
version "6.0.2"
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77"
integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==
dependencies:
debug "4"
agent-base@^4.3.0:
version "4.3.0"
resolved "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz"
@ -754,6 +791,11 @@ ansi-escapes@^4.2.1, ansi-escapes@^4.3.0:
dependencies:
type-fest "^0.11.0"
ansi-regex@^2.0.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"
integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8=
ansi-regex@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz"
@ -804,11 +846,24 @@ append-transform@^2.0.0:
dependencies:
default-require-extensions "^3.0.0"
aproba@^1.0.3:
version "1.2.0"
resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a"
integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==
archy@^1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz"
integrity sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=
are-we-there-yet@~1.1.2:
version "1.1.5"
resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21"
integrity sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==
dependencies:
delegates "^1.0.0"
readable-stream "^2.0.6"
arg@^4.1.0:
version "4.1.3"
resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089"
@ -1058,6 +1113,14 @@ bcrypt-pbkdf@^1.0.0:
dependencies:
tweetnacl "^0.14.3"
bcrypt@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/bcrypt/-/bcrypt-5.0.1.tgz#f1a2c20f208e2ccdceea4433df0c8b2c54ecdf71"
integrity sha512-9BTgmrhZM2t1bNuDtrtIMVSmmxZBrJ71n8Wg+YgdjHuIWYF7SjjmCPZFB+/5i/o/PIeRpwVJR3P+NrpIItUjqw==
dependencies:
"@mapbox/node-pre-gyp" "^1.0.0"
node-addon-api "^3.1.0"
bignumber.js@^9.0.0:
version "9.0.1"
resolved "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.1.tgz"
@ -1283,6 +1346,11 @@ chokidar@^3.4.1:
optionalDependencies:
fsevents "~2.1.2"
chownr@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece"
integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==
chunkd@^2.0.1:
version "2.0.1"
resolved "https://registry.npmjs.org/chunkd/-/chunkd-2.0.1.tgz"
@ -1394,6 +1462,11 @@ code-excerpt@^3.0.0:
dependencies:
convert-to-spaces "^1.0.1"
code-point-at@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=
collection-visit@^1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz"
@ -1562,6 +1635,11 @@ connect-session-knex@^2.0.0:
bluebird "^3.7.2"
knex "^0.21.5"
console-control-strings@^1.0.0, console-control-strings@~1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e"
integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=
contains-path@^0.1.0:
version "0.1.0"
resolved "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz"
@ -1813,7 +1891,7 @@ debug@3.2.6:
dependencies:
ms "^2.1.1"
debug@4.3.1:
debug@4, debug@4.3.1:
version "4.3.1"
resolved "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz"
integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==
@ -1948,6 +2026,11 @@ delayed-stream@~1.0.0:
resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz"
integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk=
delegates@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=
depd@~1.1.0, depd@~1.1.2:
version "1.1.2"
resolved "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz"
@ -1968,6 +2051,11 @@ detect-file@^1.0.0:
resolved "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz"
integrity sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=
detect-libc@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=
dicer@0.2.5:
version "0.2.5"
resolved "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz"
@ -2889,6 +2977,13 @@ fs-extra@^8.1.0:
jsonfile "^4.0.0"
universalify "^0.1.0"
fs-minipass@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb"
integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==
dependencies:
minipass "^3.0.0"
fs.realpath@^1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz"
@ -2909,6 +3004,20 @@ functional-red-black-tree@^1.0.1:
resolved "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz"
integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=
gauge@~2.7.3:
version "2.7.4"
resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7"
integrity sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=
dependencies:
aproba "^1.0.3"
console-control-strings "^1.0.0"
has-unicode "^2.0.0"
object-assign "^4.1.0"
signal-exit "^3.0.0"
string-width "^1.0.1"
strip-ansi "^3.0.1"
wide-align "^1.1.0"
gaxios@^1.0.2, gaxios@^1.0.4, gaxios@^1.2.1, gaxios@^1.2.2:
version "1.8.4"
resolved "https://registry.npmjs.org/gaxios/-/gaxios-1.8.4.tgz"
@ -3193,6 +3302,11 @@ has-symbols@^1.0.0, has-symbols@^1.0.1:
resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz"
integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==
has-unicode@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9"
integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=
has-value@^0.3.1:
version "0.3.1"
resolved "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz"
@ -3299,6 +3413,14 @@ https-proxy-agent@^2.2.1:
agent-base "^4.3.0"
debug "^3.1.0"
https-proxy-agent@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2"
integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==
dependencies:
agent-base "6"
debug "4"
human-signals@^1.1.1:
version "1.1.1"
resolved "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz"
@ -3562,6 +3684,13 @@ is-extglob@^2.1.1:
resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz"
integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=
is-fullwidth-code-point@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb"
integrity sha1-754xOG8DGn8NZDr4L95QxFfvAMs=
dependencies:
number-is-nan "^1.0.0"
is-fullwidth-code-point@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz"
@ -4233,6 +4362,13 @@ lru-cache@^5.0.0:
dependencies:
yallist "^3.0.2"
lru-cache@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==
dependencies:
yallist "^4.0.0"
lru-queue@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/lru-queue/-/lru-queue-0.1.0.tgz#2738bd9f0d3cf4f84490c5736c48699ac632cda3"
@ -4240,7 +4376,7 @@ lru-queue@^0.1.0:
dependencies:
es5-ext "~0.10.2"
make-dir@^3.0.0, make-dir@^3.0.2:
make-dir@^3.0.0, make-dir@^3.0.2, make-dir@^3.1.0:
version "3.1.0"
resolved "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz"
integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==
@ -4420,6 +4556,21 @@ minimist@^1.2.0, minimist@^1.2.5:
resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz"
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
minipass@^3.0.0:
version "3.1.3"
resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.3.tgz#7d42ff1f39635482e15f9cdb53184deebd5815fd"
integrity sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==
dependencies:
yallist "^4.0.0"
minizlib@^2.1.1:
version "2.1.2"
resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931"
integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==
dependencies:
minipass "^3.0.0"
yallist "^4.0.0"
mixin-deep@^1.2.0:
version "1.3.2"
resolved "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz"
@ -4435,7 +4586,7 @@ mkdirp@0.x.x, mkdirp@^0.5.1, mkdirp@~0.5.0:
dependencies:
minimist "^1.2.5"
mkdirp@^1.0.4:
mkdirp@^1.0.3, mkdirp@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
@ -4552,6 +4703,11 @@ nise@^4.0.4:
just-extend "^4.0.2"
path-to-regexp "^1.7.0"
node-addon-api@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.1.0.tgz#98b21931557466c6729e51cb77cd39c965f42239"
integrity sha512-flmrDNB06LIl5lywUz7YlNGZH/5p0M7W28k8hzd9Lshtdh1wshD2Y+U4h9LD6KObOy1f+fEVdgprPrEymjM5uw==
node-cleanup@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/node-cleanup/-/node-cleanup-2.1.2.tgz#7ac19abd297e09a7f72a71545d951b517e4dde2c"
@ -4592,6 +4748,13 @@ noms@0.0.0:
inherits "^2.0.1"
readable-stream "~1.0.31"
nopt@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88"
integrity sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==
dependencies:
abbrev "1"
normalize-package-data@^2.3.2, normalize-package-data@^2.5.0:
version "2.5.0"
resolved "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz"
@ -4619,6 +4782,21 @@ npm-run-path@^4.0.0:
dependencies:
path-key "^3.0.0"
npmlog@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b"
integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==
dependencies:
are-we-there-yet "~1.1.2"
console-control-strings "~1.1.0"
gauge "~2.7.3"
set-blocking "~2.0.0"
number-is-nan@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d"
integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=
nyc@^15.1.0:
version "15.1.0"
resolved "https://registry.npmjs.org/nyc/-/nyc-15.1.0.tgz"
@ -4662,7 +4840,7 @@ oauth@0.9.x:
resolved "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz"
integrity sha1-vR/vr2hslrdUda7VGWQS/2DPucE=
object-assign@^4.1.1:
object-assign@^4.1.0, object-assign@^4.1.1:
version "4.1.1"
resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz"
integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
@ -4819,6 +4997,11 @@ os-tmpdir@~1.0.2:
resolved "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz"
integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=
owasp-password-strength-test@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/owasp-password-strength-test/-/owasp-password-strength-test-1.3.0.tgz#4f629e42903e8f6d279b230d657ab61e58e44b12"
integrity sha1-T2KeQpA+j20nmyMNZXq2HljkSxI=
p-cancelable@^1.0.0:
version "1.1.0"
resolved "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz"
@ -5438,7 +5621,7 @@ readable-stream@1.1.x:
isarray "0.0.1"
string_decoder "~0.10.x"
readable-stream@^2.2.2, readable-stream@~2.3.6:
readable-stream@^2.0.6, readable-stream@^2.2.2, readable-stream@~2.3.6:
version "2.3.7"
resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz"
integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==
@ -5776,6 +5959,13 @@ semver@^7.3.2:
resolved "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz"
integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==
semver@^7.3.4:
version "7.3.5"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7"
integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==
dependencies:
lru-cache "^6.0.0"
send@0.17.1:
version "0.17.1"
resolved "https://registry.npmjs.org/send/-/send-0.17.1.tgz"
@ -5821,7 +6011,7 @@ serve-static@1.14.1:
parseurl "~1.3.3"
send "0.17.1"
set-blocking@^2.0.0:
set-blocking@^2.0.0, set-blocking@~2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz"
integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc=
@ -5865,7 +6055,7 @@ shebang-regex@^3.0.0:
resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz"
integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
signal-exit@^3.0.2:
signal-exit@^3.0.0, signal-exit@^3.0.2:
version "3.0.3"
resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz"
integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==
@ -6129,6 +6319,23 @@ string-argv@^0.1.1:
resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.1.2.tgz#c5b7bc03fb2b11983ba3a72333dd0559e77e4738"
integrity sha512-mBqPGEOMNJKXRo7z0keX0wlAhbBAjilUdPW13nN0PecVryZxdHIeM7TqbsSUA7VYuS00HGC6mojP7DlQzfa9ZA==
string-width@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3"
integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=
dependencies:
code-point-at "^1.0.0"
is-fullwidth-code-point "^1.0.0"
strip-ansi "^3.0.0"
"string-width@^1.0.2 || 2":
version "2.1.1"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==
dependencies:
is-fullwidth-code-point "^2.0.0"
strip-ansi "^4.0.0"
string-width@^3.0.0:
version "3.1.0"
resolved "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz"
@ -6191,6 +6398,13 @@ stringify-object@^3.3.0:
is-obj "^1.0.1"
is-regexp "^1.0.0"
strip-ansi@^3.0.0, strip-ansi@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf"
integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=
dependencies:
ansi-regex "^2.0.0"
strip-ansi@^4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz"
@ -6297,6 +6511,18 @@ table@^5.2.3:
slice-ansi "^2.1.0"
string-width "^3.0.0"
tar@^6.1.0:
version "6.1.0"
resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.0.tgz#d1724e9bcc04b977b18d5c573b333a2207229a83"
integrity sha512-DUCttfhsnLCjwoDoFcI+B2iJgYa93vBnDUATYEeRx6sntCTdN01VnqsIuTlALXla/LWooNg0yEGeB+Y8WdFxGA==
dependencies:
chownr "^2.0.0"
fs-minipass "^2.0.0"
minipass "^3.0.0"
minizlib "^2.1.1"
mkdirp "^1.0.3"
yallist "^4.0.0"
tarn@^3.0.1:
version "3.0.1"
resolved "https://registry.npmjs.org/tarn/-/tarn-3.0.1.tgz"
@ -6615,10 +6841,10 @@ universalify@^0.1.0:
resolved "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz"
integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==
unleash-frontend@4.0.0-alpha.1:
version "4.0.0-alpha.1"
resolved "https://registry.yarnpkg.com/unleash-frontend/-/unleash-frontend-4.0.0-alpha.1.tgz#dff9da220e406a46d70530fcd4fa2ee838c68d22"
integrity sha512-fdUh1b9qN6W8LiBdT9v2V4m5NWq9kn2dZSiZbKScRmkP3yofObKlhVeUN6WYu7gPOGVjNz108DdgF7aEURmBkQ==
unleash-frontend@4.0.0-alpha.2:
version "4.0.0-alpha.2"
resolved "https://registry.yarnpkg.com/unleash-frontend/-/unleash-frontend-4.0.0-alpha.2.tgz#c0db66b038f79adae8189840dce18271b58b5172"
integrity sha512-6JiXrVp5AtldnWxFe2zXDP72n7DHq1dVF6sg6b5gMwhBwLUKlkw4nh27TvGVhwCCFuay82cSwfDrSErsXLXZQg==
unpipe@1.0.0, unpipe@~1.0.0:
version "1.0.0"
@ -6807,6 +7033,13 @@ which@^2.0.1:
dependencies:
isexe "^2.0.0"
wide-align@^1.1.0:
version "1.1.3"
resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457"
integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==
dependencies:
string-width "^1.0.2 || 2"
widest-line@^3.1.0:
version "3.1.0"
resolved "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz"
@ -6902,6 +7135,11 @@ yallist@^3.0.2:
resolved "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz"
integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==
yallist@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
yaml@^1.7.2:
version "1.10.0"
resolved "https://registry.npmjs.org/yaml/-/yaml-1.10.0.tgz"