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 = {
|
const T = {
|
||||||
ROLE_USER: 'role_user',
|
ROLE_USER: 'role_user',
|
||||||
ROLES: 'roles',
|
ROLES: 'role_project',
|
||||||
ROLE_PERMISSION: 'role_permission',
|
ROLE_PERMISSION: 'role_permission',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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
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,
|
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,
|
||||||
|
@ -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,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
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;
|
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;
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
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);
|
).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 () =>
|
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'),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user