Skip to content

feat(python): expose Permissions and remaining user auth methods#3727

Open
ethanlin01x wants to merge 13 commits into
apache:masterfrom
ethanlin01x:feat/python-permissions
Open

feat(python): expose Permissions and remaining user auth methods#3727
ethanlin01x wants to merge 13 commits into
apache:masterfrom
ethanlin01x:feat/python-permissions

Conversation

@ethanlin01x

Copy link
Copy Markdown
Contributor

Which issue does this PR address?

Closes #3726

Rationale

The Python SDK could create users but not grant them access to anything: create_user hardcoded None for permissions, and update_permissions, change_password, and logout_user` were unexposed.

What changed?

Before, the Python SDK could create users but not grant them access to anything: create_user hardcoded None for permissions, and update_permissions, change_password, and logout_user were unexposed.
Now the Rust Permissions hierarchy is mirrored as PyO3 classes, create_user takes an optional permissions argument, UserInfoDetails exposes a permissions getter, and the three missing methods complete the UserClient surface, with the underlying Rust client handling the wire encoding.

Local Execution

  • Passed
  • Pre-commit hooks ran

AI Usage

Claude was used to help generate and review this PR and all the changes are checked by the human.

@ethanlin01x
ethanlin01x force-pushed the feat/python-permissions branch 2 times, most recently from 63a8908 to 84a9f3b Compare July 21, 2026 16:05
Mirror the Rust Permissions, GlobalPermissions, StreamPermissions, and
TopicPermissions types as PyO3 classes so user permissions can be built
and inspected from Python. Stream and topic dict keys are typed u32 to
match the wire format, so out-of-range IDs fail at construction instead
of being silently truncated. The client methods that consume these
classes follow in the next commits.
create_user hardcoded None for permissions, so every user came out
unprivileged and UserInfoDetails gave no way to inspect grants. Wire the
Permissions argument through create_user and add the permissions getter
so grants round-trip through get_user. The credential helpers shared by
the user tests move to tests/utils.py for the new enforcement tests.
Permissions set at creation were frozen: there was no way to grant,
replace, or revoke them afterwards. update_permissions is a full
replacement and None clears the permissions entirely, matching the Rust
client semantics.
Password rotation required deleting and recreating the user, losing its
id and permissions. change_password verifies the current password
server-side, and a user can rotate its own credentials without any admin
permission.
Sessions could only be abandoned by dropping the connection, leaving the
server-side session authenticated until the socket closed. logout_user
ends the session explicitly; a later login_user on the same client
starts a fresh one.
@ethanlin01x
ethanlin01x force-pushed the feat/python-permissions branch from 84a9f3b to 22bc32c Compare July 21, 2026 16:11
@codecov

codecov Bot commented Jul 21, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.70782% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 74.38%. Comparing base (a5001d3) to head (364162a).
⚠️ Report is 11 commits behind head on master.

Files with missing lines Patch % Lines
foreign/python/src/permissions.rs 95.67% 8 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##             master    #3727      +/-   ##
============================================
+ Coverage     74.34%   74.38%   +0.03%     
  Complexity      950      950              
============================================
  Files          1303     1304       +1     
  Lines        148712   148954     +242     
  Branches     124225   124225              
============================================
+ Hits         110562   110796     +234     
- Misses        34673    34681       +8     
  Partials       3477     3477              
Components Coverage Δ
Rust Core 74.57% <ø> (ø)
Java SDK 62.64% <ø> (ø)
C# SDK 72.28% <ø> (ø)
Python SDK 93.10% <96.70%> (+0.82%) ⬆️
PHP SDK 84.52% <ø> (ø)
Node SDK 92.70% <ø> (ø)
Go SDK 43.08% <ø> (ø)
Files with missing lines Coverage Δ
foreign/python/src/client.rs 99.21% <100.00%> (+0.06%) ⬆️
foreign/python/src/lib.rs 100.00% <100.00%> (ø)
foreign/python/src/user.rs 80.43% <100.00%> (+1.36%) ⬆️
foreign/python/src/permissions.rs 95.67% <95.67%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@ethanlin01x
ethanlin01x marked this pull request as ready for review July 21, 2026 16:23
@ethanlin01x

Copy link
Copy Markdown
Contributor Author

/ready

@github-actions github-actions Bot added the S-waiting-on-review PR is waiting on a reviewer label Jul 21, 2026

@slbotbm slbotbm left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

first round

Comment thread foreign/python/src/permissions.rs Outdated
#[new]
#[pyo3(signature = (global_=None, streams=None))]
fn new(
#[gen_stub(override_type(type_repr = "GlobalPermissions | None"))] global_: Option<

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why global_? why not global?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

global is a reserved keyword in Python.
The trailing underscore follows the PEP 8 convention for names that collide with keywords (like class_). An alternative would be global_permissions, but I kept global_ to mirror the Rust field name. Let me know if you'd prefer the longer name.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

let's go with global_permissions

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Renamed, 44b214c
One question: should streams / topics keep mirroring the Rust field names, or would you prefer stream_permissions / topic_permissions for symmetry?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Let's remove the following tests:

  • test_all_global_permission_flags_set_together
  • test_all_stream_permission_flags_set_together
  • test_all_topic_permission_flags_set_together

they do not add much to the current test set.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Let us add the following tests:

  • User without manage_users cannot update another user’s permissions.
  • User with manage_users can update another user’s permissions.
  • Failed update leaves target permissions unchanged.
  • calling logout twice fails

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Removed, 41497da

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added tests, 47fcb74

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Let's also add the following tests:

  • User without manage_users cannot change another user's password.
  • User with manage_users can change another user's password.
  • Failed unauthorized change leaves the old credentials valid.

@ethanlin01x ethanlin01x Jul 26, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added, 364162a. The "cannot change" and "old credentials stay valid" cases share one test (test_failed_password_change_leaves_target_credentials_valid), since the former's setup and assertions are a strict subset of the latter's.

Comment on lines +194 to +196
async def test_create_user_without_permissions_has_none(
self, iggy_client: IggyClient, unique_name
):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

test_create_user_without_permissions_has_none is valid but need not be a separate integration test. Add permissions is None assertions to existing test_create_and_get_user.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed, da2c17f

@github-actions github-actions Bot added S-waiting-on-author PR is waiting on author response and removed S-waiting-on-review PR is waiting on a reviewer labels Jul 23, 2026
@ethanlin01x
ethanlin01x requested a review from slbotbm July 23, 2026 15:56
@ethanlin01x

Copy link
Copy Markdown
Contributor Author

/ready

@github-actions github-actions Bot added S-waiting-on-review PR is waiting on a reviewer and removed S-waiting-on-author PR is waiting on author response labels Jul 23, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Let's also add the following tests:

  • User without manage_users cannot change another user's password.
  • User with manage_users can change another user's password.
  • Failed unauthorized change leaves the old credentials valid.

Comment thread foreign/python/src/client.rs Outdated
/// Raises:
/// PyValueError: If a string identifier is invalid.
/// PyRuntimeError: If the request fails.
#[pyo3(signature = (user_id, permissions=None))]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Let's make the permission explicit with #[pyo3(signature = (user_id, permissions))] so that clearing the permissions will have to be explicitly specified with None.

@ethanlin01x ethanlin01x Jul 26, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done, 306a0dc.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

GlobalPermissions, StreamPermissions, and TopicPermissions accept positional booleans. Ten adjacent booleans make this legal and dangerous:

GlobalPermissions(False, False, True, ...)

A shifted argument can grant the wrong privilege without any error. These are new security-sensitive APIs. Add * to their PyO3 signatures so permission flags must be named. Existing tests already use keyword arguments.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Empty streams and topics dictionaries are preserved locally but cannot be represented distinctly on the wire. Both become empty vectors and decode as None, so Permissions(streams={}) and StreamPermissions(topics={}) fail round-trip equality. Please normalize empty dictionaries to None in these constructors and cover both cases.

@ethanlin01x ethanlin01x Jul 26, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

  • Keyword-only flags added in 306a0dc
  • Empty dicts normalized to None in 1ff9e5f

Comment on lines +114 to +124
/// Args:
/// manage_servers: Allow managing servers; includes `read_servers`.
/// read_servers: Allow reading server info (stats, clients).
/// manage_users: Allow managing users; includes `read_users`.
/// read_users: Allow reading user info.
/// manage_streams: Allow managing all streams; includes `manage_topics`.
/// read_streams: Allow reading all streams; includes `read_topics`.
/// manage_topics: Allow managing all topics; includes `read_topics`.
/// read_topics: Allow reading all topics and consumer groups.
/// poll_messages: Allow polling messages from all streams.
/// send_messages: Allow sending messages to all streams.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This documentation does not actually describe permission inheritance:

  • read_topics also permits polling message contents.
  • manage_topics permits polling and sending messages.
  • manage_streams inherits topic management and therefore permits
    polling and sending.
  • Stream read_topics and topic read_topic permit polling.
  • Stream and topic management permissions permit sending.

@ethanlin01x ethanlin01x Jul 26, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed, aff6f0f.
Documented the direct inclusion edges per the server permissioner rules with a note that they apply transitively. Also covered a few grants beyond the listed ones: read_topics / read_topic additionally allow managing (including creating and deleting) consumer groups, poll_messages allows managing consumer offsets, and stream-level read_stream also permits polling via read_topics.

@github-actions github-actions Bot added S-waiting-on-author PR is waiting on author response and removed S-waiting-on-review PR is waiting on a reviewer labels Jul 26, 2026
The trailing underscore avoided the Python keyword collision but reads
poorly at call sites. Reviewer settled on the explicit long name.
@ethanlin01x
ethanlin01x requested a review from slbotbm July 26, 2026 10:10
@ethanlin01x

Copy link
Copy Markdown
Contributor Author

/ready

@github-actions github-actions Bot added S-waiting-on-review PR is waiting on a reviewer and removed S-waiting-on-author PR is waiting on author response labels Jul 26, 2026
@ethanlin01x
ethanlin01x force-pushed the feat/python-permissions branch from 62507bf to e1f5e2e Compare July 26, 2026 10:11
The wire format encodes an empty streams or topics map identically to
an absent one, so a Permissions(streams={}) fetched back from the
server compared unequal to what was stored. Collapse empty maps to
None at construction to keep round-trip equality.
update_permissions defaulted permissions to None, so forgetting the
argument silently wiped a user's permissions; clearing now requires an
explicit None. The permission constructors accepted up to ten adjacent
positional booleans, where a shifted argument grants the wrong
privilege without any error; the flags are now keyword-only.
The flag docs only stated same-level inclusion, hiding that read and
manage flags cascade down to message operations: read_topics permits
polling message contents, manage_topics permits polling and sending,
and manage_streams inherits both through manage_topics. Document the
direct inclusion edges per the server permissioner rules, note that
they apply transitively, and spell out the grants hidden behind read
flags: consumer group management under read_topics and consumer
offset management under poll_messages.
A user with manage_users can change another user's password; a user
without it is denied and the target's old password remains valid.
@ethanlin01x
ethanlin01x force-pushed the feat/python-permissions branch from e1f5e2e to 364162a Compare July 26, 2026 11:33
@ethanlin01x

Copy link
Copy Markdown
Contributor Author

/ready

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

S-waiting-on-review PR is waiting on a reviewer

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[python sdk] Expose Permissions and the remaining user auth methods

2 participants