kms + multi-tenancy#78
Open
JimA-cyborg wants to merge 9 commits into
Open
Conversation
There was a problem hiding this comment.
⚠️ API Contract Change Detected
This PR modifies the public API contract of the CyborgDB Python SDK.
Please provide an explanation for this change:
- Why is this change necessary?
- Is this a breaking change or backward compatible?
- Have you updated the documentation?
This review must be dismissed or addressed before the PR can be merged.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
KMS + Multi-Tenancy: per-index KMS routing
Summary
Tracks cyborgdb-service's Phase 1 per-index KMS routing slice in the Python
SDK. Regenerates the OpenAPI client against
openapi.jsonv0.16.1, makesindex_keyoptional across all data-plane request models, adds a newkms_nameparameter toClient.create_index, and ships offline + liveBYOK tests covering the three KMS posture variants.
End-state: an SDK caller can create and re-open a fully KMS-backed index
without ever holding the data key, while the legacy SDK-supplied-key path
continues to work unchanged.
Motivation
The service moved to per-index KMS routing: each index now carries a
KMSBlobdescribing whichkms.registryslot wraps its KEK(
provider: aws-kms,aws, ornone). The Python SDK was still sendingthe SDK-held key on every request, which:
using the SDK at all.
even when the service is fully capable of resolving it.
This PR brings the Python SDK to parity with the Go SDK port (cyborgdb-go
PR #27) and the spec already shipped in cyborgdb-service.
Changes
openapi.jsonbumped to v0.16.1;openapitools.jsonadded to pin
openapi-generator-clito 7.22.0 for reproducible regenerations.update-openapi-client.sh: prefers npm-distributedopenapi-generator-cli,falls back to brew's
openapi-generator.cyborgdb/openapi_client/:kms_nameadded toCreateIndexRequest;index_keyflipped toOptional[StrictStr]acrossall request models. Stale
index_config.py+ 4×index_ivf*_model.pyremoved (no longer in the spec).
cyborgdb/client/client.py:_validate_index_keyhelper.Client.create_indexrewritten to the three-mode contract:index_keyonly /
kms_nameonly / both (the latter is only valid against aprovider: noneregistry slot, enforced server-side).Client.load_index'sindex_keyis nowOptional[bytes]; absent forKMS-backed indexes.
create_indexbuilds theEncryptedIndexhandle first (no network I/O)and reuses its cached hex for the request, so the key is hex-encoded
exactly once; the now-unused
import binasciiwas dropped.cyborgdb/client/encrypted_index.py:__init__acceptsOptional[bytes]; the hex encoding is pre-computedonce and cached on
self._index_key_hex._key_to_hex()returnsOptional[str]; the ~13 callsites aretransparent through the now-optional request models.
index_type/index_configgetters no longer swallowApiExceptionand return
"unknown"/{}— they let it propagate so amissing/inaccessible index fails at
load_indextime instead ofreturning a phantom handle. Caller-visible behavior change (see
Risk assessment).
_ior()helper for the repeated name+keyIndexOperationRequestconstruction; the four describe/delete callsites now route through it.
tests/test_api_contract.py: signature expectations updated for thenew
kms_nameparameter + new defaults; existingtest_08_client_create_indexswitched to keyword args; appended
TestSDKConstructionOffline(9 offlinetests covering optional-key, KMS-only, and provider:none mixed-mode paths).
Model imports live in the file header.
tests/test_kms_byok.py(new): env-gated live BYOK round-trips forall three postures (
TestKMSReal/TestKMSSecretsManager/TestProviderNone), sharing a_KMSRoundTripBasedata-plane mixin. PlusTestKMSRealRejectsSDKKey, a negative test asserting thatindex_key+kms_nameagainst a real-provider slot is rejected (server400 surfaced as
ValueError) — the SDK forwards both fields untouched.README.md: new "Bring Your Own Key (BYOK) via KMS" section underAdvanced Usage; "Flexible Indexing" feature bullet replaced with
"Encrypted DiskIVF Indexing" (matches actual core capabilities).
Testing
pytest tests/test_api_contract.py::TestSDKConstructionOffline -v→ 9/9 pass.
pytest tests/ --collect-only -q→ 129 tests, noimport errors.
tests/test_kms_byok.py) is env-gated onCYBORGDB_KMS_NAME_REAL,CYBORGDB_KMS_NAME_SM,CYBORGDB_KMS_NAME_NONE.Skip cleanly when unset. Needs to be run against a cyborgdb-service
instance with a configured
kms.registry. Not yet verified on thisbranch — same CI gap as the Go side (called out in §9 of
python.md).inspect.signature(Client.create_index)→ ordered(index_name, index_key, kms_name, dimension, embedding_model, metric, storage_precision)with
Nonedefaults for everything pastindex_name.Risk assessment
Blast radius:
kms_nameis new and optional;index_keyflips from required to optional. Existing callers passingindex_keykeep working unchanged — the service treats key-only requestsexactly as before (
provider: none, SDK-supplied KEK).Client.create_index/load_indexparameterpositions are stable for callers using kwargs. Callers passing
client.create_index(name, key, dim)positionally still work(
kms_nameslotted afterindex_key, beforedimension). Mixed-positional-and-keyword callers passing
dimensionpositionally(
client.create_index("x", k, 128)) break: 128 now binds tokms_name. We don't expect this in practice (most callers usekwargs), but worth flagging.
touched). The only semantic delta is in the two schemas the spec changed.
the service still routes them through their stored
KMSBlob.index_type/index_confignow raise. Readingindex.index_typeorindex.index_configdirectly now raisesApiExceptionwhen the describe call fails (missing index, auth error,service down) instead of silently returning
"unknown"/{}. Auditedevery caller across
cyborgdb/,tests/, andintegrations/— nonerelied on the old sentinels. Worth a changelog note for downstream
consumers reading these properties directly.
Rollback plan:
git revert <merge>and re-publish. The spec is already v0.16.1 oncyborgdb-service so the service will still accept v0.16.0-style
requests (additive change), meaning a downgrade of just the SDK is safe.
client.py+encrypted_index.pyto restore mandatory
index_key, leaving the regenerated openapi_clientintact. Existing callers continue to work.
Reviewers, please focus on:
load_indexsilent-existence-check (client.py:252—_ = index.index_type).EncryptedIndex.index_typeswallowsApiExceptionand returns"unknown",so this line doesn't actually validate the index exists. Pre-existing on
main, but KMS-backed loads now lean on it harder. Decide: letApiExceptionpropagate, or add an explicit assertion.client.py::create_indexdocstring).Confirm the documented behavior matches your expectation, especially the
index_key + kms_namecase forprovider: noneslots vs. the 400 forreal-KMS slots.
kms_nameparameterslots between
index_keyanddimension. Anyone callingclient.create_index(name, key, dim, ...)positionally will silently binddimtokms_name. Is this acceptable, or should we putkms_nameafterstorage_precision?IndexOperationRequest(...)construction inencrypted_index.py(4 occurrences). Worth a_ior()helper, or leavealone as pre-existing scope-creep?
tests/test_kms_byok.pyordering coupling.setUpClass+type(self).index = indexassumes
test_01_*runs beforetest_03_*/test_04_*. Port of the Go SDK'sshape — should we add explicit ordering, or rely on unittest's
alphabetical default?
python.md(635 lines) intended to ship with the package? It'sthe internal porting guide. If not, add to
.gitignore+MANIFEST.inexclusions.
test_kms_byok.py. Same gap exists on the Go side— we need a CI slot wiring the three
CYBORGDB_KMS_NAME_*envs againsta configured
cyborgdb.yaml. Acceptable as follow-up, or block on it?