Skip to content

Commit

Permalink
Add getRolePermissions
Browse files Browse the repository at this point in the history
Change-type: minor
  • Loading branch information
otaviojacobi committed Feb 15, 2024
1 parent 8ec2162 commit d40c00d
Show file tree
Hide file tree
Showing 9 changed files with 248 additions and 0 deletions.
3 changes: 3 additions & 0 deletions docs/CustomServerCode.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ This returns a promise that resolves to the user permissions for the given userI
#### getApiKeyPermissions(apiKey)
This returns a promise that resolves to the api key permissions for the given apiKey

#### getUserPermissionsForRole(roleName, userId)
This returns a promise that resolves to the roleName permissions if the user given by userId has this role

#### apiKeyMiddleware(req, res, next)
This is a default `customApiKeyMiddleware`, which is useful to avoid having to create your own default one.

Expand Down
1 change: 1 addition & 0 deletions src/config-loader/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export const cache = {
userPermissions: false as CacheOpts,
apiKeyPermissions: false as CacheOpts,
apiKeyActorId: false as CacheOpts,
rolePermissions: false as CacheOpts,
};

import { boolVar } from '@balena/env-parsing';
Expand Down
93 changes: 93 additions & 0 deletions src/sbvr-api/permissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1273,6 +1273,99 @@ export const getUserPermissions = async (
}
};

export const getUserPermissionsForRole = async (
userId: number,
roleName: string,
tx?: Tx,
): Promise<string[]> => {
try {
return await $getUserPermissionsForRole(userId, roleName, tx);
} catch (err: unknown) {
sbvrUtils.api.Auth.logger.error(
`Error loading role permissions for ${userId} ${roleName}`,
err,
);
throw err;
}
};

const $getUserPermissionsForRole = (() => {
const getUserPermissionsForRoleQuery = _.once(() =>
sbvrUtils.api.Auth.prepare<{ userId: number; roleName: string }>({
resource: 'permission',
passthrough: {
req: rootRead,
},
options: {
$select: 'name',
$filter: {
is_of__role: {
$any: {
$alias: 'rhp',
$expr: {
rhp: {
role: {
$any: {
$alias: 'r',
$expr: {
r: {
name: { '@': 'roleName' },
is_of__user: {
$any: {
$alias: 'uhr',
$expr: {
uhr: { user: { '@': 'userId' } },
$or: [
{
uhr: { expiry_date: null },
},
{
uhr: {
expiry_date: { $gt: { $now: null } },
},
},
],
},
},
},
},
},
},
},
},
},
},
},
},
// We orderby to increase the hit rate for the `_checkPermissions` memoisation
$orderby: {
name: 'asc',
},
},
}),
);

return env.createCache(
'rolePermissions',
async (userId: number, roleName: string, tx?: Tx) => {
const permissions = (await getUserPermissionsForRoleQuery()(
{
userId,
roleName,
},
undefined,
{ tx },
)) as Array<{ name: string }>;
return permissions.map((permission) => permission.name);
},
{
primitive: true,
promise: true,
normalizer: ([userId, roleName]) => `${userId}${roleName}`,
},
);
})();

