Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add external policy mode #37

Merged
merged 4 commits into from
Oct 20, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,15 @@ Features:

- [Install](#install)
- [Quick Start](#quick-start)
- [Directive (default) mode](#directive-default-mode)
- [External Policy mode](#external-policy-mode)
- [Examples](#examples)
- [Benchmarks](#benchmarks)
- [API](docs/api/options.md)
- [Auth Context](docs/auth-context.md)
- [Apply Policy](docs/apply-policy.md)
- [Auth Directive](docs/auth-directive.md)
- [External Policy](docs/external-policy.md)
- [Errors](docs/errors.md)
- [Federation](docs/federation.md)

Expand All @@ -34,6 +37,12 @@ npm i fastify mercurius mercurius-auth

## Quick Start

We have two modes of operation for Mercurius Auth:

### Directive (default) mode

Setup in Directive mode as follows (this is the default mode of operation):

```js
'use strict'

Expand Down Expand Up @@ -86,6 +95,94 @@ app.register(mercuriusAuth, {
app.listen(3000)
```

### External Policy mode

Instead of using GraphQL Directives, you can implement an External Policy at plugin registration to protect GraphQL fields and types. You can find more information about implementing policy systems and how to build external policies for a GraphQL schema in the [External Policy documentation](docs/external-policy.md).

```js
'use strict'

const Fastify = require('fastify')
const mercurius = require('mercurius')
const mercuriusAuth = require('..')

const app = Fastify()

const schema = `
type Message {
title: String
message: String
adminMessage: String
}

type Query {
messages: [Message]
message(title: String): Message
}
`

const messages = [
{
title: 'one',
message: 'one',
adminMessage: 'admin message one'
},
{
title: 'two',
message: 'two',
adminMessage: 'admin message two'
}
]

const resolvers = {
Query: {
messages: async (parent, args, context, info) => {
return messages
},
message: async (parent, args, context, info) => {
return messages.find(message => message.title === args.title)
}
}
}

app.register(mercurius, {
schema,
resolvers
})

app.register(mercuriusAuth, {
// Load the permissions into the context from the request headers
authContext (context) {
const permissions = context.reply.request.headers['x-user'] || ''
return { permissions }
},
async applyPolicy (policy, parent, args, context, info) {
// When called on field `Message.adminMessage`
// policy: { requires: 'admin' }
// context.auth.permissions: ['user', 'admin'] - the permissions associated with the user (passed as headers in authContext)
return context.auth.permissions.includes(policy.requires)
},
// Enable External Policy mode
mode: 'external',
policy: {
// Associate policy with the 'Message' Object type
Message: {
// Define policy for 'Message' Object type
__typePolicy: { requires: 'user' },
// Define policy for 'adminMessage' field
adminMessage: { requires: 'admin' }
},
// Associate policy with the Query root type
Query: {
// Define policy for 'message' Query
messages: { requires: 'user' }
}
}
jonnydgreen marked this conversation as resolved.
Show resolved Hide resolved
})

app.listen(3000)
```

## Examples

Check [GitHub repo](https://github.com/mercurius-js/auth/tree/master/examples) for more examples.
Expand Down
156 changes: 154 additions & 2 deletions docs/api/options.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,90 @@

**mercurius-auth** supports the following options:

* **applyPolicy** `(authDirectiveAST: DirectiveNode, parent: object, args: Record<string, any>, context: MercuriusContext, info: GraphQLResolveInfo) => Promise<boolean | Error>` - the policy promise to run when an auth directive protected field is selected by the query. This must return `true` in order to pass the check and allow access to the protected field.
* **authDirective** `string` - the name of the directive that the Mercurius auth plugin will look for within the GraphQL schema in order to identify protected fields. For example, for directive definition `directive @auth on OBJECT | FIELD_DEFINITION`, the corresponding name would be `auth`.
* **applyPolicy** `(policy: any, parent: object, args: Record<string, any>, context: MercuriusContext, info: GraphQLResolveInfo) => Promise<boolean | Error>` - the policy promise to run when an auth protected field is selected by the query. This must return `true` in order to pass the check and allow access to the protected field.
* **authContext** `(context: MercuriusContext) => object | Promise<object>` (optional) - assigns the returned data to `MercuriusContext.auth` for use in the `applyPolicy` function. This runs within a [`preExecution`](https://mercurius.dev/#/docs/hooks?id=preexecution) Mercurius GraphQL request hook.
* **mode** `'directive' | 'external'` (optional, default: `'directive'`) - the mode of operation for the plugin. Depending on the mode of operation selected, this has the following options:

### `directive` (default) mode

* **authDirective** `string` - the name of the directive that the Mercurius auth plugin will look for within the GraphQL schema in order to identify protected fields. For example, for directive definition `directive @auth on OBJECT | FIELD_DEFINITION`, the corresponding name would be `auth`.

### `external` mode

* **policy** `MercuriusAuthPolicy` (optional) - the auth policy definition. The field definition is passed as the first argument when `applyPolicy` is called for the associated field.

#### Parameter: `MercuriusAuthPolicy`

Extends: `Record<string, MercuriusAuthTypePolicy>`

Each key within the `MercuriusAuthPolicy` type corresponds with the GraphQL type name. For example, if we wanted to protect an object type:

```graphql
type Message {
...
}
```

We would use the key: `Message`:

```js
{
Message: { ... }
}
```

#### Parameter: `MercuriusAuthTypePolicy`

Extends: `Record<string, any>`

- **__typePolicy** `any` (optional) - The policy definition for the type.

Each key within the `MercuriusAuthTypePolicy` type corresponds with the GraphQL field name on a type. For example, if we wanted to protect type field `message`:

```graphql
type Message {
title: String
message: String
}
```

We would use the key: `message`:

```js
{
Message: {
message: { requires: 'user' }
}
}
```

If we want to protect the entire type, we would use `__typePolicy`:

```js
{
Message: {
__typePolicy: { requires: 'user' }
}
}
```

This also works alongside specific field policies on the type:

```js
{
Message: {
__typePolicy: { requires: 'user', }
message: { requires: 'admin' }
}
}
```

## Registration

The plugin must be registered **after** Mercurius is registered.

### Directive (default) mode

```js
'use strict'

Expand Down Expand Up @@ -65,3 +141,79 @@ app.register(mercuriusAuth, {

app.listen(3000)
```

### External Policy mode

```js
'use strict'

const Fastify = require('fastify')
const mercurius = require('mercurius')
const mercuriusAuth = require('..')

const app = Fastify()

const schema = `
type Message {
title: String
message: String
adminMessage: String
}

type Query {
messages: [Message]
message(title: String): Message
}
`

const messages = [
{
title: 'one',
message: 'one',
adminMessage: 'admin message one'
},
{
title: 'two',
message: 'two',
adminMessage: 'admin message two'
}
]

const resolvers = {
Query: {
messages: async (parent, args, context, info) => {
return messages
},
message: async (parent, args, context, info) => {
return messages.find(message => message.title === args.title)
}
}
}

app.register(mercurius, {
schema,
resolvers
})

app.register(mercuriusAuth, {
authContext (context) {
const permissions = context.reply.request.headers['x-user'] || ''
return { permissions }
},
async applyPolicy (policy, parent, args, context, info) {
return context.auth.permissions.includes(policy.requires)
},
mode: 'external',
policy: {
Message: {
__typePolicy: { requires: 'user' },
adminMessage: { requires: 'admin' }
},
Query: {
messages: { requires: 'user' }
}
}
})

app.listen(3000)
```
Loading