mirror of
https://github.com/OpenBankProject/OBP-API.git
synced 2026-02-06 11:06:49 +00:00
webui-props path and public in v6.0.0
This commit is contained in:
parent
be69a20472
commit
66092c1513
@ -511,6 +511,185 @@ 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`?
|
||||
|
||||
@ -3405,6 +3405,7 @@ trait APIMethods600 {
|
||||
lazy val getWebUiProp: OBPEndpoint = {
|
||||
case "webui-props" :: webUiPropName :: Nil JsonGet req => {
|
||||
cc => implicit val ec = EndpointContext(Some(cc))
|
||||
logger.info(s"========== GET /obp/v6.0.0/webui-props/$webUiPropName (SINGLE PROP) called ==========")
|
||||
val active = ObpS.param("active").getOrElse("false")
|
||||
for {
|
||||
invalidMsg <- Future(s"""$InvalidFilterParameterFormat `active` must be a boolean, but current `active` value is: ${active} """)
|
||||
@ -3444,7 +3445,7 @@ trait APIMethods600 {
|
||||
implementedInApiVersion,
|
||||
nameOf(getWebUiProps),
|
||||
"GET",
|
||||
"/management/webui_props",
|
||||
"/webui-props",
|
||||
"Get WebUiProps",
|
||||
s"""
|
||||
|
|
||||
@ -3470,14 +3471,14 @@ trait APIMethods600 {
|
||||
|**Examples:**
|
||||
|
|
||||
|Get database props combined with defaults (default behavior):
|
||||
|${getObpApiRoot}/v6.0.0/management/webui_props
|
||||
|${getObpApiRoot}/v6.0.0/management/webui_props?what=active
|
||||
|${getObpApiRoot}/v6.0.0/webui-props
|
||||
|${getObpApiRoot}/v6.0.0/webui-props?what=active
|
||||
|
|
||||
|Get only database-stored props:
|
||||
|${getObpApiRoot}/v6.0.0/management/webui_props?what=database
|
||||
|${getObpApiRoot}/v6.0.0/webui-props?what=database
|
||||
|
|
||||
|Get only default props from configuration:
|
||||
|${getObpApiRoot}/v6.0.0/management/webui_props?what=config
|
||||
|${getObpApiRoot}/v6.0.0/webui-props?what=config
|
||||
|
|
||||
|For more details about WebUI Props, including how to set config file defaults and precedence order, see ${Glossary.getGlossaryItemLink("webui_props")}.
|
||||
|
|
||||
@ -3489,29 +3490,25 @@ trait APIMethods600 {
|
||||
)
|
||||
,
|
||||
List(
|
||||
UserNotLoggedIn,
|
||||
UserHasMissingRoles,
|
||||
UnknownError
|
||||
),
|
||||
List(apiTagWebUiProps),
|
||||
Some(List(canGetWebUiProps))
|
||||
List(apiTagWebUiProps)
|
||||
)
|
||||
|
||||
|
||||
lazy val getWebUiProps: OBPEndpoint = {
|
||||
case "management" :: "webui_props":: Nil JsonGet req => {
|
||||
case "webui-props":: Nil JsonGet req => {
|
||||
cc => implicit val ec = EndpointContext(Some(cc))
|
||||
val what = ObpS.param("what").getOrElse("active")
|
||||
logger.info(s"========== GET /obp/v6.0.0/management/webui_props called with what=$what ==========")
|
||||
logger.info(s"========== GET /obp/v6.0.0/webui-props (ALL PROPS) called with what=$what ==========")
|
||||
for {
|
||||
(Full(u), callContext) <- authenticatedAccess(cc)
|
||||
callContext <- Future.successful(cc.callContext)
|
||||
_ <- NewStyle.function.tryons(s"""$InvalidFilterParameterFormat `what` must be one of: active, database, config. Current value: $what""", 400, callContext) {
|
||||
what match {
|
||||
case "active" | "database" | "config" => true
|
||||
case _ => false
|
||||
}
|
||||
}
|
||||
_ <- NewStyle.function.hasEntitlement("", u.userId, ApiRole.canGetWebUiProps, callContext)
|
||||
explicitWebUiProps <- Future{ MappedWebUiPropsProvider.getAll() }
|
||||
implicitWebUiProps = getWebUIPropsPairs.map(webUIPropsPairs=>WebUiPropsCommons(webUIPropsPairs._1, webUIPropsPairs._2, webUiPropsId= Some("default")))
|
||||
result = what match {
|
||||
@ -3532,11 +3529,11 @@ trait APIMethods600 {
|
||||
explicitWebUiProps ++ implicitWebUiPropsRemovedDuplicated
|
||||
}
|
||||
} yield {
|
||||
logger.info(s"========== GET /obp/v6.0.0/management/webui_props returning ${result.size} records ==========")
|
||||
logger.info(s"========== GET /obp/v6.0.0/webui-props returning ${result.size} records ==========")
|
||||
result.foreach { prop =>
|
||||
logger.info(s" - name: ${prop.name}, value: ${prop.value}, webUiPropsId: ${prop.webUiPropsId}")
|
||||
}
|
||||
logger.info(s"========== END GET /obp/v6.0.0/management/webui_props ==========")
|
||||
logger.info(s"========== END GET /obp/v6.0.0/webui-props ==========")
|
||||
(ListResult("webui_props", result), HttpCode.`200`(callContext))
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user