TypeDB Blog


Identity and access management with TypeDB: Part II, transitivity

Dr. James Whiteside


In this blog series, we explore how TypeDB can meet all the requirements for a federated identity and access management (IAM) platform with a lean data model and intuitive query design. The primary focus will be on a role-based access control (RBAC) architecture, but our approach can be easily adapted to other frameworks.

Previously, we learned how to model polymorphic permissions. Here, we’ll show you how to automate permission inheritance. Finally, in the third blog, we’ll explain how to enforce policy compliance at the schema level.


We’ve learned how to model polymorphic permissions by leveraging TypeDB’s strong-typing features. The next hurdle to tackle is permission inheritance. We’ve stored the fact that the Engineering Department has permission to manage files under root/engineering in our database, but how can we be sure that Naomi Nagata, one of the engineers, is able to view the file root/engineering/typedb/readme.md? If we run the following query to test if Naomi has the permission, we get no results:

match
  $s isa employee, has email "naomi.nagata@vaticle.com";
  $o isa file, has path "root/engineering/typedb/readme.md";
  $a isa action, has name "view file";
  $ac ($o, $a) isa access;
  ($s, $ac) isa permission;

We know our query should return a result based on the data we’ve inserted, but the database doesn’t. At least not yet. To implement this, we’re going to need two things: type hierarchies for subjects, objects, and actions, along with inheritance logic that will function on them. Let’s start by making some more changes to our schema:

define

subject sub entity, abstract;
user sub subject, abstract;
user-group sub subject, abstract;

object sub entity, abstract;
resource sub object, abstract;
resource-collection sub object, abstract;

action sub entity, abstract;
operation sub action, owns name;
operation-set sub action, owns name;

id sub attribute, abstract, value string;
name sub id;

To begin with, we’ve defined a second tier of types: users and user groups as subtypes of subject; resources and resource collections as subtypes of object; and operation and operation set as subtypes of action. Next, we need to redefine our concrete types by subtyping the ones, plus we’re going to introduce a few new ones:

define

employee sub user, owns email;
contractor sub user, owns email;

business-unit sub user-group, owns name;
user-role sub user-group, owns name;
user-account sub user-group, owns email;

file sub resource, owns path;
purchase-order sub resource, owns reference;
record sub resource, owns primary-key;
customer-account sub resource, owns email;
pull-request sub resource, owns hash;

directory sub resource-collection, owns path;
application sub resource-collection, owns name;
repository sub resource-collection, owns name;
branch sub resource-collection, owns name;
database sub resource-collection, owns name;
table sub resource-collection, owns name;

path sub id;
email sub id;
reference sub id;
primary-key sub id;
hash sub id;

Finally, we’re going to introduce three new membership relations:

define

group-membership sub relation,
   relates group,
   relates group-member;

user-group plays group-membership:group;
subject plays group-membership:group-member;

collection-membership sub relation,
   relates collection,
   relates collection-member;

resource-collection plays collection-membership:collection;
object plays collection-membership:collection-member;

set-membership sub relation,
   relates set,
   relates set-member;

operation-set plays set-membership:set;
action plays set-membership:set-member;
Entity types (pink) and relation types (yellow) in the schema so far.

Again, we’ve taken advantage of subtyping. By allowing subjects to play the role of group members, it allows us to add both users and user groups to a user group. This design pattern is also used to allow nesting in resource collections and set memberships. Now that we’ve updated the schema, let’s insert the memberships we need with this query:

match
  $g isa business-unit, has name "Engineering";
  $m isa business-unit, has name "Core";
insert
  (group: $g, group-member: $m) isa group-membership;

match
  $g isa business-unit, has name "Core";
  $m isa employee, has email "naomi.nagata@vaticle.com";
insert
  (group: $g, group-member: $m) isa group-membership;

match
  $c isa directory, has path "root/engineering";
  $m isa directory, has path "root/engineering/typedb";
insert
  (collection: $c, collection-member: $m) isa collection-membership;

match
  $c isa directory, has path "root/engineering/typedb";
  $m isa file, has path "root/engineering/typedb/readme.md";
insert
  (collection: $c, collection-member: $m) isa collection-membership;

match
  $s isa operation-set, has name "manage directory";
  $m isa operation-set, has name "write file";
insert
  (set: $s, set-member: $m) isa set-membership;

match
  $s isa operation-set, has name "write file";
  $m isa operation, has name "view file";
insert
  (set: $s, set-member: $m) isa set-membership;

Now that we have the necessary type hierarchies set up, we can implement logic which will allow TypeDB to automatically generate the inherited permissions for us using rule-based inference. Here are the rules we’re going to define:

define

rule subject-permission-inheritance:
   when {
       $m isa subject;
       (group: $g, group-member: $m) isa group-membership;
       (subject: $g, access: $ac) isa permission;
   } then {
       (subject: $m, access: $ac) isa permission;
   };

rule object-permission-inheritance:
   when {
       $m isa object;
       (collection: $c, collection-member: $m) isa collection-membership;
       $ac-c (object: $c, action: $a) isa access;
       $ac-m (object: $m, action: $a) isa access;
       (subject: $s, access: $ac-c) isa permission;
   } then {
       (subject: $s, access: $ac-m) isa permission;
   };

rule action-permission-inheritance:
   when {
       $m isa action;
       (set: $se, set-member: $m) isa set-membership;
       $ac-se (object: $o, action: $se) isa access;
       $ac-m (object: $o, action: $m) isa access;
       (subject: $s, access: $ac-se) isa permission;
   } then {
       (subject: $s, access: $ac-m) isa permission;
   };

These rules allow permissions to be automatically inherited in the subject, object, and action type hierarchies we’ve created. Once rules are defined in our schema using first-order logic, TypeDB resolves the rules at read-time to infer new data. Now, when we run the query for Naomi’s permission in TypeDB Studio with the inference option enabled, we get the following result:

Result for query to find out if Naomi Nagata has permission to view the readme file.

The green border around the permission relation shows that it was generated by rule-inference. If we use TypeDB Studio’s Explanations feature, we can find out how this relation was inferred and see the real data behind this result:

Data used to automatically infer the query result as shown by the Explanations feature.

In this diagram, all of the permissions outlined in green were generated by recursive inference. This highly complex data pattern leads to the conclusion that Naomi has the relevant permission, but we didn’t have to state any of that in the query. We just stated the business question at the highest level:

match
  $s isa employee, has email "naomi.nagata@vaticle.com";
  $o isa file, has path "root/engineering/typedb/readme.md";
  $a isa action, has name "view file";
  $ac ($o, $a) isa access;
  ($s, $ac) isa permission;

When this query is executed, TypeDB will first search for any direct matches, but will not find any. It then attempts to search for inferred matches by utilising the inference engine, which is capable of performing deductive inference using backwards chaining using Horn-clause logic. The engine will find that the last line of our query matches the conclusion (the then clause) of one of the rules we just defined. As a result, it substitutes the matching line in the query for the rule’s condition (the when clause) to produce a new query with the same semantic meaning, so that any result for the new query is a correct result for the query we supplied. This process can be repeated, with further rules being chained onto the query both sequentially and recursively to expand the search space until either an answer is found or it is shown that no possible answer exists in the data.

All of this process of expanding the search space takes place behind the scenes without you having to specify the implementation details: which rules to use, how many times to use them, or in what order to use them. TypeDB does all the legwork for you. All you have to query for is if Naomi has the permission. There’s no need to worry about how she inherits it, thanks to the TypeDB’s declarative queries combined with its inference engine. As rules are resolved at read-time, the conclusions they generate can never be stale. If we add new data or remove existing data, then TypeDB will instantly update the results of any queries run afterwards to reflect the new information.


In this blog post, we’ve seen how we can automate permission inheritance by building on the generalized type hierarchy we built last time. In the final blog in this series, we’ll be exploring how to enforce policy compliance at the schema level. In the meantime, you can try it out by running our interactive demo, or learn more building IAM platforms on TypeDB by reading our white paper.

Entity types (pink) and relation types (yellow) in the schema available in the demo.

Share this article

TypeDB Newsletter

Stay up to date with the latest TypeDB announcements and events.

Subscribe to Newsletter
Feedback