Skip to main content

pt-access — shared permission → ClickHouse access-control logic

One place for the permission pipeline every Point Topic data app needs. Ported from the production-proven point-topic-mcp implementation (issues #100/#104/#105) so the MCP server, the ontology web app (onto-app-new) and future consumers cannot drift.

Full design context: HANDOFF.md. Tracked by onto-app-new#25 (Auth0 login + per-org dataset/geo permissions).

The pipeline

JWT claims ──► policy spec ──► SQL predicate ──► ClickHouse DDL
(what you   (normalised    (composer:      (provisioning:
 have)       dataset +      escaping,       role, grants,
             filter rows)   containment,    row policies,
                            OR-grouping)    settings profile)

Identity comes from the shared sub-site Auth0 tenant (point-topic.eu.auth0.com). The Post-Login Action stamps pt_org_id and products[] ([{name, permissions}], scalar values only). Geo filters are never in the token — list values would bloat tokens past proxy header limits (documented decision in the Action's own code) — they are read fresh from sub-site MongoDB (organisations.productPermissions.<ds> = {field: string[]}) on every login/session, then converted to ClickHouse row policies.

Modules

Module What Ported from
claims.py get_org_id, get_held_products, is_pt_admin, has_data_source_access, get_product_role MCP auth/middleware.py (pure parts)
jwt.py Auth0TokenVerifier — JWKS cache, RS256, issuer/audience/exp checks MCP auth/auth0_helpers.py (FastMCP wrapper stays in the MCP)
composer.py compose_sql_filter / compose_geo_predicate — sub-site permission values → per-dataset SQL disjunction MCP core/sql_filter_composer.py (verbatim, live-verified SQL shapes)
provisioning.py fetch_org_datasets (sub-site Mongo read) + resolve_org_config + provision_org_role / provision_org_on_login (ClickHouse DDL, serialisation lock) MCP core/org_provisioning.py (verbatim)
contract.py load_contract / validate_contract — fixture loader + code↔contract consistency check MCP core/ontology-permission-contract.json + test_permission_contract.py

Instance-specific config (CH host/port/creds, grant_to service user, measurement tables, PREWHERE profile) stays in the consuming app's environment — this package takes it as parameters, never hardcodes it. provision_org_role() already takes grant_to per call; the convenience wrapper uses GRANT_TO_USER (env MCP_CLICKHOUSE_GRANT_TO_USER, default mcp_service) — rename/re-purpose per instance when wiring a new consumer.

Usage

from pt_access.jwt import Auth0TokenVerifier
from pt_access.claims import get_org_id, get_held_products, is_pt_admin
from pt_access.provisioning import provision_org_on_login

# 1. Verify the bearer token (FastAPI dependency, etc.)
claims = await Auth0TokenVerifier(
    auth0_domain="point-topic.eu.auth0.com",
    audience="<your_client_id>",   # ID token audience = client_id
).verify_token(bearer)

# 2. Provision the org's ClickHouse role + row policy (on login / session start)
if not is_pt_admin(claims):
    summary = provision_org_on_login(get_org_id(claims), get_held_products(claims))
    # summary: {"role": "org_<id>", "granted_to", "using", "tables", "warnings"}

# 3. Per-query scoping on the app's READ client (never SET ROLE on a shared
#    connection — thread-local clients are shared across users):
#    client.query(sql, settings={"role": "org_<id>"})

provision_org_on_login is a no-op when CLICKHOUSE_PROVISIONING_USER/_PASSWORD are unset; without a provisioning credential orgs fail closed on the engine (role never granted). Requires SUB_SITE_MONGODB_URI (read-only sub-site Mongo user) for the fresh per-org read.

Behaviour contract (do not "fix" — each rule exists because of a live incident)

  • Fail closed: no held data-source products → deny (REVOKE role + row policy USING 0); unknown/empty filter fields → that dataset excluded, ALL excluded → deny; a held product with no stored values → bare (DATA_SOURCE='<ds>') (omit-empty contract = full access to that product's data).
  • Per-dataset disjunction: (DATA_SOURCE='upc' AND <geo>) OR (DATA_SOURCE='gbs') — never a bare DATA_SOURCE IN (...) AND <geo> blob.
  • OR-grouping: multiple predicates must be wrapped (A OR B) inside the DATA_SOURCE guard — SQL precedence otherwise leaks other datasets' rows at the postcode (found live on prod 2026-08-05).
  • PREWHERE: row policies on non-sorting-key columns return 0 rows under PREWHERE (ClickHouse GH #85222). Provisioning creates a settings profile (optimize_move_to_prewhere = 0) and attaches it to the service user — per-query SETTINGS is blocked under readonly=1 (error 164) and role-attached profiles don't apply. Trade-off: applies to every query through that user.
  • Convergence: full re-provision on every login (no drift check, ~320ms), serialised by a global lock (row-policy DROP+CREATE races → ACCESS_ENTITY_ALREADY_EXISTS, code 493). Deny orgs are always re-provisioned too (REVOKE converges).
  • Escaping: every literal is single-quote-doubled (King's Lynn); the row policy is the enforcement boundary, so a quote in an admin-entered value must never escape the literal.

The contract fixture — the single evolvable artifact

src/pt_access/ontology-permission-contract.json mirrors sub-site/apps/ui/src/organisations/config/ontology-permission-contract.json (sub-site UI renders exactly what this declares). Adding a new data source = a dataSources entry; a new filter field = a fields entry (+ fieldProducts / compatibility where applicable). Both are fixture changes, never composer code changes. The package CI drift-guard workflow fetches the sub-site copy via gh api and fails on mismatch; tests/test_contract.py (via contract.validate_contract()) checks the code constants against the fixture.

Consumers

App Status Notes
point-topic-mcp to migrate swap local modules for this package; keep FastMCP middleware + tool→product map
onto-app-new to integrate issue #25; adds ontology_app product (role admin/viewer), JWT auth on all routes, per-query role settings
upc_query_agent follow-up already reads pt_org_id/products[] in api/auth_handler.py — adopt claims.py
local-pricing-dashboard, european-fttp-forecasts don't break read products[].permissions.role — the claim shape is pinned by tests

Development

uv sync                        # or use any venv with the deps
uv run pytest -q               # 93 tests, no network/DB needed (all mocked)
uv run ruff check src tests
uv run pyright                 # requires pyright install; config in pyproject

Distribution: private GitHub tag, pinned by consumers (pip install git+https://github.com/Point-Topic/point-topic-access@v0.1.0).

Download files

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

Source Distribution

point_topic_access-0.1.0.tar.gz (29.8 kB view details)

Uploaded Source

Built Distribution

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

point_topic_access-0.1.0-py3-none-any.whl (20.7 kB view details)

Uploaded Python 3

File details

Details for the file point_topic_access-0.1.0.tar.gz.

File metadata

  • Download URL: point_topic_access-0.1.0.tar.gz
  • Upload date:
  • Size: 29.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.8.22

File hashes

Hashes for point_topic_access-0.1.0.tar.gz
Algorithm Hash digest
SHA256 91cc37463a0de3999e0c15eaf75ee6322ed240712fd7e9d1e54237585fac928d
MD5 8d7e5edcd27157d7e1fd1130b7cf5d98
BLAKE2b-256 a1182ffdfdac623ab218d2febf62d0a3612c1db16844eadd93f353f8f20ba1a0

See more details on using hashes here.

File details

Details for the file point_topic_access-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for point_topic_access-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 73417d48e61013e7ad6fe4461fbe8ffeb9570c2e173e4e0bcec68a1b1f4ed831
MD5 09b2416c5b8c8d2a494ec76599154568
BLAKE2b-256 155325dc8048120e594c6e56d7eab7e6db7eec5cc44daa7fbdfb071361a2280e

See more details on using hashes here.

Release history Release notifications | RSS feed

0.2.0

2 files

0.1.1

2 files

This release

0.1.0 This release

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page