Attribute-Based Access Control

Attribute-Based Access Control (ABAC) is an authorization model, which means it is a way of deciding access rather than a separate service you turn on. Each of Descope's authorization models answers a different question:

  • RBAC asks what role the user holds.
  • ReBAC asks how the user is related to the resource.
  • ABAC asks what is true about the user, the resource, or the request at the moment of the decision.

Attributes are the facts that answer that last question. They can describe the user (department, clearance level, license status, subscription tier), the tenant (organization type, paid tier, region, compliance level), or the request itself (IP address, country, time of day).

Because ABAC answers a different question than the other models, it composes with them instead of replacing them. You can use ABAC on its own, layer it on top of RBAC roles, or attach it directly to ReBAC relations and permissions. ABAC is part of Descope's Fine-Grained Authorization (FGA) system, alongside ReBAC and usable together with RBAC.

Two Ways to Implement ABAC

Descope supports two ways to apply attributes to an authorization decision. Both are ABAC. They differ in where the attribute comes from and where the decision gets evaluated.

In-schema conditions (CEL)Custom attributes in your code
Where the attribute comes fromSupplied with the check request as contextStored on the user or tenant in Descope
Where the decision happensInside the FGA check, as part of the schemaIn your application code
How it is definedcondition and constraint declarations in the FGA schema DSLCustom attribute definitions in the Descope Console
Best suited forRequest-time context such as IP, geography, time, or a role claim gating a relationDurable facts about a user or tenant
Combines withReBAC relations and permissionsRBAC, ReBAC, or on its own

Nothing stops you from using both in the same application. A schema condition can gate the ReBAC check while your code separately reads a stored custom attribute for a decision that never reaches the authz service.

In-Schema Conditions with CEL

When the attribute arrives with the request, you can express the attribute check inside the FGA schema itself using CEL (Common Expression Language). A condition takes typed parameters and evaluates a CEL expression, and you attach it to a relation or permission with with:

model AuthZ 1.0

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

type user

type doc
  relation viewer: user with IsAdmin

Here viewer is the ReBAC relation and with IsAdmin is the ABAC gate on top of it. The relation only holds if the caller supplies a role of admin at check time. Descope also ships built-in constraints for common attribute checks such as IP ranges, country codes, expiry timestamps, and numeric bounds, so you can attach those without writing CEL by hand.

The advantage of this approach is that the attribute logic lives with the rest of your authorization model and is evaluated by Descope in a single check, so your application code does not need to know which relations carry attribute constraints.

For the full syntax, see Conditions (ABAC with Common Expression Language). To pass values at check time, see checking a relation with a condition.

Custom Attributes Checked in Your Code

When the attribute is a durable fact about a user or tenant, you can store it as a custom attribute in Descope and check it in your application code. You would:

  1. Define the custom attribute in the Descope Console:

  2. Set attribute values on users or tenants through the Management SDK or the Console.

  3. Check those attributes in your application code to make the authorization decision, either on their own or alongside an RBAC role check or a ReBAC check.

How the Custom Attribute Approach Compares to ReBAC

FeatureReBACABAC with custom attributes
Schema/DSLUses the schema DSL to define types, relations, and permissionsNo schema needed, since the decision reads attribute values
RelationsCreates explicit relation tuples between users and resourcesChecks attribute values in code
ImplementationDefine schema, create relations, check permissionsSet attributes, check attributes in code
Use CaseResource-specific access, such as "user owns document X"Attribute-based access, such as "users in department Y can access"

Note that in-schema CEL conditions sit in the ReBAC column of this table as far as mechanics go, since they are declared in the DSL and evaluated by our FGA service. They are still ABAC in terms of what they express.

When to Use ABAC

ABAC fits when the authorization decision depends on:

  • User Characteristics: Department, clearance level, license status, subscription tier
  • Tenant Characteristics: Organization type, paid tier, geographical region
  • Dynamic Conditions: Time-based access, location-based access, status-based access
  • Compliance Requirements: Regulatory compliance based on user or tenant attributes
  • Complementing RBAC: Adding attribute checks to role-based permissions, such as "Editor role AND department match"
  • Complementing ReBAC: Gating a relation or permission with a condition, or filtering by attributes before a check

How ABAC Relates to RBAC and ReBAC

ABAC can be used standalone or in combination with RBAC and ReBAC. Here are the common patterns.

ABAC with ReBAC

There are two ways to bring attributes into a relationship-based decision, and which one fits depends on where the attribute lives.

Option 1: Attach a condition in the schema. When the attribute arrives with the request, such as an IP address, a country code, or the current time, the cleanest approach is a CEL condition or a built-in constraint attached to the relation or permission. Descope evaluates the relationship and the attribute together in one check:

model AuthZ 1.0

constraint GeoCountry("US", "GB")
condition DuringBusinessHours(hour int) { hour >= 9 && hour < 17 }

