Role Based Access Control (RBAC)¶
What’s in this document?
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 totrue
.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 fromTatooine
- Update
human
-s fromTatooine
- Create
human
-s fromTatooine
- 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 usingOR
notAction
filters are combined usingAND
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 astarship
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 schemaGET /soml
: Retrieve all schemasPOST /soml
: Create a particular schemaPUT /soml/{schema-id}
: Update a particular schemaPUT /soml/{schema-id}/soaas
: Bind a particular schema to the Semantic ObjectsDELETE /soml/{schema-id}
: Delete a particular schema
and the schema management endpoints:
GET /soml-rbac
: Retrieve the rbac schemaPUT /soml-rbac
: Update the rbac schema