Role Based Access Control (RBAC)

When a GraphQL query or mutation is invoked from a client, the Semantic Objects control which information is returned, as well as which data can be modified in the case of a mutation.

This is achieved by using declarative role based access control (RBAC). Declarative RBACs access to GraphQL Queries, Mutations, Objects, Nested Objects, and Object properties. It also provides fine-grained access management of Semantic Objects. Using RBAC, you can segregate duties within a team and grant users the precise level or amount of access that they need to perform their jobs.

You can follow the RBAC tutorial that applies security constraints to a Star Wars (SWAPI) schema.

Role Definitions (Objects and Schema)

A role definition is a collection of triplets called actions with the format <object>/<property>/<operation>. It lists the object/property that can be read, written (create/update), or deleted. Roles can be defined as high-level, e.g., owner/admin, or specific, such as a user with access to a particular property of a particular object.

If the user does not have an assigned role, a default one will be assigned. Requests with missing role claims (no role has been included) or roles that are not defined within the RBAC declaration will fall back to the Default role. It will restrict all access by default, but it can be redefined.

In such a case, it is a best practice to declare roles within the Semantic Objects schema, which can be registered to users with the lowest level of access to get their job done.

Role definitions are declared in two places:

  • Semantic Object Schema: RBAC controls across the query/mutation of the objects/properties contained within a particular schema.

    Note

    Role based access controls that control the Semantic Objects can also be exposed and accessible via GraphQL introspection queries when the security.exposeInGraphQl is set to true.

  • Schema Management: RBAC controls for Schema Management endpoints (Create/Update/Delete and Bind Schema).

    Note

    Role based access controls that control Schema Management are declared in a distinct and separate schema.

The Semantic Objects schema is to be augmented with role definitions.

Note

Use the same Role IDs/names that are declared in FusionAuth.

More information about FusionAuth can be found in the Authentication and Identity section.

Semantic Objects Role Definitions

The following schema is a sub-set of schema.yaml and describes a Star Wars Character object with its child objects Human and Droid.

The RBACs for these object definitions are declared within the rbac: section of the schema.

id:  /soml/swapi
label:   Star Wars API
creator: http://ontotext.com
created: 2019-06-15
updated: 2019-06-16
versionInfo: 0.1

specialPrefixes:
  base_iri:          https://swapi.co/resource/
  vocab_iri:         https://swapi.co/vocabulary/
  vocab_prefix:      voc

objects:
  Character:
    kind: abstract
    descr: "A character in a star wars film"
    name: rdfs:label
    props:
      eyeColor: {descr: "Characters eye color"}
      hairColor: {descr: "Characters  hair color"}
      homeworld: {label: "Characters homeworld(planet)", range: Planet}

  Human:
    prefix: "human/"
    descr: "Modern humans (Homo sapiens, primarily ssp. Homo sapiens sapiens)...."
    inherits: Character
    props:
      uniqueHumanProperty: {label: "Unique Human Property"}

  Droid:
    prefix: "droid/"
    descr: "Artificial sentient beings"
    inherits: Character
    props:
      uniqueDroidProperty: {label: "Unique Droid Property"}

  Film:
    descr: "Star Wars is an American epic space-opera media franchise created by George Lucas. The franchise began with the eponymous 1977 film and quickly became a worldwide pop-culture phenomenon, with many more films."
    type: ["voc:Film"]
    typeProp: "rdf:type"
    name: "rdfs:label"
    props:
      character: {descr: "characters in the film", max: inf, range: Character}
      desc: {label: "Description"}
      director: {descr: "Film Director"}
      episodeId: {range: integer}
      openingCrawl: {descr: "Opening Crawl or intro"}
      planet: {descr: "Planets which in appear in the Film", max: inf, range: Planet}
      releaseDate: {descr: "Film release date", range: date}

  Planet:
    descr: "The fictional universe of the Star Wars franchise features multiple planets and moons"
    type: ["voc:Planet"]
    typeProp: "rdf:type"
    name: "rdfs:label"
    props:
      desc: {label: "Description"}
      diameter: {label: "Diameter in Km", range: integer}
      gravity: {label: "Gravitational pressure m/s squared"}
      orbitalPeriod: {label: "Orbital period in days", range: integer}
      population: {range: integer}
      rotationPeriod: {label: "Rotation period in hours", range: integer}
      surfaceWater: {label: "Surface water in m cubed", range: integer}
      terrain: {label: "Planets terrain"}
      film: {descr: "Films which the planet appeared in", max: inf, range: Film}
      resident: {descr: "Characters which are resident on the planet", range: Character, max: inf}
      climate: {label: "Planets climate"}

