1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-07-07 01:16:28 +02:00

chore: Implement scaffolding for new rbac

This commit is contained in:
sighphyre 2021-12-03 10:18:26 +02:00 committed by Ivar Conradi Østhus
parent 0129f23e97
commit 2eb0b6a11e
No known key found for this signature in database
GPG Key ID: 31AC596886B0BD09
15 changed files with 231 additions and 4 deletions

View File

@ -12,7 +12,7 @@ import {
const T = { const T = {
ROLE_USER: 'role_user', ROLE_USER: 'role_user',
ROLES: 'roles', ROLES: 'role_project',
ROLE_PERMISSION: 'role_permission', ROLE_PERMISSION: 'role_permission',
}; };

View File

@ -27,6 +27,7 @@ import FeatureTagStore from './feature-tag-store';
import { FeatureEnvironmentStore } from './feature-environment-store'; import { FeatureEnvironmentStore } from './feature-environment-store';
import { ClientMetricsStoreV2 } from './client-metrics-store-v2'; import { ClientMetricsStoreV2 } from './client-metrics-store-v2';
import UserSplashStore from './user-splash-store'; import UserSplashStore from './user-splash-store';
import RoleStore from './role-store';
export const createStores = ( export const createStores = (
config: IUnleashConfig, config: IUnleashConfig,
@ -77,6 +78,7 @@ export const createStores = (
getLogger, getLogger,
), ),
userSplashStore: new UserSplashStore(db, eventBus, getLogger), userSplashStore: new UserSplashStore(db, eventBus, getLogger),
roleStore: new RoleStore(db, eventBus, getLogger),
}; };
}; };

View File

@ -88,7 +88,7 @@ class ProjectStore implements IProjectStore {
const row = await this.db(TABLE) const row = await this.db(TABLE)
.insert(this.fieldToRow(project)) .insert(this.fieldToRow(project))
.returning('*'); .returning('*');
return this.mapRow(row); return this.mapRow(row[0]);
} }
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types

88
src/lib/db/role-store.ts Normal file
View File

@ -0,0 +1,88 @@
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 } from 'lib/types/stores/role-store';
const TABLE = 'roles';
const COLUMNS = ['id', 'name', 'description', 'created_at'];
interface IRoleRow {
id: number;
name: string;
description: string;
created_at: Date;
}
export default class RoleStore {
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(TABLE)
.orderBy('name', 'asc');
return rows.map(this.mapRow);
}
async create(role: ICustomRoleInsert): Promise<ICustomRole> {
const row = await this.db(TABLE).insert(role).returning('*');
return this.mapRow(row[0]);
}
async delete(id: number): Promise<void> {
return this.db(TABLE).where({ id }).del();
}
async get(id: number): Promise<ICustomRole> {
const rows = await this.db
.select(COLUMNS)
.from(TABLE)
.where({ id })
.orderBy('name', 'asc');
return this.mapRow(rows[0]);
}
async exists(id: number): Promise<boolean> {
const result = await this.db.raw(
`SELECT EXISTS (SELECT 1 FROM ${TABLE} WHERE id = ?) AS present`,
[id],
);
const { present } = result.rows[0];
return present;
}
async deleteAll(): Promise<void> {
return this.db(TABLE).del();
}
mapRow(row: IRoleRow): ICustomRole {
if (!row) {
throw new NotFoundError('No project found');
}
return {
id: row.id,
name: row.name,
description: row.description,
createdAt: row.created_at,
};
}
destroy(): void {}
}
module.exports = RoleStore;

View File

