Moving GraphQL Authorization to admin API

Experience Report for Feature Request

Note: Feature requests are judged based on user experience and modeled on Go Experience Reports. These reports should focus on the problems: they should not focus on and need not propose solutions.

What you wanted to do

Manage Authorization configuration and rules outside of the schema

What you actually did

Put Authorization configuration and rules inside of the schema

Why that wasn’t great, with examples

Any external references to support your case

^ Thanks for this link @jdgamble555


they should not focus on and need not propose solutions.

Sorry, can’t really help but to provide this also.

It was talked about moving the Authorization line from the schema to the /admin GraphQL API.

This would open the door for more advanced control options

  • The input schema could be made public without also exposing all authorization rules
  • A single rule could be applied to multiple places without rewriting the same rule redundently
  • Easier iteration on schema without auth rules taking up so much schema realty
  • Admin management (Dgraph Cloud or custom Dgraph management solutions) endpoints could be made to work with these rules independently of schema alterations.

This also paves the way forward for auth rules at the field level as well.

Here is schema I propose to handle all of the above

enum Algo {
  HS256
  RS256
}
type Auth {
  header: String
  namespace: String
  algo: Algo
  verificationKey: String
  JWKurl: String
  audience: [String]
  closedByDefault: Boolean
}
type AuthRule {
  id: ID
  rule: String
  and: [AuthRule]
  or: [AuthRule]
  not: AuthRule
}
type AuthType {
  type: String! @id
  password: AuthRule
  query: AuthRule
  add: AuthRule
  update: AuthRule
  delete: AuthRule
  fields: [AuthField]
}
type AuthField {
  id: ID
  field: String! @search(by: [hash])
  query: AuthRule
  mutate: AuthRule
}

This schema proposed should work with the same kind of GraphQL generation that provides the following queries and mutations that are unique to the admin API.

type Query {
  getAuth: Auth
  getAuthRule(id: ID!): AuthRule
  queryAuthRule: [AuthRule]
  getAuthType(type: String!): AuthType
  queryAuthType(filter: AuthTypeFilter, first: Int, offset: Int): [AuthType]
  getAuthField(id: ID!): AuthField
  queryAuthField(filter: AuthFieldFilter)
}
type Mutation {
  updateAuth(input: AuthPatch): Auth
  deleteAuth: Auth
  addAuthRule(input: [AddAuthRuleInput!]!): AddAuthRulePayload
  updateAuthRule(input: UpdateAuthRuleInput!): UpdateAuthRulePayload
  deleteAuthRule(filter: AuthRuleFilter!): DeleteAuthRulePayload
  addAuthType(input: [AddAuthTypeInput!]!): AddAuthTypePayload
  updateAuthType(input: UpdateAuthTypeInput!): UpdateAuthTypePayload
  deleteAuthType(filter: AuthTypeFilter!): DeleteAuthTypePayload
  addAuthField(input: [AddAuthFieldInput!]!): AddAuthFieldPayload
  updateAuthField(input: UpdateAuthFieldInput!): UpdateAuthFieldPayload
  deleteAuthField(filter: AuthFieldFilter!): DeleteAuthFieldPayload
}
input AuthTypeFilter {
  type: StringHashFilter
  has: AuthRuleHasFilter
  and: [AuthRuleFilter]
  or: [AuthRuleFilter]
  not: AuthRuleFilter
}
enum AuthRuleHasFilter {
  password
  query
  add
  update
  delete
  fields
}
input AuthFieldFilter {
  id: [ID!]
  field: StringHashFilter
  has: AuthFieldHasFilter
  and: [AuthFieldFilter]
  or: [AuthFieldFilter]
  not: AuthFieldFilter
}
enum AuthFieldHasFilter {
  query
  mutate
}
input AuthPatch {
  header: String
  namespace: String
  algo: Algo
  verificationKey: String
  JWKurl: String
  audience: [String]
  closedByDefault: Boolean
}
input AddAuthRuleInput { ... } 
type AddAuthRulePayload { ... }
input UpdateAuthRuleInput { ... }
type UpdateAuthRulePayload { ... }
type DeleteAuthRulePayload { ... }
input AddAuthTypeInput { ... } 
type AddAuthTypePayload { ... }
input UpdateAuthTypeInput { ... }
type UpdateAuthTypePayload { ... }
type DeleteAuthTypePayload { ... }
input AddAuthFieldInput { ... } 
type AddAuthFieldPayload { ... }
input UpdateAuthFieldInput { ... }
type UpdateAuthFieldPayload { ... }
type DeleteAuthFieldPayload { ... }

