Skip to content

NXPY-266: Align on user and group generated UID#326

Open
gitofanindya wants to merge 1 commit intomasterfrom
wip-NXPY-266-Align-on-user-and-group-generated-UID
Open

NXPY-266: Align on user and group generated UID#326
gitofanindya wants to merge 1 commit intomasterfrom
wip-NXPY-266-Align-on-user-and-group-generated-UID

Conversation

@gitofanindya
Copy link
Collaborator

@gitofanindya gitofanindya commented Feb 13, 2026

Summary by Sourcery

Introduce a username↔generated-UID translation layer in the Nuxeo Python client so requests send generated UIDs while responses expose usernames transparently.

Enhancements:

  • Add client-side caching and helper methods to resolve usernames to generated UIDs and back, including support for prefixed values and bulk lists.
  • Translate known username-carrying parameters in both URL query strings and automation operation bodies to generated UIDs before sending requests.
  • Wrap HTTP responses so JSON decoding automatically converts generated UIDs back to usernames for known response fields without changing callers.
  • Wire request/response translation into the operations API execution flow while keeping non-username parameters untouched.

Tests:

  • Add an extensive unit test suite covering UID mapping, request/response translation, NuxeoClient.request integration, and API.execute integration, including round-trip and edge cases.

@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Feb 13, 2026

Reviewer's Guide

Introduce a username ↔ generated-UID translation layer in NuxeoClient and wire it into both low-level HTTP requests and automation operations, with comprehensive unit tests validating request/response mapping and cache behavior.

Sequence diagram for request/response username_UID translation in operations

sequenceDiagram
  actor Caller
  participant Ops as NuxeoOperations
  participant Client as NuxeoClient
  participant Session as RequestsSession
  participant Resp as Response

  Caller->>Ops: execute(operation, params={userId: "alice"}, json=True)
  Ops->>Ops: get_attributes(operation, **kwargs)
  Ops->>Client: _translate_request_params(params)
  activate Client
  Client->>Client: _translate_single_username_to_uid("alice")
  Client->>Client: resolve_uid("alice")
  alt uid cached
    Client-->>Ops: params with userId="uid-alice"
  else uid not cached
    Client->>Client: _fetch_uid_for_username("alice")
    Client->>Client: userid_mapper["alice"] = "uid-alice"
    Client-->>Ops: params with userId="uid-alice"
  end
  deactivate Client

  Ops->>Ops: check_params(command, translated_params)
  Ops->>Client: request(method, path, params=translated_params, **kwargs)
  activate Client
  Client->>Client: _translate_request_params(kwargs["params"])
  Client->>Session: send HTTP request
  Session-->>Client: Resp

  Client->>Client: wrap Resp.json()
  Client-->>Ops: Resp
  deactivate Client

  Ops->>Resp: json()
  activate Resp
  Resp-->>Ops: data_with_usernames
  deactivate Resp

  Ops-->>Caller: data_with_usernames
Loading

Class diagram for NuxeoClient username_UID mapping and translation

classDiagram

class NuxeoClient {
  +Dict~str,str~ userid_mapper
  +frozenset _USERNAME_REQUEST_KEYS
  +frozenset _USERNAME_RESPONSE_KEYS
  +__init__(auth, host, scheme, proxies, verify, timeout, headers, client_kwargs)
  +disable_retry()
  +query(query, **kwargs)
  +request(method, path, **kwargs)
  +_fetch_uid_for_username(username: str) str
  +_fetch_username_for_uid(uid: str) str
  +resolve_uid(username: str) str
  +resolve_username(uid: str) str
  +_translate_request_params(params: Dict~str,Any~) Dict~str,Any~
  +_translate_single_username_to_uid(value: str) str
  +_translate_response(data: Any) Any
  +_translate_uid_value_to_username(value: Any) Any
}

class NuxeoOperations {
  +client: NuxeoClient
  +execute(operation, input_obj, params, context, check_params, json, **kwargs)
  +get_attributes(operation, **kwargs)
  +check_params(command, params)
}

NuxeoOperations --> NuxeoClient : holds_reference
NuxeoOperations ..> NuxeoClient : calls _translate_request_params
NuxeoClient ..> NuxeoClient : resolve_uid
NuxeoClient ..> NuxeoClient : resolve_username
NuxeoClient ..> NuxeoClient : _translate_request_params
NuxeoClient ..> NuxeoClient : _translate_response
Loading

File-Level Changes

