Implementing ABAC

ABAC is a way of deciding access based on attributes, and Descope gives you two places to evaluate those attributes. You can declare the attribute check inside the FGA schema as a CEL condition, which Descope evaluates as part of a relation or permission check, or you can store custom attributes on users and tenants and check them in your application code.

The approach you pick depends on where the attribute comes from:

  • In-schema conditions: The attribute arrives with the request, such as an IP address, a country code, or the time of day. Descope evaluates it inside the check.
  • Custom attributes in code: The attribute is a durable fact about a user or tenant, such as a department or a subscription tier. Your code reads it and decides.

From there, the custom attribute approach combines with the rest of your authorization model in three ways:

  1. Standalone ABAC: Check custom attributes directly in your application code to make authorization decisions
  2. ABAC with RBAC: Combine attribute checks with role-based permissions
  3. ABAC with ReBAC: Check attributes first, then perform the ReBAC check

In-Schema Conditions: Evaluating Attributes Inside the Check

When the attribute is part of the request rather than something stored in Descope, you can express it as a condition in the FGA schema and attach it to a relation or permission with with. Descope then evaluates the relationship and the attribute together in a single check.

model AuthZ 1.0

condition IsAdmin(role string) { role == "admin" }

type user

type doc
  relation viewer: user with IsAdmin

Values for the condition's parameters are supplied at check time:

const checks = await descopeClient.management.fga.checkWithContext(
  [
    {
      resource: 'doc-123',
      resourceType: 'doc',
      relation: 'viewer',
      target: 'u1',
      targetType: 'user',
    },
  ],
  { role: 'admin' },
);

Descope also ships built-in constraints for common attribute checks such as IP ranges, country codes, expiry timestamps, and numeric bounds, so those need no CEL of your own.

For the full condition and constraint syntax, see Defining a Schema. For the check-time context in every SDK, see Checking Relations.

Standalone ABAC: Checking Attributes in Code

The simplest way to implement ABAC is to check custom attributes directly in your application code:

// Load user and check custom attributes
const user = await descopeClient.management.user.load(loginId);

// Check subscription tier
const hasPremiumAccess = user.customAttributes?.subscriptionTier === 'premium';

// Check department
const isInEngineering = user.customAttributes?.department === 'Engineering';

// Check multiple attributes
const canAccessFeature = 
  user.customAttributes?.subscriptionTier === 'premium' &&
  user.customAttributes?.licenseStatus === 'active';

ABAC with RBAC: Combining Attributes with Roles

Combine ABAC attribute checks with RBAC role checks to create more granular authorization. This approach is ideal when you need role-based access with additional attribute constraints.

Approach: Check Role and Attributes

  1. Load the user and check their assigned roles
  2. Check custom attributes to add additional conditions
  3. Combine both checks to make the final authorization decision

Example: Department-Based Document Editing

Here's how to check if a user with the "Editor" role can edit a document in their department:

const checkCanEditDocument = async (loginId, documentId) => {
  // 1. Load user
  const user = await descopeClient.management.user.load(loginId);
  if (!user.ok) {
    return false;
  }
  
  // 2. RBAC: Check if user has Editor role
  const hasEditorRole = user.roleNames.includes('Editor');
  
  // 3. ABAC: Check if user's department matches document's department
  const departmentMatch = 
    user.customAttributes?.department === document.department;
  
  // 4. ABAC: Check if license is active
  const hasActiveLicense = 
    user.customAttributes?.licenseStatus === 'active';
  
  // 5. Combined authorization decision
  return hasEditorRole && departmentMatch && hasActiveLicense;
};

ABAC with ReBAC: Checking Attributes Before ReBAC

When an attribute is stored on the user or tenant, you can read it first and only perform the ReBAC check if it passes. This combines attribute-based filtering with relationship-based access without requiring you to maintain relations that mirror attributes.

If the attribute instead arrives with the request, the in-schema condition approach described above is usually the better fit, since the rule lives with the schema and applies to every caller.

Check out the ReBAC Docs to learn about creating a schema and relations.

Approach: Check Attributes, Then ReBAC

  1. Load the user and check their custom attributes
  2. If attributes pass, perform the ReBAC check to verify relationship-based access
  3. Use the DSL to define permissions and leverage relation inheritance

This approach ensures you're using the DSL schema properly while incorporating attribute-based filtering without maintenance overhead.

Example: Department-Based Access

Here's how to check if a user in a specific department can access a document:

const checkDepartmentDocumentAccess = async (loginId, documentId) => {
  // 1. Load user and check department attribute
  const user = await descopeClient.management.user.load(loginId);
  if (!user.ok) {
    return false;
  }
  
  // ABAC: Check if user is in Engineering department
  if (user.customAttributes?.department !== 'Engineering') {
    return false; // Attribute check failed
  }
  
  // 2. If attributes pass, check ReBAC relation
  const canView = await descopeClient.management.fga.check([{
    resource: documentId,
    resourceType: 'doc',
    relation: 'can_view',
    target: user.userId,
    targetType: 'user',
  }]);
  
  return canView[0].allowed;
};
Was this helpful?

On this page