mirror of
https://github.com/OpenBankProject/OBP-API.git
synced 2026-02-06 11:06:49 +00:00
769 lines
23 KiB
Markdown
769 lines
23 KiB
Markdown
# 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
|
|
```
|
|
|
|
## 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 |
|
|
|
|
## 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. |