Skip to content
bitzorcas
中EN

Guide

Authorization ABAC, ReBAC, and Feature policy

Use high-value invoices, case collaboration, and tenant entitlements to understand attribute rules, resource relations, Feature overrides, priority, caching, and failure.

Last updated

RBAC says what a subject can normally do. ABAC constrains that ability with resource attributes. ReBAC supplies instance-level relation evidence. Feature says whether a tenant owns the capability. They answer different questions and the decision service merges them.

1. One case, four kinds of evidence

Alice has billing.invoice.approve. She is the Approver of an Open, Confidential invoice worth 120,000, but the tenant lacks a required entitlement.

Alice

RBAC
billing.invoice.approve = Allow

Invoice 120000
Confidential

ABAC
amount > 50000 = Deny

ReBAC
Approver + Approve = Allow

Tenant

Feature
Disabled = Deny

Unified merge

Deny

Two Allows cannot cancel either Deny. This is not voting, and no implicit relation-over-attribute hierarchy exists.

2. ABAC supports a narrow resource vocabulary

Each current ABAC rule has one condition. Rules are ordered by ascending Priority, filtered by Action, and the first matching condition returns its Verdict.

2.1 Fields and operators

FieldValueResource sourceSupported operators
Amount0ResourceDescriptor.Amountgreater/less, greater/less or equal, equals, not equals
Status1ResourceDescriptor.StatusEquals, NotEquals, In
Sensitivity2ResourceDescriptor.SensitivityEquals, NotEquals, In
CreateTime3No corresponding resource valueEnum exists, but evaluation always returns false
OperatorValueMeaning
GreaterThan / LessThan0 / 1Numeric Amount only
GreaterThanOrEqual / LessThanOrEqual2 / 3Numeric Amount only
Equals / NotEquals4 / 5Amount and text fields
In6Text only; comma-separated values are trimmed and compared case-insensitively

Verdict uses Allow=0 and Deny=1. A null ActionType means every action; otherwise it is an integer AuthorizationAction value.

2.2 Deny high-value approval

Deny Approve above 50000
# Action=4 is Approve, Field=0 is Amount, Operator=0 is GreaterThan, Verdict=1 is Deny.
# The value uses invariant culture, and a lower Priority number evaluates first.
curl --fail-with-body --request POST "$API_URL/api/authorization/abac-rules" \
--header "Authorization: Bearer $ACCESS_TOKEN" \
--header "Content-Type: application/json" \
--data '{
"module": "billing",
"resourceType": "invoice",
"actionType": 4,
"conditionField": 0,
"conditionOperator": 0,
"conditionValue": "50000",
"verdict": 1,
"priority": 10
}'

Amount parsing uses invariant culture. Use 50000.00, not a localized currency string. Successful ABAC CRUD invalidates the effective tenant’s Permission decision cache so a new Deny is not hidden by an old Allow.

2.3 First ABAC match, not automatic internal Deny priority

Global merge gives Deny priority, but the ABAC evaluator returns only its first match:

Fix ABAC rule order with a test
[Fact]
public async Task Lower_Priority_Number_Should_Deny_Before_Broad_Allow()
{
// Both rules match; Priority 10 must run before Priority 100.
// The later broad Allow proves ABAC returns its first match rather than collecting all rules.
ruleStore.Rules =
[
Rule("deny-high-value", amount: "50000", PolicyVerdict.Deny, priority: 10),
Rule("allow-general", amount: "10000", PolicyVerdict.Allow, priority: 100),
];
var result = await evaluator.EvaluateAsync(
User(),
new ResourceDescriptor("billing", "invoice", Amount: 120_000m),
AuthorizationAction.Approve,
CancellationToken.None);
result.Verdict.Should().Be(PolicyVerdict.Deny);
result.Reason.Should().Be("abac:rule:deny-high-value");
}

Do not rely on database row order for conflicting equal-priority rules. Assign reviewable priorities and test critical precedence.

ABAC returns Neutral for no rules or no match, Deny with abac:store-unavailable for a non-cancellation store exception, and lets cancellation propagate. A missing Amount or invalid numeric rule value simply does not match.

3. ReBAC Lite requires an action-compatible relation

ReBAC participates only for Case, Client, and Billing resource types, compared case-insensitively. It also requires UserId and ResourceId; collection decisions are Neutral.

RelationTypeAllowed actions
OwnerView, Create, Update, Delete, Approve, Reject, Export, Import, Assign, Transfer, Search, Manage
CollaboratorView, Update, Search
AgentView, Create, Update, Export, Import, Assign, Transfer, Search
ApproverView, Approve, Reject
Unknown or differently cased valueNone

RelationStore selects an active, non-deleted relation in the trusted tenant and applies ReBacRelationPolicy.Allows. A relation that does not allow the action becomes Neutral rather than Deny.

Supply complete instance facts for ReBAC
var resource = new ResourceDescriptor(
Module: "cases",
ResourceType: "Case",
ResourceId: caseFile.Id,
TenantId: currentTenant.Id,
OwnerUserId: caseFile.OwnerUserId,
DepartmentId: caseFile.DepartmentId,
Status: caseFile.Status,
Sensitivity: caseFile.Sensitivity);
// An Approver relation may allow Approve, but cannot allow Delete.
// Trusted tenant and resource IDs prevent a client from forging the relation lookup key.
var decision = await authorization.EvaluateAsync(
currentUser.User,
resource,
AuthorizationAction.Approve,
cancellationToken);

3.1 Relation caching and write ownership

The key contains tenant, resource type and ID, user, and action. Both positive and negative results use the 60-second Short policy and a resource tag. Every relation write must call IReBacCache.InvalidateByResourceAsync.

Authorization currently exposes a relation read store but no create, update, or delete command and no module-local resource invalidation call. A resource owner that writes SysResourceRelation must own a reviewed write protocol and cache invalidation; direct table writes can leave stale Allow and stale Neutral results.

4. Feature default and tenant override

Feature is an entitlement, not another name for permission.

NoYesYesNoYesNo

resource.Module

Hard-coded mapping?

Neutral

Feature cache hit?

Enabled=Allow
Disabled=Deny

Tenant override?

Global DefaultState

The evaluator maps only:

ModuleFeatureCode
ticketsplatform.tickets
chatplatform.chat
workflowplatform.workflow
reportingplatform.reporting

Other defined features such as notifications, webhooks, and files do not participate in this evaluator. The current seed contains platform.tickets, platform.chat, platform.workflow, and platform.reporting, and the Demo tenant has explicit workflow/reporting overrides. A genuinely missing definition still resolves to false and denies.

Enable tickets for the current tenant
# IsTenantScoped decides whether this writes a tenant override or global default.
curl --fail-with-body --request PUT \
"$API_URL/api/authorization/features/platform.tickets" \
--header "Authorization: Bearer $ACCESS_TOKEN" \
--header "Content-Type: application/json" \
--data '{ "isEnabled": true }'

Tenant-scoped definitions upsert current TenantId + FeatureCode and invalidate that tenant. Global definitions update DefaultState and call InvalidateAllAsync. Both publish FeatureChangedIntegrationEvent. Keep multi-tenant and multi-instance tests to protect the distinction between tenant-local and global convergence.

Feature returns Neutral for an unmapped module, Deny for missing/disabled definitions, and Deny when the standard Store catches a query exception and returns false. UnavailableFeatureDecisionProvider also returns false. The standard cache adapter degrades get/set exceptions; a custom throwing provider or cache can fault evaluation.

5. Combined-policy tests

Do not stop at evaluator unit tests. Add decision-level scenarios where RBAC always Allows while attributes or entitlement change:

Broad RBAC cannot bypass attribute or entitlement denial
[Theory]
[InlineData(120000, true, false)]
[InlineData(1000, false, false)]
[InlineData(1000, true, true)]
public async Task Invoice_Approval_Should_Combine_All_Policies(
decimal amount,
bool featureEnabled,
bool expectedAllowed)
{
// RBAC remains constant; only resource attributes and tenant entitlement vary.
// The rows fix ABAC denial, Feature denial, and the final Allow independently.
var user = UserWithPermission("billing.invoice.approve");
featureProvider.Set("platform.billing", featureEnabled);
abacRules.SetHighValueDeny(threshold: 50_000m);
var decision = await service.EvaluateAsync(
user,
new ResourceDescriptor("billing", "invoice", Amount: amount),
AuthorizationAction.Approve,
CancellationToken.None);
decision.IsAllowed.Should().Be(expectedAllowed);
}

This is the intended test shape. The current Feature evaluator has no billing mapping, so implement it with an existing tickets case or review and deliver a billing mapping first.

6. Policy review checklist

  1. Do tenant, amount, status, and sensitivity come from trusted server data?
  2. Are ABAC priorities unique and covered by first-match tests?
  3. Is any rule using unsupported CreateTime, an invalid pair, or localized numeric data?
  4. Who owns ReBAC writes and resource-level invalidation?
  5. Do every Feature mapping and seed definition exist together?
  6. Are explicit Deny, all Neutral, store failure, cache failure, and cancellation tested separately?

Previous: RBAC lifecycle · Next: configuration and persistence

100%

Scroll or use controls to zoom · drag when enlarged · double-click for 100% / 200%