SDS-000: Authority Sod Hooks
Authority & Separation-of-Duties Hooks Implementation
Version: 1.0.0
Status: Draft
Date: 2025-12-31
Context: shared/governance
Satisfies: SDS-031, REF-012, EU AI Act Article 14
Purpose
This specification defines the runtime hooks that enforce authority boundaries and separation-of-duties (SoD) rules per SDS-031 within the GovernedSpeed™ Governance Runtime (SDS-047).
Architecture
sequenceDiagram
participant User
participant PolicyGateway
participant AuthorityHook
participant RBACStore
participant SoDValidator
participant AuditLog
User->>PolicyGateway: RequestWaiver(invariant_id, rationale)
PolicyGateway->>AuthorityHook: validate_authority(user, "waiver.request")
AuthorityHook->>RBACStore: get_user_roles(user)
RBACStore-->>AuthorityHook: [R-DEV]
AuthorityHook-->>PolicyGateway: AuthorityValid(requires_approval: true)
PolicyGateway->>PolicyGateway: create_waiver_request(status: pending)
PolicyGateway->>AuditLog: log_event(waiver.requested)
PolicyGateway-->>User: WaiverReceipt(id, status: pending)
Note over User: Approval workflow triggered
User->>PolicyGateway: ApproveWaiver(waiver_id)
PolicyGateway->>AuthorityHook: validate_authority(user, "waiver.approve")
AuthorityHook->>RBACStore: get_user_roles(user)
RBACStore-->>AuthorityHook: [R-AG]
AuthorityHook->>SoDValidator: check_sod(requester, approver)
SoDValidator-->>AuthorityHook: SoDValid(requester != approver)
AuthorityHook-->>PolicyGateway: AuthorityValid(sod_check: passed)
PolicyGateway->>PolicyGateway: update_waiver(status: approved)
PolicyGateway->>AuditLog: log_event(waiver.approved, approver)
PolicyGateway-->>User: WaiverApproved(valid_until)
Hook Types
1. PreAuthorizationHook
Triggers: Before any privileged action
Interface:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| class PreAuthorizationHook:
def validate(
self,
principal: Principal,
action: Action,
resource: Resource,
context: RequestContext
) -> AuthorizationDecision:
"""
Validates if principal has authority to perform action.
Args:
principal: User or service identity
action: Requested action (e.g., "waiver.approve")
resource: Target resource (e.g., waiver_id)
context: Request context (environment, timestamp)
Returns:
AuthorizationDecision with allow/deny + reasons
Raises:
UnauthorizedError: If principal lacks required role
"""
pass
|
RBAC Check:
1
2
3
4
5
6
7
8
9
10
11
12
| # Example: Waiver approval requires R-AG or R-SO
def validate_waiver_approval(principal, waiver_id):
required_roles = ["R-AG", "R-SO"]
user_roles = rbac_store.get_roles(principal)
if not any(role in required_roles for role in user_roles):
return AuthorizationDecision(
allowed=False,
reason=f"Requires one of: {required_roles}"
)
return AuthorizationDecision(allowed=True)
|
2. SeparationOfDutiesHook
Triggers: Before committing transactions requiring two-party control
Interface:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| class SeparationOfDutiesHook:
def validate(
self,
transaction: Transaction,
environment: Environment
) -> SoDValidation:
"""
Validates separation-of-duties constraints.
Args:
transaction: Transaction with proposer + approver
environment: deployment environment (staging, production)
Returns:
SoDValidation with pass/fail + violated_rules
Raises:
SoDViolationError: If SoD rule is violated
"""
pass
|
SoD Rules (per SDS-031 §3):
| SoD ID |
Rule |
Enforcement |
| SOD-01 |
proposer ≠ approver (production) |
CI gate + runtime |
| SOD-02 |
debt_requester ≠ debt_acceptor |
Ledger validation |
| SOD-03 |
breakglass_requester ≠ approver |
Security Officer approval |
| SOD-04 |
key_generator ≠ key_approver |
Two-party key control |
| SOD-05 |
minter ≠ semantic_owner |
Domain Steward cannot self-mint |
Implementation:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| def validate_sod(transaction, environment):
if environment != "production":
return SoDValidation(passed=True) # Relaxed in non-prod
# SOD-01: Self-approval check
if transaction.proposer == transaction.approver:
return SoDValidation(
passed=False,
violated_rule="SOD-01",
reason="Self-approval not permitted in production"
)
# SOD-03: Break-glass requires Security Officer
if transaction.type == "break-glass":
if "R-SO" not in get_roles(transaction.approver):
return SoDValidation(
passed=False,
violated_rule="SOD-03",
reason="Break-glass approval requires Security Officer"
)
return SoDValidation(passed=True)
|
3. AuditTrailHook
Triggers: After every privileged action
Interface:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| class AuditTrailHook:
def record(
self,
event: AuditEvent,
immutable: bool = True
) -> AuditReceipt:
"""
Records audit event in immutable ledger.
Args:
event: Audit event with actor, action, resource, timestamp
immutable: If True, anchor to IFL for tamper-evidence
Returns:
AuditReceipt with event_id and ifl_anchor_id
"""
pass
|
Audit Event Schema:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| {
"event_id": "ae-01932e5f-8c7a-7b6c-9d8e-0f1a2b3c4d5e",
"event_type": "waiver.approved",
"actor": {
"principal_id": "user-alice",
"roles": ["R-AG"]
},
"action": "approve_waiver",
"resource": {
"type": "waiver",
"id": "W-2025-001"
},
"context": {
"environment": "production",
"ip_address": "10.0.1.42",
"timestamp": "2025-12-31T12:34:56Z"
},
"decision": {
"allowed": true,
"sod_check": "passed"
},
"ifl_anchor_id": "tx-2025123112345678"
}
|
Integration with SDS-047
Waiver Workflow
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| # Step 1: Request Waiver (R-DEV can request)
@requires_role("R-DEV", "R-AG")
def request_waiver(invariant_id, rationale, valid_until):
waiver = Waiver(
id=generate_waiver_id(),
invariant_id=invariant_id,
requested_by=current_user(),
rationale=rationale,
valid_until=valid_until,
status="pending"
)
waiver_repo.save(waiver)
audit_hook.record(AuditEvent("waiver.requested", waiver))
return WaiverReceipt(waiver.id, status="pending")
# Step 2: Approve Waiver (R-AG or R-SO required, SoD enforced)
@requires_role("R-AG", "R-SO")
@enforce_sod(rule="SOD-WAIVER")
def approve_waiver(waiver_id):
waiver = waiver_repo.find_by_id(waiver_id)
# SoD check
if waiver.requested_by == current_user():
raise SoDViolationError("Cannot approve own waiver request")
waiver.status = "approved"
waiver.approved_by = current_user()
waiver.approved_at = now()
waiver_repo.update(waiver)
audit_hook.record(AuditEvent("waiver.approved", waiver), immutable=True)
return WaiverApproved(waiver.id, waiver.valid_until)
|
Configuration
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
| # config/authority-hooks.yaml
rbac:
roles:
- id: R-DS
name: Domain Steward
permissions:
- semantic.change.approve_non_breaking
- semantic_debt.accept
- id: R-AG
name: Architecture Governor
permissions:
- semantic.change.approve_breaking
- calm.compliance.override
- waiver.approve
- id: R-SO
name: Security Officer
permissions:
- breakglass.approve
- key.rotation.approve
- waiver.approve
- id: R-DEV
name: Developer
permissions:
- semantic.change.propose
- waiver.request
sod_rules:
- id: SOD-01
name: Production Self-Approval Ban
applies_to: [waiver, semantic_change]
environments: [production]
constraint: proposer != approver
- id: SOD-03
name: Break-glass Security Officer Approval
applies_to: [breakglass]
constraint: approver.role == R-SO
audit:
immutable_events:
- waiver.approved
- waiver.rejected
- breakglass.activated
- key.rotated
ifl_anchoring: true
retention_days: 2555 # 7 years for compliance
|
Testing Strategy
Unit Tests
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| def test_sod_violation_self_approval():
transaction = Transaction(
proposer="alice",
approver="alice",
type="waiver"
)
result = sod_hook.validate(transaction, environment="production")
assert result.passed == False
assert result.violated_rule == "SOD-01"
def test_waiver_approval_requires_ag_role():
principal = Principal(id="bob", roles=["R-DEV"])
with pytest.raises(UnauthorizedError):
auth_hook.validate(principal, action="waiver.approve", ...)
|
Integration Tests
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| def test_end_to_end_waiver_workflow():
# Alice (R-DEV) requests waiver
alice = authenticate("alice", roles=["R-DEV"])
waiver = request_waiver(
invariant_id="INV-PERF-001",
rationale="Batch processing requires > 100ms",
valid_until="2026-01-31T23:59:59Z"
)
assert waiver.status == "pending"
# Bob (R-AG) approves waiver (SoD satisfied)
bob = authenticate("bob", roles=["R-AG"])
approval = approve_waiver(waiver.id)
assert approval.status == "approved"
# Verify audit trail
events = audit_log.query(waiver_id=waiver.id)
assert len(events) == 2 # requested + approved
assert events[1].ifl_anchor_id is not None
|
Deployment
Python Package
1
| pip install sea-authority-hooks
|
Usage in SDS-047 Policy Gateway
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| from sea_authority_hooks import (
PreAuthorizationHook,
SeparationOfDutiesHook,
AuditTrailHook
)
# Initialize hooks
auth_hook = PreAuthorizationHook.from_config("authority-hooks.yaml")
sod_hook = SeparationOfDutiesHook.from_config("authority-hooks.yaml")
audit_hook = AuditTrailHook.from_config("authority-hooks.yaml")
# Wire into Policy Gateway
app.register_pre_hook("authorization", auth_hook)
app.register_pre_hook("sod_validation", sod_hook)
app.register_post_hook("audit", audit_hook)
|
- SDS-031: Authority & Ownership Boundaries (RBAC model, SoD rules)
- SDS-047: GovernedSpeed™ Governance Runtime (waiver workflow)
- ADR-011: Internal Federated Ledger (audit anchoring)
- REF-012: Invariant Regime (waiver reference)
- OSCAL eu-ai-art14: Human Oversight (EU AI Act Article 14)
- Waiver Schema:
schemas/events/governance/waiver-record.schema.json
Revision History
| Version |
Date |
Changes |
| 1.0.0 |
2025-12-31 |
Initial specification |