contrib icon indicating copy to clipboard operation
contrib copied to clipboard

OPA and GraphQL

Open ldadams opened this issue 4 years ago • 6 comments

Has anyone attempted to integrate OPA and GraphQL for policies against Graph Nodes or other query / graph structures?

ldadams avatar Mar 10 '20 16:03 ldadams

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.

patrick-east avatar Mar 10 '20 20:03 patrick-east

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}`)
});

klemens-morgenstern avatar May 26 '20 05:05 klemens-morgenstern

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

patrick-east avatar May 26 '20 21:05 patrick-east

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}`)
});

@klemens-morgenstern : This is very useful. Can you also point, to your repo if possible. Just incase if it is open sourced?

ravichauhan03 avatar Jun 26 '20 13:06 ravichauhan03

It's not open source, sorry.

klemens-morgenstern avatar Jun 26 '20 13:06 klemens-morgenstern

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.

onelittlenightmusic avatar Dec 03 '21 23:12 onelittlenightmusic

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.

anderseknert avatar Sep 22 '22 08:09 anderseknert