rbac:
  roles:
    # Default role which does not need to be configured or declared. Included for completeness.
    Default:
      description: "Default role, which does not need to be declared restricts all access read, write and delete"
      notActions: [
        "*/*/*"
      ]
    # Example role definitions which needs to be declared by the user:
    Admin:
      description: "Administrator role, can read, write and delete objects"
      actions: [
        "*/*/*"
      ]
    ReadOnly:
      description: "User which can read all Objects and properties"
      actions: [
        "*/*/read"
      ]
    NoDroidReader:
      description: "Role which can read everything except Droid Objects and properties"
      actions: [
        "*/*/read"
      ]
      notActions: [
        "Droid/*/read"
      ]
    DroidMgr:
      description: "Role which can write any property of a Droid, but nothing else"
      actions: [
        "Droid/*/write"
      ]
    DroidPropertyMgr:
      description: "Role which can read and write the id, type and name properties of a droid"
      actions: [
        "Droid/id/read",
        "Droid/type/read",
        "Droid/name*/read",
        "Droid/id/write",
        "Droid/type/write",
        "Droid/name*/write",
      ]
    HumanImporter:
      description: "Role that can write & delete Human properties but cannot read them. Used for importing humans."
      actions: [
        "Human/*/write",
        "Human/*/delete"
      ]
    CharacterRead:
      description: "Role which can read all Character properties"
      actions: [
        "Character/*/read"
      ]
    HumanRead:
      description: "Role which can read all Human properties"
      actions: [
        "Human/*/read"
      ]

As we see here, multiple roles are defined in the rbac section defining different access to objects and properties.

Default Role Overwrite

To redefine the Default role, simply add it to the schema:

Example:

rbac:
  roles:
    Default:
       description: "Overwriting the Default role, which will allow read access to all users without a role"
       actions: [
         "*/*/read"
       ]

Here, we have overwritten the Default role to give read access to all users that do not have a role specified.

Actions

Action permissions specify the data operations that the role is allowed to perform on Semantic Objects or Semantic Object properties.

  • If a role has read access to a Semantic Object, then that user/role can perform a root level query and query/read these object types within nested queries.
  • If a role has write access to a Semantic Object, then that user/role can perform a root level create or update mutation operation and update these object types within nested queries.
  • If a role has delete access to a Semantic Object, then that user/role can perform a root level delete mutation operation and delete these object types within nested queries.

The action grammar for Semantic Objects is as follows:

{Object Regular Expression}/{Property Regular Expression}/read|write|delete|

Currently, only * is allowed for {Object Regular Expression} and {Property Regular Expression}.

Action Description/notes
"*/*/*" Read/Write/Delete access to all objects and all properties
"*/*/read" Read (Query) access to all objects and all properties
"*/*/write" Write (Create) access to all objects and all properties
"*/*/delete" Delete mutation operation access to all object and all properties
"Human/*/write" Write access to all Human object properties
"*/id/*" Read/Write/Delete access to the id property of all Objects
"Dro*/name*/read" Read access to all properties named “name*” for objects named “Dro*”
"Human/*/delete" Delete access to all Human object properties

NotActions

notActions section in the role definition specify the data operations that are excluded from the allowed Actions.

Use notActions if the set of operations that you want to allow is more easily defined by excluding restricted operations.

The access granted by a role (effective permissions) is computed by subtracting the notActions operations from the Actions operations.

The notAction grammar for Semantic Objects is as follows:

{Object Regular Expression}/{Property Regular Expression}/read|write|delete|*

notActions defined for an abstract class are overridden by the child class.

Action and NotAction Precedence

Warning

NotActions take precedence over Actions.

For example, the following role definition would allow DroidMgr to read/write/delete all Droid properties, but with one exception - the DroidMgr roles would not be allowed to read the Droid.name property:

