diff --git a/README.md b/README.md index 011ae10..1afc669 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,7 @@ Supported object options: | Option | Type | Description | Default | | -------- | ------------------------------- | -------------------------------------------- | --------------------------- | | `getSub` | `Request => string` or `string` | Extracts `sub` from the request or constant | Value from plugin options | +| `getDom` | `Request => string` or `string` | Extracts `dom` from the request or constant | Value from plugin options | | `getObj` | `Request => string` or `string` | Extracts `obj` from the request or constant | Value from plugin options | | `getAct` | `Request => string` or `string` | Extracts `act` from the request or constant | Value from plugin options | @@ -57,13 +58,16 @@ The API exposed by this plugin is the configuration options: | Option | Type | Description | Default | | -------- | ---------------------------------------------------------- | ------------------------------------------------- | ------------------------------- | | `getSub` | `Request => string` | Extracts `sub` from the request | `r => r.user` | +| `getDom` | `Request => string` | Extracts `dom` from the request | undefined | | `getObj` | `Request => string` | Extracts `obj` from the request | `r => r.url` | | `getAct` | `Request => string` | Extracts `act` from the request | `r => r.method` | -| `onDeny` | `(Reply, sub, obj, act) => any` | Invoked when Casbin's `enforce` resolves to false | Returns a `403 Forbidden` error | -| `log` | `(Fastify, Request, sub, obj, act => void` | Invoked before invoking Casbin's `enforce` | Logs using fastify.log.info | +| `onDeny` | `(Reply, { sub, obj, act, dom }) => any` | Invoked when Casbin's `enforce` resolves to false | Returns a `403 Forbidden` error | +| `log` | `(Fastify, Request, { sub, obj, act, dom }) => void` | Invoked before invoking Casbin's `enforce` | Logs using fastify.log.info | | `hook` | `'onRequest', 'preParsing', 'preValidation', 'preHandler'` | Which lifecycle to use for performing the check | `'preHandler'` | Note that extraction rules defined within route options take precedence over the rules defined in the plugin options. +If `getDom` is not set either on a route nor on a plugin level, enforcer is invoked with `(sub, obj, act)`. +If `getDom` is set, enforcer is invoked with `(sub, dom, obj, act)`. ## Examples diff --git a/package.json b/package.json index 111ecc6..cb1b915 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,13 @@ "rest" ], "author": "Simone Busoli ", + "contributors": [ + { + "name": "Igor Savin", + "email": "kibertoad@gmail.com", + "web": "https://twitter.com/kibertoad" + } + ], "license": "MIT", "bugs": { "url": "https://github.com/nearform/fastify-casbin-rest/issues" diff --git a/plugin.d.ts b/plugin.d.ts index 926bdf0..b1d6be7 100644 --- a/plugin.d.ts +++ b/plugin.d.ts @@ -21,6 +21,7 @@ declare module 'fastify' { casbin?: { rest?: boolean | { getSub?: ((request: FastifyRequest) => string) | string, + getDom?: ((request: FastifyRequest) => string) | string, getObj?: ((request: FastifyRequest) => string) | string, getAct?: ((request: FastifyRequest) => string) | string } @@ -34,12 +35,15 @@ export type Hook = | 'preValidation' | 'preHandler' +export type Permission = { sub: string, dom?: string, obj: string, act: string } + export interface FastifyCasbinRestOptions { getSub?(request: FastifyRequest): string + getDom?(request: FastifyRequest): string getObj?(request: FastifyRequest): string getAct?(request: FastifyRequest): string - onDeny?(reply: FastifyReply, sub: string, obj: string, act: string): void - log?(fastify: FastifyInstance, request: FastifyRequest, sub: string, obj: string, act: string): void + onDeny?(reply: FastifyReply, checkParams: Permission): void + log?(fastify: FastifyInstance, request: FastifyRequest, checkParams: Permission ): void hook?: Hook } diff --git a/plugin.js b/plugin.js index 43f1293..5a1a859 100644 --- a/plugin.js +++ b/plugin.js @@ -7,10 +7,10 @@ const defaultOptions = { getSub: request => request.user, getObj: request => request.url, getAct: request => request.method, - onDeny: (reply, sub, obj, act) => { - throw new Forbidden(`${sub} not allowed to ${act} ${obj}`) + onDeny: (reply, { sub, obj, act, dom }) => { + throw new Forbidden(`${sub} not allowed to ${act} ${dom ? dom + ' ' : ''}${obj}`) }, - log: (fastify, request, sub, obj, act) => { fastify.log.info({ sub, obj, act }, 'Invoking casbin enforce') }, + log: (fastify, request, permission) => { request.log.info(permission, 'Invoking casbin enforce') }, hook: 'preHandler' } @@ -35,15 +35,19 @@ async function fastifyCasbinRest (fastify, options) { const getSub = resolveParameterExtractor(routeOptions.casbin.rest.getSub, options.getSub) const getObj = resolveParameterExtractor(routeOptions.casbin.rest.getObj, options.getObj) const getAct = resolveParameterExtractor(routeOptions.casbin.rest.getAct, options.getAct) + const getDom = resolveParameterExtractor(routeOptions.casbin.rest.getDom, options.getDom) routeOptions[hook].push(async (request, reply) => { const sub = getSub(request) const obj = getObj(request) const act = getAct(request) + const dom = getDom ? getDom(request) : undefined - log(fastify, request, sub, obj, act) - if (!(await fastify.casbin.enforce(sub, obj, act))) { - await options.onDeny(reply, sub, obj, act) + log(fastify, request, { sub, dom, obj, act }) + const isAuthorized = getDom ? await fastify.casbin.enforce(sub, dom, obj, act) : await fastify.casbin.enforce(sub, obj, act) + + if (!isAuthorized) { + await options.onDeny(reply, { sub, dom, obj, act }) } }) } diff --git a/test/casbinRest.test.js b/test/casbinRest.test.js index 44b7a89..97cdc04 100644 --- a/test/casbinRest.test.js +++ b/test/casbinRest.test.js @@ -56,7 +56,7 @@ test('throws if fastify-casbin plugin is not registered', t => { }) }) -test('registration succeds if fastify-casbin providing a casbin decorator exists', t => { +test('registration succeeds if fastify-casbin providing a casbin decorator exists', t => { t.plan(1) const fastify = Fastify() @@ -96,7 +96,7 @@ test('ignores routes where plugin is not enabled', t => { }) test('allows route where plugin is enabled and enforce resolves true', t => { - t.plan(3) + t.plan(6) const fastify = Fastify() @@ -108,7 +108,41 @@ test('allows route where plugin is enabled and enforce resolves true', t => { fastify.ready(async err => { t.error(err) - fastify.casbin.enforce.resolves(true) + fastify.casbin.enforce.callsFake((sub, obj, act) => { + t.equal(sub, undefined) + t.equal(obj, '/') + t.equal(act, 'GET') + return Promise.resolve(true) + }) + + t.equal((await fastify.inject('/')).body, 'ok') + + t.ok(fastify.casbin.enforce.called) + + fastify.close() + }) +}) + +test('allows route where plugin is enabled and enforce resolves true with dom resolver enabled', t => { + t.plan(7) + + const fastify = Fastify() + + fastify.register(makeStubCasbin()) + fastify.register(plugin) + + fastify.get('/', { casbin: { rest: { getDom: 'domain' } } }, () => 'ok') + + fastify.ready(async err => { + t.error(err) + + fastify.casbin.enforce.callsFake((sub, dom, obj, act) => { + t.equal(sub, undefined) + t.equal(dom, 'domain') + t.equal(obj, '/') + t.equal(act, 'GET') + return Promise.resolve(true) + }) t.equal((await fastify.inject('/')).body, 'ok') @@ -119,7 +153,7 @@ test('allows route where plugin is enabled and enforce resolves true', t => { }) test('forbids route where plugin is enabled and enforce resolves false', t => { - t.plan(3) + t.plan(6) const fastify = Fastify() @@ -131,7 +165,12 @@ test('forbids route where plugin is enabled and enforce resolves false', t => { fastify.ready(async err => { t.error(err) - fastify.casbin.enforce.resolves(false) + fastify.casbin.enforce.callsFake((sub, obj, act) => { + t.equal(sub, undefined) + t.equal(obj, '/') + t.equal(act, 'GET') + return Promise.resolve(false) + }) t.equal((await fastify.inject('/')).statusCode, 403) @@ -141,6 +180,38 @@ test('forbids route where plugin is enabled and enforce resolves false', t => { }) }) +test('forbids route where plugin is enabled and enforce resolves false with dom resolver enabled', t => { + t.plan(7) + + const fastify = Fastify() + + fastify.register(makeStubCasbin()) + fastify.register(plugin, { + getDom: () => 'domain' + }) + + fastify.get('/', { casbin: { rest: true } }, () => 'ok') + + fastify.ready(async err => { + t.error(err) + + fastify.casbin.enforce.callsFake((sub, dom, obj, act) => { + t.equal(sub, undefined) + t.equal(dom, 'domain') + t.equal(obj, '/') + t.equal(act, 'GET') + return Promise.resolve(false) + }) + + const response = await fastify.inject('/') + t.equal(response.statusCode, 403) + + t.ok(fastify.casbin.enforce.called) + + fastify.close() + }) +}) + test('works correctly if there is an existing preHandler hook', t => { t.plan(4) @@ -211,7 +282,7 @@ test('supports specifying custom logger', t => { const fastify = Fastify() fastify.register(makeStubCasbin()) fastify.register(plugin, { - log: (fastify, request, sub, obj, act) => { + log: (fastify, request, { sub, obj, act }) => { t.equal(sub, 'a') t.equal(obj, 'b') t.equal(act, 'c') @@ -274,7 +345,7 @@ test('supports overriding plugin rules on route level', t => { }) }) -test('supports passing constants as extractor params', t => { +test('supports passing constants as extractor params without domain', t => { t.plan(4) const fastify = Fastify() @@ -311,3 +382,44 @@ test('supports passing constants as extractor params', t => { fastify.close() }) }) + +test('supports passing constants as extractor params with domain', t => { + t.plan(5) + + const fastify = Fastify() + + fastify.register(makeStubCasbin()) + fastify.register(plugin, { + hook: 'onRequest', + getSub: request => request.user, + getObj: request => request.url, + getAct: request => request.method, + getDom: (request) => 'common' + }) + + fastify.get('/', { + casbin: { + rest: { + getSub: 'a', + getObj: 'b', + getAct: 'c', + getDom: 'users' + } + } + }, () => 'ok') + + fastify.ready(async err => { + t.error(err) + + fastify.casbin.enforce.callsFake((sub, dom, obj, act) => { + t.equal(sub, 'a') + t.equal(dom, 'users') + t.equal(obj, 'b') + t.equal(act, 'c') + return Promise.resolve(false) + }) + + await fastify.inject('/') + fastify.close() + }) +}) diff --git a/test/plugin.test-d.ts b/test/plugin.test-d.ts index 1858699..b39f495 100644 --- a/test/plugin.test-d.ts +++ b/test/plugin.test-d.ts @@ -7,8 +7,8 @@ const server = fastify() server.register(casbinRest) server.register(casbinRest, { - log: (fastify, request, sub, obj, act) => { fastify.log.info({ sub, obj, act }, 'Invoking casbin enforce') }, - onDeny: (reply, sub, obj, act) => { + log: (fastify, request, { sub, obj, act }) => { fastify.log.info({ sub, obj, act }, 'Invoking casbin enforce') }, + onDeny: (reply, { sub, obj, act }) => { expectType(reply) expectType(sub) expectType(obj) @@ -25,12 +25,17 @@ server.register(casbinRest, { getAct: request => { expectType(request) return '' + }, + getDom: request => { + expectType(request) + return '' } }) server.get('/', { casbin: { rest: { + getDom: (request: FastifyRequest) => 'users', getSub: (request: FastifyRequest) => '1', getObj: (request: FastifyRequest) => request.url, getAct: (request: FastifyRequest) => request.method @@ -41,6 +46,7 @@ server.get('/', { server.get('/entity', { casbin: { rest: { + getDom: 'users', getSub: '1', getObj: 'entity', getAct: 'read'