π‘οΈ Policy-Based Access Control (PBAC) Best Practices
This guide provides comprehensive best practices for designing, implementing, and maintaining effective Policy-Based Access Control systems with Control Core, with emphasis on regulatory compliance for financial services and healthcare.
π§ API-First References
For engineering teams implementing PBAC through APIs and SDK-style workflows, pair this guide with:
π‘οΈ Understanding PBAC
PBAC vs RBAC vs ABAC
Role-Based Access Control (RBAC):
User β Has Role β Role Has Permissions β Access Granted
Example: "Admin role can delete users"
Limitations:
- Rigid role structures
- Role explosion (too many specific roles)
- Cannot handle contextual access
- Difficult to express complex rules
Attribute-Based Access Control (ABAC):
User Attributes + Resource Attributes + Context β Access Decision
Example: "Users in Engineering department can access development resources"
Benefits:
- More flexible than RBAC
- Uses attributes for decisions
- Can handle some context
Limitations:
- Can become complex
- Performance challenges with many attributes
- Difficult to audit and debug
Coming Soon: ReBAC (Relationship-Based Access Control)
ReBAC extends the ideas behind RBAC/ABAC by making authorization decisions based on relationships between subjects and resources (for example: ownership, org hierarchy, trust, or delegation).
Common ReBAC questions it helps answer:
- Can this user access resources they are connected to (directly or through a chain of relationships)?
- Can permissions flow through managed relationships (with clear, auditable rules)?
- How do you model βwho can act on whose behalfβ without an explosion of roles?
Status: ReBAC is coming soon to Control Core. For now, you can model many relationship-driven requirements with PBAC by encoding relationships as policy inputs (attributes and context) and evaluating them in Rego.
Policy-Based Access Control (PBAC):
Declarative Policies (Rego) + Dynamic Context β Access Decision
Example: Complex rules including time, location, risk score, compliance requirements
Benefits:
- Extremely flexible and expressive
- Handles complex authorization logic
- Supports dynamic context
- Testable and auditable
- Centrally managed
- Version controlled
- Can express RBAC and ABAC patterns
When to Use PBAC
Use PBAC when:
- Complex authorization requirements
- Multiple factors in access decisions
- Context-aware access control needed
- Regulatory compliance requirements (FINTRAC, OSFI, HIPAA)
- Fine-grained permissions required
- Frequently changing access rules
- Need for audit trail and compliance reporting
RBAC might be sufficient when:
- Simple, static role hierarchy
- Few roles and permissions
- No contextual requirements
- Low compliance requirements
π‘οΈ PBAC Design Principles
1. Default Deny (Fail Secure)
Always start with default deny:
package controlcore.policy
import rego.v1
# Good: Explicit default deny
default allow := false
allow if {
# Explicit conditions to allow
input.user.roles[_] == "admin"
}
# Bad: No default (undefined behavior)
allow if {
input.user.roles[_] == "admin"
}
2. Principle of Least Privilege
Grant minimum necessary permissions:
# Good: Specific, minimal permissions
allow if {
input.user.roles[_] == "developer"
input.action.name == "read" # Only read
input.resource.environment == "development" # Only dev environment
}
# Bad: Overly broad permissions
allow if {
input.user.roles[_] == "developer"
# Allows all actions on all resources!
}
3. Policy Scoping: Define What You Apply To, Not What You Exclude
CRITICAL PBAC BEST PRACTICE: Policies should explicitly define the resources they apply to, not list exclusions.
Good: Explicit Scope Definition
package controlcore.demoapp_api.data_protection
import rego.v1
# Default to NOT processing - only process if resource matches scope
default should_process_response := false
# Only process resources this policy is designed to protect
should_process_response if {
is_protected_resource
}
# Define what this policy protects (banking/financial customer data)
is_protected_resource if {
protected_resource_paths[input.resource.path]
}
# Explicitly list resources this policy applies to
protected_resource_paths contains "/api/banking/customers"
protected_resource_paths contains "/api/financial"
protected_resource_paths contains "/api/customers"
Bad: Exclusion-Based Approach
# β BAD: Listing what to exclude
default should_process_response := true
should_process_response := false if {
public_endpoints[input.resource.path] # Excluding public endpoints
}
public_endpoints contains "/auth/login"
public_endpoints contains "/health"
# ... many exclusions ...
Why This Matters:
- β Clear Intent: Policy scope is immediately obvious
- β Maintainable: Adding new resources is explicit
- β Safe Defaults: Unknown resources are not processed
- β Composable: Multiple policies can coexist without conflicts
- β Auditable: Easy to understand what each policy protects
Example: Multiple Policies, Clear Scopes
# Policy 1: Data masking for banking endpoints
package controlcore.banking.data_protection
default should_process_response := false
should_process_response if {
input.resource.path == "/api/banking/customers"
}
# Policy 2: Access control for admin endpoints
package controlcore.admin.access_control
default should_process_response := false
should_process_response if {
input.resource.path == "/api/admin/users"
}
# Policy 3: Rate limiting for public APIs
package controlcore.api.rate_limit
default should_process_response := false
should_process_response if {
input.resource.path == "/api/public"
}
Each policy has a clear, explicit scope. No conflicts, no ambiguity.
4. Policy-Driven Path Control (Control Core Adoption Principle)
To control which API or other calls are allowed or in scope, the path must be part of your Rego policy. Enforcement components do not hardcode paths; they evaluate the policy. You define allowed paths, public endpoints, or in-scope resources in Rego.
Principle:
- Paths are policy-defined. Any path that should be allowed, treated as public, or subject to a specific policy (e.g. masking) must appear in your Rego codeβfor example in a path whitelist, a set of path prefixes, or explicit rules that use the request path.
- Single source of truth. The policy bundle loaded from Control Core is the only place that defines which paths get which behavior. Changing path behavior is done by changing policy, not by changing enforcement code or configuration outside the policy.
Good: Path whitelist in Rego
package controlcore.policy
import rego.v1
default allow := false
# Paths that do not require authentication; defined in policy only
allow if {
public_path(request_path)
}
request_path := input.resource.id
request_path := input.context.raw_path
public_path(p) if { startswith(p, "/api/auth/") }
public_path(p) if { startswith(p, "/health") }
# Add every path prefix you intend to allow as public or in-scope here
Why this matters:
- Consistency: All environments use the same policy; path behavior is not scattered across config or code.
- Auditability: You can review and version exactly which paths are allowed or in scope.
- Control Core adoption: This principle is enforced so that adoption stays policy-centric and portable.
5. Separation of Concerns
Modular policy design:
# authentication.rego - Handle authentication
package controlcore.auth
import rego.v1
user_authenticated if {
input.user.authenticated == true
token_valid(input.user.token)
}
# authorization.rego - Handle authorization
package controlcore.authz
import rego.v1
import data.controlcore.auth
default allow := false
allow if {
data.controlcore.auth.user_authenticated # Reuse auth check
user_has_permission
}
# compliance.rego - Handle compliance rules
package controlcore.compliance
import rego.v1
fintrac_compliant if {
# FINTRAC-specific rules
}
6. Explicit is Better Than Implicit
# Good: Explicit conditions
allow if {
input.user.roles[_] == "manager"
input.resource.attributes.department == input.user.department
input.action.name in ["read", "update"]
not user_on_leave(input.user.id)
}
# Bad: Implicit assumptions
allow if {
input.user.roles[_] == "manager"
# Assumes manager has all permissions
}
7. Test Everything
Comprehensive testing:
package controlcore.policy
import rego.v1
default allow := false
allow if {
input.user.roles[_] == "admin"
}
# Test cases
test_admin_allowed if {
allow with input as {"user": {"roles": ["admin"]}}
}
test_developer_denied if {
not allow with input as {"user": {"roles": ["developer"]}}
}
test_no_role_denied if {
not allow with input as {"user": {"roles": []}}
}
test_empty_input_denied if {
not allow with input as {}
}
π‘οΈ Access Control Patterns
Pattern 1: Role Hierarchy
package controlcore.policy
import rego.v1
default allow := false
# Define role levels
role_levels := {
"superadmin": 5,
"admin": 4,
"manager": 3,
"developer": 2,
"viewer": 1
}
# Get user's highest role level
user_level := max([level |
role := input.user.roles[_]
level := role_levels[role]
])
# Action requirements
action_requirements := {
"delete": 4,
"write": 3,
"update": 2,
"read": 1
}
# Allow if user level meets requirement
allow if {
required_level := action_requirements[input.action.name]
user_level >= required_level
}
Pattern 2: Department-Based Access
package controlcore.policy
import rego.v1
default allow := false
# Users can only access resources in their department
allow if {
input.user.department == input.resource.attributes.department
input.action.name in ["read", "update"]
}
# Managers can access any department resource
allow if {
input.user.roles[_] == "manager"
input.user.attributes.manages_departments[_] == input.resource.attributes.department
}
# Cross-department access requires approval
allow if {
input.user.department != input.resource.attributes.department
approval := data.cross_dept_approvals[input.user.id][input.resource.id]
approval.status == "approved"
approval.expiry > time.now_ns()
}
Pattern 3: Time-Based Access
package controlcore.policy
import rego.v1
default allow := false
# Business hours access
allow if {
input.user.roles[_] in ["employee", "contractor"]
is_business_hours
is_weekday
}
is_business_hours if {
[hour, _, _] := time.clock([time.now_ns()])
hour >= 9
hour < 17
}
is_weekday if {
weekday := time.weekday([time.now_ns()])
weekday >= 1 # Monday
weekday <= 5 # Friday
}
# After-hours access requires approval
allow if {
not is_business_hours
input.user.attributes.after_hours_approved == true
input.action.name == "read" # Read-only after hours
}
Pattern 4: Geo-Location Based Access
package controlcore.policy
import rego.v1
default allow := false
# Allowed countries for data access
allowed_countries := ["US", "CA", "UK", "DE"]
# Geo-restriction for sensitive data
allow if {
input.resource.attributes.sensitivity == "restricted"
input.context.geo_location.country in allowed_countries
input.user.attributes.international_access_approved == true
}
# OSFI: Canadian data must be accessed from Canada
allow if {
input.resource.attributes.data_residency == "Canada"
input.context.geo_location.country == "CA"
}
Pattern 5: Risk-Based Adaptive Access
package controlcore.policy
import rego.v1
default allow := false
# Calculate risk score
risk_score := calculate_risk(input)
calculate_risk(input) := score if {
factors := [
unusual_location_risk,
unusual_time_risk,
unusual_resource_risk,
user_risk_profile
]
score := sum(factors) / count(factors)
}
unusual_location_risk := 0.5 if {
input.context.geo_location.country != input.user.attributes.usual_country
} else := 0
unusual_time_risk := 0.3 if {
not is_usual_hours
} else := 0
# Low risk: Normal access
allow if {
risk_score < 0.3
input.user.authenticated == true
}
# Medium risk: Require MFA
allow if {
risk_score >= 0.3
risk_score < 0.7
input.user.attributes.mfa_verified == true
}
# High risk: Deny and flag for review
allow if {
risk_score >= 0.7
input.user.roles[_] == "admin"
input.context.security_review_completed == true
}
π Financial Services Compliance Patterns
FINTRAC Transaction Monitoring
package fintrac.monitoring
import rego.v1
default allow := false
default requires_reporting := false
default requires_investigation := false
# Large Cash Transaction Reporting (LCTR)
requires_reporting if {
input.transaction.amount >= 10000
input.transaction.currency == "CAD"
input.transaction.type == "cash"
}
# Suspicious Transaction Reporting (STR) indicators
str_indicators[indicator] if {
# Structuring: Multiple transactions under threshold
recent := data.transactions_30days[input.transaction.customer_id]
near_threshold := [t |
t := recent[_]
t.amount >= 9000
t.amount < 10000
t.type == "cash"
]
count(near_threshold) >= 3
indicator := {
"type": "structuring",
"severity": "high",
"transactions": near_threshold
}
}
str_indicators[indicator] if {
# Unusual pattern
customer := data.customers[input.transaction.customer_id]
input.transaction.amount > (customer.avg_monthly_volume * 5)
indicator := {
"type": "unusual_amount",
"severity": "medium",
"deviation": input.transaction.amount / customer.avg_monthly_volume
}
}
# Allow with reporting/investigation
allow if {
input.user.roles[_] in ["teller", "financial-advisor"]
input.user.attributes.fintrac_trained == true
# If reporting required, ensure it's documented
not requires_reporting
}
allow if {
requires_reporting
input.transaction.lctr_number != null # LCTR filed
input.user.roles[_] in ["teller", "compliance-officer"]
}
# Suspicious transactions need compliance review
allow if {
count(str_indicators) > 0
input.user.roles[_] == "compliance-officer"
input.transaction.str_filed == true
input.transaction.senior_management_notified == true
}
OSFI Guideline B-10 Implementation
package osfi.segregation_duties
import rego.v1
default allow := false
# Define conflicting duty pairs
conflicting_duties := {
"payments": [
["payment-initiator", "payment-approver"],
["payment-creator", "payment-authorizer"]
],
"accounts": [
["account-opener", "account-approver"],
["account-creator", "account-reviewer"]
],
"limits": [
["limit-requester", "limit-approver"],
["limit-setter", "limit-authorizer"]
]
}
# Check if user has conflicting roles
has_conflicting_roles(user_roles, operation_type) if {
pairs := conflicting_duties[operation_type]
some pair in pairs
pair[0] in user_roles
pair[1] in user_roles
}
# Allow if no conflicts
allow if {
operation_type := input.resource.attributes.operation_type
not has_conflicting_roles(input.user.roles, operation_type)
input.user.roles[_] in authorized_roles[operation_type][input.action.name]
}
authorized_roles := {
"payments": {
"initiate": ["payment-initiator", "payment-creator"],
"approve": ["payment-approver", "payment-authorizer"],
"review": ["compliance-officer", "auditor"]
},
"accounts": {
"create": ["account-opener", "account-creator"],
"approve": ["account-approver", "account-reviewer"],
"audit": ["compliance-officer"]
}
}
# Dual authorization for high-value transactions
allow if {
input.transaction.amount >= 100000
input.transaction.currency == "CAD"
# Requires two approvers
count(input.transaction.approvers) >= 2
# Current user is one of the approvers
input.user.id in input.transaction.approvers
# Approver has sufficient authority
input.user.roles[_] == "senior-manager"
}
AML/KYC Risk-Based Approach
package aml.risk_based
import rego.v1
default allow := false
# Risk levels and requirements
risk_requirements := {
"low": {
"kyc_required": true,
"enhanced_dd": false,
"ongoing_monitoring": "annual"
},
"medium": {
"kyc_required": true,
"enhanced_dd": false,
"source_of_funds": true,
"ongoing_monitoring": "quarterly"
},
"high": {
"kyc_required": true,
"enhanced_dd": true,
"source_of_funds": true,
"source_of_wealth": true,
"beneficial_owner": true,
"ongoing_monitoring": "monthly"
}
}
# Determine customer risk level
customer_risk_level(customer) := "high" if {
# High-risk indicators
customer.pep_status == true
} else := "high" if {
customer.high_risk_jurisdiction == true
} else := "high" if {
customer.business_type in ["money_service", "casino", "virtual_currency"]
} else := "medium" if {
customer.annual_revenue > 10000000
} else := "medium" if {
customer.international_transactions_percentage > 50
} else := "low"
# Verify risk-appropriate controls
allow if {
customer := data.customers[input.resource.attributes.customer_id]
risk_level := customer_risk_level(customer)
requirements := risk_requirements[risk_level]
# Verify KYC
customer.kyc_status == "verified"
kyc_current(customer.kyc_date)
# Verify enhanced DD if required
enhanced_dd_satisfied(customer, requirements.enhanced_dd)
# Verify monitoring is current
monitoring_current(customer, requirements.ongoing_monitoring)
}
kyc_current(kyc_date) if {
age_days := (time.now_ns() - time.parse_rfc3339_ns(kyc_date)) / (24 * 60 * 60 * 1000000000)
age_days < 365
}
enhanced_dd_satisfied(customer, required) if {
not required
}
enhanced_dd_satisfied(customer, required) if {
required == true
customer.enhanced_due_diligence == true
customer.beneficial_owner_identified == true
customer.source_of_wealth_verified == true
}
π€ AI/LLM Authorization Patterns
Pattern 1: Prompt Filtering and Validation
package ai.prompt_filtering
import rego.v1
default allow := false
# Allow AI access with comprehensive safety checks
allow if {
user_authorized_for_ai
prompt_passes_safety_checks
usage_within_limits
}
user_authorized_for_ai if {
input.user.roles[_] in ["developer", "data-scientist", "ai-engineer"]
input.user.attributes.ai_safety_trained == true
}
prompt_passes_safety_checks if {
prompt := input.context.prompt
# Length check
prompt_length := count(prompt)
prompt_length > 0
prompt_length < 8000
# No PII patterns
not contains_pii_patterns(prompt)
# No injection attempts
not contains_injection_patterns(prompt)
# Safety score from ML model
input.context.safety_score > 0.85
# Content policy compliance
complies_with_content_policy(prompt)
}
contains_pii_patterns(text) if {
# SSN pattern
regex.match(`\d{3}-\d{2}-\d{4}`, text)
}
contains_pii_patterns(text) if {
# Credit card pattern
regex.match(`\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}`, text)
}
contains_pii_patterns(text) if {
# SIN (Canadian) pattern
regex.match(`\d{3}[-\s]?\d{3}[-\s]?\d{3}`, text)
}
contains_injection_patterns(text) if {
injection_attempts := [
"ignore previous instructions",
"disregard safety",
"jailbreak",
"bypass filter"
]
lower_text := lower(text)
some attempt in injection_attempts
contains(lower_text, attempt)
}
usage_within_limits if {
usage := data.usage_stats[input.user.id]
usage.requests_today < 1000
usage.tokens_today < 500000
usage.cost_today < 100.00 # USD
}
Pattern 2: Content Injection for Context
package ai.content_injection
import rego.v1
# Pre-prompt content injection
pre_prompt_content := {
"user_context": user_context_block,
"compliance_guidelines": compliance_block,
"safety_instructions": safety_block
} if {
input.context.injection_enabled == true
}
user_context_block := sprintf(`
User Context:
- Email: %s
- Department: %s
- Clearance Level: %d
- Jurisdiction: %s
`, [
input.user.email,
input.user.department,
input.user.attributes.clearance_level,
input.user.attributes.jurisdiction
])
compliance_block := sprintf(`
Compliance Guidelines:
- Do not discuss customer financial information
- Follow FINTRAC reporting requirements
- Maintain confidentiality per OSFI guidelines
- Do not generate content that could violate AML regulations
`) if {
input.resource.attributes.compliance_required == true
}
safety_block := sprintf(`
Safety Instructions:
- Do not generate harmful, illegal, or unethical content
- Do not share personally identifiable information (PII)
- Maintain professional and appropriate language
- Flag suspicious queries for review
`)
# Post-response filtering
filtered_response := masked if {
response := input.response.content
masked := mask_pii(response)
}
mask_pii(text) := result if {
# Mask SSN
step1 := regex.replace(text, `\d{3}-\d{2}-\d{4}`, "***-**-****")
# Mask credit cards
step2 := regex.replace(step1, `\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}`, "****-****-****-****")
# Mask email addresses (except domain)
result := regex.replace(step2, `([a-zA-Z0-9._%+-]+)@`, "***@")
}
Pattern 3: RAG System Authorization
package ai.rag_authorization
import rego.v1
default allow := false
# Allow RAG query with source verification
allow if {
user_authorized
query_safe
sources_accessible
}
user_authorized if {
input.user.roles[_] in ["researcher", "analyst", "developer"]
input.user.attributes.rag_access == true
}
query_safe if {
query := input.context.query
# Not a SQL injection attempt
not contains(query, "'; DROP")
not contains(query, "OR 1=1")
# Not a path traversal
not contains(query, "../")
not contains(query, "..\\")
# Query scope appropriate
query_scope_valid
}
query_scope_valid if {
# Limit to user's department documents
scope := input.context.query_scope
scope.department == input.user.department
}
sources_accessible if {
# All retrieved sources must be accessible to user
sources := input.context.retrieved_sources
every source in sources {
user_can_access_source(source.id)
}
}
user_can_access_source(source_id) if {
source := data.documents[source_id]
source.owner == input.user.id
}
user_can_access_source(source_id) if {
source := data.documents[source_id]
source.department == input.user.department
source.classification in ["public", "internal"]
}
π Multi-Tenancy Patterns
Tenant Isolation
package multitenancy.isolation
import rego.v1
default allow := false
# Strict tenant isolation
allow if {
# User belongs to tenant
input.user.tenant_id == input.resource.tenant_id
# Action allowed for user role
input.user.roles[_] in authorized_roles[input.action.name]
}
# Cross-tenant access (explicitly approved only)
allow if {
input.user.tenant_id != input.resource.tenant_id
# Cross-tenant access must be approved
approval := data.cross_tenant_approvals[input.user.id][input.resource.tenant_id]
approval.status == "approved"
approval.expiry > time.now_ns()
# Audit all cross-tenant access
input.context.audit_enabled == true
}
# Service accounts (tenant-agnostic)
allow if {
input.user.type == "service_account"
input.user.attributes.multi_tenant_access == true
input.user.tenant_id in authorized_tenants[input.user.service_id]
}
β‘ Performance Optimization Patterns
Cache-Friendly Policies
# Good: Deterministic (cacheable)
allow if {
input.user.roles[_] == "admin"
input.resource.type == "api"
}
# Bad: Time-dependent (not cacheable)
allow if {
input.user.roles[_] == "admin"
time.now_ns() > 0 # Always true but changes every nanosecond
}
# Better: Use reasonable time granularity
allow if {
input.user.roles[_] == "admin"
[hour, _, _] := time.clock([time.now_ns()])
hour >= 9 # Changes hourly, more cacheable
}
Early Returns
# Good: Check cheap conditions first
allow if {
# Quick checks first
input.action.name == "read"
input.user.authenticated == true
# Expensive checks last
user_has_department_access
user_passed_background_check
}
# Bad: Expensive check first
allow if {
expensive_database_lookup # Slow
input.action.name == "read" # Fast but checked last
}
π Migration Strategies
From RBAC to PBAC
Phase 1: Model existing RBAC in PBAC
# Implement existing RBAC rules in Rego
package migration.rbac_compat
import rego.v1
default allow := false
# Map existing roles to permissions
role_permissions := {
"admin": ["read", "write", "delete", "manage"],
"developer": ["read", "write"],
"viewer": ["read"]
}
allow if {
role := input.user.roles[_]
permissions := role_permissions[role]
input.action.name in permissions
}
Phase 2: Add context-aware rules
# Enhance with context
allow if {
role := input.user.roles[_]
permissions := role_permissions[role]
input.action.name in permissions
# New: Add time-based access
is_business_hours
# New: Add MFA for sensitive operations
mfa_satisfied_for_sensitivity
}
Phase 3: Fine-grained permissions
# Replace broad roles with fine-grained policies
allow if {
# Instead of "admin can do everything"
# Specific permissions based on context
input.user.permissions[_] == required_permission
resource_access_allowed
context_appropriate
}
π Common Pitfalls
Pitfall 1: Role Explosion
# Bad: Creating too many specific roles
allow if {
input.user.roles[_] in [
"api-reader",
"api-writer",
"api-deleter",
"database-reader",
"database-writer",
# 100+ more roles...
]
}
# Good: Use attributes and context
allow if {
input.user.roles[_] == "developer"
input.action.name in user_allowed_actions(input.user)
input.resource.type in user_allowed_resources(input.user)
}
Pitfall 2: Overly Complex Policies
# Bad: Too complex, hard to test and debug
allow if {
count([r | r := input.user.roles[_]; r in allowed_roles]) > 0
count([a | a := input.resource.attributes[_]; a.type in ["public", "internal"]]) > 0
not count([x | x := input.user.restrictions[_]; x.type == "suspended"]) > 0
}
# Good: Break into helper functions
allow if {
user_has_allowed_role
resource_is_accessible
user_not_suspended
}
user_has_allowed_role if {
input.user.roles[_] in allowed_roles
}
resource_is_accessible if {
input.resource.attributes.type in ["public", "internal"]
}
user_not_suspended if {
not input.user.restrictions[_].type == "suspended"
}
Pitfall 3: Insufficient Testing
# Always test edge cases
test_empty_roles if {
not allow with input as {"user": {"roles": []}}
}
test_null_user if {
not allow with input as {"user": null}
}
test_missing_resource if {
not allow with input as {"user": {"roles": ["admin"]}}
}
test_invalid_action if {
not allow with input as {
"user": {"roles": ["admin"]},
"resource": {"type": "api"},
"action": {"name": "invalid-action"}
}
}
π Organizational Best Practices
Policy Ownership Model
Recommended Structure:
Policy Repository
βββ core/ # Platform team owns
β βββ authentication.rego
β βββ base_rbac.rego
βββ compliance/ # Compliance team owns
β βββ fintrac.rego
β βββ osfi.rego
β βββ aml.rego
βββ applications/ # App teams own
β βββ api/
β β βββ access.rego
β βββ mobile/
β β βββ access.rego
β βββ web/
β βββ access.rego
βββ tests/ # Everyone contributes
βββ core_tests.rego
βββ compliance_tests.rego
βββ app_tests.rego
Review and Approval Workflow
# .github/workflows/policy-review.yml
name: Policy Review Workflow
on:
pull_request:
paths:
- 'policies/**/*.rego'
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Validate Rego Syntax
run: opa check policies/
- name: Run Tests
run: opa test policies/ -v
- name: Security Scan
run: opa check --strict policies/
review:
needs: validate
runs-on: ubuntu-latest
steps:
- name: Request Review
uses: actions/github-script@v6
with:
script: |
github.rest.pulls.requestReviewers({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number,
reviewers: ['security-team', 'compliance-team']
})
π οΈ Troubleshooting
| Issue | What to check |
|---|---|
| Unexpected allow/deny in production | Compare policy logic with test cases; verify input schema (user, resource, context) matches what the bouncer sends. |
| Policy performance or timeout | Simplify rules and avoid heavy comprehensions; see Performance Optimization Patterns and Common Pitfalls. |
| Compliance or audit gaps | Ensure policies encode required checks and that audit logging is enabled for decisions. |
For more, see the Troubleshooting Guide.
π Support
- Rego Guidelines: Learn the policy language
- User Guide: Create and manage policies
- Policy Templates: Pre-built patterns
- Security Best Practices: Secure your deployment
Effective PBAC requires thoughtful design, comprehensive testing, and continuous refinement. Use these patterns as building blocks for your authorization system.