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 from | Supplied with the check request as context | Stored on the user or tenant in Descope |
| Where the decision happens | Inside the FGA check, as part of the schema | In your application code |
| How it is defined | condition and constraint declarations in the FGA schema DSL | Custom attribute definitions in the Descope Console |
| Best suited for | Request-time context such as IP, geography, time, or a role claim gating a relation | Durable facts about a user or tenant |
| Combines with | ReBAC relations and permissions | RBAC, 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 IsAdminHere 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:
-
Define the custom attribute in the Descope Console:
- For users: Users > Custom Attributes
- For tenants: Tenants > Custom Attributes
-
Set attribute values on users or tenants through the Management SDK or the Console.
-
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
| Feature | ReBAC | ABAC with custom attributes |
|---|---|---|
| Schema/DSL | Uses the schema DSL to define types, relations, and permissions | No schema needed, since the decision reads attribute values |
| Relations | Creates explicit relation tuples between users and resources | Checks attribute values in code |
| Implementation | Define schema, create relations, check permissions | Set attributes, check attributes in code |
| Use Case | Resource-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 DuringBusinessHoursThe 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.