Relationship-Based Access Control

Understanding ReBAC

Descope supports full Relationship-Based Access Control (ReBAC) to manage access permissions in your application based on relationships between entities. Create a schema, define relationships, and add checks to your application using our APIs and SDKs.

Practical Scenario for ReBAC instead of RBAC

An E-commerce platform

With RBAC, access is granted based on roles like Seller, Buyer, or Admin. A seller with a generic seller role would have the same access rights as any other seller, which might include managing listings, processing orders, and handling customer queries.

However, RBAC could have limitations in this scenario:

  • One-Size-Fits-All: All sellers have identical access privileges regardless of their specific relationship with the buyer or the product. For example, sellers cannot offer personalized deals or discounts to repeat customers or manage after-sale services uniquely for each customer.
  • Static Permissions: The RBAC system cannot easily accommodate temporary or conditional permissions, such as allowing a buyer to track the progress of a custom-made item directly through the seller's interface.

ReBAC, on the other hand, can provide access controls that adapt to the relationships between users:

  • Seller-Customer Relationships: Sellers might give specific customers special access to pre-orders or exclusive products based on their purchase history or membership status.
  • Collaborative Features: For custom-made items, the relationship between a buyer and seller could allow for a direct communication channel within the platform, enabling the buyer to provide input or get updates on their order's progress.
  • Dynamic Permissions: Access permissions can evolve in real-time. For instance, a buyer might have temporary access to a seller's design tool to customize a product, which expires after the order is finalized. In the e-commerce platform scenario, ReBAC allows for a more nuanced and customer-centric approach. It enables dynamic access control based on the evolving relationships between sellers, buyers, and products. In contrast, RBAC’s static roles might not effectively handle the varied and personalized interactions that modern e-commerce platforms often facilitate.

Implementing ReBAC in Descope

The general process of setting up ReBAC in Descope is as follows:

  1. Defining a schema: Establish the types of entities (e.g., user, document) and the possible relationships (e.g., owner, editor) that exist within your application.
  2. Saving a schema: Store the schema in a way that it can be efficiently queried and managed, ensuring it can be updated as the relationships evolve.
  3. Creating relations: Build out the actual linkages between specific entities based on your defined schema, which will serve as the foundation for making access decisions.
  4. Adding relations checks: Integrate checks into your application to consult the defined relationships and confirm if a user's action is authorized.

A full code example

After you've completed the aforementioned steps, you'll have employed code looking something like the below, with everything from implementing a schema to creating and checking relations. The schema is loaded from a YAML file (as shown in the define schema example section), and the relations are created and checked using the Descope SDK.

NodeJSPythonGoJava
const file = fs.readFileSync(option.input, 'utf8'); // Read the file
const schema: AuthzSchema = JSON.parse(file); // Parse the file into a schema object
const authzService = descopeClient.management.authz;
const existingSchema: AuthzSchema = await authzService.loadSchema();
if (existingSchema.name !== schema.name) {
  await authzService.deleteSchema();
  await authzService.saveSchema(schema, true);
  const relations: AuthzRelation[] = [{
    resource: 'some-doc',
    relationDefinition: 'owner',
    namespace: 'doc',
    target: 'u1'
  },
  {
    resource: 'some-other-doc',
    relationDefinition: 'editor',
    namespace: 'doc',
    target: 'u2'
  }]
  await authzService.createRelations(relations);
}
const relationQueries: AuthzRelationQuery[] = [{
  resource: 'some-doc',
  relationDefinition: 'owner',
  namespace: 'doc',
  target: 'u1'
},
{
  resource: 'some-other-doc',
  relationDefinition: 'editor',
  namespace: 'doc',
  target: 'u2'
}];
const resp = await authzService.hasRelations(relationQueries);

assert(resp[0].hasRelation);
assert(resp[1].hasRelation);

const resource: string = 'some-doc';
const relationDefinition: string = 'editor';
const namespace: string = 'doc';
const users: string[] = await authzService.whoCanAccess(resource, relationDefinition, namespace);

const relations: AuthzRelation[] = authzService.resourceRelations(resource);

const targets: string[] = ['u1', 'u2'];
const relations: AuthzRelation[] = authzService.targetsRelations(targets);

const target: string = 'u1';
const relations: AuthzRelation[] = authzService.whatCanTargetAccess(target);
newSchema = <define schema here>
existingSchema = client.mgmt.authz.load_schema()
if existingSchema !== newSchema:
  client.mgmt.authz.delete_schema()
  client.mgmt.authz.save_schema(newSchema, True)
  client.mgmt.authz.create_relations(
      [
          {
              "resource": "some-doc",
              "relationDefinition": "owner",
              "namespace": "doc",
              "target": "u1",
          }
      ]
  )
  relations = descope_client.mgmt.authz.has_relations(
      [
          {
              "resource": "some-doc",
              "relationDefinition": "viewer",
              "namespace": "doc",
              "target": "u1",
          }
      ]
  )

#  Finds the list of targets (usually users) who can access the given resource with the given RD
client.mgmt.authz.who_can_access("a", "b", "c")
// Load the existing schema
// Args:
//    ctx: context.Context - Application context for the transmission of context capabilities like
//        cancellation signals during the function call. In cases where context is absent, the context.Background()
//        function serves as a viable alternative.
//        Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher.
ctx := context.Background()

schema, err := descopeClient.Management.Authz().LoadSchema(ctx)
if err != nil {
    // handle error
}

// Save the schema you'd like to
err := descopeClient.Management.Authz().SaveSchema(schema, true)

// Create relations
err := descopeClient.Management.Authz().CreateRelations([]*descope.AuthzRelation {
    {
        resource: "some-doc",
        relationDefinition: "owner",
        namespace: "doc",
        target: "u1",
    },
})

// Check if target has the relevant relation
// The answer should be true because an owner is also a viewer
relations, err := descopeClient.Management.Authz().HasRelations([]*descope.AuthzRelationQuery{
    {
        resource: "some-doc",
        relationDefinition: "viewer",
        namespace: "doc",
        target: "u1",
    }
})
ObjectMapper mapper = new ObjectMapper(new YAMLFactory());
mapper.findAndRegisterModules();
Schema s = mapper.readValue(new File("src/test/data/files.yaml"), Schema.class);
Schema existingSchema = authzService.loadSchema();
if (!existingSchema.getName().equals(s.getName())) {
  authzService.deleteSchema();
  authzService.saveSchema(s, true);
  authzService.createRelations(List.of(
    new Relation("Dev", "parent", "org", "Descope", null, null, null, null),
    new Relation("Sales", "parent", "org", "Descope", null, null, null, null),
    new Relation("Dev", "member", "org", "u1", null, null, null, null),
    new Relation("Dev", "member", "org", "u3", null, null, null, null),
    new Relation("Sales", "member", "org", "u2", null, null, null, null),
    new Relation("Presentations", "parent", "folder", "Internal", null, null, null, null),
    new Relation("roadmap.ppt", "parent", "doc", "Presentations", null, null, null, null),
    new Relation("roadmap.ppt", "owner", "doc", "u1", null, null, null, null),
    new Relation("Internal", "viewer", "folder", null, "Descope", "member", "org", null),
    new Relation("Presentations", "editor", "folder", null, "Sales", "member", "org", null)
  ));
}
var resp = authzService.hasRelations(List.of(
  new RelationQuery("roadmap.ppt", "owner", "doc", "u1", false),
  new RelationQuery("roadmap.ppt", "editor", "doc", "u1", false),
  new RelationQuery("roadmap.ppt", "viewer", "doc", "u1", false),
  new RelationQuery("roadmap.ppt", "viewer", "doc", "u3", false),
  new RelationQuery("roadmap.ppt", "editor", "doc", "u3", false),
  new RelationQuery("roadmap.ppt", "editor", "doc", "u2", false)
));

assertTrue(resp.get(0).isHasRelation());
assertTrue(resp.get(1).isHasRelation());
assertTrue(resp.get(2).isHasRelation());
assertTrue(resp.get(3).isHasRelation());
assertFalse(resp.get(4).isHasRelation());
assertTrue(resp.get(5).isHasRelation());

var respWho = authzService.whoCanAccess("roadmap.ppt", "editor", "doc");
assertThat(respWho).hasSameElementsAs(List.of("u1", "u2"));
var respResourceRelations = authzService.resourceRelations("roadmap.ppt");
assertThat(respResourceRelations).size().isEqualTo(2);
var respUsersRelations = authzService.targetsRelations(List.of("u1"));
assertThat(respUsersRelations).size().isEqualTo(2);
var respWhat = authzService.whatCanTargetAccess("u1");
assertThat(respWhat).size().isEqualTo(7);

Next

Now that you have a high-level vision of the ReBAC implementation process, let's move on to defining a schema.