Implementing ABAC

ABAC in Descope is implemented by checking custom attributes stored on users or tenants. There are three main approaches:

  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: Query users by custom attributes, then create explicit relations using the ReBAC DSL schema

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';
# Load user and check custom attributes
user = descope_client.mgmt.user.load(login_id)

# Check subscription tier
custom_attrs = user.custom_attributes or {}
has_premium_access = custom_attrs.get('subscriptionTier') == 'premium'

# Check department
is_in_engineering = custom_attrs.get('department') == 'Engineering'

# Check multiple attributes
can_access_feature = (
    custom_attrs.get('subscriptionTier') == 'premium' and
    custom_attrs.get('licenseStatus') == 'active'
)
// Load user and check custom attributes
ctx := context.Background()
user, err := descopeClient.Management.User().Load(ctx, loginID)

// Check subscription tier
tier, _ := user.CustomAttributes["subscriptionTier"].(string)
hasPremiumAccess := tier == "premium"

// Check department
dept, _ := user.CustomAttributes["department"].(string)
isInEngineering := dept == "Engineering"

// Check multiple attributes
licenseStatus, _ := user.CustomAttributes["licenseStatus"].(string)
canAccessFeature := tier == "premium" && 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;
};
def check_can_edit_document(login_id, document_id):
    # 1. Load user
    user = descope_client.mgmt.user.load(login_id)
    if not user:
        return False
    
    # 2. RBAC: Check if user has Editor role
    has_editor_role = 'Editor' in user.role_names
    
    # 3. ABAC: Check if user's department matches document's department
    custom_attrs = user.custom_attributes or {}
    department_match = (
        custom_attrs.get('department') == document.department
    )
    
    # 4. ABAC: Check if license is active
    has_active_license = (
        custom_attrs.get('licenseStatus') == 'active'
    )
    
    # 5. Combined authorization decision
    return has_editor_role and department_match and has_active_license
func checkCanEditDocument(ctx context.Context, loginID string, documentID string) bool {
    // 1. Load user
    user, err := descopeClient.Management.User().Load(ctx, loginID)
    if err != nil {
        return false
    }
    
    // 2. RBAC: Check if user has Editor role
    hasEditorRole := false
    for _, role := range user.RoleNames {
        if role == "Editor" {
            hasEditorRole = true
            break
        }
    }
    
    // 3. ABAC: Check if user's department matches document's department
    dept, _ := user.CustomAttributes["department"].(string)
    departmentMatch := dept == document.Department
    
    // 4. ABAC: Check if license is active
    licenseStatus, _ := user.CustomAttributes["licenseStatus"].(string)
    hasActiveLicense := licenseStatus == "active"
    
    // 5. Combined authorization decision
    return hasEditorRole && departmentMatch && hasActiveLicense
}

ABAC with ReBAC: Checking Attributes Before ReBAC

ABAC works best with ReBAC when you check attributes first, then perform the ReBAC check. This approach combines attribute-based filtering with relationship-based access without requiring you to maintain relations based on attributes.

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;
};
def check_department_document_access(login_id, document_id):
    # 1. Load user and check department attribute
    user = descope_client.mgmt.user.load(login_id)
    if not user:
        return False
    
    # ABAC: Check if user is in Engineering department
    custom_attrs = user.custom_attributes or {}
    if custom_attrs.get('department') != 'Engineering':
        return False  # Attribute check failed
    
    # 2. If attributes pass, check ReBAC relation
    can_view = descope_client.mgmt.fga.check([
        {
            "resource": document_id,
            "resourceType": "doc",
            "relation": "can_view",
            "target": user.user_id,
            "targetType": "user",
        }
    ])
    
    return can_view[0]["allowed"]
func checkDepartmentDocumentAccess(ctx context.Context, loginID string, documentID string) bool {
    // 1. Load user and check department attribute
    user, err := descopeClient.Management.User().Load(ctx, loginID)
    if err != nil {
        return false
    }
    
    // ABAC: Check if user is in Engineering department
    dept, _ := user.CustomAttributes["department"].(string)
    if dept != "Engineering" {
        return false // Attribute check failed
    }
    
    // 2. If attributes pass, check ReBAC relation
    canView, err := descopeClient.Management.FGA().Check(ctx, []*descope.FGARelation{
        {
            Resource: documentID,
            ResourceType: "doc",
            Relation: "can_view",
            Target: user.UserID,
            TargetType: "user",
        },
    })
    if err != nil {
        return false
    }
    
    return canView[0].Allowed
}
Was this helpful?

On this page