Skip to content

fix(http): raise a distinct exception per Arc refusal on the database listings - #3

Open
xe-nvdk wants to merge 1 commit into
feat/sdk-0.2.0-feature-parityfrom
fix/databases-listing-403-handling
Open

xe-nvdk wants to merge 1 commit into
feat/sdk-0.2.0-feature-parityfrom
fix/databases-listing-403-handling

Conversation

@xe-nvdk

@xe-nvdk xe-nvdk commented Oct 2, 2026

Copy link
Copy Markdown
Member

Summary

Base is feat/sdk-0.2.0-feature-parity (#2), not main. management/databases.py only exists on that branch — there is nothing to change on main. Merge #2 after this.

Arc's three database listing endpoints (GET /api/v1/databases, /:name, /:name/measurements) previously carried no authentication at all. They now require read permission and, where the server restricts reads per database, a grant covering the database being listed.

handle_response_error mapped both 401 and 403 onto ArcAuthenticationError, so a caller could not tell "re-authenticate" from "this token lacks a permission". And 403 is not one condition either — Arc answers it for four different things, told apart only by message.

Arc's answer Exception now raised
401 ArcAuthenticationError — token missing, invalid or expired
403 no permission for read on database 'x' / access denied: no read permission for database 'x' ArcScopedAccessError — may read, but not that database; .database carries it
403 token does not have 'read' permission / Permission denied: read required ArcPermissionError — no read permission at all
403 permission data unavailable ArcPermissionDataUnavailableError — Arc could not load the grants; a server-side fault
403 naming an enterprise licence ArcLicenseError (unchanged)
404 ArcNotFoundError — unchanged, so "you may not read it" and "it is not there" stay apart

ArcAuthenticationError now means 401 only. The three new classes all subclass ArcPermissionError, which subclasses ArcAuthenticationError, so existing except blocks that caught 403 as an authentication failure keep working.

databases.list() and a scoped token

Arc refuses the list-everything route for a token restricted to particular databases rather than returning a filtered list, the same way SHOW DATABASES does. That is a normal state for a tenant-scoped token, not a fault to retry:

from arc_client.exceptions import ArcScopedAccessError

try:
    databases = client.databases.list().databases
except ArcScopedAccessError:
    measurements = client.databases.list_measurements("metrics")

.database carries the refused database, and is empty or "*" when the call asked for every database — so test it against None, not truthiness, to tell "not scoped" from "scoped, no name".

Nothing changes for an admin token, for a read token on a server that does not restrict reads per database, or for a server with authentication disabled.

Notes

  • Matching is by stable message fragment, not whole string, and covers both the read middleware's wording and the handler gates' — so a change to the permission word cannot reclassify one refusal as another. An unrecognised 403 falls back to ArcPermissionError, which is safe to say about any refusal.
  • permission data unavailable is checked before the scoped shape: a broken permission store must never send a caller after a database that was never the problem.

Companion to the Arc server change on fix/rbac-measurement-where-and-databases-auth; see that branch's release notes under "RBAC read and write restrictions were unreachable, and the database listings had no authorization".

Test plan

  • uv run ruff check . + uv run ruff format --check . clean
  • uv run mypy src/arc_client --ignore-missing-imports — no issues (the CI invocation)
  • uv run pytest tests/unit — 184 passed, 9 skipped
  • New tests/unit/http/test_databases_permissions.py (26 tests, pytest-httpx as the rest of the suite uses): the exception hierarchy; list() / get() / list_measurements() against every body; a parametrised matrix asserting the exact type per 403 message (not just "a subclass of something"); 404 staying separate; the licence 403 keeping its own exception; and the async twin
  • Verified to fail before the fix (revert http/base.py, run, restore): 9 failures
  • The existing test_error_status_mapping case (403, ArcAuthenticationError) still passes via the subclass chain
  • Docs updated: README (Databases + Error Handling), CHANGELOG under [0.2.0], and the Raises: sections of all six database methods

… listings

Arc's three database listing endpoints carried no authentication at all. They
now require read permission and, where the server restricts reads per
database, a grant covering the database being listed.

handle_response_error mapped both 401 and 403 onto ArcAuthenticationError, so
a caller could not tell "re-authenticate" from "this token lacks a
permission" — and 403 is not one condition either. Arc answers it for a token
with no read permission, for a token whose grants do not cover the database
asked for, for a licence-gated feature, and for its own permission data being
unreadable, distinguishing them only by message.

ArcAuthenticationError now means 401 only. Added:

- ArcPermissionError: the token does not carry the permission the endpoint
  requires.
- ArcScopedAccessError: Arc's per-database denial. The token may read, but
  not what was asked for. Carries the refused database on .database — empty
  or "*" when the call asked for every database, since Arc refuses
  databases.list() for a tenant-scoped token rather than returning a filtered
  list, matching SHOW DATABASES. A normal state, not a fault to retry.
- ArcPermissionDataUnavailableError: Arc could not load the token's grants and
  failed closed. A server-side fault that says nothing about the token, so it
  is checked before the scoped shape and never reported as one.

All subclass ArcPermissionError, which subclasses ArcAuthenticationError, so
existing except blocks that caught 403 as an authentication failure keep
working. Matching is by stable message fragment and covers both the read
middleware's wording and the handler gates', so a change to the permission
word cannot reclassify one refusal as another; an unrecognised 403 falls back
to ArcPermissionError.

Docstrings on list/get/list_measurements, sync and async, document which
exception each refusal raises.

This branch has not been deployed

No deployments
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.

1 participant