@knotel/authorize
v0.11.0
Published
Opinionated authorization middleware for federated GraphQL services.
Downloads
6
Keywords
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
andRole
tables. - The
Role
table has apolicy
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." |