Skip to main content

opteryx-access

Permission checks and grant/revoke for the Opteryx platform, as an installable library rather than a service. Every consumer imports opteryx_access and calls it in-process against policies it already has in hand -- a JWT's policies claim, or a PolicyStore backed by whatever it uses for storage (Firestore, today). There is no opteryx-access HTTP surface, and this package makes no network calls of its own beyond what a storage adapter does.

Why this exists

Three independent, subtly incompatible implementations of "does this role satisfy this requirement" already exist in the fleet:

  • policy.opteryx / control.opteryx (app/routes/v1/access.py, app/models/policy.py -- byte-for-byte duplicated between the two repos): a rank-based ROLES = ("owner", "admin", "writer", "reader") used to decide who may create/update/revoke a policy, and whether a new grant is redundant against one the principal already holds.
  • opteryx-core (opteryx/managers/permissions/__init__.py): a set-based ACTION_MAP deciding which roles may READ/DELETE/DROP/etc. a resource once a query actually runs. admin is deliberately absent from every entry -- it never grants data access, only grant-management authority. This is the piece the rank-based model above cannot replace: a naive owner > admin > writer > reader comparison would silently hand admins data-write access they've never had.
  • odata.opteryx (app/auth/permissions.py): a binary role_allows_read used for listing/visibility, plus its own fnmatch.fnmatchcase-based pattern matcher -- deliberately case-sensitive, unlike the plain fnmatch.fnmatch used elsewhere, which folds case per-OS.

On top of that, policy.opteryx/control.opteryx/odata.opteryx/ register.opteryx each carry their own copy of "parse the policies claim out of a decoded JWT." Comments in several of these point at an authorize.opteryx service (app.routes.v1.evaluate) as the semantics every copy is meant to mirror -- but no such repo exists anywhere in this workspace. Whether it's a real service in another org/remote or was never built, every consumer today reimplements its own understanding of "role + pattern -> allowed" independently, which is exactly the drift this package is meant to stop.

What lives here

Module Ported from Purpose
roles.py policy.opteryx/app/models/policy.py The canonical ROLES tuple. Two separate notions of "outranks": ADMINISTRATIVE_ROLES (owner/admin -- may manage grants) and rank (ROLE_RANK, used only for conflict detection). Not used to decide data actions.
actions.py opteryx-core/opteryx/managers/permissions/__init__.py ACTION_ROLES: which roles may perform READ/WRITE/DELETE/CREATE/DROP/ALTER/REFRESH/MANIFEST, plus GRANT/REVOKE (new -- makes policy-administration authority explicit in the same table instead of an implicit rule elsewhere).
patterns.py policy.opteryx/app/models/policy.py + app/routes/v1/access.py resource_matches (case-sensitive fnmatchcase -- see "Behavior changes" below), the wildcard-principal rule, and the reserved-workspace (public/personal/information_schema) rule.
models.py authenticate.opteryx/app/policies.py Grant (role+pattern, the JWT-carried shape) and Policy (principal+role+pattern+metadata, the stored shape), plus parse_policy_claim for the [role, pattern] pairs a token carries.
checks.py opteryx-core's can_perform_action/can_perform_workspace_action + policy.opteryx's _check_pattern_access/_check_workspace_access/_check_workspace_owner_access The evaluation layer: data-plane checks over Grants, administrative-plane checks over Policy documents.
store.py policy.opteryx/app/routes/v1/access.py's create_policy/update_policy/delete_policy/create_genesis_policies Where policies are actually granted, updated, and revoked: the PolicyStore protocol plus grant()/update_grant()/revoke()/bootstrap_workspace(), which enforce every invariant those routes did (self-grant prevention, pattern-authority, conflict detection, wildcard/reserved-resource validation) against any storage backend.
adapters/firestore.py (new) FirestorePolicyStore, matching the {workspace}/$policies/access layout policy.opteryx/control.opteryx already write to -- a drop-in for their inline Firestore calls.
exceptions.py (new) Plain exceptions (SelfAccessError, AccessDeniedError, PolicyConflictError, ...) instead of HTTPException -- each caller translates to its own transport.

Two axes, not one rank

