Skip to content

Add admin role and policy management APIs#4782

Closed
drernie wants to merge 2 commits intomasterfrom
codex/create-role-admin-client
Closed

Add admin role and policy management APIs#4782
drernie wants to merge 2 commits intomasterfrom
codex/create-role-admin-client

Conversation

@drernie
Copy link
Copy Markdown
Member

@drernie drernie commented Mar 25, 2026

Summary

  • add admin role create/update/delete/get/set-default helpers and a new admin policies module
  • add the GraphQL operations and regenerate the quilt3 admin client bindings
  • extend admin API tests and document the new surface in the changelog

Verification

  • uv run pytest tests/test_admin_api.py
  • uv run ruff check quilt3/admin tests/test_admin_api.py

Greptile Summary

This PR expands the quilt3.admin Python module with full CRUD operations for roles (managed and unmanaged) and a new policies module, plus set_default for roles. New GraphQL queries and mutations are added to queries.graphql, the ariadne-codegen-generated client is regenerated accordingly, thin Python wrappers are added in admin/roles.py and admin/policies.py, and the test suite is extended with parametrized happy-path and error-path coverage.

Key observations:

  • The overall architecture is consistent with existing admin API patterns (pydantic dataclasses, discriminated unions for GraphQL union types, per-error exception classes).
  • raise_invalid_input and raise_operation_error in util.py always raise but are missing -> NoReturn annotations, which causes type-checker noise in the callers (_handle_policy_result, policies.delete).
  • roles.delete() silently falls through to generic Quilt3AdminError for RoleAssigned and RoleNameUsedBySsoConfig responses, which the generated client explicitly models but the test suite does not exercise.
  • The defaultRoleGet GraphQL query and generated client method are added in this PR but no corresponding public admin API function (roles.get_default()) is implemented or tested — the generated code is effectively dead.

Confidence Score: 4/5

  • PR is safe to merge; all three issues are non-blocking P2 style/completeness concerns with no runtime impact.
  • The new role and policy admin APIs are well-structured, follow existing patterns, and are covered by comprehensive parametrized tests. The only issues found are missing NoReturn annotations (type-checker noise, not a runtime bug), two unhandled but gracefully caught delete error variants, and a generated-but-unused defaultRoleGet query. None of these affect production behaviour.
  • api/python/quilt3/admin/util.py (NoReturn annotations), api/python/quilt3/admin/roles.py (RoleAssigned/RoleNameUsedBySsoConfig handling), api/python/quilt3-graphql/queries.graphql (unused defaultRoleGet query)

Important Files Changed

Filename Overview
api/python/quilt3/admin/roles.py New role CRUD + set-default helpers; unhandled RoleAssigned/RoleNameUsedBySsoConfig cases in delete() fall through to generic error.
api/python/quilt3/admin/policies.py New policy CRUD helpers; error dispatch is sound but relies on raise_invalid_input/raise_operation_error being NoReturn (not yet annotated).
api/python/quilt3/admin/util.py New raise_invalid_input and raise_operation_error helpers always raise but lack -> NoReturn annotations, causing downstream type-checking noise.
api/python/quilt3/admin/types.py Adds Permission and Policy pydantic dataclasses with parse helpers; logic is correct and handles nested bucket dict/string normalization.
api/python/quilt3/admin/exceptions.py Adds new exception classes for policies and role edge-cases; hierarchy is clean.
api/python/quilt3-graphql/queries.graphql Adds role/policy CRUD mutations and queries; defaultRoleGet query is generated but unused in the admin Python module.
api/python/tests/test_admin_api.py Good parametrized test coverage for new role/policy APIs; RoleAssigned and RoleNameUsedBySsoConfig delete paths not exercised.
api/python/quilt3/admin/init.py Correctly re-exports all new types and exceptions; policies module added to namespace.

Sequence Diagram

