Skip to content

permit 3.0.0

Latest

Choose a tag to compare

@zeevmoney zeevmoney released this 28 Sep 15:39
· 2 commits to main since this release
99707ce

permit 3.0.0

permit 3.0.0 is a major release. It requires Python 3.10 or later and raises dependency floors to clear known advisories. It also fixes SDK bugs, ships type information and supports Python 3.14.

Before you upgrade, read the migration guide. Each change below has an ID there (for example C1 or A2), with who is affected, what to do, and a before/after example.

Upgrade with an AI agent. The permit-python-3-migration skill takes an agent such as Claude Code through the guide:

  • It stops if your project still runs on Python 3.8 or 3.9.
  • It applies the mechanical edits and asks you about each judgement call.
  • It runs your tests, type checker and linter.

To install it, download permit-python-3-migration.skill below and unzip it into .claude/skills/, or copy skills/permit-python-3-migration. Its scanner is read-only and runs on Python 3.8+:

python3 .claude/skills/permit-python-3-migration/scripts/scan.py .

Breaking changes

Compatibility

  • Python 3.10 or later (C1). aiohttp 3.14.3 requires it, and it is the only release that fixes CVE-2026-69244. On Python 3.9, pip install -U permit keeps your current version without an error.

  • httpx is no longer installed with permit (C2). The packages that came only through it are gone too: h11, httpcore, anyio, certifi, and with some versions sniffio and exceptiongroup. zipp is also gone. permit never imported any of them, so declare the ones your code uses.

  • Higher dependency floors (C3):

    Package Python 3.0.0 requires
    pydantic 3.10–3.12 >=1.10.18,<2 or >=2.4.2
    pydantic 3.13 >=1.10.18,<2 or >=2.8.0
    pydantic 3.14 >=1.10.25,<2 or >=2.13
    aiohttp all >=3.14.3,<4
    typing-extensions all >=4.14.0,<5
    loguru all >=0.7.3,<1
  • permit ships type information (T1–T3, PEP 561 py.typed).

    • Type checkers now check your calls into permit instead of skipping it, so real type errors in your code may show up.
    • You can remove ignore_missing_imports or # type: ignore[import-untyped] for permit.
    • SDK models are typed as the pydantic v1 models they are at runtime. So pydantic 2 methods such as .model_dump() on them are now type errors; they already failed at runtime.

API

  • resource_relations.list() returns PaginatedResultRelationRead (A1). Read .data. In 2.x every call raised ValidationError.
  • On permit.sync.Permit, authorized_users(), get_user_permissions() and filter_objects() are synchronous (A2). Drop the await.
    • In 2.x they returned a coroutine, and awaiting it raised RuntimeError: This event loop is already running.
    • The async permit.Permit is unchanged.
  • Removed (A3, A6):
    • ContextStore.register_transform(), ContextStore.transform() and ContextTransform. A registered transform was never applied.
    • ApiKeyLevel. Use ApiKeyAccessLevel.
    • LoginAsErrorMessages, OpaResult and JWT.
    • permit.PYDANTIC_VERSION, and the same name in permit.api.models and permit.pdp_api.base.
    • A few names that modules only re-exported from their own imports. A6 lists them.
  • Audit-log models accept what the API returns (A4).
    • pdp_config_id on AuditLogModel and DetailedAuditLogModel is Optional[UUID].
    • DetailedAuditLogModel.objects is optional.
    • New: Engine.GENERIC and GenericEngineDecisionLog.
  • Relationship-tuple and API-key models accept what the API returns (A5).
    • object_id on RelationshipTupleRead and RelationshipTupleDetailedRead is Optional[UUID].
    • The four *_details fields on RelationshipTupleDetailedRead are optional.
    • APIKeyOwnerType gains nats_pdp_config.
    • In 2.x, relationship_tuples.list() and create() raised on a tuple with no object_id, and environments.get_api_key() raised on a key owned by nats_pdp_config.

