apollographql / apollo-server

🌍  Spec-compliant and production ready JavaScript GraphQL server that lets you develop in a schema-first way. Built for Express, Connect, Hapi, Koa, and more.
https://www.apollographql.com/docs/apollo-server/
MIT License
13.8k stars 2.03k forks source link

Dynamic schema selection based on the role of the current user #2010

Closed gsamartian closed 2 years ago

gsamartian commented 5 years ago

hi,

We are using apollo-server as the graphql server.

We have multiple graphql endpoints ( each in its own apollo graphql server ) created each for a specific business capability/domain/bounded content.

We now want to expose these to the clients. we have clients who may or may not have access to all the business capabilities.

For example, Client A may have access to business capability 1 and Client B may have access to business capability 1 as well as business capability 2, while another client may have access to all other business capabilities.

i want to know if i can create a graphql gateway or something similar with an api endpoint which when exposed provides the access to the right business capabilities based on the access permissions of the current client.

Also, i want to only expose the only business capabilities/fields in the graphql explorer/playground in the docs/schema section on the right pane based on the permission/role of the current user who is logged-in to view and explore the features exposed to him/her.

I see there is a feature of schema stitching where-in we can have a schema composed of multiple remote schemas each from a separate graphql endpoint. But, i do not see an option of fine grained selection of schemas which can be passed to apollo server based on the profile/role/permissions of the current user. basically, i want to refresh the schema based on the current user and not have a a single static schema as constructed in the startup.

Is there a way to achieve that with apollo graphql or is there another tools in the graphql ecosystem which i can leverage for my requirement.

syffs commented 5 years ago

@gsamartian have you found a solution to handle hot schema updates ?

babinc commented 5 years ago

This would also benefits my business use case.

smolinari commented 5 years ago

