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

fix: fetching user root roles include custom ones (#4068)

## About the changes
`getUserRootRoles` should also consider custom root roles

This introduces test cases that unveiled a dependency between stores
(this happens actually at the DB layer having access-service access
tables from two different stores but skipping the store layer).


https://linear.app/unleash/issue/2-1161/a-user-with-custom-root-role-and-permission-to-create-client-api

---------

Co-authored-by: Nuno Góis <github@nunogois.com>
This commit is contained in:
Gastón Fournier 2023-06-22 16:42:01 +02:00 committed by GitHub
parent 40a4451818
commit 4cedb00e04
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 90 additions and 49 deletions

View File

@ -45,7 +45,7 @@ export const createFakeAccessService = (
const accountStore = new FakeAccountStore(); const accountStore = new FakeAccountStore();
const roleStore = new FakeRoleStore(); const roleStore = new FakeRoleStore();
const environmentStore = new FakeEnvironmentStore(); const environmentStore = new FakeEnvironmentStore();
const accessStore = new FakeAccessStore(); const accessStore = new FakeAccessStore(roleStore);
const groupService = new GroupService( const groupService = new GroupService(
{ groupStore, eventStore, accountStore }, { groupStore, eventStore, accountStore },
{ getLogger }, { getLogger },

View File

@ -1,35 +1,33 @@
import NameExistsError from '../error/name-exists-error'; import NameExistsError from '../error/name-exists-error';
import getLogger from '../../test/fixtures/no-logger'; import getLogger from '../../test/fixtures/no-logger';
import createStores from '../../test/fixtures/store'; import { createFakeAccessService } from '../features/access/createAccessService';
import { AccessService, IRoleValidation } from './access-service'; import { IRoleValidation } from './access-service';
import { GroupService } from './group-service';
import { createTestConfig } from '../../test/config/test-config'; import { createTestConfig } from '../../test/config/test-config';
import { CUSTOM_ROOT_ROLE_TYPE } from '../util/constants';
function getSetup(withNameInUse: boolean) { function getSetup(customRootRoles: boolean = false) {
const stores = createStores();
const config = createTestConfig({ const config = createTestConfig({
getLogger, getLogger,
experimental: {
flags: {
customRootRoles: customRootRoles,
},
},
}); });
stores.roleStore = {
...stores.roleStore,
async nameInUse(): Promise<boolean> {
return withNameInUse;
},
};
return { return {
accessService: new AccessService(stores, config, {} as GroupService), accessService: createFakeAccessService(config),
stores,
}; };
} }
test('should fail when name exists', async () => {
const { accessService } = getSetup(true);
const existingRole: IRoleValidation = { test('should fail when name exists', async () => {
const { accessService } = getSetup();
const existingRole = await accessService.createRole({
name: 'existing role', name: 'existing role',
description: 'description', description: 'description',
}; permissions: [],
});
expect(accessService.validateRole(existingRole)).rejects.toThrow( expect(accessService.validateRole(existingRole)).rejects.toThrow(
new NameExistsError( new NameExistsError(
`There already exists a role with the name ${existingRole.name}`, `There already exists a role with the name ${existingRole.name}`,
@ -38,7 +36,7 @@ test('should fail when name exists', async () => {
}); });
test('should validate a role without permissions', async () => { test('should validate a role without permissions', async () => {
const { accessService } = getSetup(false); const { accessService } = getSetup();
const withoutPermissions: IRoleValidation = { const withoutPermissions: IRoleValidation = {
name: 'name of the role', name: 'name of the role',
@ -50,7 +48,7 @@ test('should validate a role without permissions', async () => {
}); });
test('should complete description field when not present', async () => { test('should complete description field when not present', async () => {
const { accessService } = getSetup(false); const { accessService } = getSetup();
const withoutDescription: IRoleValidation = { const withoutDescription: IRoleValidation = {
name: 'name of the role', name: 'name of the role',
}; };
@ -61,7 +59,7 @@ test('should complete description field when not present', async () => {
}); });
test('should accept empty permissions', async () => { test('should accept empty permissions', async () => {
const { accessService } = getSetup(false); const { accessService } = getSetup();
const withEmptyPermissions: IRoleValidation = { const withEmptyPermissions: IRoleValidation = {
name: 'name of the role', name: 'name of the role',
description: 'description', description: 'description',
@ -75,7 +73,7 @@ test('should accept empty permissions', async () => {
}); });
test('should complete environment field of permissions when not present', async () => { test('should complete environment field of permissions when not present', async () => {
const { accessService } = getSetup(false); const { accessService } = getSetup();
const withoutEnvironmentInPermissions: IRoleValidation = { const withoutEnvironmentInPermissions: IRoleValidation = {
name: 'name of the role', name: 'name of the role',
description: 'description', description: 'description',
@ -100,7 +98,7 @@ test('should complete environment field of permissions when not present', async
}); });
test('should return the same object when all fields are valid and present', async () => { test('should return the same object when all fields are valid and present', async () => {
const { accessService } = getSetup(false); const { accessService } = getSetup();
const roleWithAllFields: IRoleValidation = { const roleWithAllFields: IRoleValidation = {
name: 'name of the role', name: 'name of the role',
@ -125,7 +123,7 @@ test('should return the same object when all fields are valid and present', asyn
}); });
test('should be able to validate and cleanup with additional properties', async () => { test('should be able to validate and cleanup with additional properties', async () => {
const { accessService } = getSetup(false); const { accessService } = getSetup();
const base = { const base = {
name: 'name of the role', name: 'name of the role',
description: 'description', description: 'description',
@ -152,3 +150,22 @@ test('should be able to validate and cleanup with additional properties', async
], ],
}); });
}); });
test('user with custom root role should get a user root role', async () => {
const { accessService } = getSetup(true);
const customRootRole = await accessService.createRole({
name: 'custom-root-role',
description: 'test custom root role',
type: CUSTOM_ROOT_ROLE_TYPE,
permissions: [],
});
const user = {
id: 1,
rootRole: customRootRole.id,
};
await accessService.setUserRootRole(user.id, customRootRole.id);
const roles = await accessService.getUserRootRoles(user.id);
expect(roles).toHaveLength(1);
expect(roles[0].name).toBe('custom-root-role');
});

View File

@ -18,7 +18,6 @@ import {
IRoleData, IRoleData,
IUserWithRole, IUserWithRole,
RoleName, RoleName,
RoleType,
} from '../types/model'; } from '../types/model';
import { IRoleStore } from 'lib/types/stores/role-store'; import { IRoleStore } from 'lib/types/stores/role-store';
import NameExistsError from '../error/name-exists-error'; import NameExistsError from '../error/name-exists-error';
@ -30,6 +29,7 @@ import {
ALL_PROJECTS, ALL_PROJECTS,
CUSTOM_ROOT_ROLE_TYPE, CUSTOM_ROOT_ROLE_TYPE,
CUSTOM_PROJECT_ROLE_TYPE, CUSTOM_PROJECT_ROLE_TYPE,
ROOT_ROLE_TYPES,
} from '../util/constants'; } from '../util/constants';
import { DEFAULT_PROJECT } from '../types/project'; import { DEFAULT_PROJECT } from '../types/project';
import InvalidOperationError from '../error/invalid-operation-error'; import InvalidOperationError from '../error/invalid-operation-error';
@ -253,10 +253,10 @@ export class AccessService {
const newRootRole = await this.resolveRootRole(role); const newRootRole = await this.resolveRootRole(role);
if (newRootRole) { if (newRootRole) {
try { try {
await this.store.removeRolesOfTypeForUser(userId, [ await this.store.removeRolesOfTypeForUser(
RoleType.ROOT, userId,
RoleType.ROOT_CUSTOM, ROOT_ROLE_TYPES,
]); );
await this.store.addUserToRole( await this.store.addUserToRole(
userId, userId,
@ -275,7 +275,7 @@ export class AccessService {
async getUserRootRoles(userId: number): Promise<IRoleWithProject[]> { async getUserRootRoles(userId: number): Promise<IRoleWithProject[]> {
const userRoles = await this.store.getRolesForUserId(userId); const userRoles = await this.store.getRolesForUserId(userId);
return userRoles.filter((r) => r.type === RoleType.ROOT); return userRoles.filter(({ type }) => ROOT_ROLE_TYPES.includes(type));
} }
async removeUserFromRole( async removeUserFromRole(

View File

@ -386,11 +386,11 @@ export interface IProject {
defaultStickiness: string; defaultStickiness: string;
} }
export interface ICustomRole { /**
id: number; * Extends IRole making description mandatory
name: string; */
export interface ICustomRole extends IRole {
description: string; description: string;
type: string;
} }
export interface IProjectWithCount extends IProject { export interface IProjectWithCount extends IProject {

View File

@ -8,8 +8,18 @@ import {
IUserRole, IUserRole,
} from '../../lib/types/stores/access-store'; } from '../../lib/types/stores/access-store';
import { IPermission } from 'lib/types/model'; import { IPermission } from 'lib/types/model';
import { IRoleStore } from 'lib/types';
import FakeRoleStore from './fake-role-store';
class AccessStoreMock implements IAccessStore { class AccessStoreMock implements IAccessStore {
fakeRolesStore: IRoleStore;
userToRoleMap: Map<number, number> = new Map();
constructor(roleStore?: IRoleStore) {
this.fakeRolesStore = roleStore ?? new FakeRoleStore();
}
addAccessToProject( addAccessToProject(
users: IAccessInfo[], users: IAccessInfo[],
groups: IAccessInfo[], groups: IAccessInfo[],
@ -88,13 +98,9 @@ class AccessStoreMock implements IAccessStore {
role_id: number, role_id: number,
permissions: IPermission[], permissions: IPermission[],
): Promise<void> { ): Promise<void> {
throw new Error('Method not implemented.'); return Promise.resolve(undefined);
} }
userPermissions: IUserPermission[] = [];
roles: IRole[] = [];
getAvailablePermissions(): Promise<IPermission[]> { getAvailablePermissions(): Promise<IPermission[]> {
throw new Error('Method not implemented.'); throw new Error('Method not implemented.');
} }
@ -123,8 +129,17 @@ class AccessStoreMock implements IAccessStore {
throw new Error('Method not implemented.'); throw new Error('Method not implemented.');
} }
getRolesForUserId(userId: number): Promise<IRoleWithProject[]> { async getRolesForUserId(userId: number): Promise<IRoleWithProject[]> {
return Promise.resolve([]); const roleId = this.userToRoleMap.get(userId);
const found =
roleId === undefined
? undefined
: await this.fakeRolesStore.get(roleId);
if (found) {
return Promise.resolve([found as IRoleWithProject]);
} else {
return Promise.resolve([]);
}
} }
getUserIdsForRole(roleId: number, projectId: string): Promise<number[]> { getUserIdsForRole(roleId: number, projectId: string): Promise<number[]> {
@ -132,7 +147,8 @@ class AccessStoreMock implements IAccessStore {
} }
addUserToRole(userId: number, roleId: number): Promise<void> { addUserToRole(userId: number, roleId: number): Promise<void> {
throw new Error('Method not implemented.'); this.userToRoleMap.set(userId, roleId);
return Promise.resolve(undefined);
} }
addPermissionsToRole( addPermissionsToRole(
@ -140,7 +156,8 @@ class AccessStoreMock implements IAccessStore {
permissions: string[], permissions: string[],
projectId?: string, projectId?: string,
): Promise<void> { ): Promise<void> {
throw new Error('Method not implemented.'); // do nothing for now
return Promise.resolve(undefined);
} }
removePermissionFromRole( removePermissionFromRole(

View File

@ -19,7 +19,9 @@ export default class FakeRoleStore implements IRoleStore {
} }
nameInUse(name: string, existingId?: number): Promise<boolean> { nameInUse(name: string, existingId?: number): Promise<boolean> {
throw new Error('Method not implemented.'); return Promise.resolve(
this.roles.find((r) => r.name === name) !== undefined,
);
} }
async getAll(): Promise<ICustomRole[]> { async getAll(): Promise<ICustomRole[]> {
@ -29,7 +31,7 @@ export default class FakeRoleStore implements IRoleStore {
async create(role: ICustomRoleInsert): Promise<ICustomRole> { async create(role: ICustomRoleInsert): Promise<ICustomRole> {
const roleCreated = { const roleCreated = {
...role, ...role,
type: 'some-type', type: role.roleType,
id: this.roles.length, id: this.roles.length,
}; };
this.roles.push(roleCreated); this.roles.push(roleCreated);
@ -49,7 +51,7 @@ export default class FakeRoleStore implements IRoleStore {
} }
async getRoleByName(name: string): Promise<IRole> { async getRoleByName(name: string): Promise<IRole> {
return this.roles.find((r) => (r.name = name)); return this.roles.find((r) => (r.name = name)) as IRole;
} }
getRolesForProject(projectId: string): Promise<IRole[]> { getRolesForProject(projectId: string): Promise<IRole[]> {
@ -72,8 +74,13 @@ export default class FakeRoleStore implements IRoleStore {
throw new Error('Method not implemented.'); throw new Error('Method not implemented.');
} }
get(key: number): Promise<ICustomRole> { get(id: number): Promise<ICustomRole> {
throw new Error('Method not implemented.'); const found = this.roles.find((r) => r.id === id);
if (!found) {
// this edge case is not properly contemplated in the type definition
throw new Error('Not found');
}
return Promise.resolve(found);
} }
exists(key: number): Promise<boolean> { exists(key: number): Promise<boolean> {