Change Details Files
Add username↔UID mapping, translation helpers, and request/response wiring in NuxeoClient.
  • Introduce a userid_mapper cache and constants listing known username-carrying request keys and username-bearing response keys.
  • Implement dummy _fetch_uid_for_username and _fetch_username_for_uid helpers plus resolve_uid/resolve_username with caching and reverse lookup.
  • Add helpers to translate request params (_translate_request_params, _translate_single_username_to_uid) and recursively translate response payloads (_translate_response, _translate_uid_value_to_username).
  • Hook request() to translate outgoing URL query params and to wrap Response.json() so JSON bodies are auto-translated from UIDs back to usernames.
nuxeo/client.py
Ensure operations API goes through the username→UID request translation and keeps JSON-path unchanged.
  • Inject a call to client._translate_request_params() in API.execute() before param validation and sending.
  • Keep execute()’s JSON handling behavior but route through the now-wrapped Response.json() result.
nuxeo/operations.py
Add exhaustive unit test suite covering userid_mapper behavior and integration points in client.request() and API.execute().
  • Create helper constructors for a minimal NuxeoClient and API wired with mocks for isolated testing.
  • Cover all mapper helpers (fetch, resolve, single-value translations, dict/list translations) including edge cases and in-place mutation semantics.
  • Add integration tests verifying URL params translation, response.json wrapping, round-trips, and failure/default-path behaviors.
tests/unit/test_client_userid_mapper.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 3 issues, and left some high level feedback:

  • Consider making userid_mapper an instance attribute initialized in __init__ instead of a class attribute, otherwise all NuxeoClient instances will share a global user→UID cache which may be surprising in multi-tenant or test scenarios.
  • resolve_username does a linear scan over userid_mapper values on every cache miss; if this mapping can grow large or be heavily used, it may be worth maintaining a reverse uid -> username index to avoid O(n) lookups.
  • The updated comment in request now says "Set the default value to object to allow someone to set default to None.", but the code still concerns the timeout parameter; it would be clearer to keep the wording consistent with the actual argument name.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Consider making `userid_mapper` an instance attribute initialized in `__init__` instead of a class attribute, otherwise all `NuxeoClient` instances will share a global user→UID cache which may be surprising in multi-tenant or test scenarios.
- `resolve_username` does a linear scan over `userid_mapper` values on every cache miss; if this mapping can grow large or be heavily used, it may be worth maintaining a reverse `uid -> username` index to avoid O(n) lookups.
- The updated comment in `request` now says "Set the default value to `object` to allow someone to set `default` to `None`.", but the code still concerns the `timeout` parameter; it would be clearer to keep the wording consistent with the actual argument name.

## Individual Comments

### Comment 1
<location> `nuxeo/client.py:93` </location>
<code_context>

+    # Cache mapping username -> generated UID.
+    # Populated lazily on first encounter via resolve_uid / resolve_username.
+    userid_mapper = {}  # type: Dict[str, str]
+
+    # --- Known param keys that carry a username value in REQUESTS ---
</code_context>

<issue_to_address>
**issue (bug_risk):** Shared class-level userid_mapper cache may cause cross-client leakage and threading issues.

Because `userid_mapper` is a mutable class attribute, all `NuxeoClient` instances share the same mapping. This risks leaking username→UID mappings across clients (e.g., with different auth contexts) and is not thread-safe. Prefer an instance attribute initialized in `__init__` so each client maintains its own cache.
</issue_to_address>

### Comment 2
<location> `tests/unit/test_client_userid_mapper.py:499-508` </location>
<code_context>
+    def test_response_is_translated(self):
</code_context>

<issue_to_address>
**issue (testing):** This test name and docstring suggest UID→username translation, but the assertions only validate that `execute()` calls `resp.json()`.

Since `client.request` is mocked at the API level, the `NuxeoClient.request()` wrapper (which patches `resp.json` to call `_translate_response`) isn’t exercised. The test only checks that `execute()` returns the mock JSON, not that translation occurs. Either rename the test/docstring to match this behavior, or use a real `NuxeoClient.request()` (as in `TestRequestIntegration`) so you can assert on translated fields (e.g. `lockOwner == 'admin'`).
</issue_to_address>

### Comment 3
<location> `tests/unit/test_client_userid_mapper.py:267-273` </location>
<code_context>
+        result = client._translate_request_params(params)
+        assert result["delegatedActors"] == ["user:uid-carol"]
+
+    def test_users_key_scalar(self):
+        client = _make_client()
+        params = {"users": "alice"}
+        result = client._translate_request_params(params)
+        assert result["users"] == "uid-alice"
+
+    def test_non_string_non_list_value_passthrough(self):
</code_context>