type user

type doc
  relation viewer: user with GeoCountry
  relation editor: user with DuringBusinessHours

The values are then supplied with the check:

const canView = await descopeClient.management.fga.checkWithContext(
  [
    {
      resource: 'engineering-roadmap',
      resourceType: 'doc',
      relation: 'viewer',
      target: 'u1',
      targetType: 'user',
    },
  ],
  { country_code: 'US' },
);

return canView[0].allowed;

See Defining a Schema for the condition syntax and Checking Relations for how context is passed.

Option 2: Check stored attributes before the ReBAC check. When the attribute is stored on the user or tenant, you can read it first and only perform the ReBAC check if it passes. This keeps attribute-based filtering out of the schema and avoids maintaining relations that mirror attributes.

Example: Department-Based Document Access

Check if a user in the "Engineering" department can view a document:

// 1. Check user's department attribute (ABAC)
const user = await descopeClient.management.user.load(loginId);
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: 'engineering-roadmap',
  resourceType: 'doc',
  relation: 'can_view',
  target: user.userId,
  targetType: 'user',
}]);

return canView[0].allowed;

Example: Time-Based Access with ReBAC

Grant access to a document only if the user is in the correct department and it is during business hours:

const user = await descopeClient.management.user.load(loginId);
const currentHour = new Date().getHours();

// ABAC checks
const departmentMatch = user.customAttributes?.department === 'Engineering';
const isBusinessHours = currentHour >= 9 && currentHour < 17;

if (!departmentMatch || !isBusinessHours) {
  return false;
}

// ReBAC check
const canView = await descopeClient.management.fga.check([{
  resource: 'engineering-roadmap',
  resourceType: 'doc',
  relation: 'can_view',
  target: user.userId,
  targetType: 'user',
}]);

return canView[0].allowed;

A time check like this one is a good candidate for moving into the schema as a CEL condition, since the hour is request-time context rather than a stored fact. Keeping it in the schema means every caller gets the same rule without each of them remembering to apply it.

ABAC with RBAC

Attributes can also add conditions to role-based permissions. This pattern suits cases where a role grants broad access and attributes narrow it.

Example: Healthcare Records Access

A doctor can only access patient records if they meet multiple conditions:

  • They have the "Doctor" role (RBAC)
  • Their department matches the patient's department (ABAC)
  • Their medical license status is "active" (ABAC)
  • Their clearance level meets or exceeds the record's sensitivity level (ABAC)
// Check RBAC role
const hasDoctorRole = user.roleNames.includes('Doctor');

// Check ABAC attributes
const departmentMatch = user.customAttributes.department === patient.department;
const hasActiveLicense = user.customAttributes.licenseStatus === 'active';
const hasRequiredClearance = 
  user.customAttributes.clearanceLevel >= record.sensitivityLevel;

// Combined authorization decision
const canAccessRecord = hasDoctorRole && departmentMatch && 
                        hasActiveLicense && hasRequiredClearance;

Example: Department-Based Document Editing

An editor can only edit documents in their department:

  • They have the "Editor" role (RBAC)
  • Their department matches the document's department (ABAC)
  • Their account status is "active" (ABAC)
const canEdit = user.roleNames.includes('Editor') && 
                user.customAttributes.department === document.department &&
                user.customAttributes.accountStatus === 'active';

Standalone ABAC

When authorization is purely attribute-based, with no roles or relations involved, ABAC can stand on its own. This suits feature flags, subscription tiers, and other simple attribute gates.

Example: Subscription-Based Feature Access

Allow access to premium features based on subscription tier:

const canAccessPremiumFeature = 
  user.customAttributes.subscriptionTier === 'premium' ||
  user.customAttributes.subscriptionTier === 'enterprise';

Example: Multi-Tenant Feature Access

Control feature access based on tenant attributes:

const tenant = await descopeClient.management.tenant.load(tenantId);

// Premium features only for premium tier tenants
const canAccessPremiumFeature = 
  tenant.customAttributes.subscriptionTier === 'premium';

// Geographic restrictions
const canAccessRegionalFeature = 
  tenant.customAttributes.region === 'US' || 
  tenant.customAttributes.region === 'EU';

// Compliance-based features
const canAccessHIPAAFeature = 
  tenant.customAttributes.complianceLevel === 'HIPAA';

Example: Time-Based Access Control

Restrict access based on time attributes:

const currentHour = new Date().getHours();
const userShift = user.customAttributes.shift; // 'morning', 'afternoon', 'night'

// Only allow access during user's shift
const canAccess = 
  (userShift === 'morning' && currentHour >= 6 && currentHour < 14) ||
  (userShift === 'afternoon' && currentHour >= 14 && currentHour < 22) ||
  (userShift === 'night' && (currentHour >= 22 || currentHour < 6));

Next

Continue to implementing ABAC for detailed code examples and best practices.

Was this helpful?

On this page