Conversation
permit.api.groups covers the ten GA group operations (PER-16677): list and get through the /groups/direct reads, create, delete, adding and removing users, granting and revoking roles, and making one group's members members of another. Model arguments take the model or a dict. The docstrings state which identifier forms each call accepts, the API key it needs, the direction of assign_group, and that a group's roles reach its members through ReBAC role derivation. The sync stub is regenerated, the sub-API count sentinel goes to 18, and the type-check consumer calls the new API on both clients. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each of the ten methods is called through the async and the blocking client against pytest-httpserver. The tests check the method, path, query, headers and JSON body sent, the model the response parses into, the identifier forms passed through the path, model and dict arguments sending the same body, 404 and 409 raising PermitApiError with that status, and an invalid dict being rejected before anything is sent. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Cover the ten GA group operations against the Permit API and a PDP (PER-16677): create, get and list; assign and remove a user; grant and revoke a role, checking through permit.check() that a member gets the group's role on that resource instance only; nest one group in another, checking which group's members gain the other's roles; and the 404 and 409 errors. One test drives a group through the blocking client. The tests pin which identifier forms each call takes: the resource instance id or <resource_key>:<instance_key> in the path, a bare key only for a group of the default "group" type, and the instance key or id (not the qualified form) for the group in the assign_group body. Each test creates its own tenant, resource types, users and groups with unique keys and registers every delete before the create it undoes; a 404 at teardown counts as success. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* per-16677/impl-sdk: Document the Groups API in the README Test the Groups API requests offline on both clients Add the Groups API to the async and sync clients Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* per-16677/impl-e2e: Add end-to-end tests for the Groups API Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GroupsApi.create documents a 409 for a group that already exists. The e2e test now creates the same group a second time and expects that status, so CI confirms the documented error. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The GroupsApi docstrings and the README call the qualified identifier "<type>:<key>". The e2e module now uses the same name in its parameter ids, helper docstring and comments. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The README's identifier bullet read as if every group_instance_key in the Groups API took "<type>:<key>". It applies to the first argument only. The group in the assign_group()/remove_group() body is named by its instance id or its key alone, and both groups must be of the same resource type; "<type>:<key>" there answers 404. The docstrings and the e2e tests already say this. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
assign_role() looks up the resource instance in the group's tenant and creates it there when the lookup misses. An instance key is unique within its resource type regardless of tenant, so an instance with that key in another tenant makes the create fail with 409. The docstring now says the instance must be in the group's tenant and lists the 409. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The API treats any resource type with a "member" role as a group resource type, so groups.list() also returns the instances of types that were never meant as groups. The class docstring now defines a group resource type that way, and list() says what it returns. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The 404/409 test answered with a body that is not an ErrorDetails, so the SDK raised its plain PermitApiError fallback, which a real API error never reaches. The test now answers with an API-shaped body (id, title, NOT_FOUND or DUPLICATE_ENTITY) and checks that every groups method raises PermitNotFoundError or PermitAlreadyExistsError, with the status and the body. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every GroupsApi docstring says it needs an environment-level key or a broader key with the API context set to an environment, but no test pinned it. A project-level key whose context is the project now gets PermitContextError from every groups method, on both clients, and nothing is sent. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
test_cloud_pdp_e2e.py and test_groups_e2e.py each had the same delete_quietly and the same bounded poll loop, differing only in their timeout and interval. Both now live in tests/utils.py: delete_quietly as it was, and poll_for, which takes the timeout and interval. Each module binds its own values as settled, so the call sites and the polling bounds are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Dependency Security AuditScanned: pyproject.toml dependencies + dev group, resolved at Python 3.10 (the current resolution, and the lowest versions the published specs permit under each pydantic major) ✅ No known vulnerabilities found. Both the resolved dependency set and the lowest versions the published specs permit are clean at HIGH and CRITICAL. |
* origin/per-16680/stop-logging-api-key: Invite with a role of the invited resource in the invites e2e test
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * origin/per-16680/stop-logging-api-key: Poll for the PDP's role assignment list in the RBAC e2e tests
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
zeevmoney
added this pull request to stack #145
October 2, 2026 18:24
This branch has not been deployed
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.
Linear issues
permit.api.groupswith the 10 GA group operations to both clients.Why
The SDK had no way to manage groups. Callers had to send raw requests to the
/groupsroutes, and it was unclear which group identifier each route takes.What changed
permit/api/groups.py: newGroupsApi, under/v2/schema/{proj}/{env}:list(page, per_page)GET /groups/directget(group_instance_key)GET /groups/direct/{g}create(group_data)POST /groupsdelete(group_instance_key)DELETE /groups/{g}assign_user(g, user_key, tenant)/remove_user(...)PUT/DELETE /groups/{g}/users/{user}assign_role(g, role_data)/remove_role(...)POST/DELETE /groups/{g}/rolesassign_group(g, assignment)/remove_group(...)PUT/DELETE /groups/{g}/assign_groupThe reads use the
/groups/directroutes, not the deprecatedGET /groupsandGET /groups/{key}. Model arguments take the model or an equivalent dict, like the other modules.The docstrings say which API key each method needs (environment-level, or a broader key with the API context set to the environment). They also say that a role granted to a group is a resource role on one instance, which reaches members through ReBAC role derivation and is not a tenant-wide role. For identifiers:
group_instance_key(first argument)"<type>:<key>"; a bare key, read as"group:<key>", so only for groups of thegrouptypeGroupAssignment.group_instance_key"<type>:<key>"answers 404GroupCreate.group_instance_keyGroupAddRole.resource_instanceassign_group(P, {"group_instance_key": C})makes P's members members of C, so they get C's roles; C's members get nothing from P.permit.api.groupsis wired intoPermitApiClientandSyncPermitApiClient(SyncGroupsApi), andpermit/_sync_types.pyiwas regenerated.API_SUB_API_COUNTgoes from 17 to 18, andtests/type_check/consumer.pycalls the groups API on both clients.README: a short Groups section.
Tests:
tests/test_groups_offline.py(offline wire tests) andtests/test_groups_e2e.py(e2e).tests/utils.pynow holdsdelete_quietlyandpoll_for, whichtests/test_cloud_pdp_e2e.pyandtests/test_groups_e2e.pyshare. Each module keeps its own polling bounds.Behaviour changes
permit.api.groupsonpermit.Permitandpermit.sync.Permit, plus the importable namespermit.api.groups.GroupsApiandpermit.api.sync_api_client.SyncGroupsApi. No existing behaviour changes.How it was tested
AuthorizationandContent-Typeheaders, JSON body, and the parsed return type;PermitNotFoundError/PermitAlreadyExistsErrorwith the status;PermitContextErrorbefore anything is sent;groups.py, the client wiring and the 404/409 mapping fail the tests. The one survivor, dropping the access-level check, is equivalent: every key level meets an environment-level requirement.tests/test_groups_e2e.pycollects 7 tests, which CI runs. They cover create/get/list (and a duplicate create answering 409), both identifier forms, the bare key of the default type, a group role reaching a member throughpermit.check()and being revoked, group-to-group direction, and the blocking client. Cleanup is registered at creation, and a 404 counts as success. They have not run against the API yet. Against a local fake of the API and PDP, all 7 pass on both lanes, and each of 14 deliberate fake behaviour changes makes the suite fail.uv lock --check,actionlintandzizmorare clean.Owner actions before merge
tests/test_groups_e2e.pyagainst the API for the first time. In particular,test_assign_group_makes_the_group_a_member_of_the_otherconfirms theassign_groupdirection the docstrings and README state. Thee2e (cloud PDP)job also runstests/test_cloud_pdp_e2e.pywith the shared helpers.🤖 Generated with Claude Code