@ -9,7 +9,10 @@ import {
CREATE_FEATURE, CREATE_FEATURE,
DELETE_FEATURE, DELETE_FEATURE,
CREATE_FEATURE_STRATEGY, CREATE_FEATURE_STRATEGY,
DELETE_FEATURE_STRATEGY,
UPDATE_FEATURE, UPDATE_FEATURE,
UPDATE_FEATURE_ENVIRONMENT,
UPDATE_FEATURE_STRATEGY,
} from '../../../types/permissions'; } from '../../../types/permissions';
import { import {
FeatureToggleDTO, FeatureToggleDTO,

View File

@ -28,6 +28,7 @@ import EnvironmentService from './environment-service';
import FeatureTagService from './feature-tag-service'; import FeatureTagService from './feature-tag-service';
import ProjectHealthService from './project-health-service'; import ProjectHealthService from './project-health-service';
import UserSplashService from './user-splash-service'; import UserSplashService from './user-splash-service';
import RoleService from './role-service';
export const createServices = ( export const createServices = (
stores: IUnleashStores, stores: IUnleashStores,
@ -75,6 +76,8 @@ export const createServices = (
); );
const userSplashService = new UserSplashService(stores, config); const userSplashService = new UserSplashService(stores, config);
const roleService = new RoleService(stores, config);
return { return {
accessService, accessService,
addonService, addonService,
@ -103,6 +106,7 @@ export const createServices = (
featureTagService, featureTagService,
projectHealthService, projectHealthService,
userSplashService, userSplashService,
roleService,
}; };
}; };

View File

@ -297,7 +297,7 @@ export default class ProjectService {
const alreadyHasAccess = users.some((u) => u.id === userId); const alreadyHasAccess = users.some((u) => u.id === userId);
if (alreadyHasAccess) { 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);

View File

@ -0,0 +1,31 @@
import { IUnleashConfig } from 'lib/server-impl';
import { IUnleashStores } from 'lib/types';
import { ICustomRole } from 'lib/types/model';
import { ICustomRoleInsert, IRoleStore } from 'lib/types/stores/role-store';
import { Logger } from '../logger';
export default class RoleService {
private logger: Logger;
private store: IRoleStore;
constructor(
{ roleStore }: Pick<IUnleashStores, 'roleStore'>,
{ getLogger }: Pick<IUnleashConfig, 'getLogger'>,
) {
this.logger = getLogger('lib/services/session-service.ts');
this.store = roleStore;
}
async getAll(): Promise<ICustomRole[]> {
return this.store.getAll();
}
async create(role: ICustomRoleInsert): Promise<ICustomRole> {
return this.store.create(role);
}
async delete(id: number): Promise<void> {
return this.store.delete(id);
}
}

View File

@ -313,6 +313,13 @@ export interface IProject {
updatedAt?: Date; updatedAt?: Date;
} }
export interface ICustomRole {
id: number;
name: string;
description: string;
createdAt: Date;
}
export interface IProjectWithCount extends IProject { export interface IProjectWithCount extends IProject {
featureCount: number; featureCount: number;
memberCount: number; memberCount: number;

View File

@ -24,6 +24,7 @@ import FeatureTagService from '../services/feature-tag-service';
import ProjectHealthService from '../services/project-health-service'; import ProjectHealthService from '../services/project-health-service';
import ClientMetricsServiceV2 from '../services/client-metrics/metrics-service-v2'; import ClientMetricsServiceV2 from '../services/client-metrics/metrics-service-v2';
import UserSplashService from '../services/user-splash-service'; import UserSplashService from '../services/user-splash-service';
import RoleService from 'lib/services/role-service';
export interface IUnleashServices { export interface IUnleashServices {
accessService: AccessService; accessService: AccessService;
@ -53,4 +54,5 @@ export interface IUnleashServices {
userService: UserService; userService: UserService;
versionService: VersionService; versionService: VersionService;
userSplashService: UserSplashService; userSplashService: UserSplashService;
roleService: RoleService;
} }

View File

@ -23,6 +23,7 @@ import { IEnvironmentStore } from './stores/environment-store';
import { IFeatureToggleClientStore } from './stores/feature-toggle-client-store'; import { IFeatureToggleClientStore } from './stores/feature-toggle-client-store';
import { IClientMetricsStoreV2 } from './stores/client-metrics-store-v2'; import { IClientMetricsStoreV2 } from './stores/client-metrics-store-v2';
import { IUserSplashStore } from './stores/user-splash-store'; import { IUserSplashStore } from './stores/user-splash-store';
import { IRoleStore } from './stores/role-store';
export interface IUnleashStores { export interface IUnleashStores {
accessStore: IAccessStore; accessStore: IAccessStore;
@ -50,4 +51,5 @@ export interface IUnleashStores {
userFeedbackStore: IUserFeedbackStore; userFeedbackStore: IUserFeedbackStore;
userStore: IUserStore; userStore: IUserStore;
userSplashStore: IUserSplashStore; userSplashStore: IUserSplashStore;
roleStore: IRoleStore;
} }

View File

@ -0,0 +1,13 @@
import { ICustomRole } from '../model';
import { Store } from './store';
export interface ICustomRoleInsert {
name: string;
description: string;
}
export interface IRoleStore extends Store<ICustomRole, number> {
getAll(): Promise<ICustomRole[]>;
create(role: ICustomRoleInsert): Promise<ICustomRole>;
delete(id: number): Promise<void>;
}

View File

@ -0,0 +1,23 @@
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
);
`,
cb,
);
};
exports.down = function (db, cb) {
db.runSql(
`
DROP TABLE roles;
ALTER TABLE role_project RENAME TO roles;
`,
cb,
);
};

View File

@ -526,3 +526,55 @@ test('Should be denied access to create a strategy in an environment the user do
), ),
).toBe(false); ).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 be denied access to edit a strategy in an environment the user does not have access to', async () => {
const { UPDATE_FEATURE_STRATEGY } = permissions;
const user = editorUser;
expect(
await accessService.hasPermission(
user,
UPDATE_FEATURE_STRATEGY,
'default',
'noaccess',
),
).toBe(false);
});
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);
});

View File

@ -354,7 +354,7 @@ test('add user should fail if user already have access', async () => {
await expect(async () => await expect(async () =>
projectService.addUser(project.id, memberRole.id, projectMember1.id), projectService.addUser(project.id, memberRole.id, projectMember1.id),
).rejects.toThrow( ).rejects.toThrow(
new Error('User already have access to project=add-users-twice'), new Error('User already has access to project=add-users-twice'),
); );
}); });