1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-07-21 13:47:39 +02:00

feat: Add api endpoints for roles and permissions list

This commit is contained in:
sighphyre 2021-12-13 11:14:26 +02:00 committed by Ivar Conradi Østhus
parent 817f960765
commit cb02ae9c92
No known key found for this signature in database
GPG Key ID: 31AC596886B0BD09
14 changed files with 221 additions and 81 deletions

View File

@ -9,13 +9,26 @@ import {
IUserPermission, IUserPermission,
IUserRole, IUserRole,
} from '../types/stores/access-store'; } from '../types/stores/access-store';
import {
IAvailablePermissions,
IEnvironmentPermission,
IPermission,
} from 'lib/types/model';
const T = { const T = {
ROLE_USER: 'role_user', ROLE_USER: 'role_user',
ROLES: 'role_project', ROLES: 'roles',
ROLE_PERMISSION: 'role_permission', ROLE_PERMISSION: 'role_permission',
PERMISSIONS: 'permissions',
}; };
interface IPermissionRow {
id: number;
permission: string;
display_name: string;
environment: string;
}
export class AccessStore implements IAccessStore { export class AccessStore implements IAccessStore {
private logger: Logger; private logger: Logger;
@ -33,6 +46,20 @@ export class AccessStore implements IAccessStore {
}); });
} }
async setupPermissionsForEnvironment(
environmentName: string,
permissions: string[],
): Promise<void> {
const rows = permissions.map((permission) => {
return {
permission: permission,
display_name: '',
environment: environmentName,
};
});
await this.db.batchInsert(T.PERMISSIONS, rows);
}
async delete(key: number): Promise<void> { async delete(key: number): Promise<void> {
await this.db(T.ROLES).where({ id: key }).del(); await this.db(T.ROLES).where({ id: key }).del();
} }
@ -64,12 +91,56 @@ export class AccessStore implements IAccessStore {
return Promise.resolve([]); return Promise.resolve([]);
} }
async getAvailablePermissions(): Promise<IAvailablePermissions> {
const rows = await this.db
.select(['id', 'permission', 'environment', 'display_name'])
.from(T.PERMISSIONS);
let projectPermissions: IPermission[] = [];
let rawEnvironments = new Map<string, IPermissionRow[]>();
for (let permission of rows) {
if (!permission.environment) {
projectPermissions.push(this.mapPermission(permission));
} else {
if (!rawEnvironments.get(permission.environment)) {
rawEnvironments.set(permission.environment, []);
}
rawEnvironments.get(permission.environment).push(permission);
}
}
let allEnvironmentPermissions: Array<IEnvironmentPermission> =
Array.from(rawEnvironments).map(
([environmentName, environmentPermissions]) => {
return {
environmentName: environmentName,
permissions: environmentPermissions.map(
this.mapPermission,
),
};
},
);
return {
project: projectPermissions,
environments: allEnvironmentPermissions,
};
}
mapPermission(permission: IPermissionRow): IPermission {
return {
id: permission.id,
name: permission.permission,
displayName: permission.display_name,
};
}
async getPermissionsForUser(userId: number): Promise<IUserPermission[]> { async getPermissionsForUser(userId: number): Promise<IUserPermission[]> {
const stopTimer = this.timer('getPermissionsForUser'); const stopTimer = this.timer('getPermissionsForUser');
const rows = await this.db const rows = await this.db
.select('project', 'permission', 'environment') .select('project', 'permission', 'environment')
.from<IUserPermission>(`${T.ROLE_PERMISSION} AS rp`) .from<IUserPermission>(`${T.ROLE_PERMISSION} AS rp`)
.leftJoin(`${T.ROLE_USER} AS ur`, 'ur.role_id', 'rp.role_id') .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); .where('ur.user_id', '=', userId);
stopTimer(); stopTimer();
return rows; return rows;
@ -109,7 +180,7 @@ export class AccessStore implements IAccessStore {
async getRootRoles(): Promise<IRole[]> { async getRootRoles(): Promise<IRole[]> {
return this.db return this.db
.select(['id', 'name', 'type', 'project', 'description']) .select(['id', 'name', 'type', 'description'])
.from<IRole>(T.ROLES) .from<IRole>(T.ROLES)
.andWhere('type', 'root'); .andWhere('type', 'root');
} }
@ -138,10 +209,15 @@ export class AccessStore implements IAccessStore {
return rows.map((r) => r.user_id); 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({ return this.db(T.ROLE_USER).insert({
user_id: userId, user_id: userId,
role_id: roleId, role_id: roleId,
project: projecId,
}); });
} }
@ -171,7 +247,6 @@ export class AccessStore implements IAccessStore {
async createRole( async createRole(
name: string, name: string,
type: string, type: string,
project?: string,
description?: string, description?: string,
): Promise<IRole> { ): Promise<IRole> {
const [id] = await this.db(T.ROLES) const [id] = await this.db(T.ROLES)
@ -179,7 +254,6 @@ export class AccessStore implements IAccessStore {
name, name,
description, description,
type, type,
project,
}) })
.returning('id'); .returning('id');
return { return {
@ -187,22 +261,25 @@ export class AccessStore implements IAccessStore {
name, name,
description, description,
type, type,
project,
}; };
} }
async addPermissionsToRole( async addPermissionsToRole(
role_id: number, role_id: number,
permissions: string[], permissions: string[],
projectId?: string,
environment?: string, environment?: string,
): Promise<void> { ): Promise<void> {
const rows = permissions.map((permission) => ({ const result = await this.db.raw(
`SELECT id FROM ${T.PERMISSIONS} where environment = ? and permission = ANY(?)`,
[environment, permissions],
);
const ids = result.rows.map((x) => x.id);
const rows = ids.map((permission_id) => ({
role_id, role_id,
project: projectId, permission_id,
permission,
environment,
})); }));
return this.db.batchInsert(T.ROLE_PERMISSION, rows); return this.db.batchInsert(T.ROLE_PERMISSION, rows);
} }

