Documentation Index
Fetch the complete documentation index at: https://docs.provisionr.io/llms.txt
Use this file to discover all available pages before exploring further.
Dynamic groups sound elegant in principle. Instead of manually adding users, define rules. Users who match the rules are automatically included.
IF department = "Engineering" THEN add to Engineering-All group
Simple. Automatic. Clean.
That simplicity lasts about three months.
The Evolution of a Rule
Every dynamic group rule follows a predictable lifecycle. What begins as a single condition transforms into something unrecognizable.
Day 1: The Simple Rule
# Engineering-All Group
rule: department = "Engineering"
Clean. Everyone in Engineering is automatically added.
Users matched: 150
The rule works perfectly.
Month 3: The First Exception
An Engineering Manager sends a request: “Sarah is technically in Engineering, but she’s on loan to Product for 6 months. She shouldn’t have Engineering-level access right now.”
# Engineering-All Group
rule:
department = "Engineering"
AND employee_id != "emp_sarah_123"
One hardcoded exception. Not ideal, but manageable.
Users matched: 149
Month 6: Location Matters
Security raises a concern: “Our India engineering team shouldn’t have access to the US customer data environment. Can you exclude them from certain groups?”
# Engineering-All Group (US Customer Access)
rule:
department = "Engineering"
AND employee_id != "emp_sarah_123"
AND location NOT IN ["Bangalore", "Hyderabad", "Mumbai"]
Users matched: 98
The rule now includes location logic. The group name no longer matches what it does.
Month 9: Contractors Are Different
Legal intervenes: “Contractors shouldn’t have access to production systems. Can you update the Engineering group to exclude them?”
# Engineering-All Group
rule:
department = "Engineering"
AND employee_id != "emp_sarah_123"
AND location NOT IN ["Bangalore", "Hyderabad", "Mumbai"]
AND employment_type = "FTE"
Users matched: 72
Half the original members have been excluded by accumulating conditions.
Month 12: The Reorganization
The company reorganizes. “Engineering” splits into “Product Engineering” and “Platform Engineering.” HRIS (Human Resource Information System) updates slowly. Some employees are still labeled “Engineering,” some are “Product Engineering,” some are “Eng” (data entry error).
# Engineering-All Group
rule:
department IN ["Engineering", "Product Engineering", "Platform Engineering", "Eng", "Product Eng"]
AND employee_id != "emp_sarah_123"
AND location NOT IN ["Bangalore", "Hyderabad", "Mumbai"]
AND employment_type = "FTE"
Users matched: 68
The rule now includes data quality workarounds.
Month 18: Seniority Requirements
Security adds another requirement: “Junior engineers (L1-L2) shouldn’t be in the production access group. Can you add a seniority check?”
# Engineering-All Group (Production Access)
rule:
department IN ["Engineering", "Product Engineering", "Platform Engineering", "Eng", "Product Eng"]
AND employee_id != "emp_sarah_123"
AND employee_id != "emp_mike_456" # Added after incident
AND employee_id != "emp_alex_789" # On PIP
AND location NOT IN ["Bangalore", "Hyderabad", "Mumbai"]
AND employment_type = "FTE"
AND job_level IN ["L3", "L4", "L5", "L6", "Senior", "Staff", "Principal"]
Users matched: 34
Eight conditions with hardcoded exceptions.
Year 3: The Unmaintainable Rule
After 3 years of accumulated requirements:
# Engineering-All Group (Production Access)
rule:
(
department IN ["Engineering", "Product Engineering", "Platform Engineering",
"Eng", "Product Eng", "Core Engineering", "Engineering - Platform"]
OR manager_email IN ["alice@company.com", "bob@company.com"] # Embedded teams
)
AND employee_id NOT IN [
"emp_sarah_123", # Product loan
"emp_mike_456", # Incident followup
"emp_alex_789", # PIP
"emp_janet_012", # Leave of absence
"emp_tom_345" # Unknown - don't remove per ticket #4521
]
AND location NOT IN ["Bangalore", "Hyderabad", "Mumbai", "Delhi", "Chennai"]
AND employment_type IN ["FTE", "FTE-Convert"] # Added contractor conversion status
AND job_level IN ["L3", "L4", "L5", "L6", "Senior", "Staff", "Principal", "Senior Engineer", "Staff Engineer"]
AND completed_training("security-101") = true
AND completed_training("prod-access-2024") = true
AND NOT (department = "Engineering" AND team = "Documentation") # Writers don't need prod
AND hire_date < (TODAY - 90 days) # 90-day waiting period
Users matched: 23
Can anyone determine what this rule does without studying it for 10 minutes? Can anyone predict whether Sarah Chen (new Senior Engineer, Platform team, Bay Area) should be included without running a test? Can anyone explain why John (Staff Engineer, Core Engineering, 2 years tenure) is NOT included without debugging every condition?
This is the hidden complexity of group membership rules.
Five Forces That Create Complexity
Edge Cases Accumulate
Every organization has exceptions: employees on loan to other teams, contractors who need employee-level access, employees who had security incidents, teams structured differently from the norm.
Each exception adds a condition. Conditions accumulate. Rules grow.
After 3 years, a typical rule carries 8-12 conditions.
Organizational Changes Break Rules
When companies reorganize, department names change, team structures change, reporting relationships change, job titles change.
Rules that worked yesterday break today. Two options exist: update the rule (adding complexity) or create a new rule (more rules to maintain).
Most teams do both, creating a patchwork of overlapping rules.
Attribute Inconsistency
HRIS data is rarely clean. “Senior Software Engineer” appears alongside “Sr. Software Engineer,” “Software Engineer III,” and “SWE Senior.” “Engineering” coexists with “Eng” and “Product Engineering.” “San Francisco” shares a database with “SF,” “Bay Area,” and “US-West.”
Rules must handle all variations:
job_title IN [
"Senior Software Engineer",
"Sr. Software Engineer",
"Sr Software Engineer",
"Software Engineer III",
"Software Engineer Senior",
"SWE Senior",
"Senior SWE"
]
This is no longer a rule. It is a data quality workaround.
Dependencies Between Rules
Rules depend on other rules. “Production Access” requires “Engineering Base Access.” “Engineering Base Access” requires “All Employees.” “All Employees” has its own conditions.
All Employees (Rule A)
--> Engineering Base (Rule B) - depends on Rule A
--> Production Access (Rule C) - depends on Rule B
--> Database Admin (Rule D) - depends on Rule C
Changing Rule A can affect 50 downstream rules.
Rule Explosion for Combinations
Access requirements often span multiple dimensions: 5 departments by 3 seniority levels by 4 locations equals 60 combinations.
The options are all problematic: 60 separate rules (unmanageable), complex conditional logic (unreadable), or nested rules with inheritance (hard to debug).
No good answer exists.
Real-World Rule Complexity
Production Database Access
# Who can access production database?
rule:
# Base requirement: Engineering department
department IN ["Engineering", "Platform", "Infrastructure", "SRE", "Database"]
# Seniority requirement: L4+ or specific roles
AND (
job_level IN ["L4", "L5", "L6", "Staff", "Principal"]
OR job_title CONTAINS "DBA"
OR job_title CONTAINS "Database"
)
# Employment type
AND employment_type = "FTE"
# Tenure requirement
AND hire_date < (TODAY - 90 days)
# Training requirement
AND completed_training("prod-db-access")
AND completed_training("data-handling")
# Not on restricted list
AND employee_id NOT IN [restricted_employees]
# Approval requirement
AND has_approval("prod-db-access", approved_by=["cto", "vp-eng", "director-db"])
Nine conditions. No one can evaluate this in their head.
Complex access rules create audit challenges. Auditors must understand the rule logic to verify that access grants are appropriate. When rules contain 9+ conditions with nested OR clauses, demonstrating that access follows policy becomes difficult. Simpler, declarative policies produce cleaner audit evidence.
Customer PII Access
# Who can access customer PII?
rule:
# Department restriction
(
department IN ["Customer Success", "Support", "Legal", "Compliance"]
OR (department = "Engineering" AND team = "Customer Data")
)
# Background check
AND background_check_status = "Cleared"
# Training
AND completed_training("pii-handling")
AND completed_training("gdpr-basics")
AND completed_training("ccpa-basics")
# Geographic restriction (GDPR)
AND location IN ["US-*", "UK", "Ireland", "Germany"] # Countries with adequacy
# Employment type
AND employment_type = "FTE"
# Time-based: Only during business hours?
AND (
is_on_call = true
OR current_time BETWEEN "08:00" AND "18:00" user_timezone
)
# Manager approval
AND has_active_approval("pii-access")
Ten-plus conditions including time-based logic.
Finance System Access
# Finance system access for non-Finance employees
rule:
# Not in Finance (Finance has separate rule)
department NOT IN ["Finance", "Accounting", "FP&A"]
# Has business need
AND (
# Executives
job_level IN ["VP", "SVP", "C-Level"]
# Or department heads
OR job_title CONTAINS "Director"
# Or specific approved roles
OR employee_id IN [approved_finance_viewers]
)
# Training
AND completed_training("financial-data-handling")
# SOX compliance: Manager approval within 90 days
AND has_approval("finance-view", max_age_days=90)
# Audit: Only read access, no export
AND access_level = "read-only"
Complex conditional logic with audit requirements layered on top.
SOX compliance requires demonstrable separation of duties and time-bound access approvals. Rules that embed approval requirements with max_age constraints satisfy control objectives, but the rule complexity makes it difficult to prove that all access grants actually meet these criteria. Policy-based systems that enforce these constraints automatically generate cleaner audit trails.
Five Problems with Complex Rules
No Way to Test Rules Before Production
When IT teams update a rule, no mechanism exists to verify correctness. Which users will be added? Removed? Will something break?
Most systems lack rule testing. Teams deploy and hope.
Impact: Rule changes cause incidents when users lose access unexpectedly.
No Diff View for Changes
What changed between version 1 and version 2 of a rule? Without diffing, teams cannot review changes, cannot understand why rules were modified, and cannot roll back to a known-good state.
Impact: Changes occur without proper review.
Rules Conflict with Each Other
Rule A: “All Engineering gets GitHub access”
Rule B: “Contractors don’t get GitHub access”
Rule C: “Platform team gets GitHub admin”
What happens to a contractor on the Platform team?
Rule priority and evaluation order become critical. Which rule wins? Is it additive or override? Who knows?
Impact: Access decisions become unpredictable.
Most systems evaluate rules periodically. Okta group rules: 1-24 hours. Azure AD dynamic groups: 15-60 minutes. Google Workspace: 24 hours.
When someone’s department changes:
- HRIS updates (immediate)
- IdP (Identity Provider) syncs from HRIS (15-60 minutes)
- Group rules re-evaluate (1-24 hours)
- Downstream systems sync (additional time)
Total: 2-48 hours for attribute changes to affect access.
Impact: Users have incorrect access during transition periods.
No Visibility Into Why
When users ask “Why am I not in the Production Access group?” answering requires finding the rule definition, understanding all conditions, retrieving the user’s attributes, evaluating each condition manually, and finding which condition failed.
This takes 20+ minutes per investigation.
Impact: IT teams spend hours debugging access issues.
Solutions (Ranked by Effectiveness)
Solution 1: Rule Linting and Validation (Low Effort)
Before deploying a rule, validate it:
def validate_rule(rule):
errors = []
# Check for unknown attributes
for attr in rule.referenced_attributes:
if attr not in known_attributes:
errors.append(f"Unknown attribute: {attr}")
# Check for syntax errors
try:
rule.compile()
except SyntaxError as e:
errors.append(f"Syntax error: {e}")
# Check for conflicting conditions
if rule.has_contradictions():
errors.append("Rule has contradictory conditions")
# Check for performance issues
if rule.complexity_score > MAX_COMPLEXITY:
errors.append("Rule is too complex")
return errors
This catches obvious mistakes before they reach production.
Solution 2: Rule Preview / Dry-Run (Medium Effort)
Before deploying, show the impact:
Current rule matches: 34 users
New rule matches: 28 users
Users who will LOSE access:
- Sarah Chen (job_level changed from L3 to L2)
- John Smith (missing required training)
- Alex Johnson (location changed to restricted region)
- 3 others...
Users who will GAIN access:
- Mike Brown (completed required training)
Do you want to proceed? [y/N]
This enables teams to understand impact before committing.
Solution 3: Attribute-Based Policies Instead of Rules (High Effort, Best Results)
Instead of complex rules, define access at the policy level:
# Policy: Production Database Access
policy:
name: Production Database Access
description: Access to production databases for qualified engineers
requirements:
- type: department
values: ["Engineering", "Platform", "SRE"]
- type: seniority
minimum: L4
- type: employment_type
value: FTE
- type: training
required: ["prod-db-access", "data-handling"]
- type: tenure
minimum_days: 90
grants:
- system: postgresql-prod
role: read-write
- system: mysql-prod
role: read-only
Policies are declarative (what access should exist) rather than imperative (how to calculate it). The system evaluates policies against user attributes and calculates access.
Benefits: policies are readable, changes are clear, evaluation is consistent, testing is straightforward.
Declarative policies directly map to compliance frameworks. SOC 2 CC6.1 (logical access controls) and ISO 27001 A.9.2.1 (user access provisioning) require documented access policies. A Ruleset that explicitly states requirements---department, seniority, training, tenure---provides self-documenting control evidence. Complex imperative rules do not.
Solution 4: Continuous Validation and Alerting (Medium Effort)
Rather than evaluating rules once, continuously validate:
async def continuous_validation():
while True:
# Get all users and their current access
users = await get_all_users()
for user in users:
# Calculate expected access
expected = policy_engine.calculate_access(user)
# Get actual access
actual = await get_actual_access(user)
# Compare
drift = compare_access(expected, actual)
if drift:
alert_drift(user, drift)
await sleep(timedelta(hours=1))
This catches when reality diverges from rules.
Solution 5: Rule Dependency Graphing (High Effort)
Visualize how rules depend on each other:
+--------------+
| All Employees|
+------+-------+
|
+---------------+---------------+
v v v
+---------------+ +-----------+ +---------------+
| Engineering | | Sales | | Finance |
| Base Access | | Access | | Access |
+-------+-------+ +-----------+ +---------------+
|
+-------+-------+
v v
+-----------+ +-----------+
| Prod Access| | Dev Access |
+-----------+ +-----------+
Understanding impact of changes before making them prevents cascading failures.
The Path Forward
Group membership rules do not scale.
Rules start simple:
IF department = "Engineering" THEN add to group
Rules become unmaintainable:
(department IN [10 values] OR manager IN [list])
AND NOT employee_id IN [exception list]
AND location NOT IN [restricted locations]
AND employment_type IN [allowed types]
AND job_level IN [allowed levels]
AND completed_training([required courses])
AND has_approval([required approvals])
AND hire_date < (TODAY - 90 days)
At 200+ groups with complex conditions: rules are unreadable, changes are risky, testing is manual, debugging is slow, and nobody understands the full picture.
A better abstraction exists. Instead of “rules that calculate group membership,” think “Rulesets that define access requirements.”
Rulesets are declarative, testable, and auditable. Rules are imperative, complex, and fragile.
Choose Rulesets.
Learn how to build policy-based access: The Policy-First Approach