Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
gsamartian opened this issue Nov 22, 2018 · 18 comments
Closed

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

gsamartian opened this issue Nov 22, 2018 · 18 comments
Labels
⛲️ feature New addition or enhancement to existing solutions

Comments

@gsamartian
Copy link

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
Copy link

syffs commented May 14, 2019

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

@babinc
Copy link

babinc commented Jun 1, 2019

This would also benefits my business use case.

@abernix abernix added the ⛲️ feature New addition or enhancement to existing solutions label Jul 31, 2019
@smolinari
Copy link

smolinari commented Jul 31, 2019

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
Copy link
Contributor

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
Copy link

smolinari commented Jul 31, 2019

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.

graphql/graphql-js#113 (comment)
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
Copy link
Contributor

wmertens commented Aug 2, 2019

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
Copy link

smolinari commented Aug 2, 2019

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
Copy link
Contributor

wmertens commented Aug 2, 2019

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

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

@smolinari
Copy link

smolinari commented Aug 2, 2019

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
Copy link

smolinari commented Aug 3, 2019

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
Copy link
Contributor

wmertens commented Aug 4, 2019

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
Copy link

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

Scott

@wmertens
Copy link
Contributor

wmertens commented Aug 5, 2019

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
Copy link

smolinari commented Aug 5, 2019

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
Copy link
Contributor

wmertens commented Aug 5, 2019

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

@wmertens
Copy link
Contributor

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
Copy link

voodooattack commented Mar 16, 2021

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
Copy link
Member

glasser commented Oct 3, 2022

Duplicate of #5786.

@glasser glasser closed this as not planned Won't fix, can't repro, duplicate, stale Oct 3, 2022
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Mar 15, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
⛲️ feature New addition or enhancement to existing solutions
Projects
None yet
Development

No branches or pull requests

8 participants