This got me thinking maybe the entire schema could be held piece by piece in a management graph and then queried and iterated upon in pieces and the functions that generate a more static schema file, could read from this schema graph.

This tangent idea would take the form of AuthType above, but have information about specific fields. Adding a single field to a type would be then making a mutation to update that one type which would trigger the full API schema rebuild process. :bulb:

3 Likes

@verneleem Thanks for posting this comprehensive proposal.

I just would like to point out (since my comments were used as an example) that for me, having just the meta information (# Dgraph.Authorization…) controlled by the admin API is the most pressing need. At the moment, we’re forced to inject or remove that line via file munging depending on the deployment scenario (local testing, staging, production, etc) and it just feels dirty.

1 Like

Are there any updates for this?

While I still believe Dgraph needs pre-hooks as well as post-hooks, some kind of security rules are a must. @auth is just a taste of what is needed.

Here are some more examples from other similar database systems (not even including Firestore, Firestore Auth, Auth0, or neo4js):

There are really 3 types of rules here:

  • SQL like syntax with checks (postgresql, mysql)
  • Python like syntax with basic functions (firestore, nhost)
  • Where clauses (supabase, @auth)

– But really we are just talking about two things Validation (what) and Authorization (who).

My suggestion would be to have something like Schema Rules separate from the regular schema for readability, basically just a small extension of @auth.

Which brings me to simply extending what we already have:

Current solutions (untested):

type Todo @auth(
    add: { rule: """
         .... insert rule here
        }"""
    }
){
...

required field (not null) - although schema already has this with “!”:

queryPosts(filter: { has: [name, description] } }

field restriction (null) - don’t allow certain fields to be added

queryUsers(filter: { not: { has: role } }

required field - only allow a field to have a certain value

queryUsers(filter: { role: { eq: 'user' } }

min, max restraints - field value can only be between x and y

queryStudent(filter: {age: between: {min: 10, max: 20}}){

required field (allow enum only values)

queryState(filter: {code: {in : ["WA", "VA"]}}){

required field (allow only email type)

queryUser(filter: { email: { regexp: '/^\S+@\S+\.\S+$/' } })

Note: These may not work exactly like this, and will need lots of ands and ors, but you get the point. Please post any corrections here, as I am still learning…


Suggestions (Feature requests):

So in order to have real validation, graphql needs to be more robust in dgraph. This is happening naturally over time with new features to meet the needs of all users, and be as useful as dql.

So, we really just need to add two things here to @auth graphql particularly:

  1. Request data access - a $DATA variable to access incoming request values, or events, to compare data in an update rule
  2. Update after logic - basically add a rule like update-after or update: { after: { that works exactly like the add rule which can run logic on the incoming data as if it were already in the database

This should take care of 99% of the user cases for validation. We can check case logic, incoming update regex, update field validation, old value must equal new value, etc, etc…

And a possible more advanced feature later:

  1. Variable Queries - allow querying a field value and saving it in a variable to compare it to $DATA.field (same way upserts work) – particularly necessary if two nodes are the same but different ids… not sure if this is a big use case or not

With these other requested features to make it complete of course:

  • pre-hooks can handle any custom cases - which is really just @lambdas on existing add / update mutations and not custom mutations, different from post-hook
  • A separate @auth schema for readability
  • Automatic Timestamps to cover date constraints

I feel like this would solve the majority of validation problems many complain is missing… ( or maybe it is just me :wink: )

Thanks,

J

Related Post:

Prehooks seem to be a separate feature.

Yes, but related (mentioned at the bottom of my post in other feature requests).

To sum up, all databases have something similar, and adding these two features alone makes the current @auth directive a bazillion times more powerful and solves many problems:

J