-
Notifications
You must be signed in to change notification settings - Fork 12
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
Redesign @hasRole directive: make it as flexible as possible #17
Comments
Regarding the syntax for logical operations, I propose the syntax similar to the syntax that is used in Prisma: https://www.prisma.io/docs/prisma-graphql-api/reference/queries-qwe1/#combining-multiple-filters. On the other hand, since
and
type User {
verified: Boolean! @hasRole(roles_in: [OWNER, ADMIN]) @hasRole(roles_in: [MANAGER])
} logical operations may not be needed. The only downside is the need to use 2 directives alongside instead of 1. |
@FluorescentHallucinogen Great research, thanks for your effort! The main idea of this directives was to create that Enum with roles and allow users to put custom logic into the directives. About using a current directive implementation with query or mutation - it's working; I'm using it all the time in my side project. I checked the graphql-shield project, and It's so good at flexibility. The logical operator is cool, and we can put custom login there what it's hard to do with directives - correct me If I'm wrong. I was also thinking about support simple permissions tree, like:
It's mean that if we have an I'd like to do redesign the API and adapt your thoughts. 👍 |
In current implementation, if the query contains a field, to which the Proposed implementation: type User {
name: String!
address: String
email: String
phone: String! @hasRole(roles_in: [OWNER, ADMIN], strategy: STRIP)
...
} query {
users {
name
address
email
phone
}
} should (if the user role doesn't match with the provided one in the directive) just strip the {
"data": {
"users": [
{
"name": "Bob",
"address": "",
"email": "[email protected]",
"phone": null
},
{
"name": "Alice",
"address": "",
"email": "[email protected]",
"phone": null
}
]
},
"errors": [
{
"code": "ERR_GRAPHQL_PERMISSION_VALIDATION",
"fieldNames": ["phone"],
...
}
]
} @hasRole(roles_in: [OWNER, ADMIN], strategy: REJECT) should reject the entire query (all fields). |
This doesn't cover all use cases. E.g. in my app roles are non-overlapping. If the user has the |
I was wrong. According to https://facebook.github.io/graphql/June2018/#sec-Directives-Are-Unique-Per-Location, it's not possible to use more than one directive of the same name for the same location. |
How to use @isAuthenticated with mutation? |
@FluorescentHallucinogen But I'm talking about the separate directive. @amitava82 Sure, it works ;) |
@czystyl Is it possible to implement all my proposals using custom |
@FluorescentHallucinogen Custom func allows you to define your own logic so probably you are able to do it. |
BTW would be great to migrate from string to Enum - @FluorescentHallucinogen Would you like to take care of it? |
I've implemented a prototype of logical operations support using custom const authDirectives = require('graphql-directive-auth').default;
const logicalExpressionParser = require('logical-expression-parser');
function checkRoleCustomFunc(context, requiredRoles) {
const userRole = context.auth.role;
if (!userRole) {
throw new Error(`Invalid token payload, missing role property inside!`);
}
const hasNeededRole = logicalExpressionParser.parse(requiredRoles, token => userRole.indexOf(token) > -1);
if (!hasNeededRole) {
throw new Error(
`Must have role: ${requiredRoles}, you have role: ${userRole}`
);
}
}
const customAuth = authDirectives({
checkRoleFunc: checkRoleCustomFunc
});
const schemaDirectives = {
...customAuth
}; It's possible to use something like this: @hasRole(role: "EDITOR&(ADMIN|OWNER)") |
The only advantage of using enum over string that I see is type safety (including autocomplete). In current implementation that use string it's possible to make mistakes: typos, wrong case, nonexistent roles, etc. On the other hand, string is more flexible than enum: no need to change enum in the GraphQL SDL to add a new role. We have to decide which is better. :) |
As for the logical-expression-parser NPM package, I haven't found a repo on GitHub. As for the syntax, I prefer Polish prefix notation + |
It seems all that is needed to do to migrate from string to enum is to change directive @hasRole(role: String) on FIELD | FIELD_DEFINITION to directive @hasRole(role: Role) on FIELD | FIELD_DEFINITION
enum Role {
USER
OWNER
EDITOR
ADMIN
} or directive @hasRole(role: [Role]) on FIELD | FIELD_DEFINITION
enum Role {
USER
OWNER
EDITOR
ADMIN
} Moreover, it's possible to set the default role directive @hasRole(role: Role = USER) on FIELD | FIELD_DEFINITION or directive @hasRole(role: [Role] = [USER]) on FIELD | FIELD_DEFINITION to use just The disadvantage of using enum over the string is that enum is incompatible with logical operations proposal. Please correct me if I'm wrong. (Please also keep in mind that it's a best practice to use the plural for arrays, i.e. |
As for proposal #4, I was wrong. If a field’s resolve function throws an error, the error will be inserted into the response’s "errors" key and the field will resolve to null. If a a null value returns in a resolver for a non-null field, the null result bubbles up to the nearest nullable parent. |
BTW, it would be great if the @czystyl Could you please implement |
You are very welcome to send a PR for that one. |
I'll implement support of IMO, it would also be great to have support of Currently, it's possible to restrict the whole mutation, e.g.: type Mutation {
createUser(name: String, email: String, age: Int, verified: Boolean): User!
updateUser(id: ID!, name: String, email: String, age: Int, verified: Boolean): User! @hasRole(roles: [MANAGER])
deleteUser(id: ID!): User @hasRole(roles: [ADMIN])
...
} But what if I want to restrict only updating of the type Mutation {
updateUser(id: ID!, name: String, email: String, age: Int, verified: Boolean @hasRole(roles: [MANAGER])): User!
...
} The mutation arguments may be extracted to separate input types. In this case the directive must support type Mutation {
updateUser(data: UserUpdateInput!, where: UserWhereUniqueInput!): User!
...
}
...
input UserUpdateInput {
name: String
email: String
age: Int
verified: Boolean @hasRole(roles: [MANAGER])
} |
Thank you so much. I appreciate it a lot 💪 If you need any help just let me know :) |
Good news! I've implemented a
title: String @hasRole(has_some: [ADMIN], NOT: [{ has_every: [MANAGER] }])
PTAL at https://gist.github.com/FluorescentHallucinogen/75d12beb731f1a253c8f188919d5f5a7 😉 @czystyl Could you please integrate my code into your package? I'm new to TypeScript. 🙈 |
Hi, @FluorescentHallucinogen great news! I'll try to have a look into that in the upcoming weekend. I'm very happy that you are so involved in this repository 💪 |
@czystyl Have you had a chance to look? |
Sorry for the late response. I try to do my best this weekend. |
@FluorescentHallucinogen wondering if you could open and PR with your gist and then I'll add the types there - then would be great to have you as a contributor. I'm a bit busy currently. |
@hasRole
.(Please also keep in mind that roles (permission subsets) can be non-overlapping.)
Current implementation:
Proposed implementation:
It seems that an array of strings would be more flexible than an array of enums (because you don't need to update enum in the GraphQL SDL to add a new role), but it less type-safe (you don't get a list of possible values e.g. for autocomplete).
FIELD_DEFINITION
to apply the directive to e.g. mutations and fields in the input types used in these mutations for more granular permissions.Did I understand correctly that
@hasRole
directive can only be applied toFIELD_DEFINITION
, while there are many places where directives (in general) can be applied (see the full list here: https://github.com/graphql/graphql-js/blob/master/src/language/directiveLocation.js)?Example use case:
Everyone can query, create and update users (except
canPost
andbanned
fields), but only users with roleADMIN
can delete users.Only users with role
REVIEWER
orADMIN
can have access (query, create and update)canPost
field.Only users with role
ADMIN
can have access (query, create and update)banned
field.Only users with role
MANAGER
can updateverified
field.Proposed implementation:
Currently,
@hasRole(role: "user, admin")
checks if user has some (at least one any) of specified roles. What about all, none and other more complex cases? (Please also keep in mind that roles (permission subsets) can be non-overlapping.)What about adding support of logic operations for nest rules (similar to https://github.com/maticzav/graphql-shield#and-or-not)? Something like (the syntax is subject to discussion):
The text was updated successfully, but these errors were encountered: