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.
Most companies approach access management backward.
They start with systems—Okta, Google, AWS, Salesforce—and ask: “How do we manage access in each system?”
Per-system admin consoles proliferate
Each has different UIs, different mental models, different ways of expressing the same underlying concepts. The team managing Google groups thinks about access differently than the team managing AWS IAM roles.
Per-system group rules accumulate
Okta rules, Google rules, AWS rules—each maintained separately, each drifting from the others. What should be a single source of truth becomes a dozen competing approximations.
Per-system provisioning workflows create ticket queues
IT receives requests for each system independently. No unified view exists of what someone should have access to—only fragmented records of what they’ve requested.
The result: access management (lots of tools managing access) without access governance (clear policies defining what access should exist).
The alternative is policy-first: define what access people should have based on their job function, then let systems enforce those policies automatically.
This is how modern organizations are building access control.
The Traditional Approach: System-First
A Sales Engineer joins the company. IT receives a ticket: “Provision access for new Sales Engineer.”
IT consults a checklist—or worse, relies on memory. They manually add the user to groups in Okta, Google, Salesforce, Slack, GitLab. Something gets missed or granted incorrectly.
More tickets arrive: “I don’t have access to X” and “Why do I have access to Y?”
This is reactive provisioning. IT responds to requests. No single source of truth defines what a Sales Engineer should have.
The Policy-First Approach
Define the policy once. “Sales Engineers should have access to…”
policy:
role: "Sales Engineer"
inherits:
- "Base Employee" # Everyone gets this
- "Sales Team Member" # Sales-specific access
- "Engineering Tools Reader" # Cross-functional needs
grants:
# CRM access
- salesforce:profile:sales-engineer
# Collaboration tools
- slack:channel:#sales-team
- slack:channel:#customer-success
- slack:channel:#engineering-general (read-only)
# Code repositories
- gitlab:group:customer-solutions (reporter)
# Documentation
- notion:space:sales-playbooks
- notion:space:technical-docs (read-only)
Then provisioning becomes automatic. Someone joins as a Sales Engineer. HRIS updates their attributes: job_title: "Sales Engineer". The policy engine sees the attribute change, calculates required access. The orchestration layer provisions access across all systems automatically.
The user has correct access on day one. No tickets. No waiting.
This is proactive provisioning. Access is defined by policy. IT maintains the policy, not individual access grants.
Policy-based provisioning provides continuous evidence of least-privilege enforcement. Instead of point-in-time attestation spreadsheets, auditors receive system-generated proof that access matches policy—satisfying SOC 2 CC6.1 (logical access controls) and ISO 27001 A.9.2.1 (user registration and de-registration).
The Core Insight
In system-first access management, organizations manage:
- 10 systems
- 50 groups per system
- 500 users
That’s 500 users x 50 groups x 10 systems = 250,000 potential relationships.
In policy-first access management, organizations manage:
- 20 job roles
- 10 policies per role
- Policies automatically apply to users
That’s 20 roles x 10 policies = 200 objects.
Three orders of magnitude less complexity.
Building Policy-First Access: The Layers
Layer 1: Identity Data (Source of Truth)
The HRIS is the source of truth for user attributes:
{
"user_id": "sarah.chen",
"email": "sarah@company.com",
"first_name": "Sarah",
"last_name": "Chen",
"job_title": "Sales Engineer",
"department": "Sales",
"team": "Enterprise",
"employment_type": "FTE",
"start_date": "2024-11-24",
"manager": "john.doe@company.com",
"location": "San Francisco",
"cost_center": "CC-1234"
}
This data flows automatically from HRIS to the policy engine. When attributes change—promotion, transfer, termination—the policy engine re-evaluates access.
Layer 2: Policy Definitions
Policies define what access each role should have:
# Base policy: Everyone gets this
policy:
name: "Base Employee Access"
triggers:
- employment_status: "Active"
grants:
- google:workspace-account
- slack:workspace-member
- okta:base-employee-group
- notion:space:company-handbook
---
# Department policy: All sales roles
policy:
name: "Sales Team Access"
triggers:
- department: "Sales"
grants:
- salesforce:user-license
- slack:channel:#sales-team
- google:group:sales@company.com
- google:drive:sales-shared-drive
---
# Role-specific policy
policy:
name: "Sales Engineer Access"
triggers:
- job_title: "Sales Engineer"
inherits:
- "Base Employee Access"
- "Sales Team Access"
grants:
- gitlab:group:customer-solutions (reporter)
- slack:channel:#engineering-general (guest)
- notion:space:technical-docs (read)
Policies are hierarchical. “Sales Engineer” inherits from “Sales Team” and “Base Employee”. No duplication.
Layer 3: Policy Engine (Evaluation)
The policy engine continuously evaluates: Given this user’s attributes, what access should they have?
def calculate_expected_access(user):
access = []
# Find all policies that apply to this user
applicable_policies = []
for policy in all_policies:
if policy.triggers_match(user.attributes):
applicable_policies.append(policy)
# Resolve inheritance
for policy in applicable_policies:
access.extend(policy.grants)
for inherited_policy in policy.inherits:
access.extend(inherited_policy.grants)
# Remove duplicates, resolve conflicts
return deduplicate_and_resolve(access)
This runs continuously. Not just when someone joins. When someone’s attributes change—promotion, transfer, team change—access automatically re-calculates.
Layer 4: Orchestration (Execution)
The orchestration layer translates policy into system-specific actions:
def provision_access(user, expected_access):
current_access = fetch_current_access(user)
delta = calculate_delta(current_access, expected_access)
# What needs to be added?
for grant in delta.additions:
adapter = get_adapter(grant.system)
adapter.add_access(user, grant)
# What needs to be removed?
for revocation in delta.removals:
adapter = get_adapter(revocation.system)
adapter.remove_access(user, revocation)
# Log everything
log_changes(user, delta)
This layer handles all the system-specific complexity: API calls, rate limits, retries, error handling.
Layer 5: Continuous Reconciliation (Drift Detection)
Compare expected state (policy) to actual state (systems):
def detect_drift():
for user in all_users:
expected = calculate_expected_access(user)
actual = fetch_current_access(user)
drift = compare(expected, actual)
if drift:
# User has access they shouldn't
report_excess_access(user, drift.excess)
# User is missing access they should have
report_missing_access(user, drift.missing)
# Optionally: auto-remediate
if auto_remediate_enabled:
remediate_drift(user, drift)
Drift detection runs daily. Manual changes—someone granted access directly in a system—are flagged and can be auto-corrected.
Continuous drift detection demonstrates that access controls operate as designed between formal audits. This directly addresses SOC 2 requirements for monitoring and SOX IT general controls for access management—transforming compliance from periodic attestation to ongoing assurance.
Policy-First in Practice: Sarah’s Lifecycle
Day 1: Onboarding
HRIS event:
{
"event": "employee.hired",
"user_id": "sarah.chen",
"job_title": "Sales Engineer",
"department": "Sales",
"start_date": "2024-11-24"
}
Policy engine evaluates:
Policies that apply to Sarah:
- Base Employee Access
- Sales Team Access
- Sales Engineer Access
Required access (combined):
- Google Workspace account
- Okta base-employee-group
- Salesforce user (Sales Engineer profile)
- Slack workspace + #sales-team + #engineering-general
- GitLab customer-solutions group
- Notion sales + technical spaces
Orchestration provisions:
09:00:00 | Create Google Workspace account
09:00:05 | Create Okta account
09:00:10 | Add to Okta groups
09:00:15 | Okta → Salesforce provisioning
09:00:20 | Invite to Slack channels
09:00:25 | Add to GitLab group
09:00:30 | Grant Notion space access
09:00:30 | Provisioning complete
Sarah logs in on day one. Everything works.
HRIS event:
{
"event": "employee.updated",
"user_id": "sarah.chen",
"changes": {
"job_title": {
"old": "Sales Engineer",
"new": "Senior Sales Engineer"
},
"effective_date": "2025-05-24"
}
}
Policy engine evaluates:
Policies that applied (Sales Engineer):
- Base Employee Access
- Sales Team Access
- Sales Engineer Access
Policies that apply now (Senior Sales Engineer):
- Base Employee Access
- Sales Team Access
- Senior Sales Engineer Access
Delta:
- Add: Enhanced Salesforce permissions
- Add: Slack #sales-leadership channel
- Add: Mentor access to Sales Engineer resources
- Keep: Everything else (still in Sales)
No ticket. No manual intervention. Policy updated, access followed.
Month 18: Transfer
HRIS event:
{
"event": "employee.updated",
"user_id": "sarah.chen",
"changes": {
"job_title": {
"old": "Senior Sales Engineer",
"new": "Solutions Architect"
},
"department": {
"old": "Sales",
"new": "Engineering"
},
"effective_date": "2026-05-24"
}
}
Policy engine evaluates:
Policies that applied (Senior Sales Engineer in Sales):
- Base Employee Access
- Sales Team Access
- Senior Sales Engineer Access
Policies that apply now (Solutions Architect in Engineering):
- Base Employee Access
- Engineering Team Access
- Solutions Architect Access
Delta:
- Remove: Salesforce (no longer in Sales)
- Remove: Sales-specific Slack channels
- Add: Engineering GitHub organization
- Add: Engineering deployment tools
- Add: Architecture review access
- Keep (with modification): GitLab (upgrade from Reporter to Developer)
Orchestration provisions with graceful deprecation:
11:00:00 | Mark Salesforce for removal (30-day grace period)
11:00:05 | Remove #sales-team channel (immediate)
11:00:05 | Keep #sales-leadership (30-day grace for handoff)
11:00:10 | Add GitHub org access
11:00:15 | Add deployment tool access
11:00:20 | Upgrade GitLab permissions
11:00:20 | Update complete
Sarah transitions smoothly. She keeps sales access for 30 days to help with handoff, gains engineering access immediately, and submits no tickets for either.
Termination
HRIS event:
{
"event": "employee.terminated",
"user_id": "sarah.chen",
"termination_date": "2027-03-15",
"last_day": "2027-03-15"
}
Policy engine evaluates:
Employment status: Inactive
Policies that apply: NONE (all require active employment)
Delta:
- Remove: Everything
Orchestration deprovisions:
17:00:00 | Disable Okta account
17:00:05 | Convert Google account to suspended
17:00:10 | Remove from all Slack channels
17:00:15 | Revoke GitLab access
17:00:20 | Remove Notion access
17:00:20 | Deprovisioning complete
Last day at 5pm, all access is revoked. No lingering accounts.
Automated deprovisioning tied to HR termination events demonstrates control over the joiner-mover-leaver lifecycle. This directly addresses SOX IT general controls for access management and eliminates the “orphaned accounts” finding common in SOC 2 audits.
Policy-First Benefits
Single source of truth
“What access should a Sales Engineer have?” Look at the policy, not 10 system admin consoles.
Consistent provisioning
Every Sales Engineer receives exactly the same access. No drift between “the person hired in Q1” and “the person hired in Q4.”
Automatic lifecycle management
Promotions, transfers, and terminations update attributes. Policies re-evaluate. Access changes automatically.
Audit readiness
“Why does Sarah have Salesforce access?” “Because she’s a Sales Engineer, and the Sales Engineer policy grants Salesforce.” Policy documentation is compliance evidence.
Drift detection
Someone manually grants Sarah access outside policy? The system flags it:
Drift detected:
- User: sarah.chen
- Access: gitlab:group:finance (Developer)
- Policy: Not in any applicable policy
- Action: Remove or add exception to policy
Reduced IT overhead. IT maintains policies (200 objects), not individual access grants (250,000 relationships).
Day-one access. New hires never wait for tickets. Policy defines access. System provisions automatically.
Implementing Policy-First: The Migration Path
Organizations don’t flip a switch and go from system-first to policy-first. Here’s the migration path:
Phase 1: Document Current State (Weeks 1-4)
Task: Export who has access to what from all systems.
Output:
User: sarah.chen
Access:
- okta:group:sales-team
- okta:group:engineering-team
- google:group:sales@company.com
- salesforce:profile:sales-engineer
- slack:channel:#sales-team
- gitlab:group:customer-solutions (reporter)
Do this for all users. The result is a snapshot of current state.
Task: Group users by role. Identify common access patterns.
Output:
Sales Engineers (23 people):
Common access (100% have):
- salesforce:profile:sales-engineer
- slack:channel:#sales-team
- gitlab:group:customer-solutions
Common access (90%+ have):
- slack:channel:#customer-success
- notion:space:sales-playbooks
Outliers (< 50% have):
- gitlab:group:platform-engineering (2 people - investigate)
- google:group:finance-reports (1 person - investigate)
The 100% access becomes baseline policy. The 90%+ access likely belongs in the policy. The outliers are exceptions to investigate.
Phase 3: Define Policies (Weeks 9-12)
Task: Convert patterns to policy definitions.
Output: YAML files or policy objects defining expected access per role.
policy:
name: "Sales Engineer Access"
version: "1.0"
effective_date: "2025-01-01"
triggers:
- job_title: "Sales Engineer"
grants:
- salesforce:profile:sales-engineer
- slack:channel:#sales-team
- gitlab:group:customer-solutions (reporter)
- slack:channel:#customer-success
- notion:space:sales-playbooks
Phase 4: Validate Policies (Weeks 13-16)
Task: Compare policy-defined access to current access. Identify drift.
Output:
Drift Report:
Users with excess access (have access not in policy):
- sarah.chen: gitlab:group:platform-engineering
→ Action: Remove or document as exception
Users with missing access (policy says they should have, but don't):
- john.doe: slack:channel:#customer-success
→ Action: Grant access
Policy coverage: 94% of access is defined by policy
Remediate drift. Update policies where needed. Document exceptions.
Phase 5: Pilot with One Ruleset (Weeks 17-20)
Task: Enable policy-based provisioning for one Ruleset (e.g., “Sales Engineer”).
Process:
- New Sales Engineer joins
- Policy engine calculates required access
- IT reviews and approves before execution
- Orchestration provisions access
- Validate that user has correct access
Run this in “review before execution” mode. IT sees what would happen and approves. Builds confidence.
Phase 6: Expand to All Rulesets (Weeks 21-30)
Task: Define policies for all roles. Enable policy-based provisioning broadly.
Process:
- 1-2 Rulesets per week
- Document policies
- Validate against current access
- Enable provisioning
- Monitor for issues
Phase 7: Enable Continuous Compliance (Week 31+)
Task: Turn on daily drift detection and auto-remediation (optional).
Process:
- Daily: Compare policy to reality
- Flag drift
- Optionally: Auto-remediate (remove excess access, grant missing access)
- Log everything
The organization is now fully policy-first. Access is governed by policy. Systems are execution layers.
Common Objections
“This sounds like a lot of upfront work.”
It is. Documenting current state and extracting patterns takes 8-12 weeks.
But consider the alternative: managing 250,000 relationships forever. The upfront investment pays off in reduced ongoing overhead.
“What about exceptions?”
Exceptions are first-class citizens in the policy model. They’re tracked explicitly:
exception:
user: "sarah.chen"
grant: "gitlab:group:platform-engineering"
justification: "Temporary access for Q4 migration project"
approved_by: "cto@company.com"
expires: "2025-01-31"
Exceptions don’t pollute policies. They’re separate.
“What if HRIS data is wrong?”
Then system-first access is also wrong. The problem isn’t policy-first. The problem is data quality.
Policy-first makes data quality issues visible—because the system explicitly depends on attributes. System-first hides them—because manual grants mask underlying data problems.
“Our auditors require quarterly access reviews.”
Policy-first satisfies the same compliance requirement—demonstrating access is appropriate—with stronger evidence:
- System-first: “We reviewed access quarterly, managers approved.”
- Policy-first: “We defined policies, validate daily, exceptions are logged.”
Auditors prefer policy-first.
The Bottom Line
System-first access management is backward. Organizations manage symptoms (access in 10 systems) instead of causes (what access people should have).
Policy-first flips this:
- Define what access roles should have (policy)
- Let systems enforce policies automatically (orchestration)
- Detect when reality drifts from policy (continuous compliance)
This is how modern access governance works. It takes upfront effort to migrate. But once there, organizations manage 200 policies instead of 250,000 relationships.
Three orders of magnitude less complexity.
Start the migration. Document current state. Extract patterns. Define policies. Pilot with one Ruleset. Expand.
Within 6 months, policy-first access is achievable. And there’s no going back.
Next up: The end-state vision—what fully automated onboarding looks like when policy-first access is combined with infrastructure as code.
Ready to implement policy-first access? Explore Provisionr’s policy framework