Skip to content

Commit

Permalink
Refactorings
Browse files Browse the repository at this point in the history
  • Loading branch information
voxpelli committed Jun 3, 2023
1 parent 95002de commit 1486e83
Show file tree
Hide file tree
Showing 12 changed files with 135 additions and 351 deletions.
2 changes: 1 addition & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"extends": "@voxpelli",
"extends": "@voxpelli/eslint-config/esm",
"root": true,
"rules": {
"semi": 0,
Expand Down
170 changes: 49 additions & 121 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@

ACL-like authorization for [*fastify*](https://fastify.io) apps.

With *fastify-acl-auth* you can secure routes with roles, like **admin**, **superuser**, or **user:write**. Then you just tell the plugin how to determine which roles a user has, and you're set. You can also:
With `@voxpelli/fastify-acl-auth` you can secure routes with roles, like **admin**, **superuser**, or **user:write**. Then you just tell the plugin how to determine which roles a user has, and you're set. You can also:

* Specify any/all functionality (allow if user has any of these roles, allow if users has all of these roles, for example)
* Specify a hierarchy of roles ("admins" are clearly "users" too, so let them through without explicitly letting "admins" through, for example)
* Easily use *fastify-acl-auth* as an authentication strategy with [*fastify-auth*](https://www.npmjs.com/package/fastify-auth), or anything else really
* Easily use `@voxpelli/fastify-acl-auth` as an authentication strategy with [*fastify-auth*](https://www.npmjs.com/package/fastify-auth), or anything else really

<!-- toc -->

Expand All @@ -26,159 +26,87 @@ With *fastify-acl-auth* you can secure routes with roles, like **admin**, **supe

**NOTE:** If you're not familiar with [scoping in *fastify*](https://www.fastify.io/docs/master/Plugins/) this plugin isn't going to make much sense to you. I'd highly recommend making sure that you're solid with this concept before proceeding.

**ANOTHER NOTE:** _fastify-acl-auth_ needs to have a way to know what roles a user has, right? By default it assumes that you have a session provider available at `request.session` (and that roles are available at `request.session.credentials.roles`, which you can easily change). In many examples I simulate this with a request decorator (`fastify.decorateRequest('session', { ... })`), I recommend [*fastify-server-session*](https://www.npmjs.com/package/fastify-server-session) in practice.
You can use `@voxpelli/fastify-acl-auth` in a few ways, ways that depend on how you want to structure your application and leverage *fastify*'s scoping.

You can use *fastify-acl-auth* in a few ways, ways that depend on how you want to structure your application and leverage *fastify*'s scoping.

### Simple Example
### Example

```js
'use strict'

const fastify = require('fastify')()

const aclFactory = require('@voxpelli/fastify-acl-auth')

const credentials = {
id: 'bc965eb1-a8a4-4320-9172-726e9a7e83c9',
username: 'cread',
roles: 'vendor'
}

fastify.decorateRequest('session', {credentials})

fastify.register(function (fastifyScope, opts, next) {
fastifyScope.register(
aclFactory(
{
allowedRoles: ['customer']
}
)
)
// 403
fastifyScope.get('/customers', function (request, reply) {
return reply.send('/customers')
})
next()
})

fastify.register(function (fastifyScope, opts, next) {
fastifyScope.register(
aclFactory(
{
allowedRoles: ['vendor']
}
)
)
// 200
fastifyScope.get('/vendors', function (request, reply) {
return reply.send('/vendors')
})
next()
})
import createFastify from 'fastify'
import { fastifyAclAuth } from '../plugin.js'

fastify.listen(8080, function (err) {
if (err) throw err
console.log('listening on %s', fastify.server.address().port)
const hierarchyAclPlugin = aclFactory({
actualRoles: (_req) => 'admin',
hierarchy: ['user', 'admin', 'superuser'],
})

```

### Using a Hierarchy

```js
'use strict'

const fastify = require('fastify')()

const aclFactory = require('@voxpelli/fastify-acl-auth')

const hierarchyAcl = aclFactory({hierarchy: ['user', 'admin', 'superuser']})

const credentials = {
id: 'bc965eb1-a8a4-4320-9172-726e9a7e83c9',
username: 'cread',
roles: 'admin'
}

fastify.decorateRequest('session', {credentials})
fastify.register(async (fastifyScope, opts) => {
fastifyScope.register(hierarchyAclPlugin, {
allowedRoles: ['user']
})

fastify.register(function (fastifyScope, opts, next) {
fastifyScope.register(
hierarchyAcl,
{
allowedRoles: ['user']
}
)
// 200, because 'admin' > 'user' in hierarchy
fastifyScope.get('/user', function (request, reply) {
return reply.send('/user')
})
next()
fastifyScope.get('/user', (_request, reply) => reply.send('/user'))
})

fastify.register(function (fastifyScope, opts, next) {
fastifyScope.register(
hierarchyAcl,
{
allowedRoles: ['admin']
}
)
// 200
fastifyScope.get('/admin', function (request, reply) {
return reply.send('/admin')
fastify.register(async (fastifyScope, opts) => {
fastifyScope.register(hierarchyAclPlugin, {
allowedRoles: ['admin']
})
next()

// 200
fastifyScope.get('/admin', (_request, reply) => reply.send('/admin'))
})

fastify.register(function (fastifyScope, opts, next) {
fastifyScope.register(
hierarchyAcl,
{
allowedRoles: ['superuser']
}
)
// 403
fastifyScope.get('/superuser', function (request, reply) {
return reply.send('/superuser')
fastify.register(async (fastifyScope, opts, next) {
fastifyScope.register(hierarchyAclPlugin, {
allowedRoles: ['superuser']
})
next()

// 403, because 'superuser' > 'admin' in hierarchy
fastifyScope.get('/superuser', (_request, reply) => reply.send('/superuser'))
})

fastify.listen(8080, function (err) {
if (err) throw err
console.log('listening on %s', fastify.server.address().port)
fastify.listen({ port: 8080 }, (err) => {
if (err) {
fastify.log.error(err)
process.exit(1)
}
})

```

## API

_fastify-acl-auth_ exports a factory function; a function that _makes_ the plugin that you'll use.
`@voxpelli/fastify-acl-auth` exports a factory function; a function that _makes_ the plugin that you'll use.

```js
const aclFactory = require('@voxpelli/fastify-acl-auth')
import { fastifyAclAuth } from '@voxpelli/fastify-acl-auth';
```

### `options`

`options` is a simple object with the following properties:

| Property | Default | Type | Notes |
| --- | --- | --- | --- |
| `actualRoles` | `request.session.credentials.roles` | `Array`, `string`, `[async] function` | Since `fastify-acl-auth` is all about comparing what roles a user _actually_ has to what a route _allows_ then this property is pretty important. This property can be an `Array` of roles (`string`s), a role itself (`string`), or an `[async] function` that returns an `Array` of roles (`string`s). |
| `allowedRoles` | `[]` | `Array`, `string`, `[async] function` | ^ that whole thing. Except this property tells `fastify-acl-auth` which roles are _allowed_ for a route or routes. ([scoping!!!](https://www.fastify.io/docs/latest/Plugins/)) |
| `any` | `true` | `boolean` | If `true` a `200` will be returned if `actualRoles` contains _any_ of the roles in `allowedRoles`, `403` otherwise. |
| `all` | `false` | `boolean` | If `true` a `200` will be returned [iff](https://en.wikipedia.org/wiki/If_and_only_if) `actualRoles` contains _ALL_ of the roles in `allowedRoles`, `403` otherwise. |
| `hierarchy` | `undefined` | `Array` | An `Array` that specifies the privilege hierarchy of roles in order of ascending privilege. For instance, suppose we have `hierarchy: ['user', 'admin', 'superuser]`, `allowedRoles : ['admin']`, and `actualRoles: ['superuser]` configured for a route. A user with the `superuser` role will be able to access that route because the `superuser` role is of higher privilege than the `user` and `admin` roles, as specified in the hierarchy. |
| `actualRoles` | - | `[async] function` | Since `fastify-acl-auth` is all about comparing what roles a user _actually_ has to what a route _allows_ then this property is pretty important. Should be a sync or async function that's given the Fastify request and which returns a `string`, an array of `string`:s or `undefined`. |
| `allowedRoles` | `[]` | `string[]`, `string` | ^ that whole thing. Except this property tells `fastify-acl-auth` which roles are _allowed_ for a route or routes. ([scoping!!!](https://www.fastify.io/docs/latest/Plugins/)) |
| `all` | `false` | `boolean` | If `true`, will pass if `actualRoles` contains _ALL_ of the roles in `allowedRoles`, else error return a HTTP `httpErrorCode`. |
| `hierarchy` | `undefined` | `Array` | An `Array` that specifies the privilege hierarchy of roles in order of ascending privilege. For instance, suppose we have `hierarchy: ['user', 'admin', 'superuser']`, `allowedRoles : ['admin']`, and `actualRoles: ['superuser']` configured for a route. A user with the `superuser` role will be able to access that route because the `superuser` role is of higher privilege than the `user` and `admin` roles, as specified in the hierarchy. |
| `httpErrorCode` | `403` | `number` | The error code to use when the authorization fails. |
| `pathExempt` | `undefined` | `Array` | An `Array` that specifies the path patterns that should be exempt from enforcement; `['/login', '/callback**']` for example. Uses the NPM module `url-pattern` internally for URL pattern matching. |

### `aclFactory([options])`
This will create an instance of `fastify-acl-auth`. It can be used with `fastify.register()` just like any other plugin.
### `fastifyAclAuth(options): FastifyAclAuthPlugin`

```js
fastify.register(aclFactory([options]), [options])
```
This will create a plugin for `@voxpelli/fastify-acl-auth`. It can be used with `fastify.register()` just like any other plugin.

Nope, that's not a typo, `options` is there twice; `aclFactory([options])` is setting the options of your _plugin instance_, whereas passing `options` during _registration_ is setting, or overriding, the _plugin instance_ options for _that registration_ of the plugin instance. So you can create an instance of `fastify-acl-auth` and "carry it around with you" for later use. Passing `options` _when you register_ the plugin will _override_ the `options` set when creating the plugin instance with the factory function.
```js
const fastifyAclPlugin = fastifyAclAuth({
actualRoles: request => request.user?.role
});

Lots of words, right? This architecture really comes from the architecture (really talking about scoping here) of `fastify` itself, and should make sense with a [basic knowledge of scoping](https://www.fastify.io/docs/latest/Plugins/). It's actually very logical when it sinks in.
fastify.register(fastifyAclPlugin, {
allowedRoles: 'admin'
})
```
6 changes: 0 additions & 6 deletions example/.eslintrc

This file was deleted.

42 changes: 0 additions & 42 deletions example/1 - simple.js

This file was deleted.

53 changes: 0 additions & 53 deletions example/2 - hierarchy.js

This file was deleted.

15 changes: 6 additions & 9 deletions lib/auth.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict'
import createDebug from 'debug';
import { ensureArray } from './util.js';

const debug = require('debug')('fastify-acl-auth:auth')
const debug = createDebug('fastify-acl-auth:auth');

/** @typedef {{ all?: boolean, hierarchy?: ReadonlyArray<string> }} CheckRolesOptions */

Expand All @@ -10,7 +11,7 @@ const debug = require('debug')('fastify-acl-auth:auth')
* @param {CheckRolesOptions} [options]
* @returns {boolean}
*/
const checkRoles = function (rawActual, rawAllowed, options = {}) {
export function checkRoles (rawActual, rawAllowed, options = {}) {
debug('auth implementation called')

const {
Expand All @@ -21,8 +22,8 @@ const checkRoles = function (rawActual, rawAllowed, options = {}) {
...options,
};

const actual = Array.isArray(rawActual) ? rawActual : [rawActual];
const allowed = Array.isArray(rawAllowed) ? rawAllowed : [rawAllowed];
const actual = ensureArray(rawActual);
const allowed = ensureArray(rawAllowed);

if (hierarchy) {
let lowestAllowedIndex = 50
Expand All @@ -47,7 +48,3 @@ const checkRoles = function (rawActual, rawAllowed, options = {}) {
? (intersection.length === allowed.length)
: intersection.length > 0
}

module.exports = {
checkRoles,
}

0 comments on commit 1486e83

Please sign in to comment.