const $getApiKeyPermissions = (() => {
const getApiKeyPermissionsQuery = _.once(() =>
sbvrUtils.api.Auth.prepare<{ apiKey: string }>({
Expand Down
105 changes: 105 additions & 0 deletions test/08-sbvrApi.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import supertest from 'supertest';
import { expect } from 'chai';
const configPath = __dirname + '/fixtures/08-sbvrApi/config.js';
import { testInit, testDeInit, testLocalServer } from './lib/test-init';
import * as pine from '../src/server-glue/module';
import { PineTest } from 'pinejs-client-supertest';
import type { AnyObject } from 'pinejs-client-core';

const { permissions } = pine;

describe('08 sbvrApi', function () {
let pineServer: Awaited<ReturnType<typeof testInit>>;
let pineTest: PineTest;
before(async () => {
pineServer = await testInit({
configPath,
deleteDb: true,
exposeAuthEndpoints: true,
});
pineTest = new PineTest({}, { app: testLocalServer });
});

after(async () => {
testDeInit(pineServer);
});

describe('permissions.getUserPermissionsForRole', () => {
let guestId: number;
const testPermissions = ['test.permission1', 'test.permission2'];
before(async () => {
guestId = await getUserId('guest');
const permissionIds = await createPermissions(testPermissions);
const roleId = await createRole('test', permissionIds);
await grantRoleToUser(roleId, guestId);
});

it(`should be able to get a specific role's permissions if role exists and user has it`, async () => {
// pine API only exists on the process it is currently running.
// We use custom endpoints to expose the specific funcionality being tested
const response = await supertest(testLocalServer)
.get('/auth-test/getUserPermissionsForRole')
.send({ userId: guestId, roleName: 'test' });
expect(response.body).to.deep.equal(testPermissions);
});
});

const getUserId = async (username: string): Promise<number> => {
const {
d: [{ id }],
} = await doAuthRequest('GET', 'user', { username });
return id;
};

const createPermissions = async (
permissionNames: string[],
): Promise<number[]> => {
return (
await Promise.all(
permissionNames.map(async (permissionName) =>
doAuthRequest('POST', 'permission', { name: permissionName }),
),
)
).map((response) => response.id);
};

const createRole = async (
roleName: string,
permissionIds: number[],
): Promise<number> => {
const { id: roleId } = await doAuthRequest('POST', 'role', {
name: roleName,
});
await Promise.all(
permissionIds.map(async (permissionId) =>
doAuthRequest('POST', 'role__has__permission', {
role: roleId,
permission: permissionId,
}),
),
);

return roleId;
};

const grantRoleToUser = async (role: number, user: number) => {
await doAuthRequest('POST', 'user__has__role', { role, user });
};

const doAuthRequest = async (
method: 'GET' | 'POST',
resource: string,
body: AnyObject,
) => {
return (
await pineTest.request({
apiPrefix: 'Auth/',
method,
resource,
body,
passthrough: { req: permissions.root },
options: { returnResource: false },
})
).body;
};
});
4 changes: 4 additions & 0 deletions test/fixtures/08-sbvrApi/basic.sbvr
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Vocabulary: basic

Term: name
Concept Type: Short Text (Type)
22 changes: 22 additions & 0 deletions test/fixtures/08-sbvrApi/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { ConfigLoader } from '../../../src/server-glue/module';

const apiRoot = 'basic';
const modelName = 'basic';
const modelFile = __dirname + '/basic.sbvr';

export default {
models: [
{
modelName,
modelFile,
apiRoot,
},
],
users: [
{
username: 'guest',
password: ' ',
permissions: ['resource.all'],
},
],
} as ConfigLoader.Config;
1 change: 1 addition & 0 deletions test/lib/pine-in-process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export async function forkInit() {
processArgs.listenPort,
processArgs.deleteDb,
processArgs.withLoginRoute,
processArgs.exposeAuthEndpoints,
);

// load hooks
Expand Down
18 changes: 18 additions & 0 deletions test/lib/pine-init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export type PineTestOptions = {
hooksPath?: string;
routesPath?: string;
withLoginRoute?: boolean;
exposeAuthEndpoints?: boolean;
deleteDb: boolean;
listenPort: number;
};
Expand All @@ -17,11 +18,28 @@ export async function init(
initPort: number,
deleteDb: boolean = false,
withLoginRoute: boolean = false,
exposeAuthEndpoints: boolean = false,
) {
const app = express();
app.use(express.urlencoded({ extended: true }));
app.use(express.json());

if (exposeAuthEndpoints) {
app.all('/Auth/*', pine.sbvrUtils.handleODataRequest);

// pine object that can actually call the helper functions we want to test only exist on the server process
// in order to be able to invoke these functions (in a different process) and properly test we create a custom
// endpoint which allows us to execute the api function we are testing
app.get('/auth-test/getUserPermissionsForRole', async (req, res) => {
const body = req.body as { userId: number; roleName: string };
const permissions = await pine.permissions.getUserPermissionsForRole(
body.userId,
body.roleName,
);
res.status(200).send(permissions);
});
}

if (withLoginRoute) {
/* eslint-disable @typescript-eslint/no-var-requires */
const expressSession: typeof ExpressSession = require('express-session');
Expand Down
1 change: 1 addition & 0 deletions test/lib/test-init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export async function testInit(
hooksPath: options.hooksPath,
routesPath: options.routesPath,
withLoginRoute: options.withLoginRoute,
exposeAuthEndpoints: options.exposeAuthEndpoints,
};
const testServer = fork(
__dirname + '/pine-in-process.ts',
Expand Down

0 comments on commit d40c00d

Please sign in to comment.