Skip to content

Commit 26fe203

Browse files
jfrancoaclaude
andcommitted
feat: create db users via namespace-qualified id, drop namespace arg
The server no longer accepts a separate `namespace` field when creating a DB user; the namespace is now derived from a namespace-qualified id of the form "<namespace>:<user>" passed in the URL path (weaviate PR #11501, resolveUserKeyForCreate). Update the client to match: - users.db.create: remove the `namespace` parameter; always POST an empty body. The (optionally qualified) id is already carried in the path. Drop the now-unused 1.38 version guard tied to the namespace field. - sync/async .pyi stubs: update the create signature accordingly. - UserDB.namespace and get/list parsing are unchanged: the server still returns `namespace` in the user response for global operators (handlers_db_users.go). Tests: - mock_tests: replace the namespace-in-body tests with test_users_db_create_qualified_user_id_goes_in_path (asserts the qualified id lands in the URL path and the body stays empty) and test_users_db_create_posts_empty_body. These pin the wire contract and are the version-independent regression guard. - integration: create the namespaced user with the qualified id directly. - Bump WEAVIATE_138 to 1.38.0-rc.0-b9ea106, the first build containing PR #11501. Verified: mock + unit suites pass (34); 8/9 namespace integration tests pass against a live cluster. The qualified-create integration test requires the b9ea106 image, which was still publishing to Docker Hub at commit time; the server source at b9ea106 is git-verified to implement this contract and CI runs the live check. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent a6a58a3 commit 26fe203

6 files changed

Lines changed: 25 additions & 28 deletions

File tree

.github/workflows/main.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ env:
2929
WEAVIATE_135: 1.35.18
3030
WEAVIATE_136: 1.36.12
3131
WEAVIATE_137: 1.37.5-e0fe0d5.amd64
32-
WEAVIATE_138: 1.38.0-rc.0
32+
WEAVIATE_138: 1.38.0-rc.0-b9ea106
3333

3434
jobs:
3535
lint-and-format:

integration/test_namespaces.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -126,12 +126,12 @@ def test_create_namespaced_user(client_factory: ClientFactory) -> None:
126126
_skip_if_unsupported(client)
127127

128128
client.namespaces.create(name="usernstest")
129-
# On namespace-enabled clusters the server qualifies the userId as
130-
# "namespace:user_id" in storage. Operators must use that qualified form
131-
# when calling get/delete.
129+
# On namespace-enabled clusters an operator creates a user with a
130+
# namespace-qualified id "<namespace>:<user>"; the same qualified id is
131+
# used for get/delete.
132132
qualified_id = "usernstest:nsuser1"
133133
try:
134-
api_key = client.users.db.create(user_id="nsuser1", namespace="usernstest")
134+
api_key = client.users.db.create(user_id=qualified_id)
135135
assert isinstance(api_key, str)
136136
assert len(api_key) > 0
137137

mock_tests/test_namespaces.py

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -354,14 +354,14 @@ def test_namespaces_methods_require_1_38(
354354
# ---------------------------------------------------------------------------
355355

356356

357-
def test_users_db_create_includes_namespace_in_body(
357+
def test_users_db_create_qualified_user_id_goes_in_path(
358358
ns_client: Tuple[weaviate.WeaviateClient, HTTPServer],
359359
) -> None:
360-
"""When ``namespace`` is provided, the request body must carry it.
360+
"""A namespace-qualified user id must be sent in the URL path, not the body.
361361
362-
Without this assertion, dropping ``body['namespace'] = namespace`` from
363-
``users/base.py`` would compile and pass type-checks but silently break
364-
namespace-binding on multi-tenant clusters.
362+
The server derives the namespace from the ``"<namespace>:<user>"`` id in the
363+
path, so the request body stays empty. This guards against reintroducing a
364+
``body['namespace']`` field, which the server no longer accepts.
365365
"""
366366
client, server = ns_client
367367
captured: Dict[str, Any] = {}
@@ -370,22 +370,24 @@ def handler(request: Request) -> Response:
370370
captured["body"] = json.loads(request.get_data(as_text=True) or "{}")
371371
return Response(json.dumps({"apikey": "secret-key"}), status=201)
372372

373-
server.expect_request("/v1/users/db/alice", method="POST").respond_with_handler(handler)
373+
server.expect_request("/v1/users/db/customer1:alice", method="POST").respond_with_handler(
374+
handler
375+
)
374376

375-
api_key = client.users.db.create(user_id="alice", namespace="customer1")
377+
api_key = client.users.db.create(user_id="customer1:alice")
376378

377379
assert api_key == "secret-key"
378-
assert captured["body"] == {"namespace": "customer1"}
380+
assert captured["body"] == {}
379381
server.check_assertions()
380382

381383

382-
def test_users_db_create_omits_namespace_when_not_provided(
384+
def test_users_db_create_posts_empty_body(
383385
ns_client: Tuple[weaviate.WeaviateClient, HTTPServer],
384386
) -> None:
385-
"""The ``namespace`` key must not appear in the body when omitted by the caller.
387+
"""``create`` must POST an empty body — there is no separate ``namespace`` field.
386388
387-
Otherwise we'd send ``"namespace": null`` and break older clusters that
388-
don't recognize the field.
389+
Sending anything else (e.g. a stray ``namespace`` key) would diverge from the
390+
server contract, which expects only the (possibly qualified) id in the path.
389391
"""
390392
client, server = ns_client
391393
captured: Dict[str, Any] = {}

weaviate/users/async_.pyi

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ class _UsersDBAsync(_UsersDBExecutor[ConnectionAsync]):
5252
) -> Union[Dict[str, Role], Dict[str, RoleBase]]: ...
5353
async def assign_roles(self, *, user_id: str, role_names: Union[str, List[str]]) -> None: ...
5454
async def revoke_roles(self, *, user_id: str, role_names: Union[str, List[str]]) -> None: ...
55-
async def create(self, *, user_id: str, namespace: Optional[str] = None) -> str: ...
55+
async def create(self, *, user_id: str) -> str: ...
5656
async def delete(self, *, user_id: str) -> bool: ...
5757
async def rotate_key(self, *, user_id: str) -> str: ...
5858
async def activate(self, *, user_id: str) -> bool: ...

weaviate/users/base.py

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -360,12 +360,12 @@ def revoke_roles(
360360
USER_TYPE_DB,
361361
)
362362

363-
def create(self, *, user_id: str, namespace: Optional[str] = None) -> executor.Result[str]:
363+
def create(self, *, user_id: str) -> executor.Result[str]:
364364
"""Create a new db user and return its API key.
365365
366366
Args:
367-
user_id: The id of the new user.
368-
namespace: The namespace to bind the user to. Required on namespace-enabled clusters.
367+
user_id: The id of the new user. On namespace-enabled clusters an operator must
368+
pass a namespace-qualified id of the form ``"<namespace>:<user>"``.
369369
370370
Returns:
371371
The API key of the newly created user. This key can not be retrieved later.
@@ -376,16 +376,11 @@ def resp(res: Response) -> str:
376376
assert resp is not None
377377
return str(resp["apikey"])
378378

379-
body: Dict[str, Any] = {}
380-
if namespace is not None:
381-
self._connection._weaviate_version.check_is_at_least_1_38_0("users.db.create")
382-
body["namespace"] = namespace
383-
384379
return executor.execute(
385380
response_callback=resp,
386381
method=self._connection.post,
387382
path=f"/users/db/{user_id}",
388-
weaviate_object=body,
383+
weaviate_object={},
389384
error_msg=f"Could not create user '{user_id}'",
390385
status_codes=_ExpectedStatusCodes(ok_in=[201], error="Create user"),
391386
)

weaviate/users/sync.pyi

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ class _UsersDB(_UsersDBExecutor[ConnectionSync]):
5252
) -> Union[Dict[str, Role], Dict[str, RoleBase]]: ...
5353
def assign_roles(self, *, user_id: str, role_names: Union[str, List[str]]) -> None: ...
5454
def revoke_roles(self, *, user_id: str, role_names: Union[str, List[str]]) -> None: ...
55-
def create(self, *, user_id: str, namespace: Optional[str] = None) -> str: ...
55+
def create(self, *, user_id: str) -> str: ...
5656
def delete(self, *, user_id: str) -> bool: ...
5757
def rotate_key(self, *, user_id: str) -> str: ...
5858
def activate(self, *, user_id: str) -> bool: ...

0 commit comments

Comments
 (0)