DroidMgr:
  description: "Role that accesses every property of Droid except name"
  actions: [
    "Droid/*/*"
  ]
  notActions: [
    "Droid/name/read"
  ]

RBAC Filters

If you need to fine-tune the role access, you can use filters with the RBAC actions. The Semantic Objects have implemented RBAC filters to use the same syntax as GraphQL filters.

To add a filter, simply add a fourth section to the action containing the filter you wish to apply. The grammar is as follows:

{Object Regular Expression}/{Property Regular Expression}/read|write|delete|*/(where:{Filter expression})

Let’s say you want to allow the role to read Human but only if their name does not contain “Solo”. We will extend the role HumanRead with a filter to accomplish this.

HumanRead:
  description: "Role which can read all Human properties"
  actions: [
    'Human/*/read/(where:{name:{NIRE:"solo"}})'
  ]

Now the role we have defined will filter the results and return only Humans that do not contain “Solo” in their name. Of course, we can use any property we have available in the schema just like we are filtering via GraphQL queries. For example, we may want to show only Humans that have homeworld named “Tatooine”:

HumanRead:
  description: "Role which can read all Human properties"
    actions: [
      'Human/*/read/(where:{homeworld:{name:{EQ: "Tatooine"}}})'
    ]

Now the role will return only Humans which have homeworld named “Tatooine”, all other Humans will be skipped.

Filters are also applicable for notAction, and we can limit the restriction to the specific objects we want.

In the role NoDroidReader, we have explicitly forbidden the reading of Droid:

NoDroidReader:
  description: "Role which can read everything except Droid Objects and properties"
    actions: [
      "*/*/read"
    ]
    notActions: [
      "Droid/*/read"
    ]

But we want to specify the objects to which the restriction will be applied, for example: do not show any droids that have appeared in “The Empire Strikes Back” to users with this role.

There are several ways to achieve this as we know the name and ID of the movie, so you can choose which of the different filters to use. Here is an example of how the role should look if we restrict by name:

NoDroidReader:
  description: "Role which can read everything except Droid Objects and properties"
    actions: [
      "*/*/read"
    ]
    notActions: [
      'Droid/*/read/(where:{film:{name:{EQ: "The Empire Strikes Back"})'
    ]

This role will allow the user to read everything including Droid that was not in the film “The Empire Strikes Back”.

If you want to filter by an abstract class, e.g., Character, then you can use _ifType. For example:

FilterAbstractClass:
   description: "Role which can read everything if Droid and Human have uniqueDroidProperty or uniqueHumanProperty"
   actions: [
    'Character/*/read/(where:{_ifDroid:{eyeColor:{EQ: "yellow"}}_ifHuman:{hairColor:{EQ: "gray"}}})'
   ]

This role will check all Characters, and return only Droids with eyeColor with value “red” and only Humans with hairColor with value “gray”.

When such a filter is in place, the following warning will be returned in the GraphQL response:

{
    "errors": [{
            "message": "WARN: The Authorization JWT Token has `roles` claims :[ROLES] which limits the result."
        }
    ],
    "data": {
        "filtered": "data"
    }
}

Filters in Mutations

Filters can be used in all types of actions - read, write, and delete.

So let’s take a look at the following role:

HumanFromTatooineAdmin:
  description: "Can do everything with Tatooine and its residents."
    actions: [
      'Human/*/*/(where:{homeworld:{name:{EQ: "Tatooine"}}})',
      'Planet/*/*/(where:{name: {EQ: "Tatooine"}})'
    ]

It gives the following permissions:

  • Delete human-s from Tatooine
  • Update human-s from Tatooine
  • Create human-s from Tatooine
  • Create/Update/Delete over Tatooine planet itself

So the following mutations are valid:

Delete and Update, because https://swapi.co/resource/human/4 (Darth Vader) is from Tatooine:

mutation {
  delete_Human (where: {ID: "https://swapi.co/resource/human/4"}) {
    human {
      id
    }
  }
}
mutation {
  update_Human (
    objects: {
      desc: "From tatooine"
    },
    where: {ID: "https://swapi.co/resource/human/4"}) {
    human {
      id
    }
  }
}

Create, because https://swapi.co/resource/planet/1 is actually Tatooine:

