mercurius-auth
Advanced tools
Comparing version 1.1.0 to 1.2.0
@@ -56,1 +56,61 @@ # Auth directive | ||
``` | ||
The auth directive can also be use at the type level, to wrap all fields of a type (useful when working with federated types). You can nest auth directives this way as well to protect certain types/fields of a parent type. | ||
```js | ||
'use strict' | ||
const Fastify = require('fastify') | ||
const mercurius = require('mercurius') | ||
const mercuriusAuth = require('mercurius-auth') | ||
const app = Fastify() | ||
const schema = ` | ||
directive @auth( | ||
requires: Role = ADMIN, | ||
) on OBJECT | FIELD_DEFINITION | ||
enum Role { | ||
ADMIN | ||
REVIEWER | ||
USER | ||
UNKNOWN | ||
} | ||
type Query { | ||
user: User | ||
} | ||
type User @auth(requires: USER) { | ||
id: Int | ||
name: String | ||
location: String @auth(requires: ADMIN) | ||
} | ||
` | ||
const resolvers = { | ||
Query: { | ||
add: async (_, { x, y }) => x + y | ||
} | ||
} | ||
app.register(mercurius, { | ||
schema, | ||
resolvers | ||
}) | ||
app.register(mercuriusAuth, { | ||
authContext (context) { | ||
return { | ||
identity: context.reply.request.headers['x-user'] | ||
} | ||
}, | ||
async applyPolicy (authDirectiveAST, parent, args, context, info) { | ||
return context.auth.identity === 'admin' | ||
}, | ||
authDirective: 'auth' | ||
}) | ||
app.listen(3000) | ||
``` |
@@ -37,2 +37,22 @@ 'use strict' | ||
wrapFields (schemaType, authDirective) { | ||
// Handle fields on schema type | ||
if (typeof schemaType.getFields === 'function') { | ||
for (const [fieldName, field] of Object.entries(schemaType.getFields())) { | ||
if (typeof field.astNode !== 'undefined') { | ||
// Override resolvers on protected fields | ||
const authDirectiveASTForField = authDirective || this[kGetAuthDirectiveAST](field.astNode) | ||
if (authDirectiveASTForField !== null) { | ||
if (typeof field.resolve === 'function') { | ||
const originalFieldResolver = field.resolve | ||
field.resolve = this[kMakeProtectedResolver](authDirectiveASTForField, originalFieldResolver) | ||
} else { | ||
field.resolve = this[kMakeProtectedResolver](authDirectiveASTForField, (parent) => parent[fieldName]) | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
registerAuthHandlers (schema) { | ||
@@ -42,19 +62,10 @@ // Traverse schema types and override resolvers with auth protection where necessary | ||
for (const schemaType of Object.values(schemaTypeMap)) { | ||
// Handle fields on schema type | ||
if (typeof schemaType.getFields === 'function') { | ||
for (const [fieldName, field] of Object.entries(schemaType.getFields())) { | ||
if (typeof field.astNode !== 'undefined') { | ||
// Override resolvers on protected fields | ||
const authDirectiveASTForField = this[kGetAuthDirectiveAST](field.astNode) | ||
if (authDirectiveASTForField !== null) { | ||
if (typeof field.resolve === 'function') { | ||
const originalFieldResolver = field.resolve | ||
field.resolve = this[kMakeProtectedResolver](authDirectiveASTForField, originalFieldResolver) | ||
} else { | ||
field.resolve = this[kMakeProtectedResolver](authDirectiveASTForField, (parent) => parent[fieldName]) | ||
} | ||
} | ||
} | ||
// Handle directive on type | ||
if (typeof schemaType.astNode !== 'undefined') { | ||
const authDirectiveASTForType = this[kGetAuthDirectiveAST](schemaType.astNode) | ||
if (authDirectiveASTForType !== null) { | ||
this.wrapFields(schemaType, authDirectiveASTForType) | ||
} | ||
} | ||
this.wrapFields(schemaType) | ||
} | ||
@@ -61,0 +72,0 @@ } |
{ | ||
"name": "mercurius-auth", | ||
"version": "1.1.0", | ||
"version": "1.2.0", | ||
"description": "Mercurius Auth Plugin adds configurable Authentication and Authorization support to Mercurius.", | ||
@@ -34,3 +34,3 @@ "main": "index.js", | ||
"@sinonjs/fake-timers": "^7.0.5", | ||
"@types/node": "^15.0.2", | ||
"@types/node": "^16.0.0", | ||
"@types/ws": "^7.4.2", | ||
@@ -42,3 +42,3 @@ "@typescript-eslint/eslint-plugin": "^4.1.0", | ||
"fastify": "^3.0.2", | ||
"mercurius": "^7.5.0", | ||
"mercurius": "^8.0.0", | ||
"pre-commit": "^1.2.2", | ||
@@ -48,5 +48,5 @@ "snazzy": "^9.0.0", | ||
"tap": "^15.0.2", | ||
"tsd": "^0.14.0", | ||
"tsd": "^0.17.0", | ||
"typescript": "^4.0.3", | ||
"wait-on": "^5.3.0" | ||
"wait-on": "^6.0.0" | ||
}, | ||
@@ -53,0 +53,0 @@ "dependencies": { |
@@ -779,1 +779,261 @@ 'use strict' | ||
}) | ||
test('basic - should work at type level with field resolvers', async (t) => { | ||
t.plan(1) | ||
const schema = ` | ||
directive @auth( | ||
requires: Role = ADMIN, | ||
) on OBJECT | FIELD_DEFINITION | ||
enum Role { | ||
ADMIN | ||
REVIEWER | ||
USER | ||
UNKNOWN | ||
} | ||
type Query { | ||
getUser: User | ||
} | ||
type User @auth(requires: USER) { | ||
id: Int | ||
name: String | ||
}` | ||
const resolvers = { | ||
Query: { | ||
getUser: async (_, obj) => ({ | ||
id: 1, | ||
name: 'testuser', | ||
test: 'TEST' | ||
}) | ||
}, | ||
User: { | ||
id: async (src) => src.id | ||
} | ||
} | ||
const query = `query { | ||
getUser { | ||
id | ||
name | ||
} | ||
}` | ||
const app = Fastify() | ||
t.teardown(app.close.bind(app)) | ||
app.register(mercurius, { | ||
schema, | ||
resolvers | ||
}) | ||
app.register(mercuriusAuth, { | ||
authContext (context) { | ||
return { | ||
identity: context.reply.request.headers['x-user'] | ||
} | ||
}, | ||
async applyPolicy (authDirectiveAST, parent, args, context, info) { | ||
return context.auth.identity === 'user' | ||
}, | ||
authDirective: 'auth' | ||
}) | ||
const response = await app.inject({ | ||
method: 'POST', | ||
headers: { 'content-type': 'application/json', 'X-User': 'user' }, | ||
url: '/graphql', | ||
body: JSON.stringify({ query }) | ||
}) | ||
t.same(JSON.parse(response.body), { | ||
data: { | ||
getUser: { | ||
id: 1, | ||
name: 'testuser' | ||
} | ||
} | ||
}) | ||
}) | ||
test('basic - should work at type level with nested directive', async (t) => { | ||
t.plan(1) | ||
const schema = ` | ||
directive @auth( | ||
requires: Role = ADMIN, | ||
) on OBJECT | FIELD_DEFINITION | ||
enum Role { | ||
ADMIN | ||
REVIEWER | ||
USER | ||
UNKNOWN | ||
} | ||
type Query { | ||
getUser: User | ||
} | ||
type User @auth(requires: USER) { | ||
id: Int | ||
name: String | ||
protected: String @auth(requires: ADMIN) | ||
}` | ||
const resolvers = { | ||
Query: { | ||
getUser: async (_, obj) => ({ | ||
id: 1, | ||
name: 'testuser', | ||
protected: 'protected data' | ||
}) | ||
}, | ||
User: { | ||
id: async (src) => src.id | ||
} | ||
} | ||
const query = `query { | ||
getUser { | ||
id | ||
name | ||
protected | ||
} | ||
}` | ||
const app = Fastify() | ||
t.teardown(app.close.bind(app)) | ||
app.register(mercurius, { | ||
schema, | ||
resolvers | ||
}) | ||
app.register(mercuriusAuth, { | ||
authContext (context) { | ||
return { | ||
identity: context.reply.request.headers['x-user'].toUpperCase().split(',') | ||
} | ||
}, | ||
async applyPolicy (authDirectiveAST, parent, args, context, info) { | ||
const findArg = (arg, ast) => { | ||
let result | ||
ast.arguments.forEach((a) => { | ||
if (a.kind === 'Argument' && | ||
a.name.value === arg) { | ||
result = a.value.value | ||
} | ||
}) | ||
return result | ||
} | ||
const requires = findArg('requires', authDirectiveAST) | ||
return context.auth.identity.includes(requires) | ||
}, | ||
authDirective: 'auth' | ||
}) | ||
const response = await app.inject({ | ||
method: 'POST', | ||
headers: { 'content-type': 'application/json', 'X-User': 'user' }, | ||
url: '/graphql', | ||
body: JSON.stringify({ query }) | ||
}) | ||
t.same(JSON.parse(response.body), { | ||
data: { | ||
getUser: { | ||
id: 1, | ||
name: 'testuser', | ||
protected: null | ||
} | ||
}, | ||
errors: [ | ||
{ message: 'Failed auth policy check on protected', locations: [{ line: 5, column: 7 }], path: ['getUser', 'protected'] } | ||
] | ||
}) | ||
}) | ||
test('basic - should error for all fields in type', async (t) => { | ||
t.plan(1) | ||
const schema = ` | ||
directive @auth( | ||
requires: Role = ADMIN, | ||
) on OBJECT | FIELD_DEFINITION | ||
enum Role { | ||
ADMIN | ||
REVIEWER | ||
USER | ||
UNKNOWN | ||
} | ||
type Query { | ||
getUser: User | ||
} | ||
type User @auth(requires: ADMIN) { | ||
id: Int | ||
name: String | ||
}` | ||
const resolvers = { | ||
Query: { | ||
getUser: async (_, obj) => ({ | ||
id: 1, | ||
name: 'testuser' | ||
}) | ||
}, | ||
User: { | ||
id: async (src) => src.id | ||
} | ||
} | ||
const query = `query { | ||
getUser { | ||
id | ||
name | ||
} | ||
}` | ||
const app = Fastify() | ||
t.teardown(app.close.bind(app)) | ||
app.register(mercurius, { | ||
schema, | ||
resolvers | ||
}) | ||
app.register(mercuriusAuth, { | ||
authContext (context) { | ||
return { | ||
identity: context.reply.request.headers['x-user'] | ||
} | ||
}, | ||
async applyPolicy (authDirectiveAST, parent, args, context, info) { | ||
return context.auth.identity === 'admin' | ||
}, | ||
authDirective: 'auth' | ||
}) | ||
const response = await app.inject({ | ||
method: 'POST', | ||
headers: { 'content-type': 'application/json', 'X-User': 'user' }, | ||
url: '/graphql', | ||
body: JSON.stringify({ query }) | ||
}) | ||
t.same(JSON.parse(response.body), { | ||
data: { | ||
getUser: { | ||
id: null, | ||
name: null | ||
} | ||
}, | ||
errors: [ | ||
{ message: 'Failed auth policy check on id', locations: [{ line: 3, column: 7 }], path: ['getUser', 'id'] }, | ||
{ message: 'Failed auth policy check on name', locations: [{ line: 4, column: 7 }], path: ['getUser', 'name'] } | ||
] | ||
}) | ||
}) |
Sorry, the diff of this file is not supported yet
115532
44
3372