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:
parent
b7b19de442
commit
9bd425c193
@ -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",
|
||||
|
@ -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) {
|
||||
|
@ -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,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
@ -96,8 +96,10 @@ test('should not add user if disabled', async t => {
|
||||
|
||||
const disabledConfig = {
|
||||
getLogger,
|
||||
baseUriPath: '',
|
||||
authentication: {
|
||||
enableApiToken: false,
|
||||
createAdminUser: false,
|
||||
},
|
||||
};
|
||||
|
||||
|
32
src/lib/middleware/oss-authentication.js
Normal file
32
src/lib/middleware/oss-authentication.js
Normal 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;
|
58
src/lib/middleware/oss-authentication.test.js
Normal file
58
src/lib/middleware/oss-authentication.test.js
Normal 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);
|
||||
});
|
@ -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();
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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) {
|
||||
|
125
src/lib/routes/admin-api/user-admin.ts
Normal file
125
src/lib/routes/admin-api/user-admin.ts
Normal 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;
|
@ -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;
|
||||
|
31
src/lib/routes/auth/simple-password-provider.js
Normal file
31
src/lib/routes/auth/simple-password-provider.js
Normal 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;
|
83
src/lib/routes/auth/simple-password-provider.test.js
Normal file
83
src/lib/routes/auth/simple-password-provider.test.js
Normal 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);
|
||||
});
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
102
src/lib/services/user-service.test.ts
Normal file
102
src/lib/services/user-service.test.ts
Normal 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);
|
||||
});
|
237
src/lib/services/user-service.ts
Normal file
237
src/lib/services/user-service.ts
Normal 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;
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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
19
src/lib/util/is-email.ts
Normal 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;
|
@ -1,5 +1,3 @@
|
||||
'use strict';
|
||||
|
||||
import test from 'ava';
|
||||
import { setupApp, setupAppWithCustomAuth } from '../../helpers/test-helper';
|
||||
import dbInit from '../../helpers/database-init';
|
||||
|
221
src/test/e2e/api/admin/user-admin.e2e.test.ts
Normal file
221
src/test/e2e/api/admin/user-admin.e2e.test.ts
Normal 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'));
|
||||
});
|
||||
});
|
@ -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);
|
||||
|
||||
|
@ -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, {
|
||||
|
93
src/test/e2e/services/user-service.e2e.test.ts
Normal file
93
src/test/e2e/services/user-service.e2e.test.ts
Normal 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);
|
||||
});
|
@ -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({
|
||||
|
99
src/test/fixtures/access-service-mock.ts
vendored
Normal file
99
src/test/fixtures/access-service-mock.ts
vendored
Normal 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
87
src/test/fixtures/fake-access-store.ts
vendored
Normal 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
106
src/test/fixtures/fake-user-store.ts
vendored
Normal 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;
|
4
src/test/fixtures/store.js
vendored
4
src/test/fixtures/store.js
vendored
@ -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
260
yarn.lock
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user