Policies for Access Control of GraphQL Fields

Allowing fine grained control of access to GraphQL fields

Field policies provide a configuration based mechanism to mange access to your GraphQL API by providing policy for GraphQL types. The policies have rules that determine the conditions under which a field may be accessed. An operation must contain only fields whose policies permit access.

By creating a field policy for a GraphQL type, you can have fine grained access control when used in conjuction with JWT based authorization to manage access to your GraphQL API. They can also be used to declare portions of your API public. Given the nature of access control, the model for field policies leans towards asking for explict specification over implied specification

Policies are restricted to Query and Mutation types. Policies are overridden if you are using the Admin or API key.

Field policies are specified in your config.yaml via following template:

access:
  policies:
    - type: Query
      rules: # rule for fields, only decision is permit
      - condition: PREDICATE 
        name: name of rule
        fields: [ fieldname ... ]
      - condition: PREDICATE 
        name: name of rule
        fields: [ fieldname ... ]
      policyDefault: # rule for unmentioned fields, decision is permit
        condition: PREDICATE
      # otherwise deny
    - type: Mutation
      rules:
      - condition: PREDICATE
        name: name of rule
        fields: [ fieldname ... ]
      # rule for unmentioned fields, decision is permit
      policyDefault:
        condition: PREDICATE
      # otherwise deny

and apply to the GraphQL endpoint (named in your stepzen.config.json).

In the above example, field policies provides policies that apply to the type that this policy targets (Query and Mutation). Each rule mentions a field or fields and a condition that allows access. name is primarily for your administrative use. So we have an explict specification of the conditions associated with a field, a single condition can be associated with each field so a field can only be specified once.

To illustrate the concept, the following policy would allow public access to the Query contents and Query pages fields and would deny any other access.

access:
  policies:
    - type: Query
      rules:
      - condition: true
        name: public fields
        fields: [ "contents", "pages" ]

and this allows the query query { contents { ... }} to anyone. No authorization required.

If a field is not mentioned in any rule, then the policyDefault is applied and access is permitted if the condition is fufilled allowing us to add this:

      policyDefault: 
        condition: "?$jwt"

to allow public access to the Query contents and Query pages fields and access to all other Query fields to requests with an allowed JWT token. If no policy is specified for a type, the normal access control rules apply to that type, so Mutation would be available to any allowed JWT. So, the GraphQL operations:

query JWTOnly { listUsers { ... } }
mutation JWTOnlyMutation { addUser { ... } }

would be available to any allowed JWT or effectively to all users--those who have provided an Authorization Bearer header with an allowed JWT.

Often, leaving Mutation fields for valid JWT's would be appropriate. You would then forward the JWT token and make decisions on the backend where the business logic is encoded. In other cases, you might want some limited business logic here, for example:

    - type: Mutation
      rules:
      - condition: "$jwt.CUSTOMGROUP : String == 'admin'"
        name: admin access
        fields: [ addUser ]

only allows allows "admin" users to access the Mutation addUser, so in the above example you could only specify operation name JWTOnlyMutation if the JWT token contained an admin group.

The condition clause takes predicates which are fully defined below but take upon the following forms:

  • true
  • false
  • $jwt.CUSTOMGROUP: String == "admin" If a predicate evaluates to true, it means the associated field is allowed. So for the above, true would allow these fields, false would reject these fields, and the last would allow for "admin JWT's".

If you wish to make all GraphQL Query fields public, then you could express it as:

access:
  policies:
  - type: Query
    policyDefault: 
      condition: true

Typical examples of rules could include:

  • if they have the "product-admin" role in their JWT token condition: $jwt.CUSTOMGROUP.role:String == "product-admin", fields: [ productAdminField, productAdminField2, ... ], name: product admin access
  • if they have the "admin" or "editor" role in their JWT token. condition: $jwt.CUSTOMGROUP.roles:String has "admin" || $jwt.CUSTOMGROUP.role:String has "editor" , fields: [ editorField, editorField2, ... ], name: editor access or any other rules that depend only upon claims in the JWT token.

