Skip to content

feat(api-keys): add multi-tenant persistence layer - #2159

Draft
nvaghela-oss wants to merge 2 commits into
feat/multi-tenant-api-keysfrom
feat/api-keys-multi-tenant-persistence
Draft

nvaghela-oss wants to merge 2 commits into
feat/multi-tenant-api-keysfrom
feat/api-keys-multi-tenant-persistence

Conversation

@nvaghela-oss

@nvaghela-oss nvaghela-oss commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

TL;DR

Adds the Java persistence layer for the multi-tenant API Keys tables: models, Spring Data repositories, DAOs, and Testcontainers integration tests. Targets feat/multi-tenant-api-keys, which already has the schema from #2157.

Additional Details (optional for docs, build, test, refactor, ci, chore, style, and revert PRs)

#2157 adds the Cassandra tables. The service cannot read or write them until there is a persistence layer. This PR adds that layer. It does not change any API behavior or existing write paths.

What changed:

  • keys gains an nca_id column. KeyModel and KeyVo both get ncaId because EncryptedModelConverter maps each model column to a same-named value-object field. nca_id is a plain column, not part of the encrypted payload. Old rows read it as null.
  • keys_by_account_owner_and_service gets KeyByAccountOwnerAndServiceModel, a @ValueObject, a converter wrapper that applies expiration status on read, and AccountKeysDao:
    • save writes the keys hash row and the account row in one logged batch.
    • get and list are partition lookups by account, owner, and optional issuer.
    • listByAccount and listByAccountAndService page through the storage-attached indexes and return AccountKeysSliceVo. This follows the Cloud Tasks paging pattern: the cursor is the hex-encoded Cassandra paging state, and cursor and limit are set only when more rows remain. A malformed cursor raises BadRequestException. Other query failures are passed through unchanged.
    • delete removes both rows in one batch.
  • owner_status_by_account and owner_status_by_account_and_service get models, repositories, and AccountOwnerStatusDao. Saves keep created_at and refresh updated_at. getEffectiveStatus returns SUSPENDED if the owner is suspended for the whole account or for the issuer service. A missing row means ACTIVE.
  • key_operations_by_id gets KeyOperationModel and KeyOperationsDao. create assigns an ID, sets PENDING status and zero counters, and uses IF NOT EXISTS so it never overwrites an existing operation. update writes progress and refreshes updated_at.

Limitations:

  • The legacy keys_by_owner_and_service write path is unchanged. KeysDao still does not write nca_id or account rows. Dual-write and read switching are follow-up work.
  • operation and actor_type are plain strings for now. Their values are not defined yet. operation_status uses a small lifecycle enum: PENDING, RUNNING, COMPLETED, FAILED.
  • Owner-status saves read and then write. Two concurrent first writes can race on created_at. The status value is still last-write-wins.

For the Reviewer

Start with AccountKeysDao (batching and cursor handling) and the KeyVo.ncaId change. Every existing key write now goes through a model that has an nca_id column. Spring Data skips null columns on insert, so single-tenant writes still work on clusters that already applied 04_add_multi_tenant_schema.up.sql.

Deployments must apply the 04 migration from #2157 before this code starts reading nca_id.

For QA (optional for docs, build, test, refactor, ci, chore, style, and revert PRs)

Ran bazel test //src/control-plane-services/api-keys/... with local Docker. tests_coverage, image_contract_test, and notice_check_test pass. All 326 JUnit tests pass. DAO integration tests run on Cassandra 5 through Testcontainers.

The new tests cover:

  • encrypted round trip, raw nca_id in CQL, and no plaintext in key_details
  • required nca_id, and repeated saves keeping one row with the latest status
  • account, owner, and issuer isolation, and full primary key lookups
  • paging across owners, exact slice boundaries, cursor and limit only when more rows remain, and empty results
  • invalid cursors as bad requests, and query failures without a cursor passed through
  • batch not applied, and rows that cannot be read back (unit tests with mocks)
  • delete by primary key only, removing only that key
  • expired status on read, and suspended keys staying suspended past expiry
  • the legacy KeysDao path writing no nca_id and no account row
  • account and service suspension precedence, reactivation, and per-owner scope
  • created_at kept on update
  • operation defaults, supplied IDs kept, no overwrite of an existing operation, cleared paging state, and frozen sets

No manual QA is needed. There is no API change.

Issues

Closes #2051

Relates to #2048

Checklist

  • I am familiar with the Contributing Guidelines.
  • I have signed off my commits for Developer Certificate of Origin (DCO) compliance.
  • New or existing tests cover these changes.
  • The documentation is up to date with these changes.

@nvaghela-oss
nvaghela-oss requested a review from a team as a code owner September 29, 2026 06:11
@coderabbitai

coderabbitai Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

Comment @coderabbitai help to get the list of available commands.

@nvaghela-oss
nvaghela-oss force-pushed the feat/api-keys-multi-tenant-schema branch from 0aee203 to 6c8c8c8 Compare September 29, 2026 21:18
@nvaghela-oss
nvaghela-oss requested a review from a team as a code owner September 29, 2026 21:18
@nvaghela-oss
nvaghela-oss force-pushed the feat/api-keys-multi-tenant-persistence branch from 13d7c44 to 847db42 Compare September 29, 2026 22:44
@nvaghela-oss
nvaghela-oss changed the base branch from feat/api-keys-multi-tenant-schema to feat/multi-tenant-api-keys September 29, 2026 22:44
@nvaghela-oss nvaghela-oss self-assigned this Sep 29, 2026
@nvaghela-oss
nvaghela-oss marked this pull request as draft September 29, 2026 22:48
@nvaghela-oss
nvaghela-oss force-pushed the feat/api-keys-multi-tenant-persistence branch from 847db42 to a161c01 Compare September 30, 2026 00:56
Add Spring Data Cassandra models, repositories, and DAOs for the
multi-tenant API Keys tables:

- keys.nca_id on KeyModel and KeyVo
- keys_by_account_owner_and_service with encrypted key details, partition
  lookups, and paged account and issuer scans on the storage-attached indexes
- owner_status_by_account and owner_status_by_account_and_service with an
  effective-status helper
- key_operations_by_id for bulk operation progress and paging state

Add Testcontainers integration tests for each DAO.

Closes #2051

Signed-off-by: Nilesh Vaghela <[email protected]>
…sor paging

Match the Cloud Tasks paging pattern: account key listings return
AccountKeysSliceVo with a hex paging-state cursor and limit set only when
more rows remain. A malformed cursor is reported as a bad request.

Add unit and integration coverage for batch writes, deletes, paging
boundaries, invalid cursors, effective owner status, and key operations.

Relates to #2051

Signed-off-by: Nilesh Vaghela <[email protected]>
@nvaghela-oss
nvaghela-oss force-pushed the feat/api-keys-multi-tenant-persistence branch from a161c01 to c7e241c Compare September 30, 2026 08:04
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