<issue_to_address>
**suggestion (testing):** Missing coverage for list-valued `"users"` (and similar) params in `_translate_request_params`.

You already test scalar `"users"` and list-valued `"actors"` / `"delegatedActors"`, but `_translate_request_params` supports list values for all `_USERNAME_REQUEST_KEYS`. Please add coverage for list-valued `"users"`, e.g.:

```python
params = {"users": ["alice", "bob"]}
result = client._translate_request_params(params)
assert result["users"] == ["uid-alice", "uid-bob"]
```

Optionally, add a similar list-valued test for `"user"` to verify consistent behavior across all username keys.

```suggestion
    def test_users_key_scalar(self):
        client = _make_client()
        params = {"users": "alice"}
        result = client._translate_request_params(params)
        assert result["users"] == "uid-alice"

    def test_users_key_list(self):
        client = _make_client()
        params = {"users": ["alice", "bob"]}
        result = client._translate_request_params(params)
        assert result["users"] == ["uid-alice", "uid-bob"]

    def test_user_key_list(self):
        client = _make_client()
        params = {"user": ["alice", "bob"]}
        result = client._translate_request_params(params)
        assert result["user"] == ["uid-alice", "uid-bob"]

    def test_non_string_non_list_value_passthrough(self):
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.


# Cache mapping username -> generated UID.
# Populated lazily on first encounter via resolve_uid / resolve_username.
userid_mapper = {} # type: Dict[str, str]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Shared class-level userid_mapper cache may cause cross-client leakage and threading issues.

Because userid_mapper is a mutable class attribute, all NuxeoClient instances share the same mapping. This risks leaking username→UID mappings across clients (e.g., with different auth contexts) and is not thread-safe. Prefer an instance attribute initialized in __init__ so each client maintains its own cache.

Comment on lines +499 to +508
def test_response_is_translated(self):
"""Response JSON with known keys is translated by resp.json() wrapper."""
api, client = _make_api()
client.userid_mapper["admin"] = "uid-admin"

mock_response = MagicMock()
mock_response.json.return_value = {
"entity-type": "document",
"uid": "doc-1",
"lockOwner": "uid-admin",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (testing): This test name and docstring suggest UID→username translation, but the assertions only validate that execute() calls resp.json().

Since client.request is mocked at the API level, the NuxeoClient.request() wrapper (which patches resp.json to call _translate_response) isn’t exercised. The test only checks that execute() returns the mock JSON, not that translation occurs. Either rename the test/docstring to match this behavior, or use a real NuxeoClient.request() (as in TestRequestIntegration) so you can assert on translated fields (e.g. lockOwner == 'admin').

Comment on lines +267 to +273
def test_users_key_scalar(self):
client = _make_client()
params = {"users": "alice"}
result = client._translate_request_params(params)
assert result["users"] == "uid-alice"

def test_non_string_non_list_value_passthrough(self):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Missing coverage for list-valued "users" (and similar) params in _translate_request_params.

You already test scalar "users" and list-valued "actors" / "delegatedActors", but _translate_request_params supports list values for all _USERNAME_REQUEST_KEYS. Please add coverage for list-valued "users", e.g.:

params = {"users": ["alice", "bob"]}
result = client._translate_request_params(params)
assert result["users"] == ["uid-alice", "uid-bob"]

Optionally, add a similar list-valued test for "user" to verify consistent behavior across all username keys.

Suggested change
def test_users_key_scalar(self):
client = _make_client()
params = {"users": "alice"}
result = client._translate_request_params(params)
assert result["users"] == "uid-alice"
def test_non_string_non_list_value_passthrough(self):
def test_users_key_scalar(self):
client = _make_client()
params = {"users": "alice"}
result = client._translate_request_params(params)
assert result["users"] == "uid-alice"
def test_users_key_list(self):
client = _make_client()
params = {"users": ["alice", "bob"]}
result = client._translate_request_params(params)
assert result["users"] == ["uid-alice", "uid-bob"]
def test_user_key_list(self):
client = _make_client()
params = {"user": ["alice", "bob"]}
result = client._translate_request_params(params)
assert result["user"] == ["uid-alice", "uid-bob"]
def test_non_string_non_list_value_passthrough(self):

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

Comments