npm package: @knotel/authorize
Description: Opinionated authorization middleware for federated GraphQL services.
Author: engineering@knotel.com
Latest version: 0.11.0

## Getting Started

Import this package into any Apollo federated service within the mono infrastructure where the User <> Profile association exists.

`@knotel/authorize` makes a few assumptions:

- The federated service has `Profile` and `Role` tables.
- The `Role` table has a `policy` column containing a valid CSV format string.
- The `policy` CSV string is formatted in the following syntax:

```
p, resource, action
```

The [node-casbin](https://github.com/casbin/node-casbin) depedency is normally case-sensitive and requires comma-separated values to be padded with whitespace. However, the `isAuthorized` method is written to be more flexible. So case and space should not matter.

## Installation

```sh
yarn add @knotel/authorize
```

## Usage

`app/graphql/permissions/index.js`

For more information on how to construct your permissions directory, see the official [graphql-shield](https://github.com/maticzav/graphql-shield) documentation.
```js
const { permissions, isAuthorized } = require('@knotel/authorize')

// permissions is a graphql-shield function (see shield)
module.exports = permissions({
    Query: {
        Resource: isAuthorized('Read'),
        allResources: isAuthorized('List'),
    },
    Mutation: {
        createResource: isAuthorized('Create'),
        updateResource: isAuthorized('Edit'),
        deleteResource: isAuthorized('Delete'),
    },
})
```
`app/graphql/schema.js`

Nothing functional is going on here. Just imported and exported through this file for convenience and cleaner imports.
```js
const typeDefFiles = require('./typedefs')
const resolvers = require('./resolvers')
const permissions = require('./permissions')

const typeDefString = typeDefFiles.join('\n')
const typeDefs = gql(typeDefString)

module.exports = {
  typeDefs,
  resolvers,
  permissions,
}
```
`app/app.js`
Import the permissions dependency and apply it as middleware to your GraphQL schema. Then, in your global context object, add the logic to derive the policy document for a given user and inject it into the context object.

`isAuthorized` checks for the scopes specified in your permissions directory in the policy document provided by the context object.
```js
const { applyMiddleware } = require('graphql-middleware')

const { typeDefs, resolvers, permissions } = require('./graphql/schema')

const schema = buildFederatedSchema([{
    typeDefs,
    resolvers
}])

const server = new ApolloServer({
    schema: applyMiddleware(schema, permissions),
    context: ({ req, context }) => {
        const profile = await Profile.findOne({
            where: {
                userId: req.user.id
            }
        })
        const role = await profile.getRole({ raw: true })
        return { user: req.user, policy: role.policy, req: req }
    },
})
```

## Creation of Policy Documents

Ideally, the policy document should be stored on the role record belonging to the user. Not the user or profile itself.

That being said, the injection of the `policy` property into the global context object offers you the flexibility to derive the policy document from wherever you choose.


`app/db/seeders/*.js`

To create your policy documents, this package also includes a small utility to use in the creation of your seed `Role` records.
```js
const { generatePolicy } = require('@knotel/authorize')

const resourcePermissions = [{
    name: 'Space',
    resource: ['List', 'Read', 'Create', 'Edit', 'Delete']
}]

module.exports = {
  up: (queryInterface, Sequelize) => {
    return queryInterface.sequelize.query(`
      UPDATE "Role" SET "policy" = '${generatePolicy(resourcePermissions)}' WHERE "Role"."name" = 'ACCOUNT_EXECUTIVE';
    `)
  },
  down: (queryInterface, Sequelize) => {
    return queryInterface.sequelize.query(`
      UPDATE "Role" SET "policy" = NULL WHERE "Role"."name" = 'ACCOUNT_EXECUTIVE';
    `)
  },
}
```

## Validation of Policy Documents

An important aspect of ensuring a bug-free authorization framework is persisting policy documents in the format expected by the `@knotel/authorize`.

```js
const { isValidPolicy } = require('@knotel/authorize')

const role = Role.findByPk(req.params.id)
const isValid = await isValidPolicy(role.policy)
```

## Resource Authorization

 | Prefix | Query/Mutation Example | Example Args | Scope     | Description                                             |
|--------|------------------------|--------------|-----------|---------------------------------------------------------|
| all    | allResources           | ()           | list      | "I want all resources without knowing IDs."             |
| own    | allResources           | ()           | ownlist   | "I want all my/our resources without knowing IDs."      |
| any    | Resource               | (id)         | read      | "I want any resource assuming I know the ID."           |
| own    | Resource               | (id)         | ownread   | "I want my resource assuming I know the ID."            |
| any    | deleteResource         | (id)         | delete    | "I want my resource assuming I know the ID."            |
| own    | deleteResource         | (id)         | owndelete | "I want to delete my resource assuming I know the ID."  |
| any    | updateResource         | (id, params) | edit      | "I want to update any resource assuming I know the ID." |
| own    | updateResource         | (id, params) | ownedit   | "I want to update my resource assuming I know the ID."  |
| new    | createResource         | (params)     | create    | "I want to create a new resource."                      |