sequenceDiagram
    participant Caller as Caller
    participant AdminAPI as admin.roles / admin.policies
    participant Util as admin.util
    participant Client as _graphql_client.Client
    participant GQL as GraphQL API

    Caller->>AdminAPI: roles.create_managed(name, policies)
    AdminAPI->>Client: role_create_managed(ManagedRoleInput)
    Client->>GQL: mutation roleCreateManaged
    GQL-->>Client: RoleCreateSuccess | RoleNameReserved | ...
    Client-->>AdminAPI: typed Union result
    AdminAPI->>AdminAPI: _handle_role_mutation_result(result)
    AdminAPI->>Util: parse_role_result(result.role)
    Util-->>AdminAPI: ManagedRole | UnmanagedRole
    AdminAPI-->>Caller: types.ManagedRole

    Caller->>AdminAPI: policies.create_managed(title, permissions, roles)
    AdminAPI->>Client: policy_create_managed(ManagedPolicyInput)
    Client->>GQL: mutation policyCreateManaged
    GQL-->>Client: Policy | InvalidInput | OperationError
    Client-->>AdminAPI: typed Union result
    AdminAPI->>AdminAPI: _handle_policy_result(result)
    alt typename == "Policy"
        AdminAPI->>Util: parse_policy_result(result)
        Util-->>AdminAPI: types.Policy
        AdminAPI-->>Caller: types.Policy
    else typename == "InvalidInput"
        AdminAPI->>Util: raise_invalid_input(result)
        Util-->>Caller: raises PolicyNotFoundError / PolicyTitleExistsError / ...
    else typename == "OperationError"
        AdminAPI->>Util: raise_operation_error(result)
        Util-->>Caller: raises OperationError
    end
Loading

Reviews (1): Last reviewed commit: "Document admin role and policy APIs" | Re-trigger Greptile

Greptile also left 3 inline comments on this PR.

Copy link
Copy Markdown

@claude claude Bot left a comment

Choose a reason for hiding this comment

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

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review to trigger a review.

Tip: disable this comment in your organization's Code Review settings.

@codecov
Copy link
Copy Markdown

codecov Bot commented Mar 25, 2026

Codecov Report

❌ Patch coverage is 98.24818% with 12 lines in your changes missing coverage. Please review.
✅ Project coverage is 45.40%. Comparing base (7077590) to head (d4f0119).
⚠️ Report is 32 commits behind head on master.

Files with missing lines Patch % Lines
api/python/quilt3/_graphql_client/client.py 95.49% 5 Missing ⚠️
api/python/quilt3/admin/roles.py 92.98% 4 Missing ⚠️
api/python/quilt3/admin/policies.py 95.34% 2 Missing ⚠️
api/python/quilt3/admin/util.py 94.73% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #4782      +/-   ##
==========================================
+ Coverage   44.32%   45.40%   +1.07%     
==========================================
  Files         813      829      +16     
  Lines       32736    33403     +667     
  Branches     5721     5721              
==========================================
+ Hits        14511    15166     +655     
- Misses      16224    16236      +12     
  Partials     2001     2001              
Flag Coverage Δ
api-python 93.07% <98.24%> (+0.42%) ⬆️
catalog 19.52% <ø> (ø)
lambda 96.63% <ø> (ø)
py-shared 98.18% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 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.

Comment on lines +23 to +39
def raise_invalid_input(error: _graphql_client.InvalidInputSelection):
first_error = error.errors[0] if error.errors else None
if first_error is None:
raise exceptions.InvalidInputError(error)
if first_error.name == "PolicyDoesNotExist":
raise exceptions.PolicyNotFoundError()
if first_error.name == "PolicyTitleConflict":
raise exceptions.PolicyTitleExistsError(error)
if first_error.name == "PolicyArnConflict":
raise exceptions.PolicyArnExistsError(error)
if first_error.name == "RoleHasTooManyPoliciesToAttach":
raise exceptions.RoleTooManyPoliciesError(error)
raise exceptions.InvalidInputError(error)


def raise_operation_error(error: _graphql_client.OperationErrorSelection):
raise exceptions.OperationError(error)
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.

P2 Missing NoReturn type annotations