Wire behaviour (same API, different request bytes)

  • An explicitly set None is sent as null (W1), so an update can clear a field. In 2.x, users.update(key, UserUpdate(email=None)) sent {}. Fields you never set are still omitted.
  • users.assign_role() and unassign_role() omit unset fields (W2), like role_assignments.assign().
  • elements.login_as() sends hyphenated UUIDs (W3). The API accepts both forms.
  • A 3xx response raises (W4) instead of counting as success.
  • The Authorization header uses Bearer (W5), not bearer. This fixes the 401 from Cloud PDP (#122).
  • The deprecated permit.api.assign_role() and unassign_role() call /users/{user}/roles (W6), through permit.api.users, instead of /role_assignments. The effect is the same.

PermitConnectionError still subclasses PermitException, so except PermitException still catches connection failures.

Deprecations

permit 4.0, a future major release, will remove the following. They work in 3.x. Each raises a DeprecationWarning that names the replacement, reported at the line that made the call.

  • pydantic 1 support (D1). On pydantic 1, import permit warns once.
  • The 21 flat methods on permit.api (D2). For example, permit.api.get_user() becomes permit.api.users.get(). The guide maps all 21.

To find every deprecated call your tests reach:

python -m pytest -W error::DeprecationWarning -W "ignore:Use PermitError instead:DeprecationWarning"

The second filter is needed in 2.x too: import permit warns because PermitConnectionError subclasses the deprecated PermitException.

Security

Package 2.8.3 allowed 3.0.0 Fixes
aiohttp >=3.12.14 >=3.14.3 32 advisories, including CVE-2026-69244 (out-of-bounds read in the HTTP response parser)
pydantic >=1.10.7 see above CVE-2024-3772 (ReDoS in EmailStr validation)
httpx >=0.24.1 removed Removes h11 (CVE-2025-43859) and anyio (CVE-2026-63374) from your dependency tree
zipp >=3.19.1 removed Unused

If you are staying on 2.x for now, see the guide's "Staying on 2.x for now". It lists constraints that clear these advisories on Python 3.10+, and the advisories' workarounds for 3.8 and 3.9.

Bug fixes

  • Sync client flat methods. On permit.sync.Permit, the flat permit.api methods sent their request and then raised ValueError: a coroutine was expected. A write took effect while the caller got an error. They now return the result, so remove any except ValueError around them.
  • pydantic 2. authorized_users() failed under pydantic 2.
  • ABAC context. Decisions can change. bulk_check() ignored a per-check context, and filter_objects() dropped your context. So ABAC checks that depend on context were evaluated against an empty one.
  • User names in checks. UserInput silently dropped first_name and last_name from every check.
  • Request bodies. A nested datetime, UUID or Enum in a dict or list request body crashed inside aiohttp.
  • PDP errors. A non-200 PDP response, such as a 403 from a wrong API key, was reported as "cannot connect to the PDP container". PermitConnectionError now carries the status code and response body. Code that matches the old message needs updating.
  • Tenant bulk operations. With proxy_facts_via_pdp, tenants.bulk_create() and tenants.bulk_delete() went to the PDP's users endpoint.
  • PDP API client. permit.pdp_api.* calls ignored pdp_timeout, and the sync pdp_api client didn't initialize correctly.
  • users.sync(). It removed key from the dict you passed, so that path always returned 422.
  • resource_instances.list(detailed_key=...). It raised TypeError.
  • pydantic pre-releases. import permit crashed on pre-releases such as 2.14.0b2.
  • Packaging. permit installed a top-level tests package into site-packages, where it could shadow your own.

Python 3.14

Python 3.14 is supported and tested (#124). The floors above keep resolvers off releases that crash or warn on 3.14. import permit no longer emits 21 DeprecationWarnings there.

Typing

  • Optional model fields show as optional in pyright and Pylance.
  • Methods that accept dicts at runtime accept them in their annotations.
  • The sync client is typed as synchronous.
  • Your code can type-check clean under mypy --strict and pyright:
    • plain str is accepted for EmailStr fields;
    • UserInput accepts both field spellings;
    • PermitConfig() without a token is a type error, as it already was at runtime.
  • On pydantic 2, mypy users who want plugin checking of SDK models can use pydantic.v1.mypy (T3).

Build and release

  • Advisory scans. Every pull request and release is scanned for dependency advisories (Trivy and pip-audit), at the lowest and the highest versions permit allows. A high or critical advisory with a fix fails the check and stops the release.
  • Release order. Releases run build → scan → publish, so nothing reaches PyPI without a passing scan.
  • Test matrix. CI runs the tests on Python 3.10–3.14, on pydantic 1 and 2, at the lowest and newest allowed versions.
  • Schema check. A weekly check compares the SDK's models with the public API schema.

Contributors

Thanks to @SureneruS for the Bearer report and fix (#122, #123), to @Tarcio0211 for the py.typed marker (#125), and to @coactive-jeffrey (#116) and @jameswu1991 (#124) for their reports. #123 and #125 shipped as part of #126.

Fixes #116, #122, #124.

Full changelog: v2.8.3...v3.0.0