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:
- Standalone ABAC: Check custom attributes directly in your application code to make authorization decisions
- ABAC with RBAC: Combine attribute checks with role-based permissions
- 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 IsAdminValues 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
- Load the user and check their assigned roles
- Check custom attributes to add additional conditions
- 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
- Load the user and check their custom attributes
- If attributes pass, perform the ReBAC check to verify relationship-based access
- 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;
};Overview
Learn how Descope supports Attribute-Based Access Control (ABAC) using schema conditions and custom attributes for fine-grained authorization decisions.
FGA Cache
Learn how to use Descope AuthZ Cache to accelerate Fine-Grained Authorization (FGA) checks for ReBAC, ABAC, and all authz service operations.