Full example:

access:
  policies:
    - type: Query
      rules:
      - condition: $jwt.CUSTOMGROUP.role:String == "product-admin"
        fields: [ productAdminField, productAdminField2, ... ]
        name: product admin access
      - condition: $jwt.CUSTOMGROUP.roles has "product-admin" || $jwt.CUSTOMGROUP.role:String has "editor"
        fields: [ editorField, editorField2, ... ]
        name: editor acess
      policyDefault:
        condition: ?$jwt # jwt access to all other `Query` fields

Fields listed in fields must be legal GraphQL identifiers.

name is limited to 99 characters.

So, if you are building a set of polices for your Query's or Mutation's, it's as simple as picking out the fields that need controlled or special access and associating conditions when them. The others can be handled via the policyDefault clause. For the vast majority of usages, this should suffice.

Indirect references to fields are not affected by field based rules. Examples of indirect references are: @sequence and @materializer. This allows you to use @sequence, @materializer based fields to manage access to controlled fields.

Predicates

Predicates are built on a simple expression language. Given that a predicate is used in access control, the language is fairly strict and rigid to minimize ambiguity.

Types parallel the builtin GraphQL scalars including: Int, Boolean, Float, and String.

Builtin values are $jwt and $variables which parallel the JWT token in the Authorization header and the variables specified in the GraphQL HTTP Request. The values are expected to be in JSON form. The name/value pairs can be accessed using a dot notation. You may escape the name identifier as "" (e.g. http://YOURDOMAIN.com/claim`).

$jwt is only available if you have configured the identity config.yaml section for the endpoint (See JWT). The JWT will only be present if:

  • the token has been properly signed
  • the token reserved claims where present are valid
  • the token reserved claims where required in identity are present
  • the token reserved claims where specified in identity match This is what we have referred to as an allowed JWT. An allowed JWT will also, as mentioned, permit access to the GraphQL endpoint configured with identity.

There is no automatic typing of values. For example, you cannot just specify $jwt.\CUSTOM/userid`, you must specify $jwt.`CUSTOM/userid : Int.

The core operators are <,<=,==, !=, >, >= and are allowed between any two operands of the same type. Additionally, there is a has operator that allows existence checks.

Processing will currently stop with an error if an allowed JWT token does not appear in a request and you refer to a JWT token. Typically, once you start using field predicates, you'll always have $jwt available and it would indeed be an error if it did not exist. But if you have a use case that requires the rule to be skipped, you should use the idiom "?$jwt && $jwt.field..." and you can write predicate such as:

  • ?$jwt, ?$jwt.\CUSTOM/bar`` - check for existence
  • $jwt.\custom/anInt`:Int > 40` - check for integer value > 40

The has operator allows checking for existence of a scalar within a JSON value. For example, if you had the JWT with OpenID and custom claims as:

{
  "email": "stepzen@example.com",
  "email_verified": true,
  "iss": "https://IDP.com/",
  "sub": "1234567890",
  "aud": "my_client_id",
  "iat": 946713599,
  "exp": 946717199,
  "CUSTOM/groups": ["admin", "user", "moderator"],
}

then $jwt.\CUSTOM/groups`: String has "admin" or $jwt.`CUSTOM/groups`: String has "moderator"` would be true if the user was in the admin or moderator groups.

Should your claim have a slightly more sophisticated structure such as:

"CUSTOM/roles": [ { "type": "user" }, { "type" : "admin" }, {"type": "moderator" }, {"notype" : ""}]

then you may specify $jwt.CUSTOM/roles.type: String has "admin" || $jwt.CUSTOM/roles.type: String has "moderator". The entry without type will be skipped.

This site uses cookies: By using this website, you consent to our use of cookies in accordance with our Website Terms of Use and Cookie Policy.