Skip to content

feat(errors): typed secret-read refusals in all four SDKs (409 SecretUnavailable*, error code field) - #59

Open
aibeshkov wants to merge 4 commits into
mainfrom
errmap1/value-read-refusal-codes
Open

aibeshkov wants to merge 4 commits into
mainfrom
errmap1/value-read-refusal-codes

Conversation

@aibeshkov

@aibeshkov aibeshkov commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

STACKED ON keyorixhq/keyorix#3175 — merge after it (it is the server change; this PR is backward compatible either way).

NEEDS ANDREI (.github): this PR now also touches .github/workflows/contract.yml (one new step, "Bootstrap contract admin (MFA stays on)", plus a header comment). Please review that file.

This is the SDK half of keyorixhq/keyorix#3175 (ERRMAP-1). The server now answers a value read that the secret's own state refuses with a client error and a stable code, instead of 500:

refusal HTTP error
outside access schedule 403 SecretAccessOutsideSchedule
expired 409 SecretExpired
suspended 409 SecretSuspended
max reads exhausted 409 SecretMaxReadsExhausted

The change is the same in all four SDKs and backward compatible:

  • The base error exposes the stable code.
    • Go: APIError.Code
    • Python: KeyorixError.code
    • Node: KeyorixError.errorCode (not code, which Node reserves for system errors)
    • Java: KeyorixException.getErrorCode()
  • A 409 with one of the three secret-state codes gets a new type: SecretUnavailableError (Go, Python, Node) and SecretUnavailableException (Java).
    • In Go it unwraps to *APIError. In the other SDKs it subclasses the base error.
    • Any other 409 stays the generic error.
  • The schedule 403 stays ForbiddenError / ForbiddenException, so existing handlers keep working. Its message now says "the secret is outside its access schedule" instead of "token lacks permission".
  • Messages are local text, never the server's body. This is the same "no untrusted content in the message" rule the error types already follow.
  • Against a server without #3175, these reads keep their old behaviour: the generic 500 error.

Tests

Each SDK adds a test that drives a by-ref value read against a stub answering each refusal. It asserts the type, the code, a local message, and that the server text is never echoed. A test for an unrelated 409 is included where the SDK has one.

  • Go: go vet ./... && go test ./...: ok. TestErrorForResponse_SecretReadRefusals passes.
  • Python: python3 -m unittest test_keyorix: Ran 21 tests ... OK.
  • Node: node test.js: 59 passed, 0 failed.
  • Java (Maven 3.9 / Temurin 17 container on the e2e VM): Tests run: 50, Failures: 0, Errors: 0, Skipped: 7 (ContractTest skips without a server, as in the unit job). BUILD SUCCESS.

The contract tests pin a server ref (.keyorix-server-ref) and do not exercise these codes yet.

Contract harness fix (SDK-CONTRACT-1)

The contract job was red for every SDK PR: keyorix main enforces MFA by default (security.require_mfa, ADR-112), so the suites' password login stopped at the MFA step. MFA is not turned off anywhere (no require_mfa:false, no server change).

  • scripts/contract_bootstrap.py (new, run once by the workflow): /system/init, password login (setup-only session), /auth/mfa/enroll, TOTP computed from the returned seed (test-only RFC 6238 helper), /auth/mfa/activate, password login, /auth/mfa/verify, then mints a 24 h PAT. The PAT goes to $GITHUB_ENV as KEYORIX_CONTRACT_ADMIN_TOKEN with ::add-mask::; it is never printed. The PAT is unscoped because the suites create projects, secrets, machine identities and PATs, and a scoped PAT cannot mint PATs.
  • Go, Node, Python and Java suites read KEYORIX_CONTRACT_ADMIN_TOKEN instead of logging in with a password.
  • One new test per SDK: a password-only login returns mfa_required plus a challenge and no session token. Each makes one login call, because a challenge login keeps its rate-limit slot (10 per 15 min per IP).
  • A second drift was hiding behind the MFA failure: current main answers a role-less machine token's project listing with an empty list instead of 403. The "machine token authenticates but is not authorized" test now accepts an empty list or a 403, and fails on any visible project or an auth error.
  • Local run against a server built from keyorix main, MFA on, fresh DB: Go ok, Node 15/0, Python 9 OK. Java could not run in my sandbox (Maven cannot write ~/.m2), so it is verified by CI only.

Session: ERRMAP-1
Session: SDK-CONTRACT-1

…MAP-1)

The server now answers a value read the secret's own state refuses with 409
SecretExpired / SecretSuspended / SecretMaxReadsExhausted, or 403
SecretAccessOutsideSchedule, instead of 500. Expose the stable code on the base
error and raise SecretUnavailable* for the three 409 codes; the schedule 403 stays
Forbidden* with an accurate message. Local message text, never the server body.

Session: ERRMAP-1
Signed-off-by: Andrei Beshkov <andrey.beshkov@gmail.com>
…ce on a ternary

Signed-off-by: Andrei Beshkov <andrey.beshkov@gmail.com>
…nstructor throws it)

Signed-off-by: Andrei Beshkov <andrey.beshkov@gmail.com>
@aibeshkov

Copy link
Copy Markdown
Contributor Author

Coordinator review (opus) at 08cf046: problems

  1. CI is red and the author report does not mention it. Contract / Contract tests (real keyorix-server) fails, and Gate fails with it. The failure is not caused by this PR:
    • I built keyorix main with .github/contract-server/keyorix.yaml and ran the Go contract test from keyorix-sdks main against it. It fails the same way: ListProjects: forbidden, CreateProject: forbidden, POST /auth/tokens: 403.
    • Server log: security.require_mfa is enforcing on its ADR-112 secure-by-default value ... Session-authenticated users without MFA are confined to MFA enrolment.
    • So every SDK PR is red against current keyorix main. This needs a separate fix in the contract harness before this PR can merge: enrol TOTP in the test setup, or drive the tests with a PAT or machine token. The fix must not set require_mfa: false.
    • Side effect: the new 403/409 typed errors have never been exercised against a real server in CI. They are only covered by mocked unit tests.

Code itself looks right and is backward compatible:

  • Go: *SecretUnavailableError unwraps to *APIError, and ForbiddenError is still returned for every 403.
  • Python: SecretUnavailableError is a subclass of KeyorixError.
  • Java: the old constructors are kept, and SecretUnavailableException extends KeyorixException.
  • Node: the error code field was added.
  • Codes match keyorix #3175 exactly.
  • Local unit runs on the PR head: Go 44 passed, Python 21 OK, Node 59 passed. Java passed in CI.

Merge order: after keyorix #3175. It is safe either way, because an unknown 409 or code falls back to the generic error.

keyorix main requires MFA by default (ADR-112), so the contract suites'
password login stopped at the MFA step. A bootstrap script now creates the
admin, enrols TOTP from the returned seed, completes the real MFA login and
mints a short-lived PAT that every SDK suite uses. Each suite also asserts a
password-only login still returns an MFA challenge. The role-less machine
token expectation accepts the current empty list as well as 403.

Signed-off-by: Andrei Beshkov <andrey.beshkov@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant