@knotel/authorize

Opinionated authorization middleware for federated GraphQL services.

Usage no npm install needed!

<script type="module">
  import knotelAuthorize from 'https://cdn.skypack.dev/@knotel/authorize';
</script>

README

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 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

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 documentation.

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.

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.

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.

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.

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."