mirror of
https://github.com/Unleash/unleash.git
synced 2025-08-18 13:48:58 +02:00
feat: custom project roles (#1220)
* wip: environment for permissions * fix: add migration for roles * fix: connect environment with access service * feat: add tests * chore: Implement scaffolding for new rbac * fix: add fake store * feat: Add api endpoints for roles and permissions list * feat: Add ability to provide permissions when creating a role and rename environmentName to name in the list permissions datastructure * fix: Make project roles resolve correctly against new environments permissions structure * fix: Patch migration to also populate permission names * fix: Make permissions actually work with new environments * fix: Add back to get permissions working for editor role * fix: Removed ability to set role type through api during creation - it's now always custom * feat: Return permissions on get role endpoint * feat: Add in support for updating roles * fix: Get a bunch of tests working and delete a few that make no sense anymore * chore: A few small cleanups - remove logging and restore default on dev server config * chore: Refactor role/access stores into more logical domains * feat: Add in validation for roles * feat: Patch db migration to handle old stucture * fix: migration for project roles * fix: patch a few broken tests * fix: add permissions to editor * fix: update test name * fix: update user permission mapping * fix: create new user * fix: update root role test * fix: update tests * feat: Validation now works when updating a role * fix: Add in very barebones down migration for rbac so that tests work * fix: Improve responses from role resolution - getting a non existant role will throw a NotFound error * fix: remove unused permissions * fix: add test for connecting roles and deleting project * fix: add test for adding a project member with a custom role * fix: add test for changing user role * fix: add guard for deleting role if the role is in use * fix: alter migration * chore: Minor code cleanups * chore: Small code cleanups * chore: More minor cleanups of code * chore: Trim some dead code to make the linter happy * feat: Schema validation for roles * fix: setup permission for variant * fix: remove unused import * feat: Add cascading delete for role_permissions when deleting a role * feat: add configuration option for disabling legacy api * chore: update frontend to beta version * 4.6.0-beta.0 * fix: export default project constant * fix: update snapshot * fix: module pattern ../../lib * fix: move DEFAULT_PROJECT to types * fix: remove debug logging * fix: remove debug log state * fix: Change permission descriptions * fix: roles should have unique name * fix: root roles should be connected to the default project * fix: typo in role-schema.ts * fix: Role permission empty string for non environment type * feat: new permission for moving project * fix: add event for changeProject * fix: Removing a user from a project will now check to see if that project has an owner, rather than checking if any project has an owner * fix: add tests for move project * fix: Add in missing create/delete tag permissions * fix: Removed duplicate impl caused by multiple good samaritans putting it back in! * fix: Trim out add tag permissions, for now at least * chore: Trim out new add and delete tag permissions - we're going with update feature instead * chore: update frontend * 4.6.0-beta.1 * feat: Prevent editing of built in roles * fix: Patch an issue where permissions for variants/environments didn't match the front end * fix: lint Co-authored-by: Ivar Conradi Østhus <ivarconr@gmail.com> Co-authored-by: Fredrik Oseberg <fredrik.no@gmail.com>
This commit is contained in:
parent
18c87cedf6
commit
0c78980502
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "unleash-server",
|
||||
"description": "Unleash is an enterprise ready feature toggles service. It provides different strategies for handling feature toggles.",
|
||||
"version": "4.5.1",
|
||||
"version": "4.6.0-beta.1",
|
||||
"keywords": [
|
||||
"unleash",
|
||||
"feature toggle",
|
||||
@ -111,7 +111,7 @@
|
||||
"response-time": "^2.3.2",
|
||||
"serve-favicon": "^2.5.0",
|
||||
"stoppable": "^1.1.0",
|
||||
"unleash-frontend": "4.4.1",
|
||||
"unleash-frontend": "4.6.0-beta.1",
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -30,13 +30,14 @@ Object {
|
||||
"user": "unleash",
|
||||
"version": undefined,
|
||||
},
|
||||
"disableLegacyFeaturesApi": false,
|
||||
"email": Object {
|
||||
"host": undefined,
|
||||
"host": "smtp.ethereal.email",
|
||||
"port": 587,
|
||||
"secure": false,
|
||||
"sender": "noreply@unleash-hosted.com",
|
||||
"smtppass": undefined,
|
||||
"smtpuser": undefined,
|
||||
"smtppass": "DtBAy8kzwhMjzbY5UJ",
|
||||
"smtpuser": "maureen.heaney@ethereal.email",
|
||||
},
|
||||
"enableOAS": false,
|
||||
"enterpriseVersion": undefined,
|
||||
|
@ -287,6 +287,10 @@ export function createConfig(options: IUnleashOptions): IUnleashConfig {
|
||||
const enableOAS =
|
||||
options.enableOAS || safeBoolean(process.env.ENABLE_OAS, false);
|
||||
|
||||
const disableLegacyFeaturesApi =
|
||||
options.disableLegacyFeaturesApi ||
|
||||
safeBoolean(process.env.DISABLE_LEGACY_FEATURES_API, false);
|
||||
|
||||
return {
|
||||
db,
|
||||
session,
|
||||
@ -301,6 +305,7 @@ export function createConfig(options: IUnleashOptions): IUnleashConfig {
|
||||
email,
|
||||
secureHeaders,
|
||||
enableOAS,
|
||||
disableLegacyFeaturesApi,
|
||||
preHook: options.preHook,
|
||||
preRouterHook: options.preRouterHook,
|
||||
eventHook: options.eventHook,
|
||||
|
@ -7,15 +7,32 @@ import {
|
||||
IAccessStore,
|
||||
IRole,
|
||||
IUserPermission,
|
||||
IUserRole,
|
||||
} from '../types/stores/access-store';
|
||||
import { IPermission } from '../types/model';
|
||||
import NotFoundError from '../error/notfound-error';
|
||||
import {
|
||||
ENVIRONMENT_PERMISSION_TYPE,
|
||||
ROOT_PERMISSION_TYPE,
|
||||
} from '../util/constants';
|
||||
|
||||
const T = {
|
||||
ROLE_USER: 'role_user',
|
||||
ROLES: 'roles',
|
||||
ROLE_PERMISSION: 'role_permission',
|
||||
PERMISSIONS: 'permissions',
|
||||
PERMISSION_TYPES: 'permission_types',
|
||||
};
|
||||
|
||||
interface IPermissionRow {
|
||||
id: number;
|
||||
permission: string;
|
||||
display_name: string;
|
||||
environment?: string;
|
||||
type: string;
|
||||
project?: string;
|
||||
role_id: number;
|
||||
}
|
||||
|
||||
export class AccessStore implements IAccessStore {
|
||||
private logger: Logger;
|
||||
|
||||
@ -53,75 +70,141 @@ export class AccessStore implements IAccessStore {
|
||||
}
|
||||
|
||||
async get(key: number): Promise<IRole> {
|
||||
return this.db
|
||||
const role = await this.db
|
||||
.select(['id', 'name', 'type', 'description'])
|
||||
.where('id', key)
|
||||
.first()
|
||||
.from<IRole>(T.ROLES);
|
||||
|
||||
if (!role) {
|
||||
throw new NotFoundError(`Could not find role with id: ${key}`);
|
||||
}
|
||||
|
||||
return role;
|
||||
}
|
||||
|
||||
async getAll(): Promise<IRole[]> {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
async getAvailablePermissions(): Promise<IPermission[]> {
|
||||
const rows = await this.db
|
||||
.select(['id', 'permission', 'type', 'display_name'])
|
||||
.where('type', 'project')
|
||||
.orWhere('type', 'environment')
|
||||
.from(`${T.PERMISSIONS} as p`);
|
||||
return rows.map(this.mapPermission);
|
||||
}
|
||||
|
||||
mapPermission(permission: IPermissionRow): IPermission {
|
||||
return {
|
||||
id: permission.id,
|
||||
name: permission.permission,
|
||||
displayName: permission.display_name,
|
||||
type: permission.type,
|
||||
};
|
||||
}
|
||||
|
||||
async getPermissionsForUser(userId: number): Promise<IUserPermission[]> {
|
||||
const stopTimer = this.timer('getPermissionsForUser');
|
||||
const rows = await this.db
|
||||
.select('project', 'permission')
|
||||
.from<IUserPermission>(`${T.ROLE_PERMISSION} AS rp`)
|
||||
.leftJoin(`${T.ROLE_USER} AS ur`, 'ur.role_id', 'rp.role_id')
|
||||
.select(
|
||||
'project',
|
||||
'permission',
|
||||
'environment',
|
||||
'type',
|
||||
'ur.role_id',
|
||||
)
|
||||
.from<IPermissionRow>(`${T.ROLE_PERMISSION} AS rp`)
|
||||
.join(`${T.ROLE_USER} AS ur`, 'ur.role_id', 'rp.role_id')
|
||||
.join(`${T.PERMISSIONS} AS p`, 'p.id', 'rp.permission_id')
|
||||
.where('ur.user_id', '=', userId);
|
||||
stopTimer();
|
||||
return rows;
|
||||
return rows.map(this.mapUserPermission);
|
||||
}
|
||||
|
||||
async getPermissionsForRole(roleId: number): Promise<IUserPermission[]> {
|
||||
mapUserPermission(row: IPermissionRow): IUserPermission {
|
||||
let project: string = undefined;
|
||||
// Since the editor should have access to the default project,
|
||||
// we map the project to the project and environment specific
|
||||
// permissions that are connected to the editor role.
|
||||
if (row.type !== ROOT_PERMISSION_TYPE) {
|
||||
project = row.project;
|
||||
}
|
||||
|
||||
const environment =
|
||||
row.type === ENVIRONMENT_PERMISSION_TYPE
|
||||
? row.environment
|
||||
: undefined;
|
||||
|
||||
const result = {
|
||||
project,
|
||||
environment,
|
||||
permission: row.permission,
|
||||
};
|
||||
return result;
|
||||
}
|
||||
|
||||
async getPermissionsForRole(roleId: number): Promise<IPermission[]> {
|
||||
const stopTimer = this.timer('getPermissionsForRole');
|
||||
const rows = await this.db
|
||||
.select('project', 'permission')
|
||||
.from<IUserPermission>(`${T.ROLE_PERMISSION}`)
|
||||
.where('role_id', '=', roleId);
|
||||
.select(
|
||||
'p.id',
|
||||
'p.permission',
|
||||
'rp.environment',
|
||||
'p.display_name',
|
||||
'p.type',
|
||||
)
|
||||
.from<IPermission>(`${T.ROLE_PERMISSION} as rp`)
|
||||
.join(`${T.PERMISSIONS} as p`, 'p.id', 'rp.permission_id')
|
||||
.where('rp.role_id', '=', roleId);
|
||||
stopTimer();
|
||||
return rows;
|
||||
return rows.map((permission) => {
|
||||
return {
|
||||
id: permission.id,
|
||||
name: permission.permission,
|
||||
environment: permission.environment,
|
||||
displayName: permission.display_name,
|
||||
type: permission.type,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async getRoles(): Promise<IRole[]> {
|
||||
return this.db
|
||||
.select(['id', 'name', 'type', 'description'])
|
||||
.from<IRole>(T.ROLES);
|
||||
async addEnvironmentPermissionsToRole(
|
||||
role_id: number,
|
||||
permissions: IPermission[],
|
||||
): Promise<void> {
|
||||
const rows = permissions.map((permission) => {
|
||||
return {
|
||||
role_id,
|
||||
permission_id: permission.id,
|
||||
environment: permission.environment,
|
||||
};
|
||||
});
|
||||
this.db.batchInsert(T.ROLE_PERMISSION, rows);
|
||||
}
|
||||
|
||||
async getRoleWithId(id: number): Promise<IRole> {
|
||||
return this.db
|
||||
.select(['id', 'name', 'type', 'description'])
|
||||
.where('id', id)
|
||||
.first()
|
||||
.from<IRole>(T.ROLES);
|
||||
}
|
||||
|
||||
async getRolesForProject(projectId: string): Promise<IRole[]> {
|
||||
return this.db
|
||||
.select(['id', 'name', 'type', 'project', 'description'])
|
||||
.from<IRole>(T.ROLES)
|
||||
.where('project', projectId)
|
||||
.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)
|
||||
async unlinkUserRoles(userId: number): Promise<void> {
|
||||
return this.db(T.ROLE_USER)
|
||||
.where({
|
||||
project: projectId,
|
||||
user_id: userId,
|
||||
})
|
||||
.delete();
|
||||
}
|
||||
|
||||
async getProjectUserIdsForRole(
|
||||
roleId: number,
|
||||
projectId?: string,
|
||||
): Promise<number[]> {
|
||||
const rows = await this.db
|
||||
.select(['user_id'])
|
||||
.from<IRole>(`${T.ROLE_USER} AS ru`)
|
||||
.join(`${T.ROLES} as r`, 'ru.role_id', 'id')
|
||||
.where('r.id', roleId)
|
||||
.andWhere('ru.project', projectId);
|
||||
return rows.map((r) => r.user_id);
|
||||
}
|
||||
|
||||
async getRolesForUserId(userId: number): Promise<IRole[]> {
|
||||
return this.db
|
||||
.select(['id', 'name', 'type', 'project', 'description'])
|
||||
@ -138,18 +221,28 @@ export class AccessStore implements IAccessStore {
|
||||
return rows.map((r) => r.user_id);
|
||||
}
|
||||
|
||||
async addUserToRole(userId: number, roleId: number): Promise<void> {
|
||||
async addUserToRole(
|
||||
userId: number,
|
||||
roleId: number,
|
||||
projecId?: string,
|
||||
): Promise<void> {
|
||||
return this.db(T.ROLE_USER).insert({
|
||||
user_id: userId,
|
||||
role_id: roleId,
|
||||
project: projecId,
|
||||
});
|
||||
}
|
||||
|
||||
async removeUserFromRole(userId: number, roleId: number): Promise<void> {
|
||||
async removeUserFromRole(
|
||||
userId: number,
|
||||
roleId: number,
|
||||
projectId?: string,
|
||||
): Promise<void> {
|
||||
return this.db(T.ROLE_USER)
|
||||
.where({
|
||||
user_id: userId,
|
||||
role_id: roleId,
|
||||
project: projectId,
|
||||
})
|
||||
.delete();
|
||||
}
|
||||
@ -168,67 +261,51 @@ export class AccessStore implements IAccessStore {
|
||||
.delete();
|
||||
}
|
||||
|
||||
async createRole(
|
||||
name: string,
|
||||
type: string,
|
||||
project?: string,
|
||||
description?: string,
|
||||
): Promise<IRole> {
|
||||
const [id] = await this.db(T.ROLES)
|
||||
.insert({
|
||||
name,
|
||||
description,
|
||||
type,
|
||||
project,
|
||||
})
|
||||
.returning('id');
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
type,
|
||||
project,
|
||||
};
|
||||
}
|
||||
|
||||
async addPermissionsToRole(
|
||||
role_id: number,
|
||||
permissions: string[],
|
||||
projectId?: string,
|
||||
environment?: string,
|
||||
): Promise<void> {
|
||||
const rows = permissions.map((permission) => ({
|
||||
const rows = await this.db
|
||||
.select('id as permissionId')
|
||||
.from<number>(T.PERMISSIONS)
|
||||
.whereIn('permission', permissions);
|
||||
|
||||
const newRoles = rows.map((row) => ({
|
||||
role_id,
|
||||
project: projectId,
|
||||
permission,
|
||||
environment,
|
||||
permission_id: row.permissionId,
|
||||
}));
|
||||
return this.db.batchInsert(T.ROLE_PERMISSION, rows);
|
||||
|
||||
return this.db.batchInsert(T.ROLE_PERMISSION, newRoles);
|
||||
}
|
||||
|
||||
async removePermissionFromRole(
|
||||
roleId: number,
|
||||
role_id: number,
|
||||
permission: string,
|
||||
projectId?: string,
|
||||
environment?: string,
|
||||
): Promise<void> {
|
||||
const rows = await this.db
|
||||
.select('id as permissionId')
|
||||
.from<number>(T.PERMISSIONS)
|
||||
.where('permission', permission);
|
||||
|
||||
const permissionId = rows[0].permissionId;
|
||||
|
||||
return this.db(T.ROLE_PERMISSION)
|
||||
.where({
|
||||
role_id: roleId,
|
||||
permission,
|
||||
project: projectId,
|
||||
role_id,
|
||||
permission_id: permissionId,
|
||||
environment,
|
||||
})
|
||||
.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,
|
||||
}));
|
||||
async wipePermissionsFromRole(role_id: number): Promise<void> {
|
||||
return this.db(T.ROLE_PERMISSION)
|
||||
.where({
|
||||
role_id,
|
||||
})
|
||||
.delete();
|
||||
}
|
||||
}
|
||||
|
@ -27,6 +27,7 @@ import FeatureTagStore from './feature-tag-store';
|
||||
import { FeatureEnvironmentStore } from './feature-environment-store';
|
||||
import { ClientMetricsStoreV2 } from './client-metrics-store-v2';
|
||||
import UserSplashStore from './user-splash-store';
|
||||
import RoleStore from './role-store';
|
||||
|
||||
export const createStores = (
|
||||
config: IUnleashConfig,
|
||||
@ -77,6 +78,7 @@ export const createStores = (
|
||||
getLogger,
|
||||
),
|
||||
userSplashStore: new UserSplashStore(db, eventBus, getLogger),
|
||||
roleStore: new RoleStore(db, eventBus, getLogger),
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -88,7 +88,7 @@ class ProjectStore implements IProjectStore {
|
||||
const row = await this.db(TABLE)
|
||||
.insert(this.fieldToRow(project))
|
||||
.returning('*');
|
||||
return this.mapRow(row);
|
||||
return this.mapRow(row[0]);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
|
185
src/lib/db/role-store.ts
Normal file
185
src/lib/db/role-store.ts
Normal file
@ -0,0 +1,185 @@
|
||||
import EventEmitter from 'events';
|
||||
import { Knex } from 'knex';
|
||||
import { Logger, LogProvider } from '../logger';
|
||||
import NotFoundError from '../error/notfound-error';
|
||||
import { ICustomRole } from 'lib/types/model';
|
||||
import {
|
||||
ICustomRoleInsert,
|
||||
ICustomRoleUpdate,
|
||||
IRoleStore,
|
||||
} from 'lib/types/stores/role-store';
|
||||
import { IRole, IUserRole } from 'lib/types/stores/access-store';
|
||||
|
||||
const T = {
|
||||
ROLE_USER: 'role_user',
|
||||
ROLES: 'roles',
|
||||
};
|
||||
|
||||
const COLUMNS = ['id', 'name', 'description', 'type'];
|
||||
|
||||
interface IRoleRow {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export default class RoleStore implements IRoleStore {
|
||||
private logger: Logger;
|
||||
|
||||
private eventBus: EventEmitter;
|
||||
|
||||
private db: Knex;
|
||||
|
||||
constructor(db: Knex, eventBus: EventEmitter, getLogger: LogProvider) {
|
||||
this.db = db;
|
||||
this.eventBus = eventBus;
|
||||
this.logger = getLogger('lib/db/role-store.ts');
|
||||
}
|
||||
|
||||
async getAll(): Promise<ICustomRole[]> {
|
||||
const rows = await this.db
|
||||
.select(COLUMNS)
|
||||
.from(T.ROLES)
|
||||
.orderBy('name', 'asc');
|
||||
|
||||
return rows.map(this.mapRow);
|
||||
}
|
||||
|
||||
async create(role: ICustomRoleInsert): Promise<ICustomRole> {
|
||||
const row = await this.db(T.ROLES)
|
||||
.insert({
|
||||
name: role.name,
|
||||
description: role.description,
|
||||
type: role.roleType,
|
||||
})
|
||||
.returning('*');
|
||||
return this.mapRow(row[0]);
|
||||
}
|
||||
|
||||
async delete(id: number): Promise<void> {
|
||||
return this.db(T.ROLES).where({ id }).del();
|
||||
}
|
||||
|
||||
async get(id: number): Promise<ICustomRole> {
|
||||
const rows = await this.db.select(COLUMNS).from(T.ROLES).where({ id });
|
||||
if (rows.length === 0) {
|
||||
throw new NotFoundError(`Could not find role with id: ${id}`);
|
||||
}
|
||||
return this.mapRow(rows[0]);
|
||||
}
|
||||
|
||||
async update(role: ICustomRoleUpdate): Promise<ICustomRole> {
|
||||
const rows = await this.db(T.ROLES)
|
||||
.where({
|
||||
id: role.id,
|
||||
})
|
||||
.update({
|
||||
id: role.id,
|
||||
name: role.name,
|
||||
description: role.description,
|
||||
})
|
||||
.returning('*');
|
||||
return this.mapRow(rows[0]);
|
||||
}
|
||||
|
||||
async exists(id: number): Promise<boolean> {
|
||||
const result = await this.db.raw(
|
||||
`SELECT EXISTS (SELECT 1 FROM ${T.ROLES} WHERE id = ?) AS present`,
|
||||
[id],
|
||||
);
|
||||
const { present } = result.rows[0];
|
||||
return present;
|
||||
}
|
||||
|
||||
async nameInUse(name: string, existingId?: number): Promise<boolean> {
|
||||
let query = this.db(T.ROLES).where({ name }).returning('id');
|
||||
if (existingId) {
|
||||
query = query.andWhereNot({ id: existingId });
|
||||
}
|
||||
const result = await query;
|
||||
return result.length > 0;
|
||||
}
|
||||
|
||||
async deleteAll(): Promise<void> {
|
||||
return this.db(T.ROLES).del();
|
||||
}
|
||||
|
||||
mapRow(row: IRoleRow): ICustomRole {
|
||||
if (!row) {
|
||||
throw new NotFoundError('No row');
|
||||
}
|
||||
|
||||
return {
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
description: row.description,
|
||||
type: row.type,
|
||||
};
|
||||
}
|
||||
|
||||
async getRoles(): Promise<IRole[]> {
|
||||
return this.db
|
||||
.select(['id', 'name', 'type', 'description'])
|
||||
.from<IRole>(T.ROLES);
|
||||
}
|
||||
|
||||
async getRoleWithId(id: number): Promise<IRole> {
|
||||
return this.db
|
||||
.select(['id', 'name', 'type', 'description'])
|
||||
.where('id', id)
|
||||
.first()
|
||||
.from<IRole>(T.ROLES);
|
||||
}
|
||||
|
||||
async getProjectRoles(): Promise<IRole[]> {
|
||||
return this.db
|
||||
.select(['id', 'name', 'type', 'description'])
|
||||
.from<IRole>(T.ROLES)
|
||||
.where('type', 'custom')
|
||||
.orWhere('type', 'project');
|
||||
}
|
||||
|
||||
async getRolesForProject(projectId: string): Promise<IRole[]> {
|
||||
return this.db
|
||||
.select(['r.id', 'r.name', 'r.type', 'ru.project', 'r.description'])
|
||||
.from<IRole>(`${T.ROLE_USER} as ru`)
|
||||
.innerJoin(`${T.ROLES} as r`, 'ru.role_id', 'r.id')
|
||||
.where('project', projectId);
|
||||
}
|
||||
|
||||
async getRootRoles(): Promise<IRole[]> {
|
||||
return this.db
|
||||
.select(['id', 'name', 'type', 'description'])
|
||||
.from<IRole>(T.ROLES)
|
||||
.where('type', 'root');
|
||||
}
|
||||
|
||||
async removeRolesForProject(projectId: string): Promise<void> {
|
||||
return this.db(T.ROLE_USER)
|
||||
.where({
|
||||
project: projectId,
|
||||
})
|
||||
.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: Number(row.id),
|
||||
userId: Number(row.user_id),
|
||||
}));
|
||||
}
|
||||
|
||||
async getRoleByName(name: string): Promise<IRole> {
|
||||
return this.db(T.ROLES).where({ name }).first();
|
||||
}
|
||||
|
||||
destroy(): void {}
|
||||
}
|
23
src/lib/error/role-in-use-error.ts
Normal file
23
src/lib/error/role-in-use-error.ts
Normal file
@ -0,0 +1,23 @@
|
||||
class RoleInUseError extends Error {
|
||||
constructor(message: string) {
|
||||
super();
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
|
||||
this.name = this.constructor.name;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
toJSON(): object {
|
||||
return {
|
||||
isJoi: true,
|
||||
name: this.constructor.name,
|
||||
details: [
|
||||
{
|
||||
message: this.message,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default RoleInUseError;
|
@ -152,6 +152,7 @@ test('should verify permission for root resource', async () => {
|
||||
req.user,
|
||||
perms.ADMIN,
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
@ -181,6 +182,7 @@ test('should lookup projectId from params', async () => {
|
||||
req.user,
|
||||
perms.UPDATE_PROJECT,
|
||||
req.params.projectId,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
@ -215,6 +217,7 @@ test('should lookup projectId from feature toggle', async () => {
|
||||
req.user,
|
||||
perms.UPDATE_FEATURE,
|
||||
projectId,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
@ -249,6 +252,7 @@ test('should lookup projectId from data', async () => {
|
||||
req.user,
|
||||
perms.CREATE_FEATURE,
|
||||
projectId,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
@ -275,6 +279,7 @@ test('Does not double check permission if not changing project when updating tog
|
||||
req.user,
|
||||
perms.UPDATE_FEATURE,
|
||||
oldProjectId,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
@ -298,6 +303,7 @@ test('UPDATE_TAG_TYPE does not need projectId', async () => {
|
||||
req.user,
|
||||
perms.UPDATE_TAG_TYPE,
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
@ -321,5 +327,6 @@ test('DELETE_TAG_TYPE does not need projectId', async () => {
|
||||
req.user,
|
||||
perms.DELETE_TAG_TYPE,
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
@ -14,6 +14,7 @@ interface PermissionChecker {
|
||||
user: User,
|
||||
permission: string,
|
||||
projectId?: string,
|
||||
environment?: string,
|
||||
): Promise<boolean>;
|
||||
}
|
||||
|
||||
@ -44,7 +45,7 @@ const rbacMiddleware = (
|
||||
}
|
||||
|
||||
// For /api/admin/projects/:projectId we will find it as part of params
|
||||
let { projectId } = params;
|
||||
let { projectId, environment } = params;
|
||||
|
||||
// Temporary workaround to figure out projectId for feature toggle updates.
|
||||
// will be removed in Unleash v5.0
|
||||
@ -55,7 +56,12 @@ const rbacMiddleware = (
|
||||
projectId = req.body.project || 'default';
|
||||
}
|
||||
|
||||
return accessService.hasPermission(user, permission, projectId);
|
||||
return accessService.hasPermission(
|
||||
user,
|
||||
permission,
|
||||
projectId,
|
||||
environment,
|
||||
);
|
||||
};
|
||||
return next();
|
||||
};
|
||||
|
@ -30,10 +30,14 @@ class AdminApi extends Controller {
|
||||
super(config);
|
||||
|
||||
this.app.get('/', this.index);
|
||||
this.app.use(
|
||||
'/features',
|
||||
new FeatureController(config, services).router,
|
||||
);
|
||||
|
||||
if (!config.disableLegacyFeaturesApi) {
|
||||
this.app.use(
|
||||
'/features',
|
||||
new FeatureController(config, services).router,
|
||||
);
|
||||
}
|
||||
|
||||
this.app.use(
|
||||
'/feature-types',
|
||||
new FeatureTypeController(config, services).router,
|
||||
|
@ -7,8 +7,12 @@ import FeatureToggleService from '../../../services/feature-toggle-service';
|
||||
import { Logger } from '../../../logger';
|
||||
import {
|
||||
CREATE_FEATURE,
|
||||
CREATE_FEATURE_STRATEGY,
|
||||
DELETE_FEATURE,
|
||||
DELETE_FEATURE_STRATEGY,
|
||||
UPDATE_FEATURE,
|
||||
UPDATE_FEATURE_ENVIRONMENT,
|
||||
UPDATE_FEATURE_STRATEGY,
|
||||
} from '../../../types/permissions';
|
||||
import {
|
||||
FeatureToggleDTO,
|
||||
@ -70,16 +74,40 @@ export default class ProjectFeaturesController extends Controller {
|
||||
|
||||
// Environments
|
||||
this.get(`${PATH_ENV}`, this.getEnvironment);
|
||||
this.post(`${PATH_ENV}/on`, this.toggleEnvironmentOn, UPDATE_FEATURE);
|
||||
this.post(`${PATH_ENV}/off`, this.toggleEnvironmentOff, UPDATE_FEATURE);
|
||||
this.post(
|
||||
`${PATH_ENV}/on`,
|
||||
this.toggleEnvironmentOn,
|
||||
UPDATE_FEATURE_ENVIRONMENT,
|
||||
);
|
||||
this.post(
|
||||
`${PATH_ENV}/off`,
|
||||
this.toggleEnvironmentOff,
|
||||
UPDATE_FEATURE_ENVIRONMENT,
|
||||
);
|
||||
|
||||
// activation strategies
|
||||
this.get(`${PATH_STRATEGIES}`, this.getStrategies);
|
||||
this.post(`${PATH_STRATEGIES}`, this.addStrategy, UPDATE_FEATURE);
|
||||
this.post(
|
||||
`${PATH_STRATEGIES}`,
|
||||
this.addStrategy,
|
||||
CREATE_FEATURE_STRATEGY,
|
||||
);
|
||||
this.get(`${PATH_STRATEGY}`, this.getStrategy);
|
||||
this.put(`${PATH_STRATEGY}`, this.updateStrategy, UPDATE_FEATURE);
|
||||
this.patch(`${PATH_STRATEGY}`, this.patchStrategy, UPDATE_FEATURE);
|
||||
this.delete(`${PATH_STRATEGY}`, this.deleteStrategy, UPDATE_FEATURE);
|
||||
this.put(
|
||||
`${PATH_STRATEGY}`,
|
||||
this.updateStrategy,
|
||||
UPDATE_FEATURE_STRATEGY,
|
||||
);
|
||||
this.patch(
|
||||
`${PATH_STRATEGY}`,
|
||||
this.patchStrategy,
|
||||
UPDATE_FEATURE_STRATEGY,
|
||||
);
|
||||
this.delete(
|
||||
`${PATH_STRATEGY}`,
|
||||
this.deleteStrategy,
|
||||
DELETE_FEATURE_STRATEGY,
|
||||
);
|
||||
|
||||
// feature toggles
|
||||
this.get(PATH, this.getFeatures);
|
||||
|
@ -5,7 +5,7 @@ import { IUnleashConfig } from '../../../types/option';
|
||||
import { IUnleashServices } from '../../../types';
|
||||
import { Request, Response } from 'express';
|
||||
import { Operation } from 'fast-json-patch';
|
||||
import { UPDATE_FEATURE } from '../../../types/permissions';
|
||||
import { UPDATE_FEATURE_VARIANTS } from '../../../types/permissions';
|
||||
import { IVariant } from '../../../types/model';
|
||||
import { extractUsername } from '../../../util/extract-user';
|
||||
import { IAuthRequest } from '../../unleash-types';
|
||||
@ -35,8 +35,8 @@ export default class VariantsController extends Controller {
|
||||
this.logger = config.getLogger('admin-api/project/variants.ts');
|
||||
this.featureService = featureToggleService;
|
||||
this.get(PREFIX, this.getVariants);
|
||||
this.patch(PREFIX, this.patchVariants, UPDATE_FEATURE);
|
||||
this.put(PREFIX, this.overwriteVariants, UPDATE_FEATURE);
|
||||
this.patch(PREFIX, this.patchVariants, UPDATE_FEATURE_VARIANTS);
|
||||
this.put(PREFIX, this.overwriteVariants, UPDATE_FEATURE_VARIANTS);
|
||||
}
|
||||
|
||||
async getVariants(
|
||||
|
@ -58,6 +58,8 @@ export const handleErrors: (
|
||||
return res.status(409).json(error).end();
|
||||
case 'FeatureHasTagError':
|
||||
return res.status(409).json(error).end();
|
||||
case 'RoleInUseError':
|
||||
return res.status(400).json(error).end();
|
||||
default:
|
||||
logger.error('Server failed executing request', error);
|
||||
return res.status(500).end();
|
||||
|
83
src/lib/schema/role-schema.test.ts
Normal file
83
src/lib/schema/role-schema.test.ts
Normal file
@ -0,0 +1,83 @@
|
||||
import { roleSchema } from './role-schema';
|
||||
|
||||
test('role schema rejects a role without a name', async () => {
|
||||
expect.assertions(1);
|
||||
const role = {
|
||||
permissions: [],
|
||||
};
|
||||
|
||||
try {
|
||||
await roleSchema.validateAsync(role);
|
||||
} catch (error) {
|
||||
expect(error.details[0].message).toBe('"name" is required');
|
||||
}
|
||||
});
|
||||
|
||||
test('role schema allows a role with an empty description', async () => {
|
||||
const role = {
|
||||
name: 'Brønsted',
|
||||
description: '',
|
||||
};
|
||||
|
||||
const value = await roleSchema.validateAsync(role);
|
||||
expect(value.description).toEqual('');
|
||||
});
|
||||
|
||||
test('role schema rejects a role with a broken permission list', async () => {
|
||||
expect.assertions(1);
|
||||
const role = {
|
||||
name: 'Mendeleev',
|
||||
permissions: [
|
||||
{
|
||||
aPropertyThatIsAproposToNothing: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
try {
|
||||
await roleSchema.validateAsync(role);
|
||||
} catch (error) {
|
||||
expect(error.details[0].message).toBe(
|
||||
'"permissions[0].id" is required',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('role schema allows a role with an empty permission list', async () => {
|
||||
const role = {
|
||||
name: 'Avogadro',
|
||||
permissions: [],
|
||||
};
|
||||
|
||||
const value = await roleSchema.validateAsync(role);
|
||||
expect(value.permissions).toEqual([]);
|
||||
});
|
||||
|
||||
test('role schema allows a role with a null list', async () => {
|
||||
const role = {
|
||||
name: 'Curie',
|
||||
permissions: null,
|
||||
};
|
||||
|
||||
const value = await roleSchema.validateAsync(role);
|
||||
expect(value.permissions).toEqual(null);
|
||||
});
|
||||
|
||||
test('role schema allows an undefined with a null list', async () => {
|
||||
const role = {
|
||||
name: 'Fischer',
|
||||
};
|
||||
|
||||
const value = await roleSchema.validateAsync(role);
|
||||
expect(value.permissions).toEqual(undefined);
|
||||
});
|
||||
|
||||
test('role schema strips roleType if present', async () => {
|
||||
const role = {
|
||||
name: 'Grignard',
|
||||
roleType: 'Organic Chemistry',
|
||||
};
|
||||
|
||||
const value = await roleSchema.validateAsync(role);
|
||||
expect(value.roleType).toEqual(undefined);
|
||||
});
|
22
src/lib/schema/role-schema.ts
Normal file
22
src/lib/schema/role-schema.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import joi from 'joi';
|
||||
|
||||
export const permissionRoleSchema = joi
|
||||
.object()
|
||||
.keys({
|
||||
id: joi.number().required(),
|
||||
environment: joi.string().optional().allow('').allow(null).default(''),
|
||||
})
|
||||
.options({ stripUnknown: true, allowUnknown: false, abortEarly: false });
|
||||
|
||||
export const roleSchema = joi
|
||||
.object()
|
||||
.keys({
|
||||
name: joi.string().required(),
|
||||
description: joi.string().optional().allow('').allow(null).default(''),
|
||||
permissions: joi
|
||||
.array()
|
||||
.allow(null)
|
||||
.optional()
|
||||
.items(permissionRoleSchema),
|
||||
})
|
||||
.options({ stripUnknown: true, allowUnknown: false, abortEarly: false });
|
@ -3,6 +3,7 @@ import User, { IUser } from '../types/user';
|
||||
import {
|
||||
IAccessStore,
|
||||
IRole,
|
||||
IRoleWithPermissions,
|
||||
IUserPermission,
|
||||
IUserRole,
|
||||
} from '../types/stores/access-store';
|
||||
@ -10,20 +11,25 @@ import { IUserStore } from '../types/stores/user-store';
|
||||
import { Logger } from '../logger';
|
||||
import { IUnleashStores } from '../types/stores';
|
||||
import {
|
||||
IAvailablePermissions,
|
||||
ICustomRole,
|
||||
IPermission,
|
||||
IRoleData,
|
||||
IUserWithRole,
|
||||
PermissionType,
|
||||
RoleName,
|
||||
RoleType,
|
||||
} from '../types/model';
|
||||
import { IRoleStore } from 'lib/types/stores/role-store';
|
||||
import NameExistsError from '../error/name-exists-error';
|
||||
import { IEnvironmentStore } from 'lib/types/stores/environment-store';
|
||||
import RoleInUseError from '../error/role-in-use-error';
|
||||
import { roleSchema } from '../schema/role-schema';
|
||||
import { CUSTOM_ROLE_TYPE } from '../util/constants';
|
||||
import { DEFAULT_PROJECT } from '../types/project';
|
||||
import InvalidOperationError from '../error/invalid-operation-error';
|
||||
|
||||
export const ALL_PROJECTS = '*';
|
||||
|
||||
const PROJECT_DESCRIPTION = {
|
||||
OWNER: 'Users with this role have full control over the project, and can add and manage other users within the project context, manage feature toggles within the project, and control advanced project features like archiving and deleting the project.',
|
||||
MEMBER: 'Users with this role within a project are allowed to view, create and update feature toggles, but have limited permissions in regards to managing the projects user access and can not archive or delete the project.',
|
||||
};
|
||||
export const ALL_ENVS = '*';
|
||||
|
||||
const { ADMIN } = permissions;
|
||||
|
||||
@ -35,11 +41,18 @@ const PROJECT_ADMIN = [
|
||||
permissions.DELETE_FEATURE,
|
||||
];
|
||||
|
||||
const PROJECT_REGULAR = [
|
||||
permissions.CREATE_FEATURE,
|
||||
permissions.UPDATE_FEATURE,
|
||||
permissions.DELETE_FEATURE,
|
||||
];
|
||||
interface IRoleCreation {
|
||||
name: string;
|
||||
description: string;
|
||||
permissions?: IPermission[];
|
||||
}
|
||||
|
||||
interface IRoleUpdate {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
permissions?: IPermission[];
|
||||
}
|
||||
|
||||
const isProjectPermission = (permission) => PROJECT_ADMIN.includes(permission);
|
||||
|
||||
@ -48,26 +61,29 @@ export class AccessService {
|
||||
|
||||
private userStore: IUserStore;
|
||||
|
||||
private logger: Logger;
|
||||
private roleStore: IRoleStore;
|
||||
|
||||
private permissions: IPermission[];
|
||||
private environmentStore: IEnvironmentStore;
|
||||
|
||||
private logger: Logger;
|
||||
|
||||
constructor(
|
||||
{
|
||||
accessStore,
|
||||
userStore,
|
||||
}: Pick<IUnleashStores, 'accessStore' | 'userStore'>,
|
||||
roleStore,
|
||||
environmentStore,
|
||||
}: Pick<
|
||||
IUnleashStores,
|
||||
'accessStore' | 'userStore' | 'roleStore' | 'environmentStore'
|
||||
>,
|
||||
{ getLogger }: { getLogger: Function },
|
||||
) {
|
||||
this.store = accessStore;
|
||||
this.userStore = userStore;
|
||||
this.roleStore = roleStore;
|
||||
this.environmentStore = environmentStore;
|
||||
this.logger = getLogger('/services/access-service.ts');
|
||||
this.permissions = Object.values(permissions).map((p) => ({
|
||||
name: p,
|
||||
type: isProjectPermission(p)
|
||||
? PermissionType.project
|
||||
: PermissionType.root,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -81,9 +97,10 @@ export class AccessService {
|
||||
user: User,
|
||||
permission: string,
|
||||
projectId?: string,
|
||||
environment?: string,
|
||||
): Promise<boolean> {
|
||||
this.logger.info(
|
||||
`Checking permission=${permission}, userId=${user.id} projectId=${projectId}`,
|
||||
`Checking permission=${permission}, userId=${user.id}, projectId=${projectId}, environment=${environment}`,
|
||||
);
|
||||
|
||||
try {
|
||||
@ -96,6 +113,12 @@ export class AccessService {
|
||||
p.project === projectId ||
|
||||
p.project === ALL_PROJECTS,
|
||||
)
|
||||
.filter(
|
||||
(p) =>
|
||||
!p.environment ||
|
||||
p.environment === environment ||
|
||||
p.environment === ALL_ENVS,
|
||||
)
|
||||
.some(
|
||||
(p) =>
|
||||
p.permission === permission || p.permission === ADMIN,
|
||||
@ -118,12 +141,43 @@ export class AccessService {
|
||||
return this.store.getPermissionsForUser(user.id);
|
||||
}
|
||||
|
||||
getPermissions(): IPermission[] {
|
||||
return this.permissions;
|
||||
async getPermissions(): Promise<IAvailablePermissions> {
|
||||
const bindablePermissions = await this.store.getAvailablePermissions();
|
||||
const environments = await this.environmentStore.getAll();
|
||||
|
||||
const projectPermissions = bindablePermissions.filter((x) => {
|
||||
return x.type === 'project';
|
||||
});
|
||||
|
||||
const environmentPermissions = bindablePermissions.filter((perm) => {
|
||||
return perm.type === 'environment';
|
||||
});
|
||||
|
||||
const allEnvironmentPermissions = environments.map((env) => {
|
||||
return {
|
||||
name: env.name,
|
||||
permissions: environmentPermissions.map((permission) => {
|
||||
return { environment: env.name, ...permission };
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
project: projectPermissions,
|
||||
environments: allEnvironmentPermissions,
|
||||
};
|
||||
}
|
||||
|
||||
async addUserToRole(userId: number, roleId: number): Promise<void> {
|
||||
return this.store.addUserToRole(userId, roleId);
|
||||
async addUserToRole(
|
||||
userId: number,
|
||||
roleId: number,
|
||||
projectId: string,
|
||||
): Promise<void> {
|
||||
return this.store.addUserToRole(userId, roleId, projectId);
|
||||
}
|
||||
|
||||
async getRoleByName(roleName: string): Promise<IRole> {
|
||||
return this.roleStore.getRoleByName(roleName);
|
||||
}
|
||||
|
||||
async setUserRootRole(
|
||||
@ -131,14 +185,18 @@ export class AccessService {
|
||||
role: number | RoleName,
|
||||
): Promise<void> {
|
||||
const newRootRole = await this.resolveRootRole(role);
|
||||
|
||||
if (newRootRole) {
|
||||
try {
|
||||
await this.store.removeRolesOfTypeForUser(
|
||||
userId,
|
||||
RoleType.ROOT,
|
||||
);
|
||||
await this.store.addUserToRole(userId, newRootRole.id);
|
||||
|
||||
await this.store.addUserToRole(
|
||||
userId,
|
||||
newRootRole.id,
|
||||
DEFAULT_PROJECT,
|
||||
);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Could not add role=${newRootRole.name} to userId=${userId}`,
|
||||
@ -154,29 +212,39 @@ export class AccessService {
|
||||
return userRoles.filter((r) => r.type === RoleType.ROOT);
|
||||
}
|
||||
|
||||
async removeUserFromRole(userId: number, roleId: number): Promise<void> {
|
||||
return this.store.removeUserFromRole(userId, roleId);
|
||||
async removeUserFromRole(
|
||||
userId: number,
|
||||
roleId: number,
|
||||
projectId: string,
|
||||
): Promise<void> {
|
||||
return this.store.removeUserFromRole(userId, roleId, projectId);
|
||||
}
|
||||
|
||||
//This actually only exists for testing purposes
|
||||
async addPermissionToRole(
|
||||
roleId: number,
|
||||
permission: string,
|
||||
projectId?: string,
|
||||
environment?: string,
|
||||
): Promise<void> {
|
||||
if (isProjectPermission(permission) && !projectId) {
|
||||
if (isProjectPermission(permission) && !environment) {
|
||||
throw new Error(
|
||||
`ProjectId cannot be empty for permission=${permission}`,
|
||||
);
|
||||
}
|
||||
return this.store.addPermissionsToRole(roleId, [permission], projectId);
|
||||
return this.store.addPermissionsToRole(
|
||||
roleId,
|
||||
[permission],
|
||||
environment,
|
||||
);
|
||||
}
|
||||
|
||||
//This actually only exists for testing purposes
|
||||
async removePermissionFromRole(
|
||||
roleId: number,
|
||||
permission: string,
|
||||
projectId?: string,
|
||||
environment?: string,
|
||||
): Promise<void> {
|
||||
if (isProjectPermission(permission) && !projectId) {
|
||||
if (isProjectPermission(permission) && !environment) {
|
||||
throw new Error(
|
||||
`ProjectId cannot be empty for permission=${permission}`,
|
||||
);
|
||||
@ -184,15 +252,24 @@ export class AccessService {
|
||||
return this.store.removePermissionFromRole(
|
||||
roleId,
|
||||
permission,
|
||||
projectId,
|
||||
environment,
|
||||
);
|
||||
}
|
||||
|
||||
async getRoles(): Promise<IRole[]> {
|
||||
return this.store.getRoles();
|
||||
return this.roleStore.getRoles();
|
||||
}
|
||||
|
||||
async getRole(roleId: number): Promise<IRoleData> {
|
||||
async getRole(id: number): Promise<IRoleWithPermissions> {
|
||||
const role = await this.store.get(id);
|
||||
const rolePermissions = await this.store.getPermissionsForRole(role.id);
|
||||
return {
|
||||
...role,
|
||||
permissions: rolePermissions,
|
||||
};
|
||||
}
|
||||
|
||||
async getRoleData(roleId: number): Promise<IRoleData> {
|
||||
const [role, rolePerms, users] = await Promise.all([
|
||||
this.store.get(roleId),
|
||||
this.store.getPermissionsForRole(roleId),
|
||||
@ -201,14 +278,22 @@ export class AccessService {
|
||||
return { role, permissions: rolePerms, users };
|
||||
}
|
||||
|
||||
async getProjectRoles(): Promise<IRole[]> {
|
||||
return this.roleStore.getProjectRoles();
|
||||
}
|
||||
|
||||
async getRolesForProject(projectId: string): Promise<IRole[]> {
|
||||
return this.store.getRolesForProject(projectId);
|
||||
return this.roleStore.getRolesForProject(projectId);
|
||||
}
|
||||
|
||||
async getRolesForUser(userId: number): Promise<IRole[]> {
|
||||
return this.store.getRolesForUserId(userId);
|
||||
}
|
||||
|
||||
async unlinkUserRoles(userId: number): Promise<void> {
|
||||
return this.store.unlinkUserRoles(userId);
|
||||
}
|
||||
|
||||
async getUsersForRole(roleId: number): Promise<IUser[]> {
|
||||
const userIdList = await this.store.getUserIdsForRole(roleId);
|
||||
if (userIdList.length > 0) {
|
||||
@ -217,16 +302,33 @@ export class AccessService {
|
||||
return [];
|
||||
}
|
||||
|
||||
async getProjectUsersForRole(
|
||||
roleId: number,
|
||||
projectId?: string,
|
||||
): Promise<IUser[]> {
|
||||
const userIdList = await this.store.getProjectUserIdsForRole(
|
||||
roleId,
|
||||
projectId,
|
||||
);
|
||||
if (userIdList.length > 0) {
|
||||
return this.userStore.getAllWithId(userIdList);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// Move to project-service?
|
||||
async getProjectRoleUsers(
|
||||
projectId: string,
|
||||
): Promise<[IRole[], IUserWithRole[]]> {
|
||||
const roles = await this.store.getRolesForProject(projectId);
|
||||
const roles = await this.roleStore.getProjectRoles();
|
||||
|
||||
const users = await Promise.all(
|
||||
roles.map(async (role) => {
|
||||
const usrs = await this.getUsersForRole(role.id);
|
||||
return usrs.map((u) => ({ ...u, roleId: role.id }));
|
||||
const projectUsers = await this.getProjectUsersForRole(
|
||||
role.id,
|
||||
projectId,
|
||||
);
|
||||
return projectUsers.map((u) => ({ ...u, roleId: role.id }));
|
||||
}),
|
||||
);
|
||||
return [roles, users.flat()];
|
||||
@ -240,36 +342,15 @@ export class AccessService {
|
||||
throw new Error('ProjectId cannot be empty');
|
||||
}
|
||||
|
||||
const ownerRole = await this.store.createRole(
|
||||
RoleName.OWNER,
|
||||
RoleType.PROJECT,
|
||||
projectId,
|
||||
PROJECT_DESCRIPTION.OWNER,
|
||||
);
|
||||
await this.store.addPermissionsToRole(
|
||||
ownerRole.id,
|
||||
PROJECT_ADMIN,
|
||||
projectId,
|
||||
);
|
||||
const ownerRole = await this.roleStore.getRoleByName(RoleName.OWNER);
|
||||
|
||||
// TODO: remove this when all users is guaranteed to have a unique id.
|
||||
if (owner.id) {
|
||||
this.logger.info(
|
||||
`Making ${owner.id} admin of ${projectId} via roleId=${ownerRole.id}`,
|
||||
);
|
||||
await this.store.addUserToRole(owner.id, ownerRole.id);
|
||||
await this.store.addUserToRole(owner.id, ownerRole.id, projectId);
|
||||
}
|
||||
const memberRole = await this.store.createRole(
|
||||
RoleName.MEMBER,
|
||||
RoleType.PROJECT,
|
||||
projectId,
|
||||
PROJECT_DESCRIPTION.MEMBER,
|
||||
);
|
||||
await this.store.addPermissionsToRole(
|
||||
memberRole.id,
|
||||
PROJECT_REGULAR,
|
||||
projectId,
|
||||
);
|
||||
}
|
||||
|
||||
async removeDefaultProjectRoles(
|
||||
@ -277,15 +358,15 @@ export class AccessService {
|
||||
projectId: string,
|
||||
): Promise<void> {
|
||||
this.logger.info(`Removing project roles for ${projectId}`);
|
||||
return this.store.removeRolesForProject(projectId);
|
||||
return this.roleStore.removeRolesForProject(projectId);
|
||||
}
|
||||
|
||||
async getRootRoleForAllUsers(): Promise<IUserRole[]> {
|
||||
return this.store.getRootRoleForAllUsers();
|
||||
return this.roleStore.getRootRoleForAllUsers();
|
||||
}
|
||||
|
||||
async getRootRoles(): Promise<IRole[]> {
|
||||
return this.store.getRootRoles();
|
||||
return this.roleStore.getRootRoles();
|
||||
}
|
||||
|
||||
public async resolveRootRole(rootRole: number | RoleName): Promise<IRole> {
|
||||
@ -300,7 +381,94 @@ export class AccessService {
|
||||
}
|
||||
|
||||
async getRootRole(roleName: RoleName): Promise<IRole> {
|
||||
const roles = await this.store.getRootRoles();
|
||||
const roles = await this.roleStore.getRootRoles();
|
||||
return roles.find((r) => r.name === roleName);
|
||||
}
|
||||
|
||||
async getAllRoles(): Promise<ICustomRole[]> {
|
||||
return this.roleStore.getAll();
|
||||
}
|
||||
|
||||
async createRole(role: IRoleCreation): Promise<ICustomRole> {
|
||||
const baseRole = {
|
||||
...(await this.validateRole(role)),
|
||||
roleType: CUSTOM_ROLE_TYPE,
|
||||
};
|
||||
|
||||
const rolePermissions = role.permissions;
|
||||
const newRole = await this.roleStore.create(baseRole);
|
||||
if (rolePermissions) {
|
||||
this.store.addEnvironmentPermissionsToRole(
|
||||
newRole.id,
|
||||
rolePermissions,
|
||||
);
|
||||
}
|
||||
return newRole;
|
||||
}
|
||||
|
||||
async updateRole(role: IRoleUpdate): Promise<ICustomRole> {
|
||||
await this.validateRole(role, role.id);
|
||||
const baseRole = {
|
||||
id: role.id,
|
||||
name: role.name,
|
||||
description: role.description,
|
||||
roleType: CUSTOM_ROLE_TYPE,
|
||||
};
|
||||
const rolePermissions = role.permissions;
|
||||
const newRole = await this.roleStore.update(baseRole);
|
||||
if (rolePermissions) {
|
||||
this.store.wipePermissionsFromRole(newRole.id);
|
||||
this.store.addEnvironmentPermissionsToRole(
|
||||
newRole.id,
|
||||
rolePermissions,
|
||||
);
|
||||
}
|
||||
return newRole;
|
||||
}
|
||||
|
||||
async deleteRole(id: number): Promise<void> {
|
||||
const roleUsers = await this.getUsersForRole(id);
|
||||
|
||||
if (roleUsers.length > 0) {
|
||||
throw new RoleInUseError(
|
||||
'Role is in use by more than one user. You cannot delete a role that is in use without first removing the role from the users.',
|
||||
);
|
||||
}
|
||||
|
||||
return this.roleStore.delete(id);
|
||||
}
|
||||
|
||||
async validateRoleIsUnique(
|
||||
roleName: string,
|
||||
existingId?: number,
|
||||
): Promise<void> {
|
||||
const exists = await this.roleStore.nameInUse(roleName, existingId);
|
||||
if (exists) {
|
||||
throw new NameExistsError(
|
||||
`There already exists a role with the name ${roleName}`,
|
||||
);
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async validateRoleIsNotBuiltIn(roleId: number): Promise<void> {
|
||||
const role = await this.store.get(roleId);
|
||||
if (role.type !== CUSTOM_ROLE_TYPE) {
|
||||
throw new InvalidOperationError(
|
||||
'You can not change built in roles.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async validateRole(
|
||||
role: IRoleCreation,
|
||||
existingId?: number,
|
||||
): Promise<IRoleCreation> {
|
||||
const cleanedRole = await roleSchema.validateAsync(role);
|
||||
if (existingId) {
|
||||
await this.validateRoleIsNotBuiltIn(existingId);
|
||||
}
|
||||
await this.validateRoleIsUnique(role.name, existingId);
|
||||
return cleanedRole;
|
||||
}
|
||||
}
|
||||
|
@ -25,20 +25,20 @@ import { IFeatureTypeStore } from '../types/stores/feature-type-store';
|
||||
import { IFeatureToggleStore } from '../types/stores/feature-toggle-store';
|
||||
import { IFeatureEnvironmentStore } from '../types/stores/feature-environment-store';
|
||||
import { IProjectQuery, IProjectStore } from '../types/stores/project-store';
|
||||
import { IRole } from '../types/stores/access-store';
|
||||
import { IRoleDescriptor } from '../types/stores/access-store';
|
||||
import { IEventStore } from '../types/stores/event-store';
|
||||
import FeatureToggleService from './feature-toggle-service';
|
||||
import { CREATE_FEATURE, UPDATE_FEATURE } from '../types/permissions';
|
||||
import { MOVE_FEATURE_TOGGLE } from '../types/permissions';
|
||||
import NoAccessError from '../error/no-access-error';
|
||||
import IncompatibleProjectError from '../error/incompatible-project-error';
|
||||
import { DEFAULT_PROJECT } from '../types/project';
|
||||
import { IFeatureTagStore } from 'lib/types/stores/feature-tag-store';
|
||||
|
||||
const getCreatedBy = (user: User) => user.email || user.username;
|
||||
|
||||
const DEFAULT_PROJECT = 'default';
|
||||
|
||||
export interface UsersWithRoles {
|
||||
users: IUserWithRole[];
|
||||
roles: IRole[];
|
||||
roles: IRoleDescriptor[];
|
||||
}
|
||||
|
||||
export default class ProjectService {
|
||||
@ -60,6 +60,8 @@ export default class ProjectService {
|
||||
|
||||
private featureToggleService: FeatureToggleService;
|
||||
|
||||
private tagStore: IFeatureTagStore;
|
||||
|
||||
constructor(
|
||||
{
|
||||
projectStore,
|
||||
@ -68,6 +70,7 @@ export default class ProjectService {
|
||||
featureTypeStore,
|
||||
environmentStore,
|
||||
featureEnvironmentStore,
|
||||
featureTagStore,
|
||||
}: Pick<
|
||||
IUnleashStores,
|
||||
| 'projectStore'
|
||||
@ -76,6 +79,7 @@ export default class ProjectService {
|
||||
| 'featureTypeStore'
|
||||
| 'environmentStore'
|
||||
| 'featureEnvironmentStore'
|
||||
| 'featureTagStore'
|
||||
>,
|
||||
config: IUnleashConfig,
|
||||
accessService: AccessService,
|
||||
@ -89,6 +93,7 @@ export default class ProjectService {
|
||||
this.featureToggleStore = featureToggleStore;
|
||||
this.featureTypeStore = featureTypeStore;
|
||||
this.featureToggleService = featureToggleService;
|
||||
this.tagStore = featureTagStore;
|
||||
this.logger = config.getLogger('services/project-service.js');
|
||||
}
|
||||
|
||||
@ -188,7 +193,7 @@ export default class ProjectService {
|
||||
const feature = await this.featureToggleStore.get(featureName);
|
||||
|
||||
if (feature.project !== currentProjectId) {
|
||||
throw new NoAccessError(UPDATE_FEATURE);
|
||||
throw new NoAccessError(MOVE_FEATURE_TOGGLE);
|
||||
}
|
||||
const project = await this.getProject(newProjectId);
|
||||
|
||||
@ -198,12 +203,12 @@ export default class ProjectService {
|
||||
|
||||
const authorized = await this.accessService.hasPermission(
|
||||
user,
|
||||
CREATE_FEATURE,
|
||||
MOVE_FEATURE_TOGGLE,
|
||||
newProjectId,
|
||||
);
|
||||
|
||||
if (!authorized) {
|
||||
throw new NoAccessError(CREATE_FEATURE);
|
||||
throw new NoAccessError(MOVE_FEATURE_TOGGLE);
|
||||
}
|
||||
|
||||
const isCompatibleWithTargetProject =
|
||||
@ -297,10 +302,10 @@ export default class ProjectService {
|
||||
|
||||
const alreadyHasAccess = users.some((u) => u.id === userId);
|
||||
if (alreadyHasAccess) {
|
||||
throw new Error(`User already have access to project=${projectId}`);
|
||||
throw new Error(`User already has access to project=${projectId}`);
|
||||
}
|
||||
|
||||
await this.accessService.addUserToRole(userId, role.id);
|
||||
await this.accessService.addUserToRole(userId, role.id, projectId);
|
||||
}
|
||||
|
||||
// TODO: should be an event too
|
||||
@ -318,13 +323,16 @@ export default class ProjectService {
|
||||
}
|
||||
|
||||
if (role.name === RoleName.OWNER) {
|
||||
const users = await this.accessService.getUsersForRole(role.id);
|
||||
const users = await this.accessService.getProjectUsersForRole(
|
||||
role.id,
|
||||
projectId,
|
||||
);
|
||||
if (users.length < 2) {
|
||||
throw new Error('A project must have at least one owner');
|
||||
}
|
||||
}
|
||||
|
||||
await this.accessService.removeUserFromRole(userId, role.id);
|
||||
await this.accessService.removeUserFromRole(userId, role.id, projectId);
|
||||
}
|
||||
|
||||
async getMembers(projectId: string): Promise<number> {
|
||||
|
@ -135,7 +135,6 @@ class UserService {
|
||||
});
|
||||
const passwordHash = await bcrypt.hash(pwd, saltRounds);
|
||||
await this.store.setPasswordHash(user.id, passwordHash);
|
||||
|
||||
await this.accessService.setUserRootRole(
|
||||
user.id,
|
||||
RoleName.ADMIN,
|
||||
@ -257,12 +256,7 @@ class UserService {
|
||||
|
||||
async deleteUser(userId: number, updatedBy?: User): Promise<void> {
|
||||
const user = await this.store.get(userId);
|
||||
const roles = await this.accessService.getRolesForUser(userId);
|
||||
await Promise.all(
|
||||
roles.map((role) =>
|
||||
this.accessService.removeUserFromRole(userId, role.id),
|
||||
),
|
||||
);
|
||||
await this.accessService.unlinkUserRoles(userId);
|
||||
await this.sessionService.deleteSessionsForUser(userId);
|
||||
|
||||
await this.store.delete(userId);
|
||||
@ -355,7 +349,7 @@ class UserService {
|
||||
token,
|
||||
);
|
||||
const user = await this.getUser(userId);
|
||||
const role = await this.accessService.getRole(user.rootRole);
|
||||
const role = await this.accessService.getRoleData(user.rootRole);
|
||||
return {
|
||||
token,
|
||||
createdBy,
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { ITagType } from './stores/tag-type-store';
|
||||
import { LogProvider } from '../logger';
|
||||
import { IRole, IUserPermission } from './stores/access-store';
|
||||
import { IRole } from './stores/access-store';
|
||||
import { IUser } from './user';
|
||||
|
||||
export interface IConstraint {
|
||||
@ -212,12 +212,25 @@ export interface IUserWithRole {
|
||||
export interface IRoleData {
|
||||
role: IRole;
|
||||
users: IUser[];
|
||||
permissions: IUserPermission[];
|
||||
permissions: IPermission[];
|
||||
}
|
||||
|
||||
export interface IAvailablePermissions {
|
||||
project: IPermission[];
|
||||
environments: IEnvironmentPermission[];
|
||||
}
|
||||
|
||||
export interface IPermission {
|
||||
id: number;
|
||||
name: string;
|
||||
type: PermissionType;
|
||||
displayName: string;
|
||||
type: string;
|
||||
environment?: string;
|
||||
}
|
||||
|
||||
export interface IEnvironmentPermission {
|
||||
name: string;
|
||||
permissions: IPermission[];
|
||||
}
|
||||
|
||||
export enum PermissionType {
|
||||
@ -313,6 +326,13 @@ export interface IProject {
|
||||
updatedAt?: Date;
|
||||
}
|
||||
|
||||
export interface ICustomRole {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface IProjectWithCount extends IProject {
|
||||
featureCount: number;
|
||||
memberCount: number;
|
||||
|
@ -101,6 +101,7 @@ export interface IUnleashOptions {
|
||||
preRouterHook?: Function;
|
||||
eventHook?: EventHook;
|
||||
enterpriseVersion?: string;
|
||||
disableLegacyFeaturesApi?: boolean;
|
||||
}
|
||||
|
||||
export interface IEmailOption {
|
||||
@ -156,4 +157,5 @@ export interface IUnleashConfig {
|
||||
eventHook?: EventHook;
|
||||
enterpriseVersion?: string;
|
||||
eventBus: EventEmitter;
|
||||
disableLegacyFeaturesApi?: boolean;
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
// Special
|
||||
//Special
|
||||
export const ADMIN = 'ADMIN';
|
||||
export const CLIENT = 'CLIENT';
|
||||
export const NONE = 'NONE';
|
||||
@ -6,6 +6,10 @@ export const NONE = 'NONE';
|
||||
export const CREATE_FEATURE = 'CREATE_FEATURE';
|
||||
export const UPDATE_FEATURE = 'UPDATE_FEATURE';
|
||||
export const DELETE_FEATURE = 'DELETE_FEATURE';
|
||||
export const CREATE_FEATURE_STRATEGY = 'CREATE_FEATURE_STRATEGY';
|
||||
export const UPDATE_FEATURE_STRATEGY = 'UPDATE_FEATURE_STRATEGY';
|
||||
export const DELETE_FEATURE_STRATEGY = 'DELETE_FEATURE_STRATEGY';
|
||||
export const UPDATE_FEATURE_ENVIRONMENT = 'UPDATE_FEATURE_ENVIRONMENT';
|
||||
export const CREATE_STRATEGY = 'CREATE_STRATEGY';
|
||||
export const UPDATE_STRATEGY = 'UPDATE_STRATEGY';
|
||||
export const DELETE_STRATEGY = 'DELETE_STRATEGY';
|
||||
@ -26,3 +30,5 @@ export const CREATE_API_TOKEN = 'CREATE_API_TOKEN';
|
||||
export const DELETE_API_TOKEN = 'DELETE_API_TOKEN';
|
||||
export const UPDATE_TAG_TYPE = 'UPDATE_TAG_TYPE';
|
||||
export const DELETE_TAG_TYPE = 'DELETE_TAG_TYPE';
|
||||
export const UPDATE_FEATURE_VARIANTS = 'UPDATE_FEATURE_VARIANTS';
|
||||
export const MOVE_FEATURE_TOGGLE = 'MOVE_FEATURE_TOGGLE';
|
||||
|
1
src/lib/types/project.ts
Normal file
1
src/lib/types/project.ts
Normal file
@ -0,0 +1 @@
|
||||
export const DEFAULT_PROJECT = 'default';
|
@ -23,6 +23,7 @@ import { IEnvironmentStore } from './stores/environment-store';
|
||||
import { IFeatureToggleClientStore } from './stores/feature-toggle-client-store';
|
||||
import { IClientMetricsStoreV2 } from './stores/client-metrics-store-v2';
|
||||
import { IUserSplashStore } from './stores/user-splash-store';
|
||||
import { IRoleStore } from './stores/role-store';
|
||||
|
||||
export interface IUnleashStores {
|
||||
accessStore: IAccessStore;
|
||||
@ -50,4 +51,5 @@ export interface IUnleashStores {
|
||||
userFeedbackStore: IUserFeedbackStore;
|
||||
userStore: IUserStore;
|
||||
userSplashStore: IUserSplashStore;
|
||||
roleStore: IRoleStore;
|
||||
}
|
||||
|
@ -1,7 +1,9 @@
|
||||
import { IPermission } from '../model';
|
||||
import { Store } from './store';
|
||||
|
||||
export interface IUserPermission {
|
||||
project?: string;
|
||||
environment?: string;
|
||||
permission: string;
|
||||
}
|
||||
|
||||
@ -10,7 +12,16 @@ export interface IRole {
|
||||
name: string;
|
||||
description?: string;
|
||||
type: string;
|
||||
project?: string;
|
||||
}
|
||||
|
||||
export interface IRoleWithPermissions extends IRole {
|
||||
permissions: IPermission[];
|
||||
}
|
||||
|
||||
export interface IRoleDescriptor {
|
||||
name: string;
|
||||
description?: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface IUserRole {
|
||||
@ -18,32 +29,37 @@ export interface IUserRole {
|
||||
userId: number;
|
||||
}
|
||||
export interface IAccessStore extends Store<IRole, number> {
|
||||
getAvailablePermissions(): Promise<IPermission[]>;
|
||||
getPermissionsForUser(userId: number): Promise<IUserPermission[]>;
|
||||
getPermissionsForRole(roleId: number): Promise<IUserPermission[]>;
|
||||
getRoles(): Promise<IRole[]>;
|
||||
getRolesForProject(projectId: string): Promise<IRole[]>;
|
||||
getRootRoles(): Promise<IRole[]>;
|
||||
removeRolesForProject(projectId: string): Promise<void>;
|
||||
getPermissionsForRole(roleId: number): Promise<IPermission[]>;
|
||||
unlinkUserRoles(userId: number): Promise<void>;
|
||||
getRolesForUserId(userId: number): Promise<IRole[]>;
|
||||
getUserIdsForRole(roleId: number): Promise<number[]>;
|
||||
addUserToRole(userId: number, roleId: number): Promise<void>;
|
||||
removeUserFromRole(userId: number, roleId: number): Promise<void>;
|
||||
getProjectUserIdsForRole(roleId: number, projectId?: string);
|
||||
getUserIdsForRole(roleId: number, projectId?: string): Promise<number[]>;
|
||||
wipePermissionsFromRole(role_id: number): Promise<void>;
|
||||
addEnvironmentPermissionsToRole(
|
||||
role_id: number,
|
||||
permissions: IPermission[],
|
||||
): Promise<void>;
|
||||
addUserToRole(
|
||||
userId: number,
|
||||
roleId: number,
|
||||
projectId?: string,
|
||||
): Promise<void>;
|
||||
removeUserFromRole(
|
||||
userId: number,
|
||||
roleId: number,
|
||||
projectId?: string,
|
||||
): Promise<void>;
|
||||
removeRolesOfTypeForUser(userId: number, roleType: string): Promise<void>;
|
||||
createRole(
|
||||
name: string,
|
||||
type: string,
|
||||
project?: string,
|
||||
description?: string,
|
||||
): Promise<IRole>;
|
||||
addPermissionsToRole(
|
||||
role_id: number,
|
||||
permissions: string[],
|
||||
projectId?: string,
|
||||
environment?: string,
|
||||
): Promise<void>;
|
||||
removePermissionFromRole(
|
||||
roleId: number,
|
||||
permission: string,
|
||||
projectId?: string,
|
||||
): Promise<void>;
|
||||
getRootRoleForAllUsers(): Promise<IUserRole[]>;
|
||||
}
|
||||
|
@ -15,4 +15,5 @@ export interface IEnvironmentStore extends Store<IEnvironment, string> {
|
||||
): Promise<void>;
|
||||
updateSortOrder(id: string, value: number): Promise<void>;
|
||||
importEnvironments(environments: IEnvironment[]): Promise<IEnvironment[]>;
|
||||
delete(name: string): Promise<void>;
|
||||
}
|
||||
|
31
src/lib/types/stores/role-store.ts
Normal file
31
src/lib/types/stores/role-store.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { ICustomRole } from '../model';
|
||||
import { IRole, IUserRole } from './access-store';
|
||||
import { Store } from './store';
|
||||
|
||||
export interface ICustomRoleInsert {
|
||||
name: string;
|
||||
description: string;
|
||||
roleType: string;
|
||||
}
|
||||
|
||||
export interface ICustomRoleUpdate {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
roleType: string;
|
||||
}
|
||||
|
||||
export interface IRoleStore extends Store<ICustomRole, number> {
|
||||
getAll(): Promise<ICustomRole[]>;
|
||||
create(role: ICustomRoleInsert): Promise<ICustomRole>;
|
||||
update(role: ICustomRoleUpdate): Promise<ICustomRole>;
|
||||
delete(id: number): Promise<void>;
|
||||
getRoles(): Promise<IRole[]>;
|
||||
getRoleByName(name: string): Promise<IRole>;
|
||||
getRolesForProject(projectId: string): Promise<IRole[]>;
|
||||
removeRolesForProject(projectId: string): Promise<void>;
|
||||
getProjectRoles(): Promise<IRole[]>;
|
||||
getRootRoles(): Promise<IRole[]>;
|
||||
getRootRoleForAllUsers(): Promise<IUserRole[]>;
|
||||
nameInUse(name: string, existingId: number): Promise<boolean>;
|
||||
}
|
@ -1 +1,7 @@
|
||||
export const DEFAULT_ENV = 'default';
|
||||
|
||||
export const ROOT_PERMISSION_TYPE = 'root';
|
||||
export const ENVIRONMENT_PERMISSION_TYPE = 'environment';
|
||||
export const PROJECT_PERMISSION_TYPE = 'project';
|
||||
|
||||
export const CUSTOM_ROLE_TYPE = 'custom';
|
||||
|
205
src/migrations/20211202120808-add-custom-roles.js
Normal file
205
src/migrations/20211202120808-add-custom-roles.js
Normal file
@ -0,0 +1,205 @@
|
||||
exports.up = function (db, cb) {
|
||||
db.runSql(
|
||||
`
|
||||
CREATE TABLE IF NOT EXISTS permissions
|
||||
(
|
||||
id SERIAL PRIMARY KEY,
|
||||
permission VARCHAR(255) NOT NULL,
|
||||
display_name TEXT,
|
||||
type VARCHAR(255),
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
|
||||
);
|
||||
|
||||
INSERT INTO permissions (permission, display_name, type) VALUES ('ADMIN', 'Admin', 'root');
|
||||
INSERT INTO permissions (permission, display_name, type) VALUES ('CREATE_FEATURE', 'Create Feature Toggles', 'project');
|
||||
INSERT INTO permissions (permission, display_name, type) VALUES ('CREATE_STRATEGY','Create Strategies', 'root');
|
||||
INSERT INTO permissions (permission, display_name, type) VALUES ('CREATE_ADDON', 'Create Addons', 'root');
|
||||
INSERT INTO permissions (permission, display_name, type) VALUES ('DELETE_ADDON', 'Delete Addons', 'root');
|
||||
INSERT INTO permissions (permission, display_name, type) VALUES ('UPDATE_ADDON', 'Update Addons', 'root');
|
||||
INSERT INTO permissions (permission, display_name, type) VALUES ('UPDATE_FEATURE', 'Update Feature Toggles', 'project');
|
||||
INSERT INTO permissions (permission, display_name, type) VALUES ('DELETE_FEATURE', 'Delete Feature Toggles', 'project');
|
||||
INSERT INTO permissions (permission, display_name, type) VALUES ('UPDATE_APPLICATION', 'Update Applications', 'root');
|
||||
INSERT INTO permissions (permission, display_name, type) VALUES ('UPDATE_TAG_TYPE', 'Update Tag Types', 'root');
|
||||
INSERT INTO permissions (permission, display_name, type) VALUES ('DELETE_TAG_TYPE', 'Delete Tag Types', 'root');
|
||||
INSERT INTO permissions (permission, display_name, type) VALUES ('CREATE_PROJECT', 'Create Projects', 'root');
|
||||
INSERT INTO permissions (permission, display_name, type) VALUES ('UPDATE_PROJECT', 'Update Projects', 'project');
|
||||
INSERT INTO permissions (permission, display_name, type) VALUES ('DELETE_PROJECT', 'Delete Projects', 'project');
|
||||
INSERT INTO permissions (permission, display_name, type) VALUES ('UPDATE_STRATEGY', 'Update Strategies', 'root');
|
||||
INSERT INTO permissions (permission, display_name, type) VALUES ('DELETE_STRATEGY', 'Delete Strategies', 'root');
|
||||
INSERT INTO permissions (permission, display_name, type) VALUES ('UPDATE_CONTEXT_FIELD', 'Update Context Fields', 'root');
|
||||
INSERT INTO permissions (permission, display_name, type) VALUES ('CREATE_CONTEXT_FIELD', 'Create Context Fields', 'root');
|
||||
INSERT INTO permissions (permission, display_name, type) VALUES ('DELETE_CONTEXT_FIELD', 'Delete Context Fields', 'root');
|
||||
INSERT INTO permissions (permission, display_name, type) VALUES ('READ_ROLE', 'Read Roles', 'root');
|
||||
INSERT INTO permissions (permission, display_name, type) VALUES ('UPDATE_ROLE', 'Update Roles', 'root');
|
||||
INSERT INTO permissions (permission, display_name, type) VALUES ('UPDATE_API_TOKEN', 'Update API Tokens', 'root');
|
||||
INSERT INTO permissions (permission, display_name, type) VALUES ('CREATE_API_TOKEN', 'Create API Tokens', 'root');
|
||||
INSERT INTO permissions (permission, display_name, type) VALUES ('DELETE_API_TOKEN', 'Delete API Tokens', 'root');
|
||||
INSERT INTO permissions (permission, display_name, type) VALUES ('CREATE_FEATURE_STRATEGY', 'Create Feature Strategies', 'environment');
|
||||
INSERT INTO permissions (permission, display_name, type) VALUES ('UPDATE_FEATURE_STRATEGY', 'Update Feature Strategies', 'environment');
|
||||
INSERT INTO permissions (permission, display_name, type) VALUES ('DELETE_FEATURE_STRATEGY', 'Delete Feature Strategies', 'environment');
|
||||
INSERT INTO permissions (permission, display_name, type) VALUES ('UPDATE_FEATURE_ENVIRONMENT', 'Enable/disable Toggles in Environment', 'environment');
|
||||
INSERT INTO permissions (permission, display_name, type) VALUES ('UPDATE_FEATURE_VARIANTS', 'Create/Edit variants', 'project');
|
||||
|
||||
ALTER TABLE role_user ADD COLUMN
|
||||
project VARCHAR(255);
|
||||
|
||||
ALTER TABLE roles
|
||||
ADD COLUMN
|
||||
updated_at TIMESTAMP WITH TIME ZONE;
|
||||
|
||||
ALTER TABLE role_permission
|
||||
ADD COLUMN
|
||||
permission_id INTEGER,
|
||||
ADD COLUMN
|
||||
environment VARCHAR (100);
|
||||
|
||||
CREATE TEMPORARY TABLE temp_primary_roles
|
||||
(
|
||||
id INTEGER,
|
||||
name TEXT,
|
||||
description TEXT,
|
||||
type TEXT,
|
||||
project TEXT,
|
||||
created_at DATE
|
||||
)
|
||||
ON COMMIT DROP;
|
||||
|
||||
CREATE TEMPORARY TABLE temp_discard_roles
|
||||
(
|
||||
id INTEGER,
|
||||
name TEXT,
|
||||
description TEXT,
|
||||
type TEXT,
|
||||
project TEXT,
|
||||
created_at DATE
|
||||
)
|
||||
ON COMMIT DROP;
|
||||
|
||||
INSERT INTO temp_primary_roles select distinct on (name) id, name ,description, type, project, created_at from roles order by name, id;
|
||||
|
||||
INSERT INTO temp_discard_roles SELECT r.id, r.name, r.description, r.type, r.project, r.created_at FROM roles r
|
||||
LEFT JOIN temp_primary_roles tpr ON r.id = tpr.id
|
||||
WHERE tpr.id IS NULL;
|
||||
|
||||
UPDATE role_user
|
||||
SET project = tpr.project
|
||||
FROM temp_primary_roles tpr
|
||||
WHERE tpr.id = role_user.role_id;
|
||||
|
||||
ALTER TABLE role_user DROP CONSTRAINT role_user_pkey;
|
||||
|
||||
WITH rtu as (
|
||||
SELECT tdr.id as old_role_id, tpr.id as new_role_id, tdr.project as project FROM temp_discard_roles tdr
|
||||
JOIN temp_primary_roles tpr ON tdr.name = tpr.name
|
||||
)
|
||||
UPDATE role_user
|
||||
SET project = rtu.project, role_id = rtu.new_role_id
|
||||
FROM rtu
|
||||
WHERE rtu.old_role_id = role_user.role_id;
|
||||
|
||||
UPDATE role_user SET project = '*' WHERE project IS NULL;
|
||||
ALTER TABLE role_user ADD PRIMARY KEY (role_id, user_id, project);
|
||||
|
||||
DELETE FROM roles WHERE EXISTS
|
||||
(
|
||||
SELECT 1 FROM temp_discard_roles tdr WHERE tdr.id = roles.id
|
||||
);
|
||||
|
||||
DELETE FROM role_permission;
|
||||
|
||||
ALTER TABLE roles DROP COLUMN project;
|
||||
ALTER TABLE role_permission
|
||||
DROP COLUMN project,
|
||||
DROP COLUMN permission;
|
||||
|
||||
INSERT INTO role_permission (role_id, permission_id, environment)
|
||||
SELECT
|
||||
(SELECT id as role_id from roles WHERE name = 'Editor' LIMIT 1),
|
||||
p.id as permission_id,
|
||||
'*' as environment
|
||||
FROM permissions p
|
||||
WHERE p.permission IN
|
||||
('CREATE_STRATEGY',
|
||||
'UPDATE_STRATEGY',
|
||||
'DELETE_STRATEGY',
|
||||
'UPDATE_APPLICATION',
|
||||
'CREATE_CONTEXT_FIELD',
|
||||
'UPDATE_CONTEXT_FIELD',
|
||||
'DELETE_CONTEXT_FIELD',
|
||||
'CREATE_PROJECT',
|
||||
'CREATE_ADDON',
|
||||
'UPDATE_ADDON',
|
||||
'DELETE_ADDON',
|
||||
'UPDATE_PROJECT',
|
||||
'DELETE_PROJECT',
|
||||
'CREATE_FEATURE',
|
||||
'UPDATE_FEATURE',
|
||||
'DELETE_FEATURE',
|
||||
'UPDATE_TAG_TYPE',
|
||||
'DELETE_TAG_TYPE',
|
||||
'UPDATE_FEATURE_VARIANTS');
|
||||
|
||||
INSERT INTO role_permission (role_id, permission_id, environment)
|
||||
SELECT
|
||||
(SELECT id as role_id from roles WHERE name = 'Owner' LIMIT 1),
|
||||
p.id as permission_id,
|
||||
null as environment
|
||||
FROM permissions p
|
||||
WHERE p.permission IN
|
||||
('UPDATE_PROJECT',
|
||||
'DELETE_PROJECT',
|
||||
'CREATE_FEATURE',
|
||||
'UPDATE_FEATURE',
|
||||
'DELETE_FEATURE',
|
||||
'UPDATE_FEATURE_VARIANTS');
|
||||
|
||||
INSERT INTO role_permission (role_id, permission_id, environment)
|
||||
SELECT
|
||||
(SELECT id as role_id from roles WHERE name = 'Member' LIMIT 1),
|
||||
p.id as permission_id,
|
||||
null as environment
|
||||
FROM permissions p
|
||||
WHERE p.permission IN
|
||||
('CREATE_FEATURE',
|
||||
'UPDATE_FEATURE',
|
||||
'DELETE_FEATURE',
|
||||
'UPDATE_FEATURE_VARIANTS');
|
||||
|
||||
INSERT INTO role_permission (role_id, permission_id, environment)
|
||||
SELECT
|
||||
(SELECT id as role_id from roles WHERE name = 'Admin' LIMIT 1),
|
||||
p.id as permission_id,
|
||||
'*' environment
|
||||
FROM permissions p
|
||||
WHERE p.permission = 'ADMIN';
|
||||
|
||||
ALTER TABLE role_permission
|
||||
ADD CONSTRAINT fk_role_permission
|
||||
FOREIGN KEY(role_id)
|
||||
REFERENCES roles(id) ON DELETE CASCADE;
|
||||
|
||||
`,
|
||||
cb,
|
||||
);
|
||||
};
|
||||
|
||||
exports.down = function (db, cb) {
|
||||
db.runSql(
|
||||
`
|
||||
ALTER TABLE role_user DROP COLUMN project;
|
||||
|
||||
ALTER TABLE roles DROP COLUMN updated_at;
|
||||
|
||||
ALTER TABLE role_permission
|
||||
DROP COLUMN
|
||||
permission_id,
|
||||
DROP COLUMN
|
||||
environment;
|
||||
|
||||
ALTER TABLE role_permission
|
||||
ADD COLUMN project TEXT,
|
||||
ADD COLUMN permission TEXT;
|
||||
`,
|
||||
cb,
|
||||
);
|
||||
};
|
@ -0,0 +1,40 @@
|
||||
exports.up = function (db, cb) {
|
||||
db.runSql(
|
||||
`
|
||||
INSERT INTO role_permission (role_id, permission_id, environment)
|
||||
SELECT
|
||||
(SELECT id as role_id from roles WHERE name = 'Owner' LIMIT 1),
|
||||
p.id as permission_id,
|
||||
e.name as environment
|
||||
FROM permissions p
|
||||
CROSS JOIN environments e
|
||||
WHERE p.permission IN
|
||||
('CREATE_FEATURE_STRATEGY',
|
||||
'UPDATE_FEATURE_STRATEGY',
|
||||
'DELETE_FEATURE_STRATEGY',
|
||||
'UPDATE_FEATURE_ENVIRONMENT');
|
||||
|
||||
INSERT INTO role_permission (role_id, permission_id, environment)
|
||||
SELECT
|
||||
(SELECT id as role_id from roles WHERE name = 'Member' LIMIT 1),
|
||||
p.id as permission_id,
|
||||
e.name as environment
|
||||
FROM permissions p
|
||||
CROSS JOIN environments e
|
||||
WHERE p.permission IN
|
||||
('CREATE_FEATURE_STRATEGY',
|
||||
'UPDATE_FEATURE_STRATEGY',
|
||||
'DELETE_FEATURE_STRATEGY',
|
||||
'UPDATE_FEATURE_ENVIRONMENT');
|
||||
`,
|
||||
cb,
|
||||
);
|
||||
};
|
||||
|
||||
exports.down = function (db, cb) {
|
||||
db.runSql(
|
||||
`
|
||||
`,
|
||||
cb,
|
||||
);
|
||||
};
|
@ -0,0 +1,27 @@
|
||||
exports.up = function (db, cb) {
|
||||
db.runSql(
|
||||
`
|
||||
INSERT INTO role_permission (role_id, permission_id, environment)
|
||||
SELECT
|
||||
(SELECT id as role_id from roles WHERE name = 'Editor' LIMIT 1),
|
||||
p.id as permission_id,
|
||||
e.name as environment
|
||||
FROM permissions p
|
||||
CROSS JOIN environments e
|
||||
WHERE p.permission IN
|
||||
('CREATE_FEATURE_STRATEGY',
|
||||
'UPDATE_FEATURE_STRATEGY',
|
||||
'DELETE_FEATURE_STRATEGY',
|
||||
'UPDATE_FEATURE_ENVIRONMENT');
|
||||
`,
|
||||
cb,
|
||||
);
|
||||
};
|
||||
|
||||
exports.down = function (db, cb) {
|
||||
db.runSql(
|
||||
`
|
||||
`,
|
||||
cb,
|
||||
);
|
||||
};
|
@ -0,0 +1,45 @@
|
||||
exports.up = function (db, cb) {
|
||||
db.runSql(
|
||||
`
|
||||
UPDATE permissions SET display_name = 'Admin' WHERE permission = 'ADMIN';
|
||||
UPDATE permissions SET display_name = 'Create feature toggles' WHERE permission = 'CREATE_FEATURE';
|
||||
UPDATE permissions SET display_name = 'Create activation strategies' WHERE permission = 'CREATE_STRATEGY';
|
||||
UPDATE permissions SET display_name = 'Create addons' WHERE permission = 'CREATE_ADDON';
|
||||
UPDATE permissions SET display_name = 'Delete addons' WHERE permission = 'DELETE_ADDON';
|
||||
UPDATE permissions SET display_name = 'Update addons' WHERE permission = 'UPDATE_ADDON';
|
||||
UPDATE permissions SET display_name = 'Update feature toggles' WHERE permission = 'UPDATE_FEATURE';
|
||||
UPDATE permissions SET display_name = 'Delete feature toggles' WHERE permission = 'DELETE_FEATURE';
|
||||
UPDATE permissions SET display_name = 'Update applications' WHERE permission = 'UPDATE_APPLICATION';
|
||||
UPDATE permissions SET display_name = 'Update tag types' WHERE permission = 'UPDATE_TAG_TYPE';
|
||||
UPDATE permissions SET display_name = 'Delete tag types' WHERE permission = 'DELETE_TAG_TYPE';
|
||||
UPDATE permissions SET display_name = 'Create projects' WHERE permission = 'CREATE_PROJECT';
|
||||
UPDATE permissions SET display_name = 'Update project' WHERE permission = 'UPDATE_PROJECT';
|
||||
UPDATE permissions SET display_name = 'Delete project' WHERE permission = 'DELETE_PROJECT';
|
||||
UPDATE permissions SET display_name = 'Update strategies' WHERE permission = 'UPDATE_STRATEGY';
|
||||
UPDATE permissions SET display_name = 'Delete strategies' WHERE permission = 'DELETE_STRATEGY';
|
||||
UPDATE permissions SET display_name = 'Update context fields' WHERE permission = 'UPDATE_CONTEXT_FIELD';
|
||||
UPDATE permissions SET display_name = 'Create context fields' WHERE permission = 'CREATE_CONTEXT_FIELD';
|
||||
UPDATE permissions SET display_name = 'Delete context fields' WHERE permission = 'DELETE_CONTEXT_FIELD';
|
||||
UPDATE permissions SET display_name = 'Read roles' WHERE permission = 'READ_ROLE';
|
||||
UPDATE permissions SET display_name = 'Update roles' WHERE permission = 'UPDATE_ROLE';
|
||||
UPDATE permissions SET display_name = 'Update API tokens' WHERE permission = 'UPDATE_API_TOKEN';
|
||||
UPDATE permissions SET display_name = 'Create API tokens' WHERE permission = 'CREATE_API_TOKEN';
|
||||
UPDATE permissions SET display_name = 'Delete API tokens' WHERE permission = 'DELETE_API_TOKEN';
|
||||
UPDATE permissions SET display_name = 'Create activation strategies' WHERE permission = 'CREATE_FEATURE_STRATEGY';
|
||||
UPDATE permissions SET display_name = 'Update activation strategies' WHERE permission = 'UPDATE_FEATURE_STRATEGY';
|
||||
UPDATE permissions SET display_name = 'Delete activation strategies' WHERE permission = 'DELETE_FEATURE_STRATEGY';
|
||||
UPDATE permissions SET display_name = 'Enable/disable toggles in this environment' WHERE permission = 'UPDATE_FEATURE_ENVIRONMENT';
|
||||
UPDATE permissions SET display_name = 'Create/edit variants' WHERE permission = 'UPDATE_FEATURE_VARIANTS';
|
||||
|
||||
`,
|
||||
cb,
|
||||
);
|
||||
};
|
||||
|
||||
exports.down = function (db, cb) {
|
||||
db.runSql(
|
||||
`
|
||||
`,
|
||||
cb,
|
||||
);
|
||||
};
|
@ -0,0 +1,35 @@
|
||||
exports.up = function (db, cb) {
|
||||
db.runSql(
|
||||
`
|
||||
INSERT INTO permissions (permission, display_name, type) VALUES ('MOVE_FEATURE_TOGGLE', 'Change feature toggle project', 'project');
|
||||
INSERT INTO role_permission (role_id, permission_id, environment)
|
||||
SELECT
|
||||
(SELECT id as role_id from roles WHERE name = 'Editor' LIMIT 1),
|
||||
p.id as permission_id,
|
||||
'*' as environment
|
||||
FROM permissions p
|
||||
WHERE p.permission IN
|
||||
('MOVE_FEATURE_TOGGLE');
|
||||
|
||||
|
||||
INSERT INTO role_permission (role_id, permission_id, environment)
|
||||
SELECT
|
||||
(SELECT id as role_id from roles WHERE name = 'Owner' LIMIT 1),
|
||||
p.id as permission_id,
|
||||
'*' as environment
|
||||
FROM permissions p
|
||||
WHERE p.permission IN
|
||||
('MOVE_FEATURE_TOGGLE');
|
||||
`,
|
||||
cb,
|
||||
);
|
||||
};
|
||||
|
||||
exports.down = function (db, cb) {
|
||||
db.runSql(
|
||||
`
|
||||
DELETE FROM permissions WHERE permission = 'MOVE_FEATURE_TOGGLE';
|
||||
`,
|
||||
cb,
|
||||
);
|
||||
};
|
17
src/migrations/20220111120346-roles-unique-name.js
Normal file
17
src/migrations/20220111120346-roles-unique-name.js
Normal file
@ -0,0 +1,17 @@
|
||||
exports.up = function (db, cb) {
|
||||
db.runSql(
|
||||
`
|
||||
ALTER TABLE roles ADD CONSTRAINT unique_name UNIQUE (name);
|
||||
`,
|
||||
cb,
|
||||
);
|
||||
};
|
||||
|
||||
exports.down = function (db, cb) {
|
||||
db.runSql(
|
||||
`
|
||||
ALTER TABLE roles DROP CONSTRAINT unique_name;
|
||||
`,
|
||||
cb,
|
||||
);
|
||||
};
|
@ -0,0 +1,19 @@
|
||||
exports.up = function (db, cb) {
|
||||
db.runSql(
|
||||
`
|
||||
UPDATE role_user set project = 'default' where role_id
|
||||
IN (SELECT id as role_id from roles WHERE name in ('Admin', 'Editor', 'Viewer') LIMIT 3)
|
||||
`,
|
||||
cb,
|
||||
);
|
||||
};
|
||||
|
||||
exports.down = function (db, cb) {
|
||||
db.runSql(
|
||||
`
|
||||
UPDATE role_user set project = '*' where role_id
|
||||
IN (SELECT id as role_id from roles WHERE name in ('Admin', 'Editor', 'Viewer') LIMIT 3)
|
||||
`,
|
||||
cb,
|
||||
);
|
||||
};
|
@ -0,0 +1,13 @@
|
||||
exports.up = function (db, cb) {
|
||||
db.runSql(
|
||||
`
|
||||
UPDATE role_permission SET environment = '' where permission_id NOT IN
|
||||
(select id from permissions WHERE type = 'environment');
|
||||
`,
|
||||
cb,
|
||||
);
|
||||
};
|
||||
|
||||
exports.down = function (db, cb) {
|
||||
cb();
|
||||
};
|
@ -1,7 +1,11 @@
|
||||
import faker from 'faker';
|
||||
import { FeatureToggleDTO, IStrategyConfig, IVariant } from 'lib/types/model';
|
||||
import dbInit, { ITestDb } from '../../helpers/database-init';
|
||||
import { IUnleashTest, setupApp } from '../../helpers/test-helper';
|
||||
import {
|
||||
IUnleashTest,
|
||||
setupApp,
|
||||
setupAppWithCustomConfig,
|
||||
} from '../../helpers/test-helper';
|
||||
import getLogger from '../../../fixtures/no-logger';
|
||||
import { DEFAULT_ENV } from '../../../../lib/util/constants';
|
||||
|
||||
@ -680,3 +684,21 @@ test('marks feature toggle as stale', async () => {
|
||||
expect(res.body.stale).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
test('should not hit endpoints if disable configuration is set', async () => {
|
||||
const appWithDisabledLegacyFeatures = await setupAppWithCustomConfig(
|
||||
db.stores,
|
||||
{
|
||||
disableLegacyFeaturesApi: true,
|
||||
},
|
||||
);
|
||||
|
||||
await appWithDisabledLegacyFeatures.request
|
||||
.get('/api/admin/features')
|
||||
.expect(404);
|
||||
|
||||
return appWithDisabledLegacyFeatures.request
|
||||
.get('/api/admin/features/featureX')
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(404);
|
||||
});
|
||||
|
@ -6,10 +6,11 @@ import {
|
||||
USER_DELETED,
|
||||
USER_UPDATED,
|
||||
} from '../../../../lib/types/events';
|
||||
import { IAccessStore, IRole } from '../../../../lib/types/stores/access-store';
|
||||
import { IRole } from '../../../../lib/types/stores/access-store';
|
||||
import { IEventStore } from '../../../../lib/types/stores/event-store';
|
||||
import { IUserStore } from '../../../../lib/types/stores/user-store';
|
||||
import { RoleName } from '../../../../lib/types/model';
|
||||
import { IRoleStore } from 'lib/types/stores/role-store';
|
||||
|
||||
let stores;
|
||||
let db;
|
||||
@ -17,7 +18,7 @@ let app;
|
||||
|
||||
let userStore: IUserStore;
|
||||
let eventStore: IEventStore;
|
||||
let accessStore: IAccessStore;
|
||||
let roleStore: IRoleStore;
|
||||
let editorRole: IRole;
|
||||
let adminRole: IRole;
|
||||
|
||||
@ -27,9 +28,9 @@ beforeAll(async () => {
|
||||
app = await setupApp(stores);
|
||||
|
||||
userStore = stores.userStore;
|
||||
accessStore = stores.accessStore;
|
||||
eventStore = stores.eventStore;
|
||||
const roles = await accessStore.getRootRoles();
|
||||
roleStore = stores.roleStore;
|
||||
const roles = await roleStore.getRootRoles();
|
||||
editorRole = roles.find((r) => r.name === RoleName.EDITOR);
|
||||
adminRole = roles.find((r) => r.name === RoleName.ADMIN);
|
||||
});
|
||||
|
@ -1,4 +1,4 @@
|
||||
import dbInit from '../helpers/database-init';
|
||||
import dbInit, { ITestDb } from '../helpers/database-init';
|
||||
import getLogger from '../../fixtures/no-logger';
|
||||
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
@ -9,11 +9,16 @@ import {
|
||||
|
||||
import * as permissions from '../../../lib/types/permissions';
|
||||
import { RoleName } from '../../../lib/types/model';
|
||||
import { IUnleashStores } from '../../../lib/types';
|
||||
import FeatureToggleService from '../../../lib/services/feature-toggle-service';
|
||||
import ProjectService from '../../../lib/services/project-service';
|
||||
import { createTestConfig } from '../../config/test-config';
|
||||
|
||||
let db;
|
||||
let stores;
|
||||
let db: ITestDb;
|
||||
let stores: IUnleashStores;
|
||||
let accessService;
|
||||
|
||||
let featureToggleService;
|
||||
let projectService;
|
||||
let editorUser;
|
||||
let superUser;
|
||||
let editorRole;
|
||||
@ -23,17 +28,172 @@ let readRole;
|
||||
const createUserEditorAccess = async (name, email) => {
|
||||
const { userStore } = stores;
|
||||
const user = await userStore.insert({ name, email });
|
||||
await accessService.addUserToRole(user.id, editorRole.id);
|
||||
await accessService.addUserToRole(user.id, editorRole.id, 'default');
|
||||
return user;
|
||||
};
|
||||
|
||||
const createUserViewerAccess = async (name, email) => {
|
||||
const { userStore } = stores;
|
||||
const user = await userStore.insert({ name, email });
|
||||
await accessService.addUserToRole(user.id, readRole.id, ALL_PROJECTS);
|
||||
return user;
|
||||
};
|
||||
|
||||
const hasCommonProjectAccess = async (user, projectName, condition) => {
|
||||
const defaultEnv = 'default';
|
||||
const developmentEnv = 'development';
|
||||
const productionEnv = 'production';
|
||||
|
||||
const {
|
||||
CREATE_FEATURE,
|
||||
UPDATE_FEATURE,
|
||||
DELETE_FEATURE,
|
||||
CREATE_FEATURE_STRATEGY,
|
||||
UPDATE_FEATURE_STRATEGY,
|
||||
DELETE_FEATURE_STRATEGY,
|
||||
UPDATE_FEATURE_ENVIRONMENT,
|
||||
UPDATE_FEATURE_VARIANTS,
|
||||
} = permissions;
|
||||
expect(
|
||||
await accessService.hasPermission(user, CREATE_FEATURE, projectName),
|
||||
).toBe(condition);
|
||||
expect(
|
||||
await accessService.hasPermission(user, UPDATE_FEATURE, projectName),
|
||||
).toBe(condition);
|
||||
expect(
|
||||
await accessService.hasPermission(user, DELETE_FEATURE, projectName),
|
||||
).toBe(condition);
|
||||
expect(
|
||||
await accessService.hasPermission(
|
||||
user,
|
||||
UPDATE_FEATURE_VARIANTS,
|
||||
projectName,
|
||||
),
|
||||
).toBe(condition);
|
||||
expect(
|
||||
await accessService.hasPermission(
|
||||
user,
|
||||
CREATE_FEATURE_STRATEGY,
|
||||
projectName,
|
||||
defaultEnv,
|
||||
),
|
||||
).toBe(condition);
|
||||
expect(
|
||||
await accessService.hasPermission(
|
||||
user,
|
||||
UPDATE_FEATURE_STRATEGY,
|
||||
projectName,
|
||||
defaultEnv,
|
||||
),
|
||||
).toBe(condition);
|
||||
expect(
|
||||
await accessService.hasPermission(
|
||||
user,
|
||||
DELETE_FEATURE_STRATEGY,
|
||||
projectName,
|
||||
defaultEnv,
|
||||
),
|
||||
).toBe(condition);
|
||||
expect(
|
||||
await accessService.hasPermission(
|
||||
user,
|
||||
UPDATE_FEATURE_ENVIRONMENT,
|
||||
projectName,
|
||||
defaultEnv,
|
||||
),
|
||||
).toBe(condition);
|
||||
expect(
|
||||
await accessService.hasPermission(
|
||||
user,
|
||||
CREATE_FEATURE_STRATEGY,
|
||||
projectName,
|
||||
developmentEnv,
|
||||
),
|
||||
).toBe(condition);
|
||||
expect(
|
||||
await accessService.hasPermission(
|
||||
user,
|
||||
UPDATE_FEATURE_STRATEGY,
|
||||
projectName,
|
||||
developmentEnv,
|
||||
),
|
||||
).toBe(condition);
|
||||
expect(
|
||||
await accessService.hasPermission(
|
||||
user,
|
||||
DELETE_FEATURE_STRATEGY,
|
||||
projectName,
|
||||
developmentEnv,
|
||||
),
|
||||
).toBe(condition);
|
||||
expect(
|
||||
await accessService.hasPermission(
|
||||
user,
|
||||
UPDATE_FEATURE_ENVIRONMENT,
|
||||
projectName,
|
||||
developmentEnv,
|
||||
),
|
||||
).toBe(condition);
|
||||
expect(
|
||||
await accessService.hasPermission(
|
||||
user,
|
||||
CREATE_FEATURE_STRATEGY,
|
||||
projectName,
|
||||
productionEnv,
|
||||
),
|
||||
).toBe(condition);
|
||||
expect(
|
||||
await accessService.hasPermission(
|
||||
user,
|
||||
UPDATE_FEATURE_STRATEGY,
|
||||
projectName,
|
||||
productionEnv,
|
||||
),
|
||||
).toBe(condition);
|
||||
expect(
|
||||
await accessService.hasPermission(
|
||||
user,
|
||||
DELETE_FEATURE_STRATEGY,
|
||||
projectName,
|
||||
productionEnv,
|
||||
),
|
||||
).toBe(condition);
|
||||
expect(
|
||||
await accessService.hasPermission(
|
||||
user,
|
||||
UPDATE_FEATURE_ENVIRONMENT,
|
||||
projectName,
|
||||
productionEnv,
|
||||
),
|
||||
).toBe(condition);
|
||||
};
|
||||
|
||||
const hasFullProjectAccess = async (user, projectName, condition) => {
|
||||
const { DELETE_PROJECT, UPDATE_PROJECT, MOVE_FEATURE_TOGGLE } = permissions;
|
||||
|
||||
expect(
|
||||
await accessService.hasPermission(user, DELETE_PROJECT, projectName),
|
||||
).toBe(condition);
|
||||
expect(
|
||||
await accessService.hasPermission(user, UPDATE_PROJECT, projectName),
|
||||
).toBe(condition);
|
||||
expect(
|
||||
await accessService.hasPermission(
|
||||
user,
|
||||
MOVE_FEATURE_TOGGLE,
|
||||
projectName,
|
||||
),
|
||||
);
|
||||
hasCommonProjectAccess(user, projectName, condition);
|
||||
};
|
||||
|
||||
const createSuperUser = async () => {
|
||||
const { userStore } = stores;
|
||||
const user = await userStore.insert({
|
||||
name: 'Alice Admin',
|
||||
email: 'admin@getunleash.io',
|
||||
});
|
||||
await accessService.addUserToRole(user.id, adminRole.id);
|
||||
await accessService.addUserToRole(user.id, adminRole.id, ALL_PROJECTS);
|
||||
return user;
|
||||
};
|
||||
|
||||
@ -41,11 +201,23 @@ beforeAll(async () => {
|
||||
db = await dbInit('access_service_serial', getLogger);
|
||||
stores = db.stores;
|
||||
// projectStore = stores.projectStore;
|
||||
const config = createTestConfig({
|
||||
getLogger,
|
||||
// @ts-ignore
|
||||
experimental: { environments: { enabled: true } },
|
||||
});
|
||||
accessService = new AccessService(stores, { getLogger });
|
||||
const roles = await accessService.getRootRoles();
|
||||
editorRole = roles.find((r) => r.name === RoleName.EDITOR);
|
||||
adminRole = roles.find((r) => r.name === RoleName.ADMIN);
|
||||
readRole = roles.find((r) => r.name === RoleName.VIEWER);
|
||||
featureToggleService = new FeatureToggleService(stores, config);
|
||||
projectService = new ProjectService(
|
||||
stores,
|
||||
config,
|
||||
accessService,
|
||||
featureToggleService,
|
||||
);
|
||||
|
||||
editorUser = await createUserEditorAccess('Bob Test', 'bob@getunleash.io');
|
||||
superUser = await createSuperUser();
|
||||
@ -106,45 +278,17 @@ test('should not have admin permission', async () => {
|
||||
expect(await accessService.hasPermission(user, ADMIN)).toBe(false);
|
||||
});
|
||||
|
||||
test('should have project admin to default project', async () => {
|
||||
const {
|
||||
DELETE_PROJECT,
|
||||
UPDATE_PROJECT,
|
||||
CREATE_FEATURE,
|
||||
UPDATE_FEATURE,
|
||||
DELETE_FEATURE,
|
||||
} = permissions;
|
||||
test('should have project admin to default project as editor', async () => {
|
||||
const projectName = 'default';
|
||||
|
||||
const user = editorUser;
|
||||
expect(
|
||||
await accessService.hasPermission(user, DELETE_PROJECT, 'default'),
|
||||
).toBe(true);
|
||||
expect(
|
||||
await accessService.hasPermission(user, UPDATE_PROJECT, 'default'),
|
||||
).toBe(true);
|
||||
expect(
|
||||
await accessService.hasPermission(user, CREATE_FEATURE, 'default'),
|
||||
).toBe(true);
|
||||
expect(
|
||||
await accessService.hasPermission(user, UPDATE_FEATURE, 'default'),
|
||||
).toBe(true);
|
||||
expect(
|
||||
await accessService.hasPermission(user, DELETE_FEATURE, 'default'),
|
||||
).toBe(true);
|
||||
hasFullProjectAccess(user, projectName, true);
|
||||
});
|
||||
|
||||
test('should grant member CREATE_FEATURE on all projects', async () => {
|
||||
const { CREATE_FEATURE } = permissions;
|
||||
test('should not have project admin to other projects as editor', async () => {
|
||||
const projectName = 'unusedprojectname';
|
||||
const user = editorUser;
|
||||
|
||||
await accessService.addPermissionToRole(
|
||||
editorRole.id,
|
||||
permissions.CREATE_FEATURE,
|
||||
ALL_PROJECTS,
|
||||
);
|
||||
|
||||
expect(
|
||||
await accessService.hasPermission(user, CREATE_FEATURE, 'some-project'),
|
||||
).toBe(true);
|
||||
hasFullProjectAccess(user, projectName, false);
|
||||
});
|
||||
|
||||
test('cannot add CREATE_FEATURE without defining project', async () => {
|
||||
@ -169,20 +313,21 @@ test('cannot remove CREATE_FEATURE without defining project', async () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('should remove CREATE_FEATURE on all projects', async () => {
|
||||
test('should remove CREATE_FEATURE on default environment', async () => {
|
||||
const { CREATE_FEATURE } = permissions;
|
||||
const user = editorUser;
|
||||
const editRole = await accessService.getRoleByName(RoleName.EDITOR);
|
||||
|
||||
await accessService.addPermissionToRole(
|
||||
editorRole.id,
|
||||
editRole.id,
|
||||
permissions.CREATE_FEATURE,
|
||||
ALL_PROJECTS,
|
||||
'*',
|
||||
);
|
||||
|
||||
await accessService.removePermissionFromRole(
|
||||
editorRole.id,
|
||||
editRole.id,
|
||||
permissions.CREATE_FEATURE,
|
||||
ALL_PROJECTS,
|
||||
'*',
|
||||
);
|
||||
|
||||
expect(
|
||||
@ -219,31 +364,10 @@ test('admin should be admin', async () => {
|
||||
});
|
||||
|
||||
test('should create default roles to project', async () => {
|
||||
const {
|
||||
DELETE_PROJECT,
|
||||
UPDATE_PROJECT,
|
||||
CREATE_FEATURE,
|
||||
UPDATE_FEATURE,
|
||||
DELETE_FEATURE,
|
||||
} = permissions;
|
||||
const project = 'some-project';
|
||||
const user = editorUser;
|
||||
await accessService.createDefaultProjectRoles(user, project);
|
||||
expect(
|
||||
await accessService.hasPermission(user, UPDATE_PROJECT, project),
|
||||
).toBe(true);
|
||||
expect(
|
||||
await accessService.hasPermission(user, DELETE_PROJECT, project),
|
||||
).toBe(true);
|
||||
expect(
|
||||
await accessService.hasPermission(user, CREATE_FEATURE, project),
|
||||
).toBe(true);
|
||||
expect(
|
||||
await accessService.hasPermission(user, UPDATE_FEATURE, project),
|
||||
).toBe(true);
|
||||
expect(
|
||||
await accessService.hasPermission(user, DELETE_FEATURE, project),
|
||||
).toBe(true);
|
||||
hasFullProjectAccess(user, project, true);
|
||||
});
|
||||
|
||||
test('should require name when create default roles to project', async () => {
|
||||
@ -253,38 +377,20 @@ test('should require name when create default roles to project', async () => {
|
||||
});
|
||||
|
||||
test('should grant user access to project', async () => {
|
||||
const {
|
||||
DELETE_PROJECT,
|
||||
UPDATE_PROJECT,
|
||||
CREATE_FEATURE,
|
||||
UPDATE_FEATURE,
|
||||
DELETE_FEATURE,
|
||||
} = permissions;
|
||||
const { DELETE_PROJECT, UPDATE_PROJECT } = permissions;
|
||||
const project = 'another-project';
|
||||
const user = editorUser;
|
||||
const sUser = await createUserEditorAccess(
|
||||
const sUser = await createUserViewerAccess(
|
||||
'Some Random',
|
||||
'random@getunleash.io',
|
||||
);
|
||||
await accessService.createDefaultProjectRoles(user, project);
|
||||
|
||||
const roles = await accessService.getRolesForProject(project);
|
||||
const projectRole = await accessService.getRoleByName(RoleName.MEMBER);
|
||||
await accessService.addUserToRole(sUser.id, projectRole.id, project);
|
||||
|
||||
const projectRole = roles.find(
|
||||
(r) => r.name === 'Member' && r.project === project,
|
||||
);
|
||||
await accessService.addUserToRole(sUser.id, projectRole.id);
|
||||
|
||||
// Should be able to update feature toggles inside the project
|
||||
expect(
|
||||
await accessService.hasPermission(sUser, CREATE_FEATURE, project),
|
||||
).toBe(true);
|
||||
expect(
|
||||
await accessService.hasPermission(sUser, UPDATE_FEATURE, project),
|
||||
).toBe(true);
|
||||
expect(
|
||||
await accessService.hasPermission(sUser, DELETE_FEATURE, project),
|
||||
).toBe(true);
|
||||
// // Should be able to update feature toggles inside the project
|
||||
hasCommonProjectAccess(sUser, project, true);
|
||||
|
||||
// Should not be able to admin the project itself.
|
||||
expect(
|
||||
@ -296,32 +402,20 @@ test('should grant user access to project', async () => {
|
||||
});
|
||||
|
||||
test('should not get access if not specifying project', async () => {
|
||||
const { CREATE_FEATURE, UPDATE_FEATURE, DELETE_FEATURE } = permissions;
|
||||
const project = 'another-project-2';
|
||||
const user = editorUser;
|
||||
const sUser = await createUserEditorAccess(
|
||||
const sUser = await createUserViewerAccess(
|
||||
'Some Random',
|
||||
'random22@getunleash.io',
|
||||
);
|
||||
await accessService.createDefaultProjectRoles(user, project);
|
||||
|
||||
const roles = await accessService.getRolesForProject(project);
|
||||
const projectRole = await accessService.getRoleByName(RoleName.MEMBER);
|
||||
|
||||
const projectRole = roles.find(
|
||||
(r) => r.name === 'Member' && r.project === project,
|
||||
);
|
||||
await accessService.addUserToRole(sUser.id, projectRole.id);
|
||||
await accessService.addUserToRole(sUser.id, projectRole.id, project);
|
||||
|
||||
// Should not be able to update feature toggles outside project
|
||||
expect(await accessService.hasPermission(sUser, CREATE_FEATURE)).toBe(
|
||||
false,
|
||||
);
|
||||
expect(await accessService.hasPermission(sUser, UPDATE_FEATURE)).toBe(
|
||||
false,
|
||||
);
|
||||
expect(await accessService.hasPermission(sUser, DELETE_FEATURE)).toBe(
|
||||
false,
|
||||
);
|
||||
hasCommonProjectAccess(sUser, undefined, false);
|
||||
});
|
||||
|
||||
test('should remove user from role', async () => {
|
||||
@ -331,14 +425,14 @@ test('should remove user from role', async () => {
|
||||
email: 'random123@getunleash.io',
|
||||
});
|
||||
|
||||
await accessService.addUserToRole(user.id, editorRole.id);
|
||||
await accessService.addUserToRole(user.id, editorRole.id, 'default');
|
||||
|
||||
// check user has one role
|
||||
const userRoles = await accessService.getRolesForUser(user.id);
|
||||
expect(userRoles.length).toBe(1);
|
||||
expect(userRoles[0].name).toBe(RoleName.EDITOR);
|
||||
|
||||
await accessService.removeUserFromRole(user.id, editorRole.id);
|
||||
await accessService.removeUserFromRole(user.id, editorRole.id, 'default');
|
||||
const userRolesAfterRemove = await accessService.getRolesForUser(user.id);
|
||||
expect(userRolesAfterRemove.length).toBe(0);
|
||||
});
|
||||
@ -350,12 +444,11 @@ test('should return role with users', async () => {
|
||||
email: 'random2223@getunleash.io',
|
||||
});
|
||||
|
||||
await accessService.addUserToRole(user.id, editorRole.id);
|
||||
|
||||
const roleWithUsers = await accessService.getRole(editorRole.id);
|
||||
await accessService.addUserToRole(user.id, editorRole.id, 'default');
|
||||
|
||||
const roleWithUsers = await accessService.getRoleData(editorRole.id);
|
||||
expect(roleWithUsers.role.name).toBe(RoleName.EDITOR);
|
||||
expect(roleWithUsers.users.length > 2).toBe(true);
|
||||
expect(roleWithUsers.users.length >= 2).toBe(true);
|
||||
expect(roleWithUsers.users.find((u) => u.id === user.id)).toBeTruthy();
|
||||
expect(
|
||||
roleWithUsers.users.find((u) => u.email === user.email),
|
||||
@ -369,39 +462,20 @@ test('should return role with permissions and users', async () => {
|
||||
email: 'random2244@getunleash.io',
|
||||
});
|
||||
|
||||
await accessService.addUserToRole(user.id, editorRole.id);
|
||||
await accessService.addUserToRole(user.id, editorRole.id, 'default');
|
||||
|
||||
const roleWithPermission = await accessService.getRole(editorRole.id);
|
||||
const roleWithPermission = await accessService.getRoleData(editorRole.id);
|
||||
|
||||
expect(roleWithPermission.role.name).toBe(RoleName.EDITOR);
|
||||
expect(roleWithPermission.permissions.length > 2).toBe(true);
|
||||
expect(
|
||||
roleWithPermission.permissions.find(
|
||||
(p) => p.permission === permissions.CREATE_PROJECT,
|
||||
(p) => p.name === permissions.CREATE_PROJECT,
|
||||
),
|
||||
).toBeTruthy();
|
||||
expect(roleWithPermission.users.length > 2).toBe(true);
|
||||
});
|
||||
|
||||
test('should return list of permissions', async () => {
|
||||
const p = await accessService.getPermissions();
|
||||
|
||||
const findPerm = (perm) => p.find((_) => _.name === perm);
|
||||
|
||||
const {
|
||||
DELETE_FEATURE,
|
||||
UPDATE_FEATURE,
|
||||
CREATE_FEATURE,
|
||||
UPDATE_PROJECT,
|
||||
CREATE_PROJECT,
|
||||
} = permissions;
|
||||
|
||||
expect(p.length > 2).toBe(true);
|
||||
expect(findPerm(CREATE_PROJECT).type).toBe('root');
|
||||
expect(findPerm(UPDATE_PROJECT).type).toBe('project');
|
||||
expect(findPerm(CREATE_FEATURE).type).toBe('project');
|
||||
expect(findPerm(UPDATE_FEATURE).type).toBe('project');
|
||||
expect(findPerm(DELETE_FEATURE).type).toBe('project');
|
||||
//This assert requires other tests to have run in this pack before length > 2 resolves to true
|
||||
// I've set this to be > 1, which allows us to run the test alone and should still satisfy the logic requirement
|
||||
expect(roleWithPermission.users.length > 1).toBe(true);
|
||||
});
|
||||
|
||||
test('should set root role for user', async () => {
|
||||
@ -414,9 +488,10 @@ test('should set root role for user', async () => {
|
||||
await accessService.setUserRootRole(user.id, editorRole.id);
|
||||
|
||||
const roles = await accessService.getRolesForUser(user.id);
|
||||
//To have duplicated roles like this may not may not be a hack. Needs some thought
|
||||
expect(roles[0].name).toBe(RoleName.EDITOR);
|
||||
|
||||
expect(roles.length).toBe(1);
|
||||
expect(roles[0].name).toBe(RoleName.EDITOR);
|
||||
});
|
||||
|
||||
test('should switch root role for user', async () => {
|
||||
@ -453,3 +528,250 @@ test('should not crash if user does not have permission', async () => {
|
||||
|
||||
expect(hasAccess).toBe(false);
|
||||
});
|
||||
|
||||
test('should support permission with "ALL" environment requirement', async () => {
|
||||
const { userStore, roleStore, accessStore } = stores;
|
||||
|
||||
const user = await userStore.insert({
|
||||
name: 'Some User',
|
||||
email: 'randomEnv1@getunleash.io',
|
||||
});
|
||||
|
||||
await accessService.setUserRootRole(user.id, readRole.id);
|
||||
|
||||
const customRole = await roleStore.create({
|
||||
name: 'Power user',
|
||||
roleType: 'custom',
|
||||
description: 'Grants access to modify all environments',
|
||||
});
|
||||
|
||||
const { CREATE_FEATURE_STRATEGY } = permissions;
|
||||
await accessStore.addPermissionsToRole(
|
||||
customRole.id,
|
||||
[CREATE_FEATURE_STRATEGY],
|
||||
'production',
|
||||
);
|
||||
await accessStore.addUserToRole(user.id, customRole.id, ALL_PROJECTS);
|
||||
|
||||
const hasAccess = await accessService.hasPermission(
|
||||
user,
|
||||
CREATE_FEATURE_STRATEGY,
|
||||
'default',
|
||||
'production',
|
||||
);
|
||||
|
||||
expect(hasAccess).toBe(true);
|
||||
|
||||
const hasNotAccess = await accessService.hasPermission(
|
||||
user,
|
||||
CREATE_FEATURE_STRATEGY,
|
||||
'default',
|
||||
'development',
|
||||
);
|
||||
expect(hasNotAccess).toBe(false);
|
||||
});
|
||||
|
||||
test('Should have access to create a strategy in an environment', async () => {
|
||||
const { CREATE_FEATURE_STRATEGY } = permissions;
|
||||
const user = editorUser;
|
||||
expect(
|
||||
await accessService.hasPermission(
|
||||
user,
|
||||
CREATE_FEATURE_STRATEGY,
|
||||
'default',
|
||||
'development',
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test('Should be denied access to create a strategy in an environment the user does not have access to', async () => {
|
||||
const { CREATE_FEATURE_STRATEGY } = permissions;
|
||||
const user = editorUser;
|
||||
expect(
|
||||
await accessService.hasPermission(
|
||||
user,
|
||||
CREATE_FEATURE_STRATEGY,
|
||||
'default',
|
||||
'noaccess',
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test('Should have access to edit a strategy in an environment', async () => {
|
||||
const { UPDATE_FEATURE_STRATEGY } = permissions;
|
||||
const user = editorUser;
|
||||
expect(
|
||||
await accessService.hasPermission(
|
||||
user,
|
||||
UPDATE_FEATURE_STRATEGY,
|
||||
'default',
|
||||
'development',
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test('Should have access to delete a strategy in an environment', async () => {
|
||||
const { DELETE_FEATURE_STRATEGY } = permissions;
|
||||
const user = editorUser;
|
||||
expect(
|
||||
await accessService.hasPermission(
|
||||
user,
|
||||
DELETE_FEATURE_STRATEGY,
|
||||
'default',
|
||||
'development',
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test('Should be denied access to delete a strategy in an environment the user does not have access to', async () => {
|
||||
const { DELETE_FEATURE_STRATEGY } = permissions;
|
||||
const user = editorUser;
|
||||
expect(
|
||||
await accessService.hasPermission(
|
||||
user,
|
||||
DELETE_FEATURE_STRATEGY,
|
||||
'default',
|
||||
'noaccess',
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test('Should be denied access to delete a role that is in use', async () => {
|
||||
const user = editorUser;
|
||||
|
||||
const project = {
|
||||
id: 'projectToUseRole',
|
||||
name: 'New project',
|
||||
description: 'Blah',
|
||||
};
|
||||
await projectService.createProject(project, user.id);
|
||||
|
||||
const projectMember = await stores.userStore.insert({
|
||||
name: 'CustomProjectMember',
|
||||
email: 'custom@getunleash.io',
|
||||
});
|
||||
|
||||
const customRole = await accessService.createRole({
|
||||
name: 'RoleInUse',
|
||||
description: '',
|
||||
permissions: [
|
||||
{
|
||||
id: 2,
|
||||
name: 'CREATE_FEATURE',
|
||||
environment: null,
|
||||
displayName: 'Create Feature Toggles',
|
||||
type: 'project',
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
name: 'DELETE_FEATURE',
|
||||
environment: null,
|
||||
displayName: 'Delete Feature Toggles',
|
||||
type: 'project',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await projectService.addUser(project.id, customRole.id, projectMember.id);
|
||||
|
||||
try {
|
||||
await accessService.deleteRole(customRole.id);
|
||||
} catch (e) {
|
||||
expect(e.toString()).toBe(
|
||||
'RoleInUseError: Role is in use by more than one user. You cannot delete a role that is in use without first removing the role from the users.',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('Should be denied move feature toggle to project where the user does not have access', async () => {
|
||||
const user = editorUser;
|
||||
const editorUser2 = await createUserEditorAccess(
|
||||
'seconduser',
|
||||
'bob2@gmail.com',
|
||||
);
|
||||
|
||||
const projectOrigin = {
|
||||
id: 'projectOrigin',
|
||||
name: 'New project',
|
||||
description: 'Blah',
|
||||
};
|
||||
const projectDest = {
|
||||
id: 'projectDest',
|
||||
name: 'New project',
|
||||
description: 'Blah',
|
||||
};
|
||||
await projectService.createProject(projectOrigin, user.id);
|
||||
await projectService.createProject(projectDest, editorUser2.id);
|
||||
|
||||
const featureToggle = { name: 'moveableToggle' };
|
||||
|
||||
await featureToggleService.createFeatureToggle(
|
||||
projectOrigin.id,
|
||||
featureToggle,
|
||||
user.username,
|
||||
);
|
||||
|
||||
try {
|
||||
await projectService.changeProject(
|
||||
projectDest.id,
|
||||
featureToggle.name,
|
||||
user,
|
||||
projectOrigin.id,
|
||||
);
|
||||
} catch (e) {
|
||||
expect(e.toString()).toBe(
|
||||
'NoAccessError: You need permission=MOVE_FEATURE_TOGGLE to perform this action',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('Should be allowed move feature toggle to project when the user has access', async () => {
|
||||
const user = editorUser;
|
||||
|
||||
const projectOrigin = {
|
||||
id: 'projectOrigin1',
|
||||
name: 'New project',
|
||||
description: 'Blah',
|
||||
};
|
||||
const projectDest = {
|
||||
id: 'projectDest2',
|
||||
name: 'New project',
|
||||
description: 'Blah',
|
||||
};
|
||||
await projectService.createProject(projectOrigin, user);
|
||||
await projectService.createProject(projectDest, user);
|
||||
|
||||
const featureToggle = { name: 'moveableToggle2' };
|
||||
|
||||
await featureToggleService.createFeatureToggle(
|
||||
projectOrigin.id,
|
||||
featureToggle,
|
||||
user.username,
|
||||
);
|
||||
|
||||
await projectService.changeProject(
|
||||
projectDest.id,
|
||||
featureToggle.name,
|
||||
user,
|
||||
projectOrigin.id,
|
||||
);
|
||||
});
|
||||
|
||||
test('Should not be allowed to edit a built in role', async () => {
|
||||
expect.assertions(1);
|
||||
|
||||
const editRole = await accessService.getRoleByName(RoleName.EDITOR);
|
||||
const roleUpdate = {
|
||||
id: editRole.id,
|
||||
name: 'NoLongerTheEditor',
|
||||
description: 'Ha!',
|
||||
};
|
||||
|
||||
try {
|
||||
await accessService.updateRole(roleUpdate);
|
||||
} catch (e) {
|
||||
expect(e.toString()).toBe(
|
||||
'InvalidOperationError: You can not change built in roles.',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
@ -3,12 +3,7 @@ import getLogger from '../../fixtures/no-logger';
|
||||
import FeatureToggleService from '../../../lib/services/feature-toggle-service';
|
||||
import ProjectService from '../../../lib/services/project-service';
|
||||
import { AccessService } from '../../../lib/services/access-service';
|
||||
import {
|
||||
CREATE_FEATURE,
|
||||
UPDATE_FEATURE,
|
||||
UPDATE_PROJECT,
|
||||
} from '../../../lib/types/permissions';
|
||||
import NotFoundError from '../../../lib/error/notfound-error';
|
||||
import { MOVE_FEATURE_TOGGLE } from '../../../lib/types/permissions';
|
||||
import { createTestConfig } from '../../config/test-config';
|
||||
import { RoleName } from '../../../lib/types/model';
|
||||
|
||||
@ -53,7 +48,12 @@ afterEach(async () => {
|
||||
.map(async (env) => {
|
||||
await stores.environmentStore.delete(env.name);
|
||||
});
|
||||
const users = await stores.userStore.getAll();
|
||||
const wipeUserPermissions = users.map(async (u) => {
|
||||
await stores.accessStore.unlinkUserRoles(u.id);
|
||||
});
|
||||
await Promise.allSettled(deleteEnvs);
|
||||
await Promise.allSettled(wipeUserPermissions);
|
||||
});
|
||||
|
||||
test('should have default project', async () => {
|
||||
@ -202,37 +202,6 @@ test('should give error when getting unknown project', async () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('(TODO: v4): should create roles for new project if userId is missing', async () => {
|
||||
const project = {
|
||||
id: 'test-roles-no-id',
|
||||
name: 'New project',
|
||||
description: 'Blah',
|
||||
};
|
||||
await projectService.createProject(project, {
|
||||
username: 'random-user',
|
||||
});
|
||||
const roles = await stores.accessStore.getRolesForProject(project.id);
|
||||
|
||||
expect(roles).toHaveLength(2);
|
||||
expect(
|
||||
await accessService.hasPermission(user, UPDATE_PROJECT, project.id),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test('should create roles when project is created', async () => {
|
||||
const project = {
|
||||
id: 'test-roles',
|
||||
name: 'New project',
|
||||
description: 'Blah',
|
||||
};
|
||||
await projectService.createProject(project, user);
|
||||
const roles = await stores.accessStore.getRolesForProject(project.id);
|
||||
expect(roles).toHaveLength(2);
|
||||
expect(
|
||||
await accessService.hasPermission(user, UPDATE_PROJECT, project.id),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test('should get list of users with access to project', async () => {
|
||||
const project = {
|
||||
id: 'test-roles-access',
|
||||
@ -240,13 +209,10 @@ test('should get list of users with access to project', async () => {
|
||||
description: 'Blah',
|
||||
};
|
||||
await projectService.createProject(project, user);
|
||||
const { roles, users } = await projectService.getUsersWithAccess(
|
||||
project.id,
|
||||
user,
|
||||
);
|
||||
const { users } = await projectService.getUsersWithAccess(project.id, user);
|
||||
|
||||
const owner = roles.find((role) => role.name === RoleName.OWNER);
|
||||
const member = roles.find((role) => role.name === RoleName.MEMBER);
|
||||
const member = await stores.roleStore.getRoleByName(RoleName.MEMBER);
|
||||
const owner = await stores.roleStore.getRoleByName(RoleName.OWNER);
|
||||
|
||||
expect(users).toHaveLength(1);
|
||||
expect(users[0].id).toBe(user.id);
|
||||
@ -272,8 +238,7 @@ test('should add a member user to the project', async () => {
|
||||
email: 'member2@getunleash.io',
|
||||
});
|
||||
|
||||
const roles = await stores.accessStore.getRolesForProject(project.id);
|
||||
const memberRole = roles.find((r) => r.name === RoleName.MEMBER);
|
||||
const memberRole = await stores.roleStore.getRoleByName(RoleName.MEMBER);
|
||||
|
||||
await projectService.addUser(project.id, memberRole.id, projectMember1.id);
|
||||
await projectService.addUser(project.id, memberRole.id, projectMember2.id);
|
||||
@ -305,10 +270,7 @@ test('should add admin users to the project', async () => {
|
||||
email: 'admin2@getunleash.io',
|
||||
});
|
||||
|
||||
const projectRoles = await stores.accessStore.getRolesForProject(
|
||||
project.id,
|
||||
);
|
||||
const ownerRole = projectRoles.find((r) => r.name === RoleName.OWNER);
|
||||
const ownerRole = await stores.roleStore.getRoleByName(RoleName.OWNER);
|
||||
|
||||
await projectService.addUser(project.id, ownerRole.id, projectAdmin1.id);
|
||||
await projectService.addUser(project.id, ownerRole.id, projectAdmin2.id);
|
||||
@ -324,15 +286,6 @@ test('should add admin users to the project', async () => {
|
||||
expect(adminUsers[2].name).toBe(projectAdmin2.name);
|
||||
});
|
||||
|
||||
test('add user only accept to add users to project roles', async () => {
|
||||
const roles = await accessService.getRoles();
|
||||
const memberRole = roles.find((r) => r.name === RoleName.MEMBER);
|
||||
|
||||
await expect(async () => {
|
||||
await projectService.addUser('some-id', memberRole.id, user.id);
|
||||
}).rejects.toThrowError(NotFoundError);
|
||||
});
|
||||
|
||||
test('add user should fail if user already have access', async () => {
|
||||
const project = {
|
||||
id: 'add-users-twice',
|
||||
@ -346,15 +299,14 @@ test('add user should fail if user already have access', async () => {
|
||||
email: 'member42@getunleash.io',
|
||||
});
|
||||
|
||||
const roles = await stores.accessStore.getRolesForProject(project.id);
|
||||
const memberRole = roles.find((r) => r.name === RoleName.MEMBER);
|
||||
const memberRole = await stores.roleStore.getRoleByName(RoleName.MEMBER);
|
||||
|
||||
await projectService.addUser(project.id, memberRole.id, projectMember1.id);
|
||||
|
||||
await expect(async () =>
|
||||
projectService.addUser(project.id, memberRole.id, projectMember1.id),
|
||||
).rejects.toThrow(
|
||||
new Error('User already have access to project=add-users-twice'),
|
||||
new Error('User already has access to project=add-users-twice'),
|
||||
);
|
||||
});
|
||||
|
||||
@ -371,8 +323,7 @@ test('should remove user from the project', async () => {
|
||||
email: 'member99@getunleash.io',
|
||||
});
|
||||
|
||||
const roles = await stores.accessStore.getRolesForProject(project.id);
|
||||
const memberRole = roles.find((r) => r.name === RoleName.MEMBER);
|
||||
const memberRole = await stores.roleStore.getRoleByName(RoleName.MEMBER);
|
||||
|
||||
await projectService.addUser(project.id, memberRole.id, projectMember1.id);
|
||||
await projectService.removeUser(
|
||||
@ -395,7 +346,7 @@ test('should not remove user from the project', async () => {
|
||||
};
|
||||
await projectService.createProject(project, user);
|
||||
|
||||
const roles = await stores.accessStore.getRolesForProject(project.id);
|
||||
const roles = await stores.roleStore.getRolesForProject(project.id);
|
||||
const ownerRole = roles.find((r) => r.name === RoleName.OWNER);
|
||||
|
||||
await expect(async () => {
|
||||
@ -426,7 +377,7 @@ test('should not change project if feature toggle project does not match current
|
||||
);
|
||||
} catch (err) {
|
||||
expect(err.message).toBe(
|
||||
`You need permission=${UPDATE_FEATURE} to perform this action`,
|
||||
`You need permission=${MOVE_FEATURE_TOGGLE} to perform this action`,
|
||||
);
|
||||
}
|
||||
});
|
||||
@ -487,7 +438,7 @@ test('should fail if user is not authorized', async () => {
|
||||
);
|
||||
} catch (err) {
|
||||
expect(err.message).toBe(
|
||||
`You need permission=${CREATE_FEATURE} to perform this action`,
|
||||
`You need permission=${MOVE_FEATURE_TOGGLE} to perform this action`,
|
||||
);
|
||||
}
|
||||
});
|
||||
@ -548,3 +499,156 @@ test('A newly created project only gets connected to enabled environments', asyn
|
||||
expect(connectedEnvs.some((e) => e === enabledEnv)).toBeTruthy();
|
||||
expect(connectedEnvs.some((e) => e === disabledEnv)).toBeFalsy();
|
||||
});
|
||||
|
||||
test('should add a user to the project with a custom role', async () => {
|
||||
const project = {
|
||||
id: 'add-users-custom-role',
|
||||
name: 'New project',
|
||||
description: 'Blah',
|
||||
};
|
||||
await projectService.createProject(project, user);
|
||||
|
||||
const projectMember1 = await stores.userStore.insert({
|
||||
name: 'Custom',
|
||||
email: 'custom@getunleash.io',
|
||||
});
|
||||
|
||||
const customRole = await accessService.createRole({
|
||||
name: 'Service Engineer2',
|
||||
description: '',
|
||||
permissions: [
|
||||
{
|
||||
id: 2,
|
||||
name: 'CREATE_FEATURE',
|
||||
environment: null,
|
||||
displayName: 'Create Feature Toggles',
|
||||
type: 'project',
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
name: 'DELETE_FEATURE',
|
||||
environment: null,
|
||||
displayName: 'Delete Feature Toggles',
|
||||
type: 'project',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await projectService.addUser(project.id, customRole.id, projectMember1.id);
|
||||
|
||||
const { users } = await projectService.getUsersWithAccess(project.id, user);
|
||||
|
||||
const customRoleMember = users.filter((u) => u.roleId === customRole.id);
|
||||
|
||||
expect(customRoleMember).toHaveLength(1);
|
||||
expect(customRoleMember[0].id).toBe(projectMember1.id);
|
||||
expect(customRoleMember[0].name).toBe(projectMember1.name);
|
||||
});
|
||||
|
||||
test('should delete role entries when deleting project', async () => {
|
||||
const project = {
|
||||
id: 'test-delete-users-1',
|
||||
name: 'New project',
|
||||
description: 'Blah',
|
||||
};
|
||||
|
||||
await projectService.createProject(project, user);
|
||||
|
||||
const user1 = await stores.userStore.insert({
|
||||
name: 'Projectuser1',
|
||||
email: 'project1@getunleash.io',
|
||||
});
|
||||
|
||||
const user2 = await stores.userStore.insert({
|
||||
name: 'Projectuser2',
|
||||
email: 'project2@getunleash.io',
|
||||
});
|
||||
|
||||
const customRole = await accessService.createRole({
|
||||
name: 'Service Engineer',
|
||||
description: '',
|
||||
permissions: [
|
||||
{
|
||||
id: 2,
|
||||
name: 'CREATE_FEATURE',
|
||||
environment: null,
|
||||
displayName: 'Create Feature Toggles',
|
||||
type: 'project',
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
name: 'DELETE_FEATURE',
|
||||
environment: null,
|
||||
displayName: 'Delete Feature Toggles',
|
||||
type: 'project',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await projectService.addUser(project.id, customRole.id, user1.id);
|
||||
await projectService.addUser(project.id, customRole.id, user2.id);
|
||||
|
||||
let usersForRole = await accessService.getUsersForRole(customRole.id);
|
||||
expect(usersForRole.length).toBe(2);
|
||||
|
||||
await projectService.deleteProject(project.id, user);
|
||||
usersForRole = await accessService.getUsersForRole(customRole.id);
|
||||
expect(usersForRole.length).toBe(0);
|
||||
});
|
||||
|
||||
test('should change a users role in the project', async () => {
|
||||
const project = {
|
||||
id: 'test-change-user-role',
|
||||
name: 'New project',
|
||||
description: 'Blah',
|
||||
};
|
||||
|
||||
await projectService.createProject(project, user);
|
||||
|
||||
const projectUser = await stores.userStore.insert({
|
||||
name: 'Projectuser3',
|
||||
email: 'project3@getunleash.io',
|
||||
});
|
||||
|
||||
const customRole = await accessService.createRole({
|
||||
name: 'Service Engineer3',
|
||||
description: '',
|
||||
permissions: [
|
||||
{
|
||||
id: 2,
|
||||
name: 'CREATE_FEATURE',
|
||||
environment: null,
|
||||
displayName: 'Create Feature Toggles',
|
||||
type: 'project',
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
name: 'DELETE_FEATURE',
|
||||
environment: null,
|
||||
displayName: 'Delete Feature Toggles',
|
||||
type: 'project',
|
||||
},
|
||||
],
|
||||
});
|
||||
const member = await stores.roleStore.getRoleByName(RoleName.MEMBER);
|
||||
|
||||
await projectService.addUser(project.id, member.id, projectUser.id);
|
||||
const { users } = await projectService.getUsersWithAccess(project.id, user);
|
||||
let memberUser = users.filter((u) => u.roleId === member.id);
|
||||
|
||||
expect(memberUser).toHaveLength(1);
|
||||
expect(memberUser[0].id).toBe(projectUser.id);
|
||||
expect(memberUser[0].name).toBe(projectUser.name);
|
||||
await projectService.removeUser(project.id, member.id, projectUser.id);
|
||||
await projectService.addUser(project.id, customRole.id, projectUser.id);
|
||||
|
||||
let { users: updatedUsers } = await projectService.getUsersWithAccess(
|
||||
project.id,
|
||||
user,
|
||||
);
|
||||
const customUser = updatedUsers.filter((u) => u.roleId === customRole.id);
|
||||
|
||||
expect(customUser).toHaveLength(1);
|
||||
expect(customUser[0].id).toBe(projectUser.id);
|
||||
expect(customUser[0].name).toBe(projectUser.name);
|
||||
});
|
||||
|
23
src/test/fixtures/access-service-mock.ts
vendored
23
src/test/fixtures/access-service-mock.ts
vendored
@ -4,12 +4,21 @@ import { AccessService } from '../../lib/services/access-service';
|
||||
import User from '../../lib/types/user';
|
||||
import noLoggerProvider from './no-logger';
|
||||
import { IRole } from '../../lib/types/stores/access-store';
|
||||
import { IPermission, IRoleData, IUserWithRole } from '../../lib/types/model';
|
||||
import {
|
||||
IAvailablePermissions,
|
||||
IRoleData,
|
||||
IUserWithRole,
|
||||
} from '../../lib/types/model';
|
||||
|
||||
class AccessServiceMock extends AccessService {
|
||||
constructor() {
|
||||
super(
|
||||
{ accessStore: undefined, userStore: undefined },
|
||||
{
|
||||
accessStore: undefined,
|
||||
userStore: undefined,
|
||||
roleStore: undefined,
|
||||
environmentStore: undefined,
|
||||
},
|
||||
{ getLogger: noLoggerProvider },
|
||||
);
|
||||
}
|
||||
@ -22,7 +31,7 @@ class AccessServiceMock extends AccessService {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
getPermissions(): IPermission[] {
|
||||
getPermissions(): Promise<IAvailablePermissions> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
@ -34,10 +43,6 @@ class AccessServiceMock extends AccessService {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
removeUserFromRole(userId: number, roleId: number): Promise<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
addPermissionToRole(
|
||||
roleId: number,
|
||||
permission: string,
|
||||
@ -58,10 +63,6 @@ class AccessServiceMock extends AccessService {
|
||||
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.');
|
||||
}
|
||||
|
60
src/test/fixtures/fake-access-store.ts
vendored
60
src/test/fixtures/fake-access-store.ts
vendored
@ -6,17 +6,60 @@ import {
|
||||
IUserPermission,
|
||||
IUserRole,
|
||||
} from '../../lib/types/stores/access-store';
|
||||
import { IAvailablePermissions, IPermission } from 'lib/types/model';
|
||||
|
||||
class AccessStoreMock implements IAccessStore {
|
||||
removeUserFromRole(
|
||||
userId: number,
|
||||
roleId: number,
|
||||
projectId: string,
|
||||
): Promise<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
wipePermissionsFromRole(role_id: number): Promise<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
unlinkUserRoles(userId: number): Promise<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
getRoleByName(name: string): Promise<IRole> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
getProjectUserIdsForRole(
|
||||
roleId: number,
|
||||
projectId?: string,
|
||||
): Promise<number[]> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
getProjectRoles(): Promise<IRole[]> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
addEnvironmentPermissionsToRole(
|
||||
role_id: number,
|
||||
permissions: IPermission[],
|
||||
): Promise<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
userPermissions: IUserPermission[] = [];
|
||||
|
||||
roles: IRole[] = [];
|
||||
|
||||
getAvailablePermissions(): Promise<IPermission[]> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
getPermissionsForUser(userId: Number): Promise<IUserPermission[]> {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
getPermissionsForRole(roleId: number): Promise<IUserPermission[]> {
|
||||
getPermissionsForRole(roleId: number): Promise<IPermission[]> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
@ -40,7 +83,7 @@ class AccessStoreMock implements IAccessStore {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
getUserIdsForRole(roleId: number): Promise<number[]> {
|
||||
getUserIdsForRole(roleId: number, projectId: string): Promise<number[]> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
@ -48,19 +91,6 @@ class AccessStoreMock implements IAccessStore {
|
||||
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[],
|
||||
|
74
src/test/fixtures/fake-role-store.ts
vendored
Normal file
74
src/test/fixtures/fake-role-store.ts
vendored
Normal file
@ -0,0 +1,74 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { ICustomRole } from 'lib/types/model';
|
||||
import { IRole, IUserRole } from 'lib/types/stores/access-store';
|
||||
import {
|
||||
ICustomRoleInsert,
|
||||
ICustomRoleUpdate,
|
||||
IRoleStore,
|
||||
} from 'lib/types/stores/role-store';
|
||||
|
||||
export default class FakeRoleStore implements IRoleStore {
|
||||
nameInUse(name: string, existingId: number): Promise<boolean> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
getAll(): Promise<ICustomRole[]> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
create(role: ICustomRoleInsert): Promise<ICustomRole> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
update(role: ICustomRoleUpdate): Promise<ICustomRole> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
delete(id: number): Promise<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
getRoles(): Promise<IRole[]> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
getRoleByName(name: string): 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.');
|
||||
}
|
||||
|
||||
getProjectRoles(): Promise<IRole[]> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
getRootRoles(): Promise<IRole[]> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
getRootRoleForAllUsers(): Promise<IUserRole[]> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
get(key: number): Promise<ICustomRole> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
exists(key: number): Promise<boolean> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
deleteAll(): Promise<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
}
|
2
src/test/fixtures/store.ts
vendored
2
src/test/fixtures/store.ts
vendored
@ -24,6 +24,7 @@ import FakeResetTokenStore from './fake-reset-token-store';
|
||||
import FakeFeatureToggleClientStore from './fake-feature-toggle-client-store';
|
||||
import FakeClientMetricsStoreV2 from './fake-client-metrics-store-v2';
|
||||
import FakeUserSplashStore from './fake-user-splash-store';
|
||||
import FakeRoleStore from './fake-role-store';
|
||||
|
||||
const createStores: () => IUnleashStores = () => {
|
||||
const db = {
|
||||
@ -59,6 +60,7 @@ const createStores: () => IUnleashStores = () => {
|
||||
resetTokenStore: new FakeResetTokenStore(),
|
||||
sessionStore: new FakeSessionStore(),
|
||||
userSplashStore: new FakeUserSplashStore(),
|
||||
roleStore: new FakeRoleStore(),
|
||||
};
|
||||
};
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user