admin sits between owner and writer on the grant-management axis (ADMINISTRATIVE_ROLES) but is excluded from every entry in ACTION_ROLES. That split is real, existing platform behavior (see opteryx-core's ACTION_MAP), not an oversight this library introduces -- an admin can grant and revoke other people's access but cannot themselves SELECT/INSERT/ DELETE against a resource they don't separately hold writer/owner on. Anything built on top of this package should keep asking the right one of the two questions:

  • "May this identity administer grants on this pattern?" -> checks.can_administer_pattern
  • "May this role perform this SQL-shaped action on this resource?" -> checks.can_perform_action

Usage

from opteryx_access import Grant, can_perform_action

grants = [Grant(role="writer", pattern="analytics.sales.*")]
can_perform_action(grants, "analytics.sales.q1", "DELETE")  # True
can_perform_action(grants, "analytics.sales.q1", "DROP")    # False -- writer, not owner
from opteryx_access import grant, revoke, AccessDeniedError
from opteryx_access.adapters.firestore import FirestorePolicyStore

store = FirestorePolicyStore(db)  # db: google.cloud.firestore.Client
try:
    policy_id = grant(
        store, actor="alice", workspace="analytics",
        principal="bob", role="writer", pattern="analytics.sales.*",
    )
except AccessDeniedError:
    ...  # translate to a 403, same as the route used to do inline

Behavior changes from the ported originals

Ported faithfully except for one deliberate fix, worth calling out before anything is cut over:

  • resource_matches uses fnmatch.fnmatchcase, not fnmatch.fnmatch. policy.opteryx/control.opteryx/opteryx-core's data-action check all use plain fnmatch, which folds case per the OS Python runs on -- the same policy would decide differently on macOS versus Linux. opteryx-core's own can_perform_action already avoids fnmatch for exactly this reason when matching its hardcoded implicit policies. odata.opteryx already uses fnmatchcase for its read check. This package standardizes on the deterministic, already-precedented behavior. If any live policy pattern relies on the OS case-folding today, cutting a service over to this package will change what that specific pattern matches -- worth an audit of stored patterns before cutover, not assumed to be a no-op.

Suggested migration (not yet done)

This repo is the library only -- nothing outside it has been changed yet. Suggested order, each independently shippable:

  1. opteryx-core: replace opteryx/managers/permissions/__init__.py's ACTION_MAP/can_perform_action/can_perform_workspace_action/ implicit_policies with thin wrappers around opteryx_access.actions/ opteryx_access.checks (converting ExecutionContext.access_policies dicts to Grants at the boundary). Zero new dependencies -- this package has none by default.
  2. odata.opteryx: replace app/auth/permissions.py's role_allows_read/read_grant_for_relation/pattern matching with opteryx_access.checks.can_perform_action (action="READ"). Leaves entitlements_from_claims/billing_account_from_claims alone -- those are a different concern (entitlements/billing), not permissions.
  3. policy.opteryx and control.opteryx: thin app/routes/v1/access.py down to request parsing, calling opteryx_access.store.grant/ update_grant/revoke/bootstrap_workspace via FirestorePolicyStore, and mapping the typed exceptions to HTTPException. The resource-existence check (_resource_exists), user-status lookup (_lookup_user_record), age gate, and audit logging all stay where they are -- see store.py's module docstring for why those are out of scope here. Given control.opteryx is the in-progress merge target for policy.opteryx (see its docs/design/consolidation.md), do this once, on control.opteryx, and let the policy.opteryx cutover carry it along rather than porting both routes separately.
  4. register.opteryx and any other claims-parsing copy: swap to opteryx_access.models.parse_policy_claim for the policies claim specifically. Token verification (signature, issuer, JWKS) stays service-specific -- this package has no opinion on how a JWT gets from bytes to a trusted claims dict, only on what to do with the policies claim once you have one.

Installing

pip install opteryx_access
pip install "opteryx_access[firestore]"  # for adapters.firestore

No hard dependencies. google-cloud-firestore is an optional extra; adapters/firestore.py never imports it at all (it's duck-typed against whatever db object it's handed), so importing opteryx_access itself never requires it -- consistent with opteryx-core's zero-dependency convention.

CI/CD

  • .github/workflows/tests.yaml -- pytest (3.13, 3.14) + ruff lint/format, on every push to main and every PR.
  • .github/workflows/release.yaml -- on a pushed tag matching version-*: runs the full test workflow, checks the tag matches pyproject.toml's version (exactly, or with a .-delimited suffix), then builds and publishes to PyPI via trusted publishing (OIDC -- no stored API token). The opteryx_access PyPI project needs this repo + the release.yaml workflow registered as a trusted publisher before the first tag push, or the publish step will fail with no valid credentials.

To cut a release: bump version in pyproject.toml, merge to main, then git tag version-X.Y.Z && git push origin version-X.Y.Z.

License

Apache 2.0. See LICENSE for details.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

opteryx_access-0.0.1.tar.gz (38.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

opteryx_access-0.0.1-py3-none-any.whl (30.6 kB view details)

Uploaded Python 3

File details

Details for the file opteryx_access-0.0.1.tar.gz.

File metadata

  • Download URL: opteryx_access-0.0.1.tar.gz
  • Upload date:
  • Size: 38.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for opteryx_access-0.0.1.tar.gz
Algorithm Hash digest
SHA256 05479da3d46d68bf14446d2f15e601596577c8a871125c17ce407f27dde98f0b
MD5 49a7e5497355091327f03f21050bfda2
BLAKE2b-256 93850e9484a9c10eef2ecf77bc99afe3022f106acc4a3b44bda46545ac355962

See more details on using hashes here.

Provenance

The following attestation bundles were made for opteryx_access-0.0.1.tar.gz:

Publisher: release.yaml on mabel-dev/opteryx-access

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file opteryx_access-0.0.1-py3-none-any.whl.

File metadata

  • Download URL: opteryx_access-0.0.1-py3-none-any.whl
  • Upload date:
  • Size: 30.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for opteryx_access-0.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 7eb26ce3b1de5b375d16e3bf64da007388a13d60b05db388e84431edb18b9e72
MD5 b3b4a968aea51d287e6e557f4ddb92f4
BLAKE2b-256 e474340f8e40a52e43eac2b99d9693e0036b78be994f3a626eef1ec34c5a9e8b

See more details on using hashes here.

Provenance

The following attestation bundles were made for opteryx_access-0.0.1-py3-none-any.whl:

Publisher: release.yaml on mabel-dev/opteryx-access

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page