Credential approvals¶
Hubuum v0.0.16 requires fresh password approval for credential management. A bearer token alone cannot create or renew a token, create a local user, change a password, import human passwords or password hashes (even in a dry run), or confirm a restore. Profile-only user updates, credential-free imports, token reads, and revocation keep their existing flow.
Use client.credential_approvals.create() with the acting human's current
password and a typed operation. Pass the returned response as approval= to
the corresponding typed mutation. Both clients expose the same interface;
await service calls when using AsyncClient.
Create or renew a token¶
from getpass import getpass
from hubuum_client import (
CreateTokenOperation,
CredentialApprovalRequest,
NewTokenRequest,
RenewTokenOperation,
RenewTokenRequest,
)
principal_id = client.me().principal.principal_id
request = NewTokenRequest(name="inventory-reader")
approval = client.credential_approvals.create(
CredentialApprovalRequest(
password=getpass("Current password: "),
operation=CreateTokenOperation(principal_id=principal_id, token=request),
)
)
token = client.tokens.for_principal(principal_id).create(request, approval=approval)
renewal = RenewTokenRequest()
approval = client.credential_approvals.create(
CredentialApprovalRequest(
password=getpass("Current password: "),
operation=RenewTokenOperation(principal_id=principal_id, token_id=token_id, token=renewal),
)
)
replacement = client.tokens.for_principal(principal_id).renew(token_id, renewal, approval=approval)
Typed token methods copy the returned token_expires_at into the final request,
even when you originally supplied an expiry. This preserves the server's UTC
microsecond normalization without mutating your request. When using
openapi.call() instead, send json=approval.token_request(request) with
OpenAPIOptions(headers=approval.headers(), path_params=...).
Approvals are bound to the exact operation, body, and originating bearer token. They last at most 120 seconds and are single use. Changing a target, scope, name, description, or other approved field requires fresh approval. Use the same client session for both calls. Service accounts cannot password-login; the acting human must have authority to manage the account. Approval never adds permissions the human lacks. Unattended bearer-only token rotation is not supported by this server release.
Users, imports, and restores¶
| Operation model | Protected mutation |
|---|---|
CreateUserOperation(user=request) |
client.users.create(request, approval=approval) |
UpdateUserOperation(user_id=id, user=request) |
client.users.update(id, request, approval=approval) |
ImportCredentialsOperation(import_=request) |
client.imports.submit(request, approval=approval, idempotency_key=key) or run(...) |
ConfirmRestoreOperation(restore_id=id, confirmation=request) |
postApiV1RestoresByRestoreIdConfirm with approval.headers() |
Each operation is wrapped in CredentialApprovalRequest(password=..., operation=...).
For user changes the top-level password authenticates the acting human; the
nested user password is the new account password. A password update can pass
options=RequestOptions(headers={"If-Match": revision_etag}) to retain its
normal concurrency precondition.
Credential-bearing imports require an unscoped human administrator. Preserve the complete approved import, including array order, and use a client-generated idempotency key. A retry of an accepted request with the same key and body returns the existing task; its approval header is still required. Use task status to resolve an ambiguous admission response. A changed payload requires a new approval and key. Imported password changes revoke the affected user's tokens and invalidate their outstanding approvals.
For a validated restore stage:
from hubuum_client import ConfirmRestoreOperation, OpenAPIOptions, RestoreConfirmRequest
confirmation = RestoreConfirmRequest(
restore_capability=stage["restore_capability"],
sha256=stage["sha256"],
confirmation="REPLACE ALL HUBUUM DATA",
)
approval = client.credential_approvals.create(
CredentialApprovalRequest(
password=getpass("Current password: "),
operation=ConfirmRestoreOperation(restore_id=stage["id"], confirmation=confirmation),
)
)
client.openapi.call(
"postApiV1RestoresByRestoreIdConfirm",
json=confirmation,
options=OpenAPIOptions(path_params={"restore_id": stage["id"]}, headers=approval.headers()),
)
The restore capability remains required. Approval is consumed when confirmation is admitted; executor failure does not make it reusable. Continue the existing capability-authenticated status polling.
Errors and evidence¶
ReauthenticationRequiredError is a subclass of PermissionDeniedError and
means HTTP 403 with the server reason reauthentication_required. Classification
uses the original reason before redaction. Other permission failures remain
PermissionDeniedError; APIError.reason exposes a redacted server reason, which
may be altered when a submitted secret overlaps the code. Use the exception type
for recovery. The client does not retry or prompt for a password automatically.
An approval-creation 401 means authentication failed; a 429 follows the normal
rate-limit handling. Do not parse human-readable error messages.
If the final mutation response is lost, inspect
client.credential_approvals.get(approval.record.id).consumed_at. Consumption
cannot recover a newly issued token secret. Inspect token metadata and revoke
unwanted credentials before requesting a replacement. Approval records also
include invalidation, actor, originating token, and optional restore-stage IDs.
Keep passwords and approvals transient. CredentialApprovalSecret redacts
string, representation, and JSON output. approval.headers() deliberately
reveals the secret for HTTP use; do not log or persist that dictionary. Approval
headers, password fields, and echoed secrets are redacted from client errors;
transport and model-decoding errors do not chain exceptions containing raw
requests or responses. A completed restore invalidates unused approvals while
preserving local evidence; backups do not transfer approval authority.
Invalid CredentialApprovalRequest inputs produce sanitized Pydantic validation
errors. Their errors() and json() diagnostics retain error codes and known
top-level fields, with generic messages and redacted inputs; nested context and
the original validation exception are discarded. Successful validation preserves
the exact wire payload, including passwords needed for the approved operation.
See the server client and rollout guide for the complete authorization, audit, retry, and deployment contract.