graphql-shield icon indicating copy to clipboard operation
graphql-shield copied to clipboard

GraphQL Shield to Work with Apollo Federation

Open fullStackDataSolutions opened this issue 3 years ago • 4 comments

Feature request

I want the Gateway layer of Apollo Federation to be able to run GraphQL shield. Currently that seems to be unspooled.

Is your feature request related to a problem? Please describe

I can't run GraphQL Shield at the Gateway layer. Our use case is to control Auth from the Gateway layer and control rules there.

We are a small team working with several sub services and find it better to have all auth related functionality in the Gateway layer.

Describe the solution you'd like

That I can use GraphQL Shield as normal in the Gateway Layer.

fullStackDataSolutions avatar Jul 14 '21 16:07 fullStackDataSolutions

Hey @blazestudios23 :wave:,

Thank you for opening an issue. We will get back to you as soon as we can. Have you seen our Open Collective page? Please consider contributing financially to our project. This will help us involve more contributors and get to issues like yours faster.

https://opencollective.com/graphql-shield

We offer priority support for all financial contributors. Don't forget to add priority label once you become one! :smile:

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

stale[bot] avatar Apr 16 '22 10:04 stale[bot]

@blazestudios23 Did you ever find an alternative to graphql-shield that works with Apollo Federation?

AlfieGoat avatar May 04 '23 14:05 AlfieGoat

@blazestudios23 Did you ever find an alternative to graphql-shield that works with Apollo Federation?

Credit to vanbujm at this post Is it possible to apply graphql middleware when using managed federation , I managed to integrate apollo gateway with graphql shield. Here's a workable example for whoever concerns:

const { ApolloServer } = require('@apollo/server');
const express = require('express');
const {expressMiddleware} = require('@apollo/server/express4');
const bodyParser = require('body-parser');
const http = require('http');
const { execute } = require('graphql');
const { ApolloGateway, RemoteGraphQLDataSource } = require('@apollo/gateway');
const { shield, rule, allow, deny } = require('graphql-shield');
const { readFileSync } = require('fs');
const { applyMiddleware } = require('graphql-middleware');
const { addMocksToSchema } = require('@graphql-tools/mock');
const supergraphSdl = readFileSync('./supergraph.graphql').toString();

const hasScope = (key) => rule()(async (parent, args, ctx, info) => {
    return ctx.scope.indexOf(key) > -1;
});

const permissions = shield({
    Query: {
        categories: hasScope('category:read'),
    },
    Category: {
        createdBy: hasScope('category:admin'),
    },
});

class AuthenticatedDataSource extends RemoteGraphQLDataSource {
    willSendRequest({ request, context }) {
        for (const [headerKey, headerValue] of Object.entries(context.headers)) {
          request.http?.headers.set(headerKey, headerValue);
        }
    }
}

const gateway = new ApolloGateway({
    supergraphSdl,
    buildService({ name, url }) {
        return new AuthenticatedDataSource({ name, url });
    },
});

(async () => {
    const app = express();
    const httpServer = http.createServer(app);

    let shieldedSchema;
    const server = new ApolloServer({
        gateway,
        debug: true,
        plugins: [
            {
                serverWillStart: async () => ({
                    schemaDidLoadOrUpdate: ({ apiSchema }) => {
                      // a fix on vanbujm's solution here, need to provide mocking result to shield nested fields
                      // Add your custom scalar mock here, e.g. Timestamp
                      shieldedSchema = applyMiddleware(addMocksToSchema({ schema: apiSchema, mocks: {
                          Timestamp: () => 0
                      } }), permissions);
                    },
                }),
                requestDidStart: async () => ({
                    responseForOperation: async ({ document, request, contextValue, operationName }) => {
                        contextValue.scope = (contextValue.headers['scope'] || '').split(' ');
                        const singleResult = await execute({ schema: shieldedSchema, document, contextValue, operationName });
                        const actualErrors = (singleResult?.errors || []).filter(
                            ({ message }) => !message.includes('Cannot return null for non-nullable field')
                        );
                        
                        if (actualErrors.length === 0) {
                            return undefined;
                        }

                        return {
                            body: { kind: 'single', singleResult },
                            http: {
                                status: undefined,
                            },
                        };
                    },
                }),
            },
        ],
    });
    await server.start();
    
    app.use(
      '/graphql',
      bodyParser.json(),
      expressMiddleware(server, {context: ({ req }) => {
        return {
          headers: req.headers,
        };
      }}),
    );
    
    await new Promise((resolve) => httpServer.listen(4000, '0.0.0.0', resolve));
    const { address, port } = httpServer.address();
    console.log(`🚀 Server ready at http://${address}:${port}/graphql`);

})();

ayame30 avatar Jul 19 '23 09:07 ayame30