Skip to main content

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.

Attribute Changes Do Not Trigger Immediately

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:
  1. HRIS updates (immediate)
  2. IdP (Identity Provider) syncs from HRIS (15-60 minutes)
  3. Group rules re-evaluate (1-24 hours)
  4. 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