raise_invalid_input and raise_operation_error always raise exceptions but are not annotated with -> NoReturn. Without this annotation, type checkers assume these helpers can return normally, which leads to false positives downstream: in _handle_policy_result (and policies.delete), the branches that call them appear to fall through to the next if or the final raise AssertionError, making the function's -> types.Policy return type unsatisfiable from a static-analysis perspective.

Suggested change
def raise_invalid_input(error: _graphql_client.InvalidInputSelection):
first_error = error.errors[0] if error.errors else None
if first_error is None:
raise exceptions.InvalidInputError(error)
if first_error.name == "PolicyDoesNotExist":
raise exceptions.PolicyNotFoundError()
if first_error.name == "PolicyTitleConflict":
raise exceptions.PolicyTitleExistsError(error)
if first_error.name == "PolicyArnConflict":
raise exceptions.PolicyArnExistsError(error)
if first_error.name == "RoleHasTooManyPoliciesToAttach":
raise exceptions.RoleTooManyPoliciesError(error)
raise exceptions.InvalidInputError(error)
def raise_operation_error(error: _graphql_client.OperationErrorSelection):
raise exceptions.OperationError(error)
from typing import NoReturn
def raise_invalid_input(error: _graphql_client.InvalidInputSelection) -> NoReturn:
first_error = error.errors[0] if error.errors else None
if first_error is None:
raise exceptions.InvalidInputError(error)
if first_error.name == "PolicyDoesNotExist":
raise exceptions.PolicyNotFoundError()
if first_error.name == "PolicyTitleConflict":
raise exceptions.PolicyTitleExistsError(error)
if first_error.name == "PolicyArnConflict":
raise exceptions.PolicyArnExistsError(error)
if first_error.name == "RoleHasTooManyPoliciesToAttach":
raise exceptions.RoleTooManyPoliciesError(error)
raise exceptions.InvalidInputError(error)
def raise_operation_error(error: _graphql_client.OperationErrorSelection) -> NoReturn:
raise exceptions.OperationError(error)

The same annotation pattern should be applied in raise_operation_error at line 38–39.

Comment on lines +85 to +100
def delete(id: str) -> None:
"""
Delete a role from the registry.

Args:
id: Role ID.
"""
result = util.get_client().role_delete(id=id)
typename = result.typename__
if typename == "RoleDeleteSuccess":
return None
if typename == "RoleDoesNotExist":
raise exceptions.RoleNotFoundError()
if typename == "RoleNameReserved":
raise exceptions.RoleNameReservedError(result)
raise exceptions.Quilt3AdminError(result)
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.

P2 Unhandled RoleAssigned and RoleNameUsedBySsoConfig delete responses

The generated role_delete client method can return five distinct union members: RoleDeleteSuccess, RoleDoesNotExist, RoleNameReserved, RoleNameUsedBySsoConfig, and RoleAssigned. The current delete() only handles the first three explicitly; the last two fall through to the generic raise exceptions.Quilt3AdminError(result).

This isn't broken at runtime, but callers cannot distinguish "role is still assigned to users" from any other unexpected error, and the test suite's ROLE_DELETE_ERRORS fixture doesn't exercise these paths. Consider either:

  • Adding dedicated exception classes (RoleAssignedError, RoleSsoConfigConflictError) and handling them here, or
  • Documenting in the docstring that these produce a generic Quilt3AdminError.

Comment on lines +64 to +68
...PermissionSelection
}
roles {
...ManagedRoleSelection
}
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.

P2 defaultRoleGet query is generated but not exposed via the admin API

The defaultRoleGet query is added to the GraphQL file and the corresponding client method default_role_get() is generated in client.py, but no wrapper function (e.g. roles.get_default()) is implemented in api/python/quilt3/admin/roles.py, and no tests exercise it.

If this query is intentionally left for future use, consider either adding a comment in the GraphQL file, or omitting it until the public API is ready to avoid dead generated code.

@nl0 nl0 mentioned this pull request Apr 7, 2026
4 tasks
@nl0
Copy link
Copy Markdown
Member

nl0 commented Apr 11, 2026

superseded by #4805

@nl0 nl0 closed this Apr 11, 2026
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.

2 participants