-
-
Notifications
You must be signed in to change notification settings - Fork 723
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
chore: improve access service #4689
Changes from all commits
4e0ba5a
45c285e
f3aa636
7ae3f98
16c6aac
dd54d05
c7b12a7
8583c22
45dacbc
7a61c4a
67fcced
a90c83d
ca01f42
7b6596e
6d8831b
84a17f9
d5c3402
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
import dbInit from '../../test/e2e/helpers/database-init'; | ||
import getLogger from '../../test/fixtures/no-logger'; | ||
import { PermissionRef } from 'lib/services/access-service'; | ||
import { AccessStore } from './access-store'; | ||
|
||
let db; | ||
|
||
beforeAll(async () => { | ||
db = await dbInit('access_store_serial', getLogger); | ||
}); | ||
|
||
afterAll(async () => { | ||
if (db) { | ||
await db.destroy(); | ||
} | ||
}); | ||
|
||
// Helper function to make the test cases more readable | ||
const args = (permissions: PermissionRef[], expectations?: PermissionRef[]) => { | ||
if (expectations) { | ||
return [permissions, expectations]; | ||
} else { | ||
return [permissions]; | ||
} | ||
}; | ||
|
||
test('resolvePermissions returns empty list if undefined', async () => { | ||
const access = db.stores.accessStore as AccessStore; | ||
const result = await access.resolvePermissions( | ||
undefined as unknown as PermissionRef[], | ||
); | ||
expect(result).toStrictEqual([]); | ||
}); | ||
|
||
test('resolvePermissions returns empty list if empty list', async () => { | ||
const access = db.stores.accessStore as AccessStore; | ||
const result = await access.resolvePermissions([] as PermissionRef[]); | ||
expect(result).toStrictEqual([]); | ||
}); | ||
|
||
test.each([ | ||
args([{ id: 1 }]), | ||
args([{ id: 4, environment: 'development' }]), | ||
args([{ id: 4, name: 'should keep the id' }]), | ||
args([ | ||
{ id: 1, environment: 'development' }, | ||
{ id: 2, name: 'ignore this name' }, | ||
]), | ||
])( | ||
'resolvePermissions with permission ids (%o) returns the list unmodified', | ||
async (permissions) => { | ||
const access = db.stores.accessStore as AccessStore; | ||
const result = await access.resolvePermissions(permissions); | ||
expect(result).toStrictEqual(permissions); | ||
}, | ||
); | ||
|
||
test.each([ | ||
args( | ||
[{ name: 'CREATE_CONTEXT_FIELD' }], | ||
[{ id: 18, name: 'CREATE_CONTEXT_FIELD' }], | ||
), | ||
args( | ||
[{ name: 'CREATE_FEATURE', environment: 'development' }], | ||
[{ id: 2, name: 'CREATE_FEATURE', environment: 'development' }], | ||
), | ||
args( | ||
[ | ||
{ name: 'CREATE_CONTEXT_FIELD' }, | ||
{ name: 'CREATE_FEATURE', environment: 'development' }, | ||
], | ||
[ | ||
{ id: 18, name: 'CREATE_CONTEXT_FIELD' }, | ||
{ id: 2, name: 'CREATE_FEATURE', environment: 'development' }, | ||
], | ||
), | ||
])( | ||
'resolvePermissions with permission names (%o) will inject the ids', | ||
async (permissions, expected) => { | ||
const access = db.stores.accessStore as AccessStore; | ||
const result = await access.resolvePermissions(permissions); | ||
expect(result).toStrictEqual(expected); | ||
}, | ||
); | ||
|
||
test.each([ | ||
args( | ||
[ | ||
{ name: 'CREATE_CONTEXT_FIELD' }, | ||
{ id: 3 }, | ||
{ name: 'CREATE_FEATURE', environment: 'development' }, | ||
{ id: 15, environment: 'development' }, | ||
{ name: 'UPDATE_FEATURE', environment: 'development' }, | ||
], | ||
[ | ||
{ id: 18, name: 'CREATE_CONTEXT_FIELD' }, | ||
{ id: 3 }, | ||
{ id: 2, name: 'CREATE_FEATURE', environment: 'development' }, | ||
{ id: 15, environment: 'development' }, | ||
{ id: 7, name: 'UPDATE_FEATURE', environment: 'development' }, | ||
], | ||
), | ||
])( | ||
'resolvePermissions mixed ids and names (%o) will inject the ids where they are missing', | ||
async (permissions, expected) => { | ||
const access = db.stores.accessStore as AccessStore; | ||
const result = await access.resolvePermissions(permissions); | ||
expect(result).toStrictEqual(expected); | ||
}, | ||
); |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -20,7 +20,11 @@ import { | |
ROOT_PERMISSION_TYPE, | ||
} from '../util/constants'; | ||
import { Db } from './db'; | ||
import { IdPermissionRef } from 'lib/services/access-service'; | ||
import { | ||
IdPermissionRef, | ||
NamePermissionRef, | ||
PermissionRef, | ||
} from 'lib/services/access-service'; | ||
|
||
const T = { | ||
ROLE_USER: 'role_user', | ||
|
@@ -46,6 +50,12 @@ interface IPermissionRow { | |
role_id: number; | ||
} | ||
|
||
type ResolvedPermission = { | ||
id: number; | ||
name?: string; | ||
environment?: string; | ||
}; | ||
|
||
export class AccessStore implements IAccessStore { | ||
private logger: Logger; | ||
|
||
|
@@ -63,6 +73,75 @@ export class AccessStore implements IAccessStore { | |
}); | ||
} | ||
|
||
private permissionHasId = (permission: PermissionRef): boolean => { | ||
return (permission as IdPermissionRef).id !== undefined; | ||
}; | ||
|
||
private permissionNamesToIds = async ( | ||
permissions: NamePermissionRef[], | ||
): Promise<ResolvedPermission[]> => { | ||
const permissionNames = (permissions ?? []) | ||
.filter((p) => p.name !== undefined) | ||
.map((p) => p.name); | ||
|
||
if (permissionNames.length === 0) { | ||
return []; | ||
} | ||
|
||
const stopTimer = this.timer('permissionNamesToIds'); | ||
|
||
const rows = await this.db | ||
.select('id', 'permission') | ||
.from(T.PERMISSIONS) | ||
.whereIn('permission', permissionNames); | ||
gastonfournier marked this conversation as resolved.
Show resolved
Hide resolved
Comment on lines
+93
to
+96
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same query as here: https://github.com/Unleash/unleash/pull/4689/files#diff-5cb4eca71e6a03ec3d123986d657368a3386f80e986dfef2e3feb0abb0673d87L706-L709 but now it can be used in all scenarios of adding permissions to a role |
||
|
||
const rowByPermissionName = rows.reduce((acc, row) => { | ||
acc[row.permission] = row; | ||
return acc; | ||
}, {} as Map<string, IPermissionRow>); | ||
|
||
const permissionsWithIds = permissions.map((permission) => ({ | ||
id: rowByPermissionName[permission.name].id, | ||
...permission, | ||
})); | ||
|
||
stopTimer(); | ||
return permissionsWithIds; | ||
}; | ||
|
||
resolvePermissions = async ( | ||
permissions: PermissionRef[], | ||
): Promise<ResolvedPermission[]> => { | ||
if (permissions === undefined || permissions.length === 0) { | ||
return []; | ||
} | ||
// permissions without ids (just names) | ||
const permissionsWithoutIds = permissions.filter( | ||
(p) => !this.permissionHasId(p), | ||
) as NamePermissionRef[]; | ||
const idPermissionsFromNamed = await this.permissionNamesToIds( | ||
permissionsWithoutIds, | ||
); | ||
|
||
if (permissionsWithoutIds.length === permissions.length) { | ||
// all named permissions without ids | ||
return idPermissionsFromNamed; | ||
} else if (permissionsWithoutIds.length === 0) { | ||
// all permissions have ids | ||
return permissions as ResolvedPermission[]; | ||
} | ||
// some permissions have ids, some don't (should not happen!) | ||
return permissions.map((permission) => { | ||
if (this.permissionHasId(permission)) { | ||
return permission as ResolvedPermission; | ||
} else { | ||
return idPermissionsFromNamed.find( | ||
(p) => p.name === (permission as NamePermissionRef).name, | ||
)!; | ||
} | ||
}); | ||
}; | ||
|
||
async delete(key: number): Promise<void> { | ||
await this.db(T.ROLES).where({ id: key }).del(); | ||
} | ||
|
@@ -222,9 +301,11 @@ export class AccessStore implements IAccessStore { | |
|
||
async addEnvironmentPermissionsToRole( | ||
role_id: number, | ||
permissions: IdPermissionRef[], | ||
permissions: PermissionRef[], | ||
): Promise<void> { | ||
const rows = permissions.map((permission) => { | ||
const resolvedPermission = await this.resolvePermissions(permissions); | ||
|
||
const rows = resolvedPermission.map((permission) => { | ||
return { | ||
role_id, | ||
permission_id: permission.id, | ||
|
@@ -700,18 +781,16 @@ export class AccessStore implements IAccessStore { | |
|
||
async addPermissionsToRole( | ||
role_id: number, | ||
permissions: string[], | ||
permissions: PermissionRef[], | ||
environment?: string, | ||
): Promise<void> { | ||
const rows = await this.db | ||
.select('id as permissionId') | ||
.from<number>(T.PERMISSIONS) | ||
.whereIn('permission', permissions); | ||
// no need to pass down the environment in this particular case because it'll be overriden | ||
const permissionsWithIds = await this.resolvePermissions(permissions); | ||
|
||
const newRoles = rows.map((row) => ({ | ||
const newRoles = permissionsWithIds.map((p) => ({ | ||
role_id, | ||
environment, | ||
permission_id: row.permissionId, | ||
permission_id: p.id, | ||
})); | ||
|
||
return this.db.batchInsert(T.ROLE_PERMISSION, newRoles); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,9 +3,11 @@ import joi from 'joi'; | |
export const permissionRoleSchema = joi | ||
.object() | ||
.keys({ | ||
id: joi.number().required(), | ||
id: joi.number(), | ||
name: joi.string(), | ||
Comment on lines
+6
to
+7
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This validation now expects that either you provide the id or the name (or both) |
||
environment: joi.string().optional().allow('').allow(null).default(''), | ||
}) | ||
.or('id', 'name') | ||
.options({ stripUnknown: true, allowUnknown: false, abortEarly: false }); | ||
|
||
export const roleSchema = joi | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -70,7 +70,7 @@ export interface IRoleValidation { | |
permissions?: PermissionRef[]; | ||
} | ||
|
||
interface IRoleUpdate { | ||
export interface IRoleUpdate { | ||
id: number; | ||
name: string; | ||
description: string; | ||
|
@@ -406,7 +406,7 @@ export class AccessService { | |
} | ||
return this.store.addPermissionsToRole( | ||
roleId, | ||
[permission], | ||
[{ name: permission }], | ||
environment, | ||
); | ||
} | ||
|
@@ -630,11 +630,13 @@ export class AccessService { | |
const newRole = await this.roleStore.create(baseRole); | ||
if (rolePermissions) { | ||
if (roleType === CUSTOM_ROOT_ROLE_TYPE) { | ||
// this branch uses named permissions | ||
await this.store.addPermissionsToRole( | ||
newRole.id, | ||
rolePermissions.map((p: NamePermissionRef) => p.name), | ||
rolePermissions, | ||
); | ||
} else { | ||
// this branch uses id permissions | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the reason why we're making this change, we want to be able to use names instead of ids for permissions |
||
await this.store.addEnvironmentPermissionsToRole( | ||
newRole.id, | ||
rolePermissions, | ||
|
@@ -673,7 +675,7 @@ export class AccessService { | |
if (roleType === CUSTOM_ROOT_ROLE_TYPE) { | ||
await this.store.addPermissionsToRole( | ||
newRole.id, | ||
rolePermissions.map((p: NamePermissionRef) => p.name), | ||
rolePermissions, | ||
); | ||
} else { | ||
await this.store.addEnvironmentPermissionsToRole( | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd love to say: no query to the DB here