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:
parent
0129f23e97
commit
2eb0b6a11e
@ -12,7 +12,7 @@ import {
|
||||
|
||||
const T = {
|
||||
ROLE_USER: 'role_user',
|
||||
ROLES: 'roles',
|
||||
ROLES: 'role_project',
|
||||
ROLE_PERMISSION: 'role_permission',
|
||||
};
|
||||
|
||||
|
@ -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
|
||||
|
88
src/lib/db/role-store.ts
Normal file
88
src/lib/db/role-store.ts
Normal 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;
|
@ -9,7 +9,10 @@ import {
|
||||
CREATE_FEATURE,
|
||||
DELETE_FEATURE,
|
||||
CREATE_FEATURE_STRATEGY,
|
||||
DELETE_FEATURE_STRATEGY,
|
||||
UPDATE_FEATURE,
|
||||
UPDATE_FEATURE_ENVIRONMENT,
|
||||
UPDATE_FEATURE_STRATEGY,
|
||||
} from '../../../types/permissions';
|
||||
import {
|
||||
FeatureToggleDTO,
|
||||
|
@ -28,6 +28,7 @@ import EnvironmentService from './environment-service';
|
||||
import FeatureTagService from './feature-tag-service';
|
||||
import ProjectHealthService from './project-health-service';
|
||||
import UserSplashService from './user-splash-service';
|
||||
import RoleService from './role-service';
|
||||
|
||||
export const createServices = (
|
||||
stores: IUnleashStores,
|
||||
@ -75,6 +76,8 @@ export const createServices = (
|
||||
);
|
||||
const userSplashService = new UserSplashService(stores, config);
|
||||
|
||||
const roleService = new RoleService(stores, config);
|
||||
|
||||
return {
|
||||
accessService,
|
||||
addonService,
|
||||
@ -103,6 +106,7 @@ export const createServices = (
|
||||
featureTagService,
|
||||
projectHealthService,
|
||||
userSplashService,
|
||||
roleService,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -297,7 +297,7 @@ 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);
|
||||
|
31
src/lib/services/role-service.ts
Normal file
31
src/lib/services/role-service.ts
Normal 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);
|
||||
}
|
||||
}
|
@ -313,6 +313,13 @@ export interface IProject {
|
||||
updatedAt?: Date;
|
||||
}
|
||||
|
||||
export interface ICustomRole {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export interface IProjectWithCount extends IProject {
|
||||
featureCount: number;
|
||||
memberCount: number;
|
||||
|
@ -24,6 +24,7 @@ import FeatureTagService from '../services/feature-tag-service';
|
||||
import ProjectHealthService from '../services/project-health-service';
|
||||
import ClientMetricsServiceV2 from '../services/client-metrics/metrics-service-v2';
|
||||
import UserSplashService from '../services/user-splash-service';
|
||||
import RoleService from 'lib/services/role-service';
|
||||
|
||||
export interface IUnleashServices {
|
||||
accessService: AccessService;
|
||||
@ -53,4 +54,5 @@ export interface IUnleashServices {
|
||||
userService: UserService;
|
||||
versionService: VersionService;
|
||||
userSplashService: UserSplashService;
|
||||
roleService: RoleService;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
13
src/lib/types/stores/role-store.ts
Normal file
13
src/lib/types/stores/role-store.ts
Normal 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>;
|
||||
}
|
23
src/migrations/20211202120808-add-custom-roles.js
Normal file
23
src/migrations/20211202120808-add-custom-roles.js
Normal 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,
|
||||
);
|
||||
};
|
@ -526,3 +526,55 @@ test('Should be denied access to create a strategy in an environment the user do
|
||||
),
|
||||
).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);
|
||||
});
|
||||
|
@ -354,7 +354,7 @@ test('add user should fail if user already have access', async () => {
|
||||
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'),
|
||||
);
|
||||
});
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user