diff --git a/src/sbvr-api/permissions.ts b/src/sbvr-api/permissions.ts index ab58a8a55..a982f488a 100644 --- a/src/sbvr-api/permissions.ts +++ b/src/sbvr-api/permissions.ts @@ -1521,6 +1521,52 @@ export const customAuthorizationMiddleware = (expectedScheme = 'Bearer') => { // A default bearer middleware for convenience export const authorizationMiddleware = customAuthorizationMiddleware(); +export const resolveBasicAuthHeader = async ( + req: Express.Request, + expectedScheme = 'Basic', +): Promise => { + const auth = req.header('Authorization'); + if (!auth) { + return; + } + + const parts = auth.split(' '); + if (parts.length !== 2) { + return; + } + + const [scheme, basicAuthContentBase64] = parts; + if (scheme.toLowerCase() !== expectedScheme.toLowerCase()) { + return; + } + + const basicAuthContent = Buffer.from(basicAuthContentBase64, 'base64') + .toString() + .trim(); + const [username, password] = basicAuthContent.split(';'); + return checkPassword(username, password); +}; + +export const basicUserPasswordAuthorizationMiddleware = ( + expectedScheme = 'Basic', +) => { + expectedScheme = expectedScheme.toLowerCase(); + return async ( + req: Express.Request, + _res?: Express.Response, + next?: Express.NextFunction, + ): Promise => { + try { + const user = await resolveBasicAuthHeader(req, expectedScheme); + if (user) { + req.user = user; + } + } finally { + next?.(); + } + }; +}; + export const resolveApiKey = async ( req: HookReq | Express.Request, paramName = 'apikey', diff --git a/test/05-permissions.test.ts b/test/05-permissions.test.ts new file mode 100644 index 000000000..b7a38141e --- /dev/null +++ b/test/05-permissions.test.ts @@ -0,0 +1,104 @@ +import * as supertest from 'supertest'; +const fixturePath = __dirname + '/fixtures/05-permissions/config'; +import { testInit, testDeInit, testLocalServer } from './lib/test-init'; + +const basicStudentAuthHeaderBase64 = + Buffer.from('student;student').toString('base64'); +const basicAdminAuthHeaderBase64 = + Buffer.from('admin;admin').toString('base64'); + +const differentUsers = [ + { name: 'student', basicBase64: basicStudentAuthHeaderBase64 }, + { name: 'admin', basicBase64: basicAdminAuthHeaderBase64 }, +]; + +describe('05 basic permission tests', function () { + let pineServer: Awaited>; + let request: any; + before(async () => { + pineServer = await testInit({ configPath: fixturePath, deleteDb: true }); + }); + + beforeEach(async () => { + request = supertest.agent(testLocalServer); + }); + + after(async () => { + await testDeInit(pineServer); + }); + + for (const [idx, user] of differentUsers.entries()) { + it(`should create a student as ${user.name} `, async () => { + await request + .set('Authorization', 'Basic ' + user.basicBase64) + .post('/university/student') + .send({ + matrix_number: idx, + name: 'John', + lastname: user.name, + birthday: new Date(), + semester_credits: 10, + }) + .expect(201); + }); + + it(`should read all students as ${user.name} `, async () => { + await request + .set('Authorization', 'Basic ' + user.basicBase64) + .get('/university/student(1)') + .expect(200); + }); + + it(`should update a student as ${user.name} `, async () => { + await request + .set('Authorization', 'Basic ' + user.basicBase64) + .patch('/university/student(1)') + .send({ + name: 'Johnny', + }) + .expect(200); + }); + } + + it(`should not allow to get students as guest `, async () => { + await request.get('/university/student').expect(401); + }); + + it('should not allow to delete a student as student', async () => { + await request + .set('Authorization', 'Basic ' + basicStudentAuthHeaderBase64) + .delete('/university/student(1)') + .expect(401); + }); + + it('should not allow to create a faculty as student', async () => { + await request + .set('Authorization', 'Basic ' + basicStudentAuthHeaderBase64) + .post('/university/faculty') + .send({ + name: 'physics', + }) + .expect(401); + }); + + it('should allow to create a faculty as admin', async () => { + await request + .set('Authorization', 'Basic ' + basicAdminAuthHeaderBase64) + .post('/university/faculty') + .send({ + name: 'physics', + }) + .expect(201); + }); + + it('should allow to delete a student as admin', async () => { + await request + .set('Authorization', 'Basic ' + basicStudentAuthHeaderBase64) + .delete('/university/student(1)') + .expect(401); + }); + + it(`should allow to get faculties as guest `, async () => { + await request.get('/university/faculty').expect(200); + }); +}); diff --git a/test/fixtures/05-permissions/config.ts b/test/fixtures/05-permissions/config.ts new file mode 100644 index 000000000..ac22e4b87 --- /dev/null +++ b/test/fixtures/05-permissions/config.ts @@ -0,0 +1,36 @@ +import type { ConfigLoader } from '../../../src/server-glue/module'; + +const apiRoot = 'university'; +const modelName = 'university'; +const modelFile = __dirname + '/university.sbvr'; + +export default { + models: [ + { + modelName, + modelFile, + apiRoot, + }, + ], + users: [ + { + username: 'guest', + password: ' ', + permissions: ['university.faculty.read'], + }, + { + username: 'student', + password: 'student', + permissions: [ + 'university.student.read', + 'university.student.create', + 'university.student.update', + ], + }, + { + username: 'admin', + password: 'admin', + permissions: ['resource.all'], + }, + ], +} as ConfigLoader.Config; diff --git a/test/fixtures/05-permissions/university.sbvr b/test/fixtures/05-permissions/university.sbvr new file mode 100644 index 000000000..2c20dad97 --- /dev/null +++ b/test/fixtures/05-permissions/university.sbvr @@ -0,0 +1,42 @@ +Vocabulary: university + +Term: name + Concept Type: Short Text (Type) + +Term: lastname + Concept Type: Short Text (Type) + +Term: birthday + Concept Type: Date Time (Type) + +Term: semester credits + Concept Type: Integer (Type) + +Term: matrix number + Concept Type: Integer (Type) + + +Term: faculty + +Fact Type: faculty has name + Necessity: each faculty has exactly one name + Necessity: each name is of exactly one faculty + +Term: student + +Fact Type: student has matrix number + Necessity: each student has exactly one matrix number + Necessity: each matrix number is of exactly one student + +Fact Type: student has name + Necessity: each student has exactly one name + +Fact Type: student has lastname + Necessity: each student has exactly one lastname + +Fact Type: student has birthday + Necessity: each student has exactly one birthday + +Fact Type: student has semester credits + Necessity: each student has at most one semester credits + Necessity: each student that has a semester credits, has a semester credits that is greater than or equal to 4 and is less than or equal to 16. diff --git a/test/lib/pine-init.ts b/test/lib/pine-init.ts index 699d09a02..ecd62a948 100644 --- a/test/lib/pine-init.ts +++ b/test/lib/pine-init.ts @@ -1,6 +1,7 @@ import * as express from 'express'; import { exit } from 'process'; import * as pine from '../../src/server-glue/module'; +import { basicUserPasswordAuthorizationMiddleware } from '../../src/sbvr-api/permissions'; export type PineTestOptions = { configPath: string; @@ -34,13 +35,15 @@ export async function init( try { await cleanInit(deleteDb); + app.use(basicUserPasswordAuthorizationMiddleware()); await pine.init(app, initConfig); + + // register default auth middleware for bearer token await new Promise((resolve) => { app.listen(initPort, () => { resolve('server started'); }); }); - return app; } catch (e) { console.log(`pineInit ${e}`); exit(1);