contrib
contrib copied to clipboard
OPA and GraphQL
Has anyone attempted to integrate OPA and GraphQL for policies against Graph Nodes or other query / graph structures?
I don't know of any integrations with GraphQL that have been done (or at least shared publicly).
If this is something you'd want to work on we'd be happy to help give any support/guidance from the OPA side of things.
I just played around with that - it should be pretty straight forward with apollo. I've appended a simple example on where you could insert the opa requests. The nice thing is that this should also work with apollo federation. I am not sure how far the WASM would take me, I think the compile API doesn't work with wasm, yet.
I just added two directives: @guard
and @filter
(see the two SchemaDirectiveVisitor
implementations). For the @guard
you can just pass whatever you got as arguments in the resolver and post that to opa, since it's JSON already. What will be a bit harder is how to handle the filtering, in the example I just used an additional argument that get's injected. If you want I can post again when I actually implemented authZ for my entire API that way. This piece is just my evaluation.
import {ApolloServer, gql, IResolvers, SchemaDirectiveVisitor} from 'apollo-server';
const typeDefs = gql`
directive @guard on FIELD_DEFINITION
directive @filter on FIELD_DEFINITION
type Foo {
value: Int @guard
even: Boolean!
}
type Query
{
foo: Foo @guard
foos(even: Boolean): [Foo!] @filter
}`;
class GuardDirective extends SchemaDirectiveVisitor {
public visitFieldDefinition(field) {
const oldResolve = field.resolve;
field.resolve = (source, args, context, info) => {
const allow = true; //check opa here and possibly return null
if (allow)
return oldResolve ? oldResolve(source, args, context, info) : source[field.name];
else
return null;
}
}
}
class FilterDirective extends SchemaDirectiveVisitor {
public visitFieldDefinition(field) {
const oldResolve = field.resolve;
field.resolve = (source, args, context, info) => {
const filter_arg = {even: true}; //use OPA Compile and then evaluate what you get back
return oldResolve ? oldResolve(source, {...args, ...filter_arg}, context, info) : null /* deny already resolved ones */;
}
}
}
const resolvers : IResolvers = {
Foo: {
even: (source, args, context, info) => {
return (source.value % 2) == 0;
}
},
Query: {
foo: (source, args, context, info) => {
return {value: 1234}
},
foos: (source, args, context, info) => {
const tmp = Array.from({length: 10}, (_, value) => ({value}));
if (args.even === undefined)
return tmp;
else if (args.even === true)
return tmp.filter(({value}) => (value % 2) == 0);
else
return tmp.filter(({value}) => (value % 2) != 0);
}
}
}
export const server = new ApolloServer({
logger: console,
debug: true,
typeDefs,
resolvers,
schemaDirectives: {
guard: GuardDirective,
filter: FilterDirective
},
context: ({ req }) => req.headers
});
server.listen(4000).then(({ url }) => {
console.log(`🚀 Server ready at ${url}`)
});
I am not sure how far the WASM would take me, I think the compile API doesn't work with wasm, yet.
Correct, you have to use the OPA golang binary if you want to do partial evaluation.
If you do not have any contextual data it seems like you should be able to do the partial evaluation ahead of time (same time when you create the WASM binaries, if you go that route). Then it would just be a matter of getting the queries translated and into your code to do the filtering. Likely some additional concerns about how to handle policy updates and stuff with that approach versus using the OPA server w/ bundles. Something to look into.
What will be a bit harder is how to handle the filtering, in the example I just used an additional argument that get's injected.
Right, I suspect it would be hard to have a general purpose translator from Rego queries -> graphql filters. If it is a matter of returning some key-value constraints it might almost be better to just have the policy evaluation result return those rather than using partial evaluation and translating the queries.
If you want I can post again when I actually implemented authZ for my entire API that way.
Please do! I'm sure others will be interested in the solution
I just played around with that - it should be pretty straight forward with apollo. I've appended a simple example on where you could insert the opa requests. The nice thing is that this should also work with apollo federation. I am not sure how far the WASM would take me, I think the compile API doesn't work with wasm, yet.
I just added two directives:
@guard
and@filter
(see the twoSchemaDirectiveVisitor
implementations). For the@guard
you can just pass whatever you got as arguments in the resolver and post that to opa, since it's JSON already. What will be a bit harder is how to handle the filtering, in the example I just used an additional argument that get's injected. If you want I can post again when I actually implemented authZ for my entire API that way. This piece is just my evaluation.import {ApolloServer, gql, IResolvers, SchemaDirectiveVisitor} from 'apollo-server'; const typeDefs = gql` directive @guard on FIELD_DEFINITION directive @filter on FIELD_DEFINITION type Foo { value: Int @guard even: Boolean! } type Query { foo: Foo @guard foos(even: Boolean): [Foo!] @filter }`; class GuardDirective extends SchemaDirectiveVisitor { public visitFieldDefinition(field) { const oldResolve = field.resolve; field.resolve = (source, args, context, info) => { const allow = true; //check opa here and possibly return null if (allow) return oldResolve ? oldResolve(source, args, context, info) : source[field.name]; else return null; } } } class FilterDirective extends SchemaDirectiveVisitor { public visitFieldDefinition(field) { const oldResolve = field.resolve; field.resolve = (source, args, context, info) => { const filter_arg = {even: true}; //use OPA Compile and then evaluate what you get back return oldResolve ? oldResolve(source, {...args, ...filter_arg}, context, info) : null /* deny already resolved ones */; } } } const resolvers : IResolvers = { Foo: { even: (source, args, context, info) => { return (source.value % 2) == 0; } }, Query: { foo: (source, args, context, info) => { return {value: 1234} }, foos: (source, args, context, info) => { const tmp = Array.from({length: 10}, (_, value) => ({value})); if (args.even === undefined) return tmp; else if (args.even === true) return tmp.filter(({value}) => (value % 2) == 0); else return tmp.filter(({value}) => (value % 2) != 0); } } } export const server = new ApolloServer({ logger: console, debug: true, typeDefs, resolvers, schemaDirectives: { guard: GuardDirective, filter: FilterDirective }, context: ({ req }) => req.headers }); server.listen(4000).then(({ url }) => { console.log(`🚀 Server ready at ${url}`) });
@klemens-morgenstern : This is very useful. Can you also point, to your repo if possible. Just incase if it is open sourced?
It's not open source, sorry.
I tried to bring authorization check functionality with OPA into GraphQL environment. opa-wasm (https://github.com/open-policy-agent/npm-opa-wasm) enabled this integration. This is a trial open source which supports RBAC. I tried unified authorization for REST/GraphQL with OPA. https://github.com/onelittlenightmusic/opa-entrypoint-authorizer Any feedback is welcome.
Support for GraphQL is now in OPA main, and a tutorial is avaialble in the OPA docs :) Credits to @philipaconrad who worked on that.
Closing this as resolved, but do let us know if there's anything more you'd want from this.