mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01: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