feat: session-scoped ACs — multiple CRs per session
Adds session_mode flag to AC lifecycle. When session_mode=true: - AC transitions to 'active' (not 'consumed') on first CR - Stays active for subsequent CRs during the session - 'session_end' outcome transitions AC to 'consumed' - Non-session ACs behave as before (consumed on first CR) Schema: - ACStatus: add ACTIVE enum value - Outcome: add SESSION_END enum value - AuthorizeRequest: add session_mode bool field - AuthorizationContextDB: add session_mode column - Auto-migration via ALTER TABLE on startup Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
5ac577af19
commit
295fe55bf2
5 changed files with 29 additions and 7 deletions
|
|
@ -8,6 +8,13 @@ engine: AsyncEngine = create_async_engine(settings.database_url, echo=False)
|
|||
async def init_db():
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(SQLModel.metadata.create_all)
|
||||
# Schema migrations for existing DBs:
|
||||
try:
|
||||
await conn.execute(
|
||||
__import__("sqlalchemy").text("ALTER TABLE authorization_contexts ADD COLUMN session_mode BOOLEAN DEFAULT 0")
|
||||
)
|
||||
except Exception:
|
||||
pass # Column already exists
|
||||
|
||||
async def get_session():
|
||||
async with AsyncSession(engine) as session:
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ class AuthorizationContextDB(SQLModel, table=True):
|
|||
issued_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
expires_at: datetime
|
||||
consumed_at: Optional[datetime] = None
|
||||
session_mode: bool = False
|
||||
poll_token: Optional[str] = None
|
||||
chronicle_event_cid: str = ""
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ from pydantic import BaseModel, Field
|
|||
class ACStatus(str, Enum):
|
||||
PENDING = "pending"
|
||||
AUTHORIZED = "authorized"
|
||||
ACTIVE = "active" # Session-scoped: accepts multiple CRs until session_end
|
||||
CONSUMED = "consumed"
|
||||
EXPIRED = "expired"
|
||||
REVOKED = "revoked"
|
||||
|
|
@ -17,6 +18,7 @@ class Outcome(str, Enum):
|
|||
FAILED = "failed"
|
||||
VIOLATED = "violated"
|
||||
TIMED_OUT = "timed_out"
|
||||
SESSION_END = "session_end" # Final CR for session-scoped ACs → transitions to consumed
|
||||
|
||||
class BehaviorStatus(str, Enum):
|
||||
VERIFIED = "verified"
|
||||
|
|
@ -73,6 +75,7 @@ class AuthorizeRequest(BaseModel):
|
|||
parameters_cid: str
|
||||
accord_template: str
|
||||
driver_id: str
|
||||
session_mode: bool = False # When true, AC stays active across multiple CRs
|
||||
|
||||
class AuthorizeResponse(BaseModel):
|
||||
status: str
|
||||
|
|
|
|||
|
|
@ -80,7 +80,8 @@ async def authorize(body: AuthorizeRequest, http_request: Request, db: AsyncSess
|
|||
playbook=request.playbook, corpus_entry_cid=request.corpus_entry_cid,
|
||||
parameters_cid=request.parameters_cid, accord_template=request.accord_template,
|
||||
token_jti=auth_result.token_jti, elevation_active=auth_result.elevation_active,
|
||||
mfa_satisfied=auth_result.mfa_satisfied, status="authorized", issued_at=now, expires_at=expires)
|
||||
mfa_satisfied=auth_result.mfa_satisfied, status="authorized", issued_at=now, expires_at=expires,
|
||||
session_mode=request.session_mode)
|
||||
db.add(ac_db)
|
||||
cid = await chronicle.emit("GSAP_AC_ISSUED", {"event_code": "0x2704", "context_id": str(ctx_id), "principal_did": auth_result.principal_did, "playbook": request.playbook})
|
||||
ac_db.chronicle_event_cid = cid
|
||||
|
|
|
|||
|
|
@ -13,14 +13,18 @@ router = APIRouter()
|
|||
@router.post("/complete/", response_model=CompleteResponse, summary="Receive CR (GSAP §5.4)")
|
||||
async def complete(request: CompleteRequest, db: AsyncSession = Depends(get_session)):
|
||||
# Pure raw SQL to avoid all SQLModel/SQLAlchemy greenlet issues
|
||||
ctx_id_str = str(request.context_id).replace("-", "")
|
||||
result = await db.execute(
|
||||
text("SELECT context_id, status FROM authorization_contexts WHERE context_id = :ctx_id AND status = 'authorized'"),
|
||||
{"ctx_id": str(request.context_id).replace("-", "")},
|
||||
text("SELECT context_id, status, session_mode FROM authorization_contexts WHERE context_id = :ctx_id AND status IN ('authorized', 'active')"),
|
||||
{"ctx_id": ctx_id_str},
|
||||
)
|
||||
ac_row = result.first()
|
||||
if not ac_row:
|
||||
raise HTTPException(status_code=404, detail="AC not found or already consumed.")
|
||||
|
||||
ac_status = ac_row[1]
|
||||
is_session = bool(ac_row[2]) if len(ac_row) > 2 else False
|
||||
|
||||
sig_verified = bool((request.signature or {}).get("value"))
|
||||
cr_id = uuid.uuid4()
|
||||
now = datetime.now(UTC)
|
||||
|
|
@ -37,7 +41,7 @@ async def complete(request: CompleteRequest, db: AsyncSession = Depends(get_sess
|
|||
:ba_status, :ffc_did, :ffc_sig,
|
||||
:sig_verified, '')"""),
|
||||
{
|
||||
"id": str(cr_id).replace("-", ""), "ctx_id": str(request.context_id).replace("-", ""),
|
||||
"id": str(cr_id).replace("-", ""), "ctx_id": ctx_id_str,
|
||||
"outcome": request.outcome.value,
|
||||
"completed_at": str(request.completed_at),
|
||||
"received_at": str(now),
|
||||
|
|
@ -52,10 +56,16 @@ async def complete(request: CompleteRequest, db: AsyncSession = Depends(get_sess
|
|||
},
|
||||
)
|
||||
|
||||
# Update AC status
|
||||
# Update AC status based on session mode:
|
||||
# - Session mode + non-final outcome → 'active' (stays open for more CRs)
|
||||
# - Session mode + session_end outcome → 'consumed' (session finished)
|
||||
# - Non-session mode → 'consumed' (single-use, original behavior)
|
||||
is_final = request.outcome.value == "session_end" or not is_session
|
||||
new_status = "consumed" if is_final else "active"
|
||||
|
||||
await db.execute(
|
||||
text("UPDATE authorization_contexts SET status = 'consumed', consumed_at = :now WHERE context_id = :ctx_id"),
|
||||
{"now": str(now), "ctx_id": str(request.context_id).replace("-", "")},
|
||||
text("UPDATE authorization_contexts SET status = :status, consumed_at = :now WHERE context_id = :ctx_id"),
|
||||
{"status": new_status, "now": str(now) if is_final else None, "ctx_id": ctx_id_str},
|
||||
)
|
||||
|
||||
# Chronicle event
|
||||
|
|
|
|||
Loading…
Reference in a new issue