OBP-API/ideas/ABAC_CONSENTS.md
2025-12-10 08:01:02 +01:00

23 KiB

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)

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

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

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

Add to MappedConsent:

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:

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):

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

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

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:

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:

if (user.ABAC_role == "vip_account_manager" &&
    account.vip_status in ["gold", "platinum"]) {
  grant(duration = 240.minutes, view = "owner")
}

Customer Service Session:

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:

if (user.ABAC_role == "branch_manager" &&
    user.ABAC_branch == account.branch) {
  grant(duration = 480.minutes, view = "owner")
}

Compliance Officer:

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:

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:

Consent contains actual account IDs:

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:

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

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:

// 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

# 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:

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:

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

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:

// 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.