There has been many a discussion about adding data viewing limitations via permissions/ roles in some fashion to GraphQL (the spec) and the reply was always (and I'm paraphrasing), permission/ roles, being a part of business logic, must be found within that logic and not in the schema.

I see it the same way. Permissions or Roles can be done in many ways and at different levels of fidelity. As long as Apollo Server doesn't go past the bounds of resolvers, then it shouldn't be opinionated as to how anything gets resolved. A GraphQL server should stay a "thin" gateway to your business logic, where either your business logic returns something to the resolver, or nothing, and all dependent on the user context.

You can find an example of what I mean here:

https://www.prisma.io/tutorials/best-practices-for-permissions-in-graphql-servers-ct07/

This is built on top of GraphQL-Yoga, which uses Apollo Server.

Most notably:

When implementing permission rules with Prisma and graphql-yoga, the basic idea is to implement a "data access check" in each resolver.

😁

Scott

wmertens commented 5 years ago

I believe that for security, more layers are better (if they don't complicate matters).

Having course-grained access control that even limits schema visibility based on user level is an easy extra security layer on top of what should have been implemented already.

smolinari commented 5 years ago

if they don't complicate matters

That's exactly the issue. Putting data viewing permissions inside the schema means a lot more work in terms of how the endpoint should traverse the schema (or not). That's assuming the Apollo team would even break from the spec too, which I highly doubt they will. It's been said a few times to other suggestions about authorization I've read that Authorization is outside the scope of the spec.

I've gathered some links to hopefully change your minds, where the founders of GraphQL chime in and/ or give some insight as to why it's better to have access permissions in the business logic level of your app's stack. And lastly the tutorial for GraphQL on Authorization.

https://github.com/graphql/graphql-js/issues/113#issuecomment-129569160 https://blog.apollographql.com/graphql-at-facebook-by-dan-schafer-38d65ef075af https://graphql.org/learn/authorization/

In big bold letters:

Delegate authorization logic to the business logic layer

I hate to say it, but you are barking up the wrong tree here, unfortunately. 😊

Scott

wmertens commented 5 years ago

I'm not proposing to extend the schema, I'm asking to select the schema based on the context of the incoming request.

In the first link you gave Lee actually says the exact same thing:

At Facebook our schema is static. More specifically we have two schema: an internal and an external schema. The internal schema includes prototype features we don't want to leak as well as site admin tools that we would rather not expose to our mobile clients.

This is exactly the same reasoning, and facebook does it too.

smolinari commented 5 years ago

Um, you are assuming they use some unknown extra parsing in their GraphQL server to split the users between the two schemas. Whereas, I'd assume they have a staging or quality server (or servers) that form a totally different end point, which uses/ serves that "internal" schema. Authorization can then happen at the point of authentication and it would be a very simple solution with "standard spec" GraphQL servers.

Scott

wmertens commented 5 years ago

I suppose, but it would still be handy to do it in-server.

(picking nits: authorization happens in the resolvers ;-) )

smolinari commented 5 years ago

For field level authorization yes, the authorization should be at or after the resolvers.

For allowing only devs and/or admins into a test server, authorization can happen during authentication (or rather authentication is authorization for access to that schema).

Scott

smolinari commented 5 years ago

One thing that might offer a means to a "selective schema", and is in fact exactly that, is the new federation feature. If the Apollo team would offer a side door to allowing devs a way to add their own logic to the current federation logic, it wouldn't break the GraphQL spec, but will offer a way to set rules like permissions on who can see what "chuck" of business logic a user can see/ use.

Scott

wmertens commented 5 years ago

I suppose that is acceptable, albeit that the stitching for local graphql services will mean parsing + stringifying + parsing the incoming queries and possibly forcing the queries to happen over http instead of running in-process.

Not terrible but of course it could be more efficient

smolinari commented 5 years ago

Um, federation isn't schema stitching. In fact, from my understanding, it's the opposite.

Scott

wmertens commented 5 years ago

I was looking at the example given

const gateway = new ApolloGateway({
  serviceList: [
    { name: 'accounts', url: 'http://localhost:4001' },
    { name: 'products', url: 'http://localhost:4002' },
    { name: 'reviews', url: 'http://localhost:4003' }
  ]
});

const server = new ApolloServer({ gateway });
server.listen();

the gateway proxies the incoming request to the given services, right? So it needs to parse and recreate the proper query?

smolinari commented 5 years ago

Ok. Reading further, the Gateway "pulls in" the schema from the different services and composes them together. I don't think that would be bad in terms of performance, if some sort of access rules were given on top of the federation system.

Off topic, but I wonder how changes get propagated, for instance when one service gets a new schema, the Gateway needs to know about that change to update its own composed schema. So, how would that work? Nevermind. It's a rhetorical question.

Scott

wmertens commented 5 years ago

Right, it seems like federation is just a declarative version of stitching, and the schema is still only prepared the one time…

wmertens commented 3 years ago

I still think it would be great to be able to say

const server = new ApolloServer({
  getSchema: (req) => req.user?.role === ROLE_ADMIN ? adminSchema : userSchema
})
voodooattack commented 3 years ago

This can be done via plugins now: (I'm using Koa in this example)

import {execute, parse} from "graphql";
import {ApolloServer} from "apollo-server-koa";
import getSchemaForRole from "./my-schema-defs";
import authenticateUser from "./my-auth-backend";
import app from "./my-koa-app";

const defaultRole = "user";

const apolloServer = new ApolloServer({
  schema: await getSchemaForRole(defaultRole),
  context: ({ ctx }) => {
    return { 
      user: await authenticateUser(ctx)  // authenticate the user however you see fit
    }; 
  },
  plugins: [{
    requestDidStart: () => ({
      responseForOperation: async operation => {
        const {context, request} = operation;
        const {query, variables, operationName} = request;
        const {role} = context.user;
        // dynamically select the schema based on the current user's role
        const schema = await getSchemaForRole(role || defaultRole);
        return execute(schema, parse(query), null, context, variables, operationName);
      }
    })
  }]
});

apolloServer.applyMiddleware({ app });

Note that this will seamlessly work for introspection queries. (if you refresh the playground after logging-in you'll get the schema for that role, etc)

Edit: The solution above breaks instrumentation and tracing in addition to blocking some server plugins from running. (any plugins that hook into executionDidStart and willResolveField)

So here's another method that doesn't have this problem. It could be considered a bit hacky, but I'm leaving it here for those that really need these features during development.

It works by calling generateSchemaDerivedData which is a private member of ApolloServerBase. (Bad practice, I know)

class ExtendedApolloServer extends ApolloServer {
  private readonly _schemaCb?: (
    ...args: Parameters<ApolloServer["createGraphQLServerOptions"]>
  ) => Promise<GraphQLSchema> | GraphQLSchema;
  private readonly _derivedData: WeakMap<GraphQLSchema, any> = new WeakMap();

  constructor({schemaCallback, ...rest}: Config & {
    schemaCallback?: ExtendedApolloServer["_schemaCb"];
  }) {
    super(rest);
    this._schemaCb = schemaCallback;
  }

  public async createGraphQLServerOptions(
    ...args: Parameters<ApolloServer["createGraphQLServerOptions"]>
  ): Promise<GraphQLOptions> {
    const options = await super.createGraphQLServerOptions.apply(this, args);
    if (this._schemaCb) {
      const schema = await this._schemaCb.apply(null, args);
      if (!this._derivedData.has(schema))
        this._derivedData.set(schema,
          this.constructor.prototype.generateSchemaDerivedData.call(this, schema));
      Object.assign(options, await this._derivedData.get(schema));
    }
    return options;
  }
}
// Example usage:
const apolloServer = new ExtendedApolloServer({ 
  ..., 
  schemaCallback: ctx => getSchemaFromConext(ctx) 
});
glasser commented 2 years ago

Duplicate of #5786.