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,
IUserRole,
} from '../types/stores/access-store';
import {
IAvailablePermissions,
IEnvironmentPermission,
IPermission,
} from 'lib/types/model';
const T = {
ROLE_USER: 'role_user',
ROLES: 'role_project',
ROLES: 'roles',
ROLE_PERMISSION: 'role_permission',
PERMISSIONS: 'permissions',
};
interface IPermissionRow {
id: number;
permission: string;
display_name: string;
environment: string;
}
export class AccessStore implements IAccessStore {
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> {
await this.db(T.ROLES).where({ id: key }).del();
}
@ -64,12 +91,56 @@ export class AccessStore implements IAccessStore {
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[]> {
const stopTimer = this.timer('getPermissionsForUser');
const rows = await this.db
.select('project', 'permission', 'environment')
.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);
stopTimer();
return rows;
@ -109,7 +180,7 @@ export class AccessStore implements IAccessStore {
async getRootRoles(): Promise<IRole[]> {
return this.db
.select(['id', 'name', 'type', 'project', 'description'])
.select(['id', 'name', 'type', 'description'])
.from<IRole>(T.ROLES)
.andWhere('type', 'root');
}
@ -138,10 +209,15 @@ 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,
});
}
@ -171,7 +247,6 @@ export class AccessStore implements IAccessStore {
async createRole(
name: string,
type: string,
project?: string,
description?: string,
): Promise<IRole> {
const [id] = await this.db(T.ROLES)
@ -179,7 +254,6 @@ export class AccessStore implements IAccessStore {
name,
description,
type,
project,
})
.returning('id');
return {
@ -187,22 +261,25 @@ export class AccessStore implements IAccessStore {
name,
description,
type,
project,
};
}
async addPermissionsToRole(
role_id: number,
permissions: string[],
projectId?: string,
environment?: string,
): 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,
project: projectId,
permission,
environment,
permission_id,
}));
return this.db.batchInsert(T.ROLE_PERMISSION, rows);
}

View File

@ -38,7 +38,13 @@ export default class RoleStore {
}
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]);
}
@ -47,12 +53,7 @@ export default class RoleStore {
}
async get(id: number): Promise<ICustomRole> {
const rows = await this.db
.select(COLUMNS)
.from(TABLE)
.where({ id })
.orderBy('name', 'asc');
const rows = await this.db.select(COLUMNS).from(TABLE).where({ id });
return this.mapRow(rows[0]);
}

View File

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

View File

@ -300,7 +300,7 @@ export default class ProjectService {
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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,13 +1,48 @@
exports.up = function (db, cb) {
db.runSql(
`
ALTER TABLE roles RENAME TO role_project;
CREATE TABLE roles (
id integer PRIMARY KEY NOT NULL,
name varchar(255),
description text
);
`,
CREATE TABLE IF NOT EXISTS permissions
( id SERIAL PRIMARY KEY,
permission VARCHAR(255) NOT NULL,
environment VARCHAR(255),
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,
);
};
@ -15,8 +50,6 @@ exports.up = function (db, cb) {
exports.down = function (db, cb) {
db.runSql(
`
DROP TABLE roles;
ALTER TABLE role_project RENAME TO roles;
`,
cb,
);

View File

@ -24,7 +24,7 @@ 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, ALL_PROJECTS);
return user;
};
@ -34,7 +34,7 @@ const createSuperUser = async () => {
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;
};
@ -384,27 +384,6 @@ test('should return role with permissions and users', async () => {
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 () => {
const { userStore } = stores;
const user = await userStore.insert({
@ -468,20 +447,16 @@ test('should support permission with "ALL" environment requirement', async () =>
const customRole = await accessStore.createRole(
'Power user',
'custom',
'default',
'Grants access to modify all environments',
);
const { CREATE_FEATURE_STRATEGY } = permissions;
await accessStore.addPermissionsToRole(
customRole.id,
[CREATE_FEATURE_STRATEGY],
'default',
ALL_PROJECTS,
'production',
);
await accessStore.addUserToRole(user.id, customRole.id);
await accessStore.addUserToRole(user.id, customRole.id, ALL_PROJECTS);
const hasAccess = await accessService.hasPermission(
user,
@ -498,7 +473,7 @@ test('should support permission with "ALL" environment requirement', async () =>
'default',
'development',
);
expect(hasNotAccess).toBe(true);
expect(hasNotAccess).toBe(false);
});
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 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() {
@ -22,7 +26,7 @@ class AccessServiceMock extends AccessService {
throw new Error('Method not implemented.');
}
getPermissions(): IPermission[] {
getPermissions(): Promise<IAvailablePermissions> {
throw new Error('Method not implemented.');
}

View File

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

View File

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