Obligations & Challenges¶
This page documents the built-in obligation types enforced by BasicObligationChecker and shows how to extend the checker with custom policies (e.g., geo-fencing). It also clarifies how to target obligations to a specific decision effect (permit vs deny) and how challenge hints are surfaced to the PEP layer.
Compatibility¶
The checker accepts both legacy and modern raw decision shapes:
- Legacy:
{"decision": "permit" | "deny", "obligations": [...]} - Modern:
{"effect": "permit" | "deny", "allowed": bool, "obligations": [...]}
Rules of thumb:
- Any non-
permitdecision fails closed:(ok=False, challenge=None). - When effect is
permit, obligations targeted atpermitare evaluated; if any fails,(ok=False, challenge=...). - Obligations can explicitly target an effect via
on: "permit" | "deny"; those not matching the current effect are ignored.
Obligations targeting deny¶
Obligations may target the deny branch via on: "deny". This is useful to surface a machine-readable challenge (e.g., http_basic) even when the PDP already decided to deny. The PEP can then translate it into WWW-Authenticate headers or other UX.
Built-in obligation types¶
Unless noted, these apply when
on: "permit".
require_mfa→challenge="mfa"whencontext.attrs["mfa"]is falsy.require_level(attrs.min) →challenge="step_up"whencontext.attrs["auth_level"] < min.http_challenge(on: permit|deny,attrs.scheme=Basic|Bearer|Digest) →challenge="http_basic" | "http_bearer" | "http_digest"; unknown/omitted scheme →http_auth.-
require_consent(optionalattrs.key) →challenge="consent"when consent is missing. -
With a key: expect
context.attrs["consent"][key] is True. - Without a key: expect any truthy
context.attrs["consent"]. require_terms_accept→challenge="tos"whencontext.attrs["tos_accepted"]is falsy.require_captcha→challenge="captcha"whencontext.attrs["captcha_passed"]is falsy.require_reauth(attrs.max_age) →challenge="reauth"whencontext.attrs["reauth_age_seconds"] > max_age.require_age_verified→challenge="age_verification"whencontext.attrs["age_verified"]is falsy.
Policy examples¶
YAML — MFA on permit¶
# A permit rule that requires MFA before access is actually granted.
obligations:
- on: permit
type: require_mfa
JSON — HTTP challenge on deny¶
{
"obligations": [
{
"on": "deny",
"type": "http_challenge",
"attrs": { "scheme": "Basic" }
}
]
}
YAML — Geo-fencing (custom extension example)¶
# Example obligation we will implement via a custom checker:
# Allow only if user's geo is in the allowed set.
obligations:
- on: permit
type: require_geo
attrs:
allow: ["EU", "US"]
Expected context for geo:
context.attrs["geo"]should contain a short region code (e.g.,"EU","US","APAC").
Extending the checker (custom obligations)¶
To add your own obligation types (e.g., require_geo), subclass BasicObligationChecker and handle your type. Always call super().check(...) first to preserve built-ins and fail-closed semantics.
# src/myapp/obligations.py
from rbacx.core.obligations import BasicObligationChecker
class CustomObligationChecker(BasicObligationChecker):
def check(self, decision, context):
# Let the base checker evaluate built-ins first.
ok, ch = super().check(decision, context)
if not ok:
return ok, ch
# Determine current effect in the same manner as the base checker does:
effect = decision.get("effect")
if effect is None:
effect = "permit" if decision.get("decision") == "permit" else "deny"
if effect not in ("permit", "deny"):
effect = "deny" # fail-closed
ctx = getattr(context, "attrs", context) or {}
obligations = decision.get("obligations") or []
for ob in obligations:
if (ob or {}).get("on", "permit") != effect:
continue
if (ob or {}).get("type") == "require_geo":
allow_list = set(((ob.get("attrs") or {}).get("allow") or []))
if not allow_list:
# No allow-list means fail-closed
return False, "geo"
if ctx.get("geo") not in allow_list:
return False, "geo"
return True, None
Then wire your checker into the Guard (where you construct your PDP/PEP integration). The Guard should consume (ok, challenge) and, on failure, flip permit → deny, adding the challenge to the final Decision.
Context contract (quick reference)¶
The checker reads the following context.attrs[...] keys when relevant:
mfa: boolauth_level: intconsent: bool | {str: bool}tos_accepted: boolcaptcha_passed: boolreauth_age_seconds: intage_verified: boolgeo: str(custom example)
Notes & best practices¶
- Keep obligation handlers pure (no I/O) and quick; they run in the request path.
- Use
on: "deny"to add diagnostics/UX to denials (e.g., prompt client re-auth). - When parsing numeric attrs (e.g.,
min,max_age), default invalid values to 0 and fail closed. - Unknown
typevalues should be treated as advice and ignored by the checker unless you explicitly implement them.