mutation {
  create_Human(
    objects: {
      rdfs_label: "Padme",
      desc: "Luke's mother",
      homeworld: {ids: "https://swapi.co/resource/planet/1"}}
  ) {
    human {
      id
    }
  }
}

The following nested create is valid as well, if you have enabled nested creates:

mutation {
  create_Human(
    objects: {
      rdfs_label: "Padme",
      desc: "Luke's mother",
      homeworld: {
       planet: {
         rdfs_label: "Tatooine"
       }
     }
   }) {
    human {
      id
    }
  }
}

However, you would not be allowed to create Padme without a howeworld or with a homeworld with a different name.

You would also not be allowed to update human-s that are not from Tatooine. So the following:

mutation {
  update_Human(
    objects: {
      desc: "Person from Tatooine"
    }
    where: {id: {}}
  ) {
    human {
      id
    }
  }
}

Will produce an error like:

{
  "message": "ERROR: The UPDATE has been prevented as the selected objects of type 'Human' failed to match the constraints '(where: [{homeworld: [{name: [{EQ: \"Tatooine\"}]}]}])'",
  "locations": [
    {
      "line": 3,
      "column": 17
    }
  ]
}

The reason is that the applied filter (where: {id: {}}) will effectively select all human-s from the database, and not all of them are from Tatooine. So instead of updating only the human-s for which you have access, the whole mutation gets blocked and nothing gets updated.

You can also assign filters to property actions. Example:

SithWriter:
  description: "Write Siths."
  actions: [
    'Human/id/write',
    'Human/type/write',
    'Human/name/write',
    'Human/desc/write',
    'Human/height/write/(where:{desc: {IRE: "Sith"}})'
  ]

This role will allow you to create Human-s but will require the desc of the human to contain Sith only if you are trying to write the height property.

So the following mutation is valid:

mutation {
  create_Human(
    objects: {
      rdfs_label: "Yoda",
      desc: "Jedi",
   }) {
    human {
      id
    }
  }
}

But the next one will fail:

mutation {
  create_Human(
    objects: {
      rdfs_label: "Yoda",
      desc: "Jedi",
      height: 130
   }) {
    human {
      id
    }
  }
}

You should be cautious regarding the permissions you are giving because actions like the ones below will effectively block you from creating Human-s:

Role1:
  description: "Not-Sith character writer."
  actions: [
    'Character/*/write/(where:{desc: {NIRE: "Sith"}})',
  ]
Role2:
  description: "Sith writer"
  actions: [
    'Human/type/write/(where:{desc: {IRE: "Sith"}})'
  ]

If a user has such roles, any attempt to create/update/delete a Human will fail with the following:

"message": "ERROR: The CREATE/UPDATE/DELETE has been roll-backed as the objects of type 'Human' failed to match the constraints '[(where: [{desc: [{IRE: \"Sith\"}]}]), (where: [{desc: [{NIRE: \"Sith\"}]}])]'"

Combining Filters

Filters coming from different actions are combined as follows:

  • action filters are combined using OR
  • notAction filters are combined using AND

So if a user has the following set of actions (regardless of whether they come from one or multiple roles):

actions: [
  'Human/*/*/(where:{homeworld:{name:{EQ: "Tatooine"}}})'
  'Human/*/*/(where:{homeworld:{name:{EQ: "Eriadu"}}})'
]
notActions: [
  'Human/*/*/(where:{starship:{name:{EQ: "Naboo star skiff"}}})'
  'Human/*/*/(where:{starship:{name:{EQ: "Trade Federation cruiser"}}})'
]

This means that they will have access to all human-s who:

  • Have homeworld Tatooine OR Eriadu
  • Do not have a starship Naboo star skiff AND do not have a starship Trade Federation cruiser.

Schema Management RBAC

Role based access control across the Semantic Object schemas are declared using a separate /soml-rbac endpoint. Only users with role SchemaRBACAdmin are able to modify the schema RBAC (read/write). A default schema is created during the Semantic Objects bootstrap process. Validation of the schema is performed on creation and update operation. The schema ID cannot be changed, and the default admin user cannot be removed.

The same rules for combining roles, actions, and notActions apply for the RBAC schema as well. A user can have multiple roles assigned, and their actions and notActions are applied. Actions defined in the notActions have precedence over actions.

The RBAC schema is similar to SOML and has the following simple format:

