Skip to content

feat(DM01-6092): add endpoint to get project secrets with decrypted values behind a temporary read-token#631

Open
Jiachen0715 wants to merge 6 commits into
masterfrom
feat/DM01-6092-secret-read-token
Open

feat(DM01-6092): add endpoint to get project secrets with decrypted values behind a temporary read-token#631
Jiachen0715 wants to merge 6 commits into
masterfrom
feat/DM01-6092-secret-read-token

Conversation

@Jiachen0715

Copy link
Copy Markdown
Contributor

Summary

Adds two endpoints for project secrets and enforces an apply-first,
time-limited two-factor gate on reading plaintext values.
Supersedes #630 (its changes are folded into this PR).

Features

  1. List decrypted secrets GET /api/v1/projects/<project_id>/secrets/values

    • Returns each secret's name + decrypted value for the project
    • Validation: bad uuid → 400, missing project → 404
  2. Apply for a temporary read-token POST /api/v1/projects/<project_id>/secrets/read-token

    • Project admins only (enforced by OPA)
    • Mints a token valid for 20 minutes; raw token returned once (only its
      SHA-256 hash is stored)
  3. Two-token read: GET /secrets/values now requires — in addition to the
    session token — a valid temporary token in the X-Secret-Read-Token header,
    bound to both the project and the requesting user. Returns 401
    (missing/invalid/expired/revoked) or 403 (wrong project/user).

Authorization

Two independent gates: OPA confirms project-admin role; the handler confirms a
valid, unexpired read-token. Neither alone is sufficient. Expiry is enforced in
Python (OPA receives no clock), so revocation/expiry take effect immediately.

Database

  • New table secret_read_token (migration 00048): mirrors the mcp_token
    design, stores only the token hash.
  • Placeholder migration 00046: fills a pre-existing numbering gap
    (00045 → 00047) so migrate.py's index-based selection does not skip 00048
    on databases already at schema_version 47.

Testing

  • Static/syntax checks done; /secrets/values verified end-to-end on the test
    environment (build_3220): 200/401/403, decryption correctness, the
    Owner/Admin/Developer authorization matrix, and role up/down cycling.
  • The two-token flow and migrations are not yet verified on a live environment.
  • Unit/integration tests: TODO.

Known limitations / follow-ups

  • Platform-level devops/admin/viewer roles can pass the OPA project gate
    without being a project collaborator (pre-existing admin.rego behavior,
    shared by all project endpoints). Consider a handler-side collaborator
    re-check as a follow-up.
  • The 00046 placeholder assumes production schema_version is 47; a more
    robust fix would make migrate.py select migrations by number, not list index.

Jiachen Fan added 2 commits July 16, 2026 16:33
…alues

Add GET /api/v1/projects/<project_id>/secrets/values returning each
secret's name and decrypted plaintext value for a project.

- New SecretValues resource: validates project uuid (400), returns 404
  when the project does not exist, decrypts each secret value.
- New OPA rule allowing GET on the .../secrets/values path, reusing the
  existing project-administrator gate (user token + role >= 20).

WARNING: this endpoint exposes plaintext secret values; access is
restricted to project administrators by the OPA policy.
…alues behind a temporary read-token

Adds an apply-first, time-limited two-factor gate on reading plaintext
secret values.

- GET .../secrets/values: returns name + decrypted value per secret
  (400 bad uuid, 404 missing project).
- POST .../secrets/read-token: project admins mint a 20-minute token;
  raw token returned once, only its SHA-256 hash stored (secret_read_token
  table, migration 00048).
- Reading values now also requires a valid, unexpired read-token in the
  X-Secret-Read-Token header, bound to the project and the requesting user.
- OPA rule for the new POST path reusing the project-administrator gate.
- Placeholder migration 00046 fills a pre-existing numbering gap so
  migrate.py does not skip 00048 on databases at schema_version 47.
- Expiry computed in Python (naive UTC) to stay consistent with the
  validation clock regardless of DB session timezone; last_used_at tracked
  best-effort.
Jiachen Fan added 4 commits July 22, 2026 10:15
secret_read_token has a FK to project(id). Postgres refuses to TRUNCATE
project while a referencing table is not also truncated, which raised
'cannot truncate a table referenced in a foreign key constraint' on the
first setUp TRUNCATE, poisoning the shared test connection and cascading
InFailedSqlTransaction into all subsequent api-tests. Truncate the child
table first.
The registry-auth test (also used by the docker-registry job) does
TRUNCATE project in setUp; secret_read_token's FK to project blocked it
with the same 'cannot truncate a table referenced in a foreign key
constraint' error. Truncate the child table first, mirroring the api
test_template fix.
Postgres refuses TRUNCATE project as long as secret_read_token's FK
references it, regardless of prior separate truncates. Splitting into two
TRUNCATE statements does not help; they must be truncated in a single
statement (TRUNCATE secret_read_token, project), matching the HINT
postgres emits and the api test_template approach.
Third and final test suite (infrabox/test-registry/test.py, used by the
docker-registry job) also did a standalone TRUNCATE project blocked by
secret_read_token's FK. Merge into a single TRUNCATE statement. Verified
via repo-wide search that no standalone 'TRUNCATE project' remains.
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