Skip to content
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

Add basic auth header middleware #579

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions src/sbvr-api/permissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<PermissionReq['user']> => {
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<void> => {
try {
const user = await resolveBasicAuthHeader(req, expectedScheme);
if (user) {
req.user = user;
}
} finally {
next?.();
}
};
};

export const resolveApiKey = async (
req: HookReq | Express.Request,
paramName = 'apikey',
Expand Down
104 changes: 104 additions & 0 deletions test/05-permissions.test.ts
Original file line number Diff line number Diff line change
@@ -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<ReturnType<typeof testInit>>;
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);
});
});
36 changes: 36 additions & 0 deletions test/fixtures/05-permissions/config.ts
Original file line number Diff line number Diff line change
@@ -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;
42 changes: 42 additions & 0 deletions test/fixtures/05-permissions/university.sbvr
Original file line number Diff line number Diff line change
@@ -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.
5 changes: 4 additions & 1 deletion test/lib/pine-init.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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);
Expand Down