View File

@ -38,7 +38,13 @@ export default class RoleStore {
} }
async create(role: ICustomRoleInsert): Promise<ICustomRole> { async create(role: ICustomRoleInsert): Promise<ICustomRole> {
const row = await this.db(TABLE).insert(role).returning('*'); const row = await this.db(TABLE)
.insert({
name: role.name,
description: role.description,
type: role.roleType,
})
.returning('*');
return this.mapRow(row[0]); return this.mapRow(row[0]);
} }
@ -47,12 +53,7 @@ export default class RoleStore {
} }
async get(id: number): Promise<ICustomRole> { async get(id: number): Promise<ICustomRole> {
const rows = await this.db const rows = await this.db.select(COLUMNS).from(TABLE).where({ id });
.select(COLUMNS)
.from(TABLE)
.where({ id })
.orderBy('name', 'asc');
return this.mapRow(rows[0]); return this.mapRow(rows[0]);
} }

View File

@ -10,10 +10,10 @@ import { IUserStore } from '../types/stores/user-store';
import { Logger } from '../logger'; import { Logger } from '../logger';
import { IUnleashStores } from '../types/stores'; import { IUnleashStores } from '../types/stores';
import { import {
IAvailablePermissions,
IPermission, IPermission,
IRoleData, IRoleData,
IUserWithRole, IUserWithRole,
PermissionType,
RoleName, RoleName,
RoleType, RoleType,
} from '../types/model'; } from '../types/model';
@ -63,12 +63,12 @@ export class AccessService {
this.store = accessStore; this.store = accessStore;
this.userStore = userStore; this.userStore = userStore;
this.logger = getLogger('/services/access-service.ts'); this.logger = getLogger('/services/access-service.ts');
this.permissions = Object.values(permissions).map((p) => ({ // this.permissions = Object.values(permissions).map((p) => ({
name: p, // name: p,
type: isProjectPermission(p) // type: isProjectPermission(p)
? PermissionType.project // ? PermissionType.project
: PermissionType.root, // : PermissionType.root,
})); // }));
} }
/** /**
@ -90,6 +90,8 @@ export class AccessService {
try { try {
const userP = await this.getPermissionsForUser(user); const userP = await this.getPermissionsForUser(user);
console.log('My checks are', permission, projectId, environment);
console.log('My permissions for user are', userP);
return userP return userP
.filter( .filter(
@ -126,12 +128,16 @@ export class AccessService {
return this.store.getPermissionsForUser(user.id); return this.store.getPermissionsForUser(user.id);
} }
getPermissions(): IPermission[] { async getPermissions(): Promise<IAvailablePermissions> {
return this.permissions; return this.store.getAvailablePermissions();
} }
async addUserToRole(userId: number, roleId: number): Promise<void> { async addUserToRole(
return this.store.addUserToRole(userId, roleId); userId: number,
roleId: number,
projectId: string,
): Promise<void> {
return this.store.addUserToRole(userId, roleId, projectId);
} }
async setUserRootRole( async setUserRootRole(
@ -139,14 +145,17 @@ export class AccessService {
role: number | RoleName, role: number | RoleName,
): Promise<void> { ): Promise<void> {
const newRootRole = await this.resolveRootRole(role); const newRootRole = await this.resolveRootRole(role);
if (newRootRole) { if (newRootRole) {
try { try {
await this.store.removeRolesOfTypeForUser( await this.store.removeRolesOfTypeForUser(
userId, userId,
RoleType.ROOT, RoleType.ROOT,
); );
await this.store.addUserToRole(userId, newRootRole.id); await this.store.addUserToRole(
userId,
newRootRole.id,
ALL_PROJECTS,
);
} catch (error) { } catch (error) {
throw new Error( throw new Error(
`Could not add role=${newRootRole.name} to userId=${userId}`, `Could not add role=${newRootRole.name} to userId=${userId}`,
@ -251,7 +260,6 @@ export class AccessService {
const ownerRole = await this.store.createRole( const ownerRole = await this.store.createRole(
RoleName.OWNER, RoleName.OWNER,
RoleType.PROJECT, RoleType.PROJECT,
projectId,
PROJECT_DESCRIPTION.OWNER, PROJECT_DESCRIPTION.OWNER,
); );
await this.store.addPermissionsToRole( await this.store.addPermissionsToRole(
@ -265,7 +273,7 @@ export class AccessService {
this.logger.info( this.logger.info(
`Making ${owner.id} admin of ${projectId} via roleId=${ownerRole.id}`, `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( const memberRole = await this.store.createRole(
RoleName.MEMBER, RoleName.MEMBER,

View File

@ -300,7 +300,7 @@ export default class ProjectService {
throw new Error(`User already has 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 // TODO: should be an event too

View File

@ -21,6 +21,10 @@ export default class RoleService {
return this.store.getAll(); return this.store.getAll();
} }
async get(id: number): Promise<ICustomRole> {
return this.store.get(id);
}
async create(role: ICustomRoleInsert): Promise<ICustomRole> { async create(role: ICustomRoleInsert): Promise<ICustomRole> {
return this.store.create(role); return this.store.create(role);
} }

View File

@ -133,14 +133,19 @@ class UserService {
const user = await this.store.insert({ const user = await this.store.insert({
username: 'admin', username: 'admin',
}); });
this.logger.info(`Did the thing"`);
const passwordHash = await bcrypt.hash(pwd, saltRounds); const passwordHash = await bcrypt.hash(pwd, saltRounds);
this.logger.info(`Bcrypted that beast"`);
await this.store.setPasswordHash(user.id, passwordHash); await this.store.setPasswordHash(user.id, passwordHash);
this.logger.info(`Set the hash"`);
await this.accessService.setUserRootRole( await this.accessService.setUserRootRole(
user.id, user.id,
RoleName.ADMIN, RoleName.ADMIN,
); );
} catch (e) { } catch (e) {
console.log(`My error ${e}`);
console.log(e);
this.logger.error(e);
this.logger.error('Unable to create default user "admin"'); this.logger.error('Unable to create default user "admin"');
} }
} }

View File

@ -215,9 +215,20 @@ export interface IRoleData {
permissions: IUserPermission[]; permissions: IUserPermission[];
} }
export interface IAvailablePermissions {
project: IPermission[];
environments: IEnvironmentPermission[];
}
export interface IPermission { export interface IPermission {
id: number;
name: string; name: string;
type: PermissionType; displayName: string;
}
export interface IEnvironmentPermission {
environmentName: string;
permissions: IPermission[];
} }
export enum PermissionType { export enum PermissionType {

View File

@ -1,3 +1,4 @@
import { IAvailablePermissions } from '../model';
import { Store } from './store'; import { Store } from './store';
export interface IUserPermission { export interface IUserPermission {
@ -11,7 +12,6 @@ export interface IRole {
name: string; name: string;
description?: string; description?: string;
type: string; type: string;
project?: string;
} }
export interface IUserRole { export interface IUserRole {
@ -19,6 +19,7 @@ export interface IUserRole {
userId: number; userId: number;
} }
export interface IAccessStore extends Store<IRole, number> { export interface IAccessStore extends Store<IRole, number> {
getAvailablePermissions(): Promise<IAvailablePermissions>;
getPermissionsForUser(userId: number): Promise<IUserPermission[]>; getPermissionsForUser(userId: number): Promise<IUserPermission[]>;
getPermissionsForRole(roleId: number): Promise<IUserPermission[]>; getPermissionsForRole(roleId: number): Promise<IUserPermission[]>;
getRoles(): Promise<IRole[]>; getRoles(): Promise<IRole[]>;
@ -27,7 +28,15 @@ export interface IAccessStore extends Store<IRole, number> {
removeRolesForProject(projectId: string): Promise<void>; removeRolesForProject(projectId: string): Promise<void>;
getRolesForUserId(userId: number): Promise<IRole[]>; getRolesForUserId(userId: number): Promise<IRole[]>;
getUserIdsForRole(roleId: number): Promise<number[]>; getUserIdsForRole(roleId: number): Promise<number[]>;
addUserToRole(userId: number, roleId: number): Promise<void>; setupPermissionsForEnvironment(
environmentName: string,
permissions: string[],
): Promise<void>;
addUserToRole(
userId: number,
roleId: number,
projectId: string,
): Promise<void>;
removeUserFromRole(userId: number, roleId: number): Promise<void>; removeUserFromRole(userId: number, roleId: number): Promise<void>;
removeRolesOfTypeForUser(userId: number, roleType: string): Promise<void>; removeRolesOfTypeForUser(userId: number, roleType: string): Promise<void>;
createRole( createRole(
@ -39,7 +48,6 @@ export interface IAccessStore extends Store<IRole, number> {
addPermissionsToRole( addPermissionsToRole(
role_id: number, role_id: number,
permissions: string[], permissions: string[],
projectId?: string,
environment?: string, environment?: string,
): Promise<void>; ): Promise<void>;
removePermissionFromRole( removePermissionFromRole(

View File

@ -4,6 +4,7 @@ import { Store } from './store';
export interface ICustomRoleInsert { export interface ICustomRoleInsert {
name: string; name: string;
description: string; description: string;
roleType: string;
} }
export interface IRoleStore extends Store<ICustomRole, number> { export interface IRoleStore extends Store<ICustomRole, number> {

View File

@ -1,13 +1,48 @@
exports.up = function (db, cb) { exports.up = function (db, cb) {
db.runSql( db.runSql(
` `
ALTER TABLE roles RENAME TO role_project; CREATE TABLE IF NOT EXISTS permissions
CREATE TABLE roles ( ( id SERIAL PRIMARY KEY,
id integer PRIMARY KEY NOT NULL, permission VARCHAR(255) NOT NULL,
name varchar(255), environment VARCHAR(255),
description text display_name TEXT,
); created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
`, );
INSERT INTO permissions (permission, environment, display_name) (SELECT DISTINCT permission, environment, '' from role_permission);
ALTER TABLE role_user ADD COLUMN
project VARCHAR(255);
UPDATE role_user
SET project = roles.project
FROM roles
WHERE role_user.role_id = roles.id;
ALTER TABLE roles DROP COLUMN project;
ALTER TABLE roles
ADD COLUMN
updated_at TIMESTAMP WITH TIME ZONE;
ALTER TABLE role_permission
ADD COLUMN
permission_id INTEGER;
UPDATE role_permission
SET permission_id = permissions.id
FROM permissions
WHERE
(role_permission.environment = permissions.environment
OR (role_permission.environment IS NULL AND permissions.environment IS NULL))
AND
role_permission.permission = permissions.permission;
ALTER TABLE role_permission
DROP COLUMN project,
DROP COLUMN permission,
DROP COLUMN environment
`,
cb, cb,
); );
}; };
@ -15,8 +50,6 @@ exports.up = function (db, cb) {
exports.down = function (db, cb) { exports.down = function (db, cb) {
db.runSql( db.runSql(
` `
DROP TABLE roles;
ALTER TABLE role_project RENAME TO roles;
`, `,
cb, cb,
); );

View File

@ -24,7 +24,7 @@ let readRole;
const createUserEditorAccess = async (name, email) => { const createUserEditorAccess = async (name, email) => {
const { userStore } = stores; const { userStore } = stores;
const user = await userStore.insert({ name, email }); const user = await userStore.insert({ name, email });
await accessService.addUserToRole(user.id, editorRole.id); await accessService.addUserToRole(user.id, editorRole.id, ALL_PROJECTS);
return user; return user;
}; };
@ -34,7 +34,7 @@ const createSuperUser = async () => {
name: 'Alice Admin', name: 'Alice Admin',
email: 'admin@getunleash.io', email: 'admin@getunleash.io',
}); });
await accessService.addUserToRole(user.id, adminRole.id); await accessService.addUserToRole(user.id, adminRole.id, ALL_PROJECTS);
return user; return user;
}; };
@ -384,27 +384,6 @@ test('should return role with permissions and users', async () => {
expect(roleWithPermission.users.length > 2).toBe(true); 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');
});
test('should set root role for user', async () => { test('should set root role for user', async () => {
const { userStore } = stores; const { userStore } = stores;
const user = await userStore.insert({ const user = await userStore.insert({
@ -468,20 +447,16 @@ test('should support permission with "ALL" environment requirement', async () =>
const customRole = await accessStore.createRole( const customRole = await accessStore.createRole(
'Power user', 'Power user',
'custom', 'custom',
'default',
'Grants access to modify all environments', 'Grants access to modify all environments',
); );
const { CREATE_FEATURE_STRATEGY } = permissions; const { CREATE_FEATURE_STRATEGY } = permissions;
await accessStore.addPermissionsToRole( await accessStore.addPermissionsToRole(
customRole.id, customRole.id,
[CREATE_FEATURE_STRATEGY], [CREATE_FEATURE_STRATEGY],
'default', 'production',
ALL_PROJECTS,
); );
await accessStore.addUserToRole(user.id, customRole.id, ALL_PROJECTS);
await accessStore.addUserToRole(user.id, customRole.id);
const hasAccess = await accessService.hasPermission( const hasAccess = await accessService.hasPermission(
user, user,
@ -498,7 +473,7 @@ test('should support permission with "ALL" environment requirement', async () =>
'default', 'default',
'development', 'development',
); );
expect(hasNotAccess).toBe(true); expect(hasNotAccess).toBe(false);
}); });
test('Should have access to create a strategy in an environment', async () => { test('Should have access to create a strategy in an environment', async () => {

View File

@ -4,7 +4,11 @@ import { AccessService } from '../../lib/services/access-service';
import User from '../../lib/types/user'; import User from '../../lib/types/user';
import noLoggerProvider from './no-logger'; import noLoggerProvider from './no-logger';
import { IRole } from '../../lib/types/stores/access-store'; 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 { class AccessServiceMock extends AccessService {
constructor() { constructor() {
@ -22,7 +26,7 @@ class AccessServiceMock extends AccessService {
throw new Error('Method not implemented.'); throw new Error('Method not implemented.');
} }
getPermissions(): IPermission[] { getPermissions(): Promise<IAvailablePermissions> {
throw new Error('Method not implemented.'); throw new Error('Method not implemented.');
} }

View File

@ -6,12 +6,24 @@ import {
IUserPermission, IUserPermission,
IUserRole, IUserRole,
} from '../../lib/types/stores/access-store'; } from '../../lib/types/stores/access-store';
import { IAvailablePermissions, IPermission } from 'lib/types/model';
class AccessStoreMock implements IAccessStore { class AccessStoreMock implements IAccessStore {
setupPermissionsForEnvironment(
environmentName: string,
permissions: string[],
): Promise<void> {
throw new Error('Method not implemented.');
}
userPermissions: IUserPermission[] = []; userPermissions: IUserPermission[] = [];
roles: IRole[] = []; roles: IRole[] = [];
getAvailablePermissions(): Promise<IAvailablePermissions> {
throw new Error('Method not implemented.');
}
getPermissionsForUser(userId: Number): Promise<IUserPermission[]> { getPermissionsForUser(userId: Number): Promise<IUserPermission[]> {
return Promise.resolve([]); return Promise.resolve([]);
} }

View File

@ -15,6 +15,7 @@ export default class FakeRoleStore implements IRoleStore {
createdAt: new Date(), createdAt: new Date(),
}); });
} }
async getAll(): Promise<ICustomRole[]> { async getAll(): Promise<ICustomRole[]> {
return Promise.resolve([ return Promise.resolve([
{ {