OBP-API/ideas/ABAC_CONSENTS.md

769 lines
23 KiB
Markdown
Raw Permalink Normal View History

2025-12-09 07:46:56 +00:00
# Thoughts on ABAC and Consents
## ABAC Overview
**Attribute-Based Access Control (ABAC)** evaluates access based on attributes:
- **User attributes**: `ABAC_role=teller`, `ABAC_branch=branch_123`, `ABAC_clearance_level=2`
- **Account attributes**: `branch=branch_123`, `account_type=checking`, `vip_status=gold`
- **Context**: time of day, business hours, customer present
Policy example: "Tellers can access accounts where user.branch == account.branch during business hours"
## The Challenge: Real Banking Workflows
Banks need staff access patterns like:
- **Branch-based**: Tellers at branch_123 see accounts at branch_123
- **Role-based**: VIP account managers see VIP accounts
- **Time-limited**: Customer service gets 30-minute access during customer interaction
- **Session-based**: Access expires when session/shift ends
- **Dynamic**: No manual pre-granting of thousands of permissions
Traditional approaches don't fit:
- Individual AccountAccess grants: doesn't scale (thousands of accounts)
- Firehose: too broad (all accounts at bank)
- Manual grants per request: too slow for operations
## Could Consents Work for ABAC?
Instead of real-time policy evaluation on every request, create a **consent** when ABAC policy matches.
### Flow Concept
1. User requests account access
2. No explicit AccountAccess grant exists
3. User has `CanUseABAC` entitlement
4. System evaluates ABAC policy (checks attributes)
5. Policy matches → Create consent with explicit account list
6. Consent valid for short period (15-60 minutes)
7. Subsequent requests check consent (fast lookup)
8. Consent expires → Re-evaluate policy on next request
### Why Consents Could Work
- Time limits built-in (`validFrom`, `validTo`)
- Status management (ACCEPTED, REVOKED, EXPIRED)
- Audit trail (creation, usage, expiry all logged)
- Explicit account list in `views` field
- Can be revoked when attributes change
- Reuses existing infrastructure (no new tables)
- Standard authorization check works (`hasAccountAccess`)
### Consent as Cache
The consent acts as a **cached ABAC decision**:
- Real-time evaluation would be slow (fetch attributes, evaluate policy)
- Consent caches: "User X can access accounts Y,Z at branch_123"
- Fast lookup: is requested account in consent's views list?
- Short TTL ensures freshness (15-60 minutes)
- Revoke on attribute change for immediate effect
## ABAC Consent vs Standard Consent
Structurally identical:
- Same fields: `userId`, `views`, `validFrom`, `validTo`, `status`
- Same table: `MappedConsent`
- Same authorization logic: lookup in `views` list
- Same usage: `hasAccountAccess()` checks for valid consent
Differences:
- **Creation**: Policy evaluation vs customer authorization
- **Lifetime**: 15-60 minutes vs 90+ days
- **Revocation**: Automatic (attribute change) vs manual (customer revokes)
- **Initiator**: System vs customer
- **Purpose**: Staff operations vs TPP access
## Schema Considerations: Marking ABAC Consents
Need to distinguish ABAC-generated consents for audit/management.
### Existing Fields Analysis
**`issuer` (iss in JWT)**
- Standard use: JWT issuer for validation (e.g., `Constant.HostName`, `"https://accounts.google.com"`)
- Used in OAuth2 flows for token validation
- **Should NOT be changed** - would break JWT validation logic
- Keep as standard OBP issuer
**`createdByUserId`**
- Standard consent: Customer who authorized TPP access
- ABAC consent: User whose attributes matched policy (even though not explicitly authorized by them)
- Semantically fits: "consent for this user, created by system evaluation"
**`consumerKey` (aud in JWT)**
- Standard use: OAuth consumer/application
- **Should NOT be overloaded** to mark ABAC - semantically wrong, could break consumer logic
**`apiStandard`**
- Meant for API specifications: "BERLIN_GROUP", "UK_OBWG", "OBP"
- **Should NOT be used** for creation method - wrong purpose
### Option A: Use `note` Field
```scala
note = "ABAC_GENERATED|policy=branch_teller_access|user_branch=branch_123|timestamp=1234567890"
// Query
consents.filter(_.note.contains("ABAC_GENERATED"))
```
**Pros**:
- No schema change
- Works immediately
- Can include rich metadata
**Cons**:
- String parsing needed
- Less structured than proper field
- Queries less efficient
### Option B: Add `source` Field (Recommended)
Add to `MappedConsent`:
```scala
object mSource extends MappedString(this, 50) {
override def defaultValue = "CUSTOMER_GRANTED"
}
override def source: String = mSource.get
// Values:
// - "CUSTOMER_GRANTED" (standard Open Banking)
// - "ABAC_GENERATED" (policy-based)
// - "SYSTEM_GENERATED" (admin/system)
```
**Pros**:
- Clean, structured
- Easy to query: `By(MappedConsent.mSource, "ABAC_GENERATED")`
- Clear semantics
- Future-proof (other generation methods)
**Cons**:
- Requires database migration
- Changes to consent schema
**Migration**:
```sql
ALTER TABLE mappedconsent ADD COLUMN msource VARCHAR(50) DEFAULT 'CUSTOMER_GRANTED';
CREATE INDEX idx_mappedconsent_source ON mappedconsent(msource);
```
## Implementation Ideas
### User Setup with Non-Personal Attributes
Users get ABAC attributes (non-personal):
```json
POST /users/USER_ID/entitlements
{ "role_name": "CanUseABAC", "bank_id": "" }
POST /users/USER_ID/non-personal-attributes
[
{ "name": "ABAC_role", "type": "STRING", "value": "teller" },
{ "name": "ABAC_branch", "type": "STRING", "value": "branch_123" },
{ "name": "ABAC_department", "type": "STRING", "value": "retail" },
{ "name": "ABAC_clearance_level", "type": "INTEGER", "value": "2" }
]
```
**Naming convention**: `ABAC_` prefix distinguishes control attributes from business attributes.
### Account Setup
```json
POST /banks/BANK_ID/accounts/ACCOUNT_ID/attributes
[
{ "name": "branch", "type": "STRING", "value": "branch_123" },
{ "name": "account_type", "type": "STRING", "value": "checking" },
{ "name": "vip_status", "type": "STRING", "value": "gold" },
{ "name": "customer_id", "type": "STRING", "value": "customer_456" }
]
```
### Access Flow Sketch
```scala
def hasAccountAccess(view, bankIdAccountId, user, callContext): Boolean = {
// Standard checks first
if (isPublicView(view)) return true
if (hasAccountFirehoseAccess(view, user)) return true
if (user.hasExplicitAccountAccess(view, bankIdAccountId, callContext)) return true
// ABAC check
if (hasEntitlement(user.userId, "CanUseABAC")) {
// Check for existing ABAC consent with this account
val existingConsent = getABACConsents(user.userId).find { c =>
c.source == "ABAC_GENERATED" && // If we add source field
c.views.exists(cv => cv.account_id == bankIdAccountId.accountId.value) &&
c.validTo > now &&
c.status == "ACCEPTED"
}
if (existingConsent.isDefined) return true // Fast path: cached
// No cached consent, evaluate ABAC policy
val decision = evaluateABACPolicy(user, bankIdAccountId, view)
if (decision.allowed) {
// Find all accounts matching same policy pattern
val matchingAccounts = findAccountsMatchingPolicy(user, decision)
// Create consent with explicit account list
createABACConsent(
user = user,
accounts = matchingAccounts, // Explicit list in views
viewId = decision.viewId,
validTo = now + decision.durationMinutes.minutes,
source = "ABAC_GENERATED",
note = s"Policy: ${decision.policyName}, Reason: ${decision.reason}"
)
return true
}
}
false
}
```
### Example Policies
**Branch Teller**:
```scala
if (user.ABAC_role == "teller" &&
user.ABAC_branch == account.branch &&
account.account_type in ["checking", "savings"] &&
isBusinessHours) {
grant(duration = 60.minutes, view = "teller")
}
```
**VIP Account Manager**:
```scala
if (user.ABAC_role == "vip_account_manager" &&
account.vip_status in ["gold", "platinum"]) {
grant(duration = 240.minutes, view = "owner")
}
```
**Customer Service Session**:
```scala
if (user.ABAC_role == "customer_service" &&
user.ABAC_customer_session_active == account.customer_id &&
sessionAge < 30.minutes) {
grant(duration = 30.minutes, view = "customer_service")
}
```
**Branch Manager**:
```scala
if (user.ABAC_role == "branch_manager" &&
user.ABAC_branch == account.branch) {
grant(duration = 480.minutes, view = "owner")
}
```
**Compliance Officer**:
```scala
if (user.ABAC_role == "compliance_officer" &&
user.ABAC_clearance_level >= 4) {
grant(duration = 480.minutes, view = "auditor")
}
```
## Attribute Changes and Revocation
Hook into attribute deletion/update:
```scala
override def deleteUserAttribute(userId: String, attributeName: String): Box[Boolean] = {
val result = super.deleteUserAttribute(userId, attributeName)
if (attributeName.startsWith("ABAC_")) {
// Revoke all ABAC-generated consents for this user
getABACConsents(userId).foreach { consent =>
consent.mStatus("REVOKED")
.mNote(s"${consent.note}|AUTO_REVOKED: ${attributeName} removed at ${now}")
.save()
}
}
result
}
```
This ensures attribute changes take immediate effect (even if consent hasn't expired yet).
## Pattern-Based vs Explicit Account List
Two approaches for what goes in `consent.views`:
### Explicit List (Recommended)
Consent contains actual account IDs:
```scala
views = List(
ConsentView("bank-123", "account-001", "teller"),
ConsentView("bank-123", "account-002", "teller"),
// ... 50 more accounts at branch_123
)
```
**Pros**:
- Fast lookup: is account in list?
- Works with existing consent logic
- Clear audit: see exactly which accounts
**Cons**:
- Large list if many accounts (50-100+)
- Must re-create if new accounts added
### Pattern-Based
Consent stores attribute pattern, not account IDs:
```scala
views = List(), // Empty
abac_pattern = Map(
"user_branch" -> "branch_123",
"account_branch" -> "branch_123",
"account_type" -> "checking,savings"
)
```
On each access, fetch account attributes and check against pattern.
**Pros**:
- Small consent record
- Automatically includes new accounts
- More flexible
**Cons**:
- Must fetch account attributes on each check
- Custom evaluation logic needed
- More complex
**Recommendation**: Start with explicit list. If >100 accounts per consent becomes common, consider pattern-based.
## Real-Time vs Cached Evaluation
**Pure ABAC (Real-time)**:
```
Request → Fetch user attributes → Fetch account attributes →
Evaluate policy → Allow/Deny
```
- Always current
- Slower (fetch + evaluate each time)
- No consent records
**Consent-Cached ABAC**:
```
Request → Check consent exists → Found? Allow (fast)
→ Not found? → Evaluate → Create consent → Allow
```
- Fast (list lookup)
- Short TTL (15-60 min) keeps it fresh
- Revoke on attribute change for immediate effect
- Full audit trail
**Hybrid** (could be interesting):
```
Request → Check consent → Valid? → Validate attributes still match → Allow/Re-evaluate
```
- Cache for performance
- Validate attributes on each use for freshness
- Best of both worlds but more complex
## Customer Service Workflow Idea
```scala
POST /customer-service/session/start
{
"customer_number": "CUST-123",
"reason": "Customer requesting balance info"
}
// Backend:
// 1. Verify user has ABAC_role=customer_service
// 2. Find customer's accounts
// 3. Create temporary user attribute: ABAC_customer_session_active=CUST-123
// 4. Create ABAC consent for customer's accounts (30 min)
// 5. Return session_id
Response:
{
"session_id": "session-abc",
"customer_id": "customer-456",
"account_ids": ["acc-1", "acc-2"],
"expires_at": "2024-01-15T10:30:00Z"
}
// User accesses accounts using normal endpoints (no special headers)
GET /banks/bank-123/accounts/acc-1/owner/account
// Works because ABAC consent exists
POST /customer-service/session/end
{
"session_id": "session-abc"
}
// Backend:
// 1. Remove ABAC_customer_session_active attribute
// 2. Revoke ABAC consents for this session
```
Clean workflow, time-bound, full audit trail.
## Endpoints: Do We Need New Ones?
**No new account endpoints needed** - existing endpoints work transparently because ABAC integrates into `hasAccountAccess()`.
**But might want management endpoints**:
```scala
// List my active ABAC consents
GET /my/abac-consents
Response: List of consent IDs, accounts, expiry times
// Revoke ABAC consent (early)
DELETE /consents/{CONSENT_ID}
// Existing endpoint already works
// Customer service workflow helper
POST /customer-service/session/start
POST /customer-service/session/end
// Admin: view ABAC usage
GET /admin/abac-consents?user_id=X&date_from=Y
GET /admin/abac-policies // List active policies
```
## Machine Learning Integration Ideas
Track ABAC consent usage and apply ML for anomaly detection:
### Normal Patterns
- Teller at branch_123 accesses 20-40 accounts/day, Mon-Fri 9am-5pm
- Customer service sessions average 3 accounts, duration 15 minutes
- Branch manager accesses 50-100 accounts/day during business hours
### Anomalies to Detect
- **Time anomaly**: Teller accessing accounts at 2am
- **Volume anomaly**: Teller accessing 200 accounts in one day
- **Scope anomaly**: Teller accessing accounts at different branch
- **Pattern anomaly**: Customer service session lasting 4 hours
- **Sequence anomaly**: Rapid access to VIP accounts by new user
### ML Approach
```
Features:
- Time of day
- Day of week
- Number of accounts accessed
- Duration of consent usage
- User role
- Account types accessed
- Deviation from user's normal pattern
- Deviation from role's normal pattern
Model: Isolation Forest or Autoencoder
Output: Anomaly score (0-1)
Action:
- Score > 0.8: Alert security, revoke consent
- Score 0.5-0.8: Flag for review
- Score < 0.5: Normal
```
Could even auto-revoke consents that trigger anomaly detection.
## Configuration Ideas
```properties
# Enable ABAC
enable_abac=true
# Auto-create consents when policy matches
abac.auto_consent_enabled=true
# Policy durations (minutes)
abac.teller_duration=60
abac.manager_duration=480
abac.customer_service_duration=30
abac.compliance_duration=480
# Business rules
abac.business_hours_start=9
abac.business_hours_end=17
abac.require_customer_present_for_cs=true
# Consent management
abac.cleanup_expired_enabled=true
abac.cleanup_interval_minutes=15
abac.max_accounts_per_consent=100
abac.revoke_on_attribute_change=true
# Security
abac.max_consents_per_user_per_day=50
abac.alert_on_excessive_consent_creation=true
abac.ml_anomaly_detection_enabled=false
# Audit
abac.log_all_evaluations=true
abac.log_denied_attempts=true
```
2025-12-10 07:01:02 +00:00
## Context Mutation Concerns: Should ABAC Auto-Generate Consents?
An important architectural question: **Should the ABAC system automatically generate consents during request processing, or should consent generation be explicit?**
### The Context Mutation Problem
**Proposed Flow:**
1. User calls endpoint with OAuth2/OIDC header + `CanUseAbac` role
2. During request processing, ABAC evaluates policies
3. If policy matches, system creates a consent
4. Consent gets attached to current call context
5. Request proceeds using the newly-created consent
**Why This Is Problematic:**
**1. Violates Principle of Least Surprise**
- Caller makes request with OAuth2 credentials
- Behind the scenes, system creates a persistent consent entity
- This implicit behavior makes debugging and understanding the system harder
- Side effects hidden from the caller violate transparency
**2. Semantic Confusion**
- **Consents** traditionally represent explicit user agreements ("I consent to share my data")
- **ABAC evaluations** are policy-based decisions ("Your attributes match policy criteria")
- Mixing these concepts muddies the semantic waters
- For compliance/regulatory purposes, this distinction matters
**3. Lifecycle Management Complexity**
- When should auto-generated consents expire?
- How do you clean them up?
- What happens if attributes change mid-request?
- Does the consent persist beyond the current call?
- Creates timing issues and potential race conditions
**4. Audit Trail Ambiguity**
- Was this consent user-initiated or system-generated?
- Who authorized it - the user or the policy engine?
- Compliance systems need clear distinctions
### Alternative Approaches
**Option 1: ABAC as Pure Policy Evaluation (Recommended)**
Keep ABAC as a **transparent evaluation layer** without side effects:
```scala
def checkAccess(user: User, resource: Resource, action: Action): Boolean = {
val attributes = gatherAttributes(user, resource, action)
val policies = findApplicablePolicies(resource, action)
evaluatePolicies(policies, attributes) match {
case PolicyResult.Allow(reason) =>
auditLog.record(s"ABAC allowed: $reason")
true
case PolicyResult.Deny(reason) =>
auditLog.record(s"ABAC denied: $reason")
false
}
}
```
The ABAC evaluation should be:
- **Fast** (in-memory policy evaluation)
- **Stateless** (no persistent side effects)
- **Auditable** (logged but not persisted as consent)
- **Transparent** (clear in logs, but invisible to caller)
**Option 2: ABAC Evaluation Result as Request Metadata**
Instead of creating a consent, attach the evaluation result to request context:
```scala
case class RequestContext(
oauth2Token: Token,
userRoles: Set[Role],
abacEvaluation: Option[AbacEvaluationResult] = None
)
case class AbacEvaluationResult(
decision: Decision,
matchedPolicies: List[Policy],
evaluatedAttributes: Map[String, String],
evaluatedAt: DateTime,
validUntil: DateTime
)
```
This allows:
- Caching evaluation results within request scope
- Passing results to downstream services
- Audit logging without persistence
- No database writes on every request
**Option 3: Explicit Two-Step Flow**
If you must use consents, make it **explicit**:
```
# Step 1: Acquire ABAC Consent (explicit call)
POST /consents/abac
Authorization: Bearer {oauth2_token}
{
"resource_type": "account",
"resource_id": "123",
"action": "view_balance"
}
Response:
{
"consent_id": "abac-consent-xyz",
"valid_until": "2024-01-15T10:30:00Z",
"granted_accounts": ["123", "456", "789"]
}
# Step 2: Use the Consent (separate call)
GET /accounts/123/balance
Authorization: Bearer {oauth2_token}
X-ABAC-Consent: abac-consent-xyz
```
**Option 4: Consent as Cache (Current Document Approach)**
The approach described in this document treats consents as a **cache** for ABAC decisions:
- First request: No consent exists → Evaluate ABAC → Create consent → Use it
- Subsequent requests: Consent exists → Skip evaluation → Use cached decision
- Consent expires: Back to evaluation on next request
This is a middle ground but still has mutation concerns:
- ✅ Reuses existing infrastructure
- ✅ Provides caching benefits
- ✅ Creates audit trail
- ⚠️ Still creates side effects on first request
- ⚠️ Requires cleanup/garbage collection
- ⚠️ Database writes on policy evaluation
### When Context Enrichment Is Acceptable
Some forms of context modification are fine:
- **Adding computed attributes** (in-memory, non-persistent)
- **Attaching evaluation results** for downstream use within request
- **Caching policy decisions** within request scope
- **Adding trace/correlation IDs** for debugging
But creating **persistent entities** (like database records) crosses from enrichment to mutation with side effects.
### Recommendation
For production ABAC implementation:
1. **Evaluation Phase**: ABAC evaluates policies (fast, stateless)
2. **Authorization Phase**: Result determines allow/deny
3. **Audit Phase**: Log decision with context
4. **No Consent Generation**: Unless explicitly requested
If caching is needed:
- Use in-memory cache (Redis, Memcached)
- Cache evaluation results, not consents
- Clear cache on attribute changes
- TTL matches policy freshness requirements
If consents are truly needed:
- Make acquisition explicit (separate endpoint)
- Document the semantic difference from user consents
- Implement clear lifecycle management
- Provide revocation endpoints
### Summary: Implicit vs Explicit
| Aspect | Implicit (Auto-Generate) | Explicit (Separate Call) |
|--------|-------------------------|--------------------------|
| Caller experience | Simple, transparent | Two-step, more complex |
| Debugging | Harder, hidden behavior | Easier, clear flow |
| Performance | Better (caching built-in) | Requires separate cache |
| Side effects | Yes (database writes) | Only when requested |
| Semantic clarity | Confused | Clear |
| Audit trail | Ambiguous source | Clear initiator |
| **Recommendation** | ❌ Avoid | ✅ Prefer if using consents |
2025-12-09 07:46:56 +00:00
## Challenges and Open Questions
1. **Schema change**: Add `source` field or use `note`?
- `source` field cleaner but requires migration
- `note` field works now but less structured
2. **Large account lists**: What if teller has access to 500 accounts?
- One consent with 500 views?
- Multiple consents (e.g., 100 accounts each)?
- Pattern-based consent?
3. **Consent lifetime**: Balance between performance and freshness
- 15 min: more real-time, less caching benefit
- 30 min: balanced
- 60 min: more caching, less fresh
4. **Policy storage**: Hard-coded in Scala or database?
- Hard-coded: simpler, requires deployment to change
- Database: flexible, could have UI, more complex
5. **View selection**: Does policy specify which view to grant?
- Yes: policy says "grant teller view" (more controlled)
- No: use requested view (more flexible)
6. **Consent in request header**:
- Standard consent: TPP includes Consent-JWT in header
- ABAC consent: No header needed, just CanUseABAC entitlement
- This works because consent is cached authorization, not passed token
7. **Attribute sync**: What if account attributes change after consent created?
- Wait for consent expiry (simpler)
- Re-validate on each use (more accurate, more complex)
- Depends on attribute change frequency
8. **Multi-bank**: Can CanUseABAC work across banks?
- Bank-specific: CanUseABAC at bank-123
- Global: CanUseABAC at any bank
- Mix: Some users global, some bank-specific
## Related: Existing OBP Attributes
OBP already has attributes on:
- **Users**: UserAttribute (personal and non-personal)
- **Accounts**: AccountAttribute
- **Transactions**: TransactionAttribute
- **Products**: ProductAttribute
- **Customers**: CustomerAttribute
ABAC can leverage all of these. Example:
```scala
// Transaction-level ABAC
if (user.ABAC_role == "fraud_investigator" &&
transaction.amount > 10000 &&
transaction.TransactionAttribute("suspicious") == "true") {
grant(duration = 120.minutes, view = "auditor")
}
```
The attribute infrastructure is already there - ABAC is about using it for access decisions.
## Summary of Approach
1. User has `CanUseABAC` entitlement
2. User has non-personal ABAC attributes (`ABAC_role`, `ABAC_branch`, etc.)
3. Accounts have attributes (`branch`, `account_type`, etc.)
4. When user requests account access:
- Check for valid ABAC consent first (fast)
- If not found, evaluate ABAC policy
- If policy matches, create consent with explicit account list
- Consent valid 15-60 minutes
5. Consent contains `source = "ABAC_GENERATED"` (if we add field) or marker in `note`
6. When user's ABAC attributes change, revoke their ABAC consents
7. Full audit trail via consent records
8. Optional: ML anomaly detection on usage patterns
This reuses consent infrastructure while providing dynamic, attribute-based access control suitable for bank staff workflows.