id:          /soml-rbac
label:       Schema Management Role Based Access Control
creator:     http://ontotext.com
created:     2019-06-15
updated:     2019-06-16
versionInfo: 0.1

rbac:
  roles:
  # Default role which does not need to be configured or declared. Included for completeness.
   Default:
     description: "Default role, which does not need to be declared restricts all schema management access read, write and delete"
     notActions: [
       "*/*"
     ]
  # Example role definitions which need to be declared by the SOML user:
  SchemaRBACAdmin:
    description: "Administrator role, can read, write and delete objects and schema"
    actions: [
      "*/*"
    ]
  ReadOnlyUser:
    description: "User which can read all schema"
    actions: [
      "*/read"
    ]
  SwapiSchemaManager:
    description: "User which has admin access (read, write and delete on the swapi schema only"
    actions: [
      "swapi/*"
    ]

The Semantic Objects expose the following endpoints schema RBAC management:

  • Read GET /soml-rbac the schema RBAC’s (only accessible for users/role SchemaRBACAdmin)

    Request Headers:

    Accept (optional): text/yaml (default)

    X-Request-ID (optional): Correlation/Transaction-Id

    Authorization: JWT Token (With schema-rbac-admin role) otherwise unauthorized

    Request Body:

    None/Ignored

    Response Headers:

    Access-Control-Allow-Credentials: true // Expose to Js in Browser

    Access-Control-Allow-Origin: * // Allow any origin

    Content-Type: text/yaml

    X-Request-ID: Correlation/Transaction-Id see spec

    Success Response Body:

    Status Code: 200

{
    "@context": {
        "@vocab": "https://data.ontotext.com/resource/error/",
        "@base": "https://data.ontotext.com/resource/status/",
        "xsd": "http://www.w3.org/2001/XMLSchema#"
    },
    "@type": "SOMLRBAC",
    "@id": "/soml-rbac",
    "rbac": "{RBAC-YAML}"
}
  • Update PUT /soml-rbac the schema RBAC (only accessible for users/role SchemaRBACAdmin)

    Request Headers:

    X-Request-ID (optional): Correlation/Transaction-Id

    Authorization: JWT Token (With schema-rbac-admin role) otherwise unauthorized

    Request Body:

    RBAC YAML see example above

    Response Headers:

    Access-Control-Allow-Credentials: true // Expose to Js in Browser

    Access-Control-Allow-Origin: * // Allow any origin

    Content-Type: application/ld+json

    X-Request-ID: Correlation/Transaction-Id see spec

    Success Response Body:

    Status Code: 200 E.g (Echo bac rbac schema updated)

{
    "@context": {
        "@vocab": "https://data.ontotext.com/resource/error/",
        "@base": "https://data.ontotext.com/resource/status/",
        "xsd": "http://www.w3.org/2001/XMLSchema#"
    },
    "@type": "SOMLRBAC",
    "@id": "/soml-rbac",
    "rbac": "{RBAC-YAML}"
}
  • Schema restrictions violation example

    Error Response Body:

    Status Code: 400

{
  "@context": {
    "@vocab": "http://ontotext.com/ontology/status/",
    "@base": "https://data.ontotext.com/resource/status/",
    "xsd": "http://www.w3.org/2001/XMLSchema#",
    "hydra": "http://www.w3.org/ns/hydra/core#"
  },
  "@type": "hydra:Collection",
  "hydra:member": [
    {
      "@type": "Error",
      "code": "4000001",
      "message": "ERROR: Could not find 'SchemaRBACAdmin' role. RBAC schema should always include schema RBAC admin.",
      "@id": "8ac60a8e-8398-11e9-bc42-526af7764f64"
    }
  ]
}

RBACs authorize the schema endpoints:

  • GET /soml/{schema-id}: Retrieve a particular schema
  • GET /soml: Retrieve all schemas
  • POST /soml: Create a particular schema
  • PUT /soml/{schema-id}: Update a particular schema
  • PUT /soml/{schema-id}/soaas: Bind a particular schema to the Semantic Objects
  • DELETE /soml/{schema-id}: Delete a particular schema

and the schema management endpoints:

  • GET /soml-rbac: Retrieve the rbac schema
  • PUT /soml-rbac: Update the rbac schema