Add named engine auth and accessor-file session revoke
Select service-jwt, bootstrap, or env exclusively: JWT login uses a JSON file, self-revokes, and never falls back to bootstrap or BAO_TOKEN. The platform JWT mount/role is still unpublished, so auto keeps named bootstrap/env providers. session revoke --accessor-file revokes an already-issued token with fingerprint-only evidence. Production remains fail-closed. Assistant: grok Assistant-Session: 01a05f07-ae72-7781-9fcb-19efd61add00
This commit is contained in:
parent
a94003de4f
commit
3abee434df
18 changed files with 698 additions and 135 deletions
|
|
@ -3,10 +3,9 @@
|
|||
Thin wrapper over the `bao` CLI. Isolated here so the rest of the engine speaks
|
||||
in lanes/plans, not in OpenBao endpoint quirks (FR: "isolate backend adapter").
|
||||
|
||||
Auth resolution order for the token:
|
||||
1. explicit bootstrap token file (--bootstrap-token-file), mode-checked;
|
||||
2. BAO_TOKEN / VAULT_TOKEN environment variable;
|
||||
3. otherwise unauthenticated (only dry-run / read-health works).
|
||||
Named engine auth providers (no implicit fallback) live in ``engine_auth``.
|
||||
``OpenBaoClient.resolve`` only materializes an already-selected bootstrap file
|
||||
or environment token.
|
||||
|
||||
This adapter NEVER returns a secret value to its callers except through the
|
||||
narrow `read_field_present()` (boolean) and the exec-delivery path, which writes
|
||||
|
|
@ -14,6 +13,7 @@ straight into a child process and never logs.
|
|||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
|
|
@ -56,6 +56,11 @@ def _check_token_file(path: Path) -> str:
|
|||
return read_strict_token_file(path, purpose="bootstrap token")
|
||||
|
||||
|
||||
def accessor_fingerprint(accessor: str) -> str:
|
||||
"""Non-secret handle for an accessor the caller already holds."""
|
||||
return hashlib.sha256(accessor.encode("utf-8")).hexdigest()[:12]
|
||||
|
||||
|
||||
@dataclass
|
||||
class ScopedTokenSession:
|
||||
"""One AppRole login token that revokes itself on close."""
|
||||
|
|
@ -266,9 +271,7 @@ class OpenBaoClient:
|
|||
raise BackendError("malformed AppRole login response") from e
|
||||
if not token:
|
||||
raise BackendError("AppRole login returned an empty token")
|
||||
import hashlib
|
||||
|
||||
fingerprint = hashlib.sha256(accessor.encode("utf-8")).hexdigest()[:12]
|
||||
fingerprint = accessor_fingerprint(accessor)
|
||||
scoped = OpenBaoClient(addr=self.addr, token=token, bao_bin=self.bao_bin)
|
||||
del token
|
||||
return ScopedTokenSession(
|
||||
|
|
@ -307,6 +310,31 @@ class OpenBaoClient:
|
|||
def delete_approle(self, role_name: str) -> None:
|
||||
self._run_ok(["delete", f"auth/approle/role/{role_name}"])
|
||||
|
||||
def login_jwt(self, mount: str, role: str, jwt: str) -> ScopedTokenSession:
|
||||
"""Login through JWT auth without putting the JWT in argv."""
|
||||
output = self._run_ok_with_json_file(
|
||||
["write", "-format=json", f"auth/{mount}/login"],
|
||||
{"role": role, "jwt": jwt},
|
||||
)
|
||||
try:
|
||||
auth = json.loads(output)["auth"]
|
||||
token = str(auth["client_token"])
|
||||
accessor = str(auth.get("accessor", ""))
|
||||
except (json.JSONDecodeError, KeyError, TypeError) as e:
|
||||
raise BackendError("malformed JWT login response") from e
|
||||
if not token:
|
||||
raise BackendError("JWT login returned an empty token")
|
||||
fingerprint = accessor_fingerprint(accessor)
|
||||
scoped = OpenBaoClient(addr=self.addr, token=token, bao_bin=self.bao_bin)
|
||||
del token
|
||||
return ScopedTokenSession(client=scoped, accessor_fingerprint=fingerprint)
|
||||
|
||||
def revoke_accessor(self, accessor: str) -> None:
|
||||
"""Revoke a token the operator already holds, by accessor only."""
|
||||
if not accessor or any(ch.isspace() for ch in accessor):
|
||||
raise BackendError("token accessor is missing or invalid")
|
||||
self._run_ok(["token", "revoke", "-accessor", accessor])
|
||||
|
||||
# -- KV v2 -------------------------------------------------------------
|
||||
|
||||
def kv_mount_exists(self, mount: str) -> bool:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue