Authentication & role-based access control#321
Merged
Merged
Conversation
Introduce a new `beacon-auth` crate and wire it through the runtime, query
pipeline, and both transports (HTTP + Flight SQL), adding role-based access
to tables and dataset paths. All enforcement is opt-in and off by default,
so existing deployments are unaffected.
beacon-auth:
- Role model: Privilege x PrivilegeTarget{Table,Path,All}, deny-wins /
default-deny evaluator, segment-aware path globs.
- Credential-typed AuthProvider trait; providers: BasicAuthProvider (argon2,
in-memory), SqliteAuthProvider/SqliteStore (persistent), OidcAuthProvider
(JWT/JWKS, configurable claims), CompositeAuthProvider (Basic->local,
Bearer->OIDC). Beacon owns grants; the IdP supplies identity + role names.
Runtime/core:
- Runtime owns an AuthContext (bootstraps admin super-user + anonymous from
config); run_query/explain_* take an AuthIdentity instead of is_super_user.
- Read enforcement (statement_plan/authz.rs): walks TableScans and requires
SELECT on each table/path; super-user / enforce=off bypass.
- SQL surface: CREATE/DROP USER/ROLE, GRANT ROLE .. TO USER, GRANT/DENY
<priv> [ON TABLE|PATH|ALL] TO ROLE, REVOKE [DENY] .. FROM ROLE; lowered to
an Extension node so it inherits the super-user gate.
Transports:
- resolve_identity axum middleware (Basic/Bearer -> AuthIdentity); admin
routes require a super-user. Flight SQL resolves identity via the runtime.
Config: AuthConfig{anonymous_enabled, enforce} and OidcConfig. New pinned
deps: rusqlite, jsonwebtoken, reqwest (rustls), argon2.
The `admin` role is the built-in super-user role (global ALL grant), now treated like a Postgres bootstrap superuser: it is re-seeded on every startup and protected against removal at runtime. `DROP ROLE admin` and revoking its global ALL grant are rejected, so any principal holding it — local or from an external IdP — is always a super-user. Add `SUPERUSER_ROLE` constant and use it in bootstrap.
- AuthContext::create_role rejects the reserved super-user role name
('admin'); bootstrap seeds it through the raw RoleProvider instead.
- Add a runtime test proving a non-super-user cannot escalate: every
auth-management statement (CREATE ROLE/USER, GRANT, GRANT ROLE) is
rejected for a role-limited caller, a super-user can run them, and
re-creating the reserved 'admin' role is rejected even for a super-user.
beacon-core/tests/auth.rs (17 tests): drive the real Runtime end to end — authentication (admin/anonymous/wrong-password/bearer-without-oidc), user and role lifecycle via SQL, the reserved super-user role, and the full read- enforcement matrix with BEACON_AUTH_ENFORCE=true (table grants, deny-wins, revoke, anonymous denial, super-user bypass, enforce-off compatibility, information_schema exemption) plus privilege-escalation/DDL gating. beacon-api axum::auth_http_tests (3 tests): drive the real router via tower oneshot — the admin gate (401 / 403 / 200), anonymous read vs. write, and read enforcement over HTTP (granted 200, ungranted 400). Enable the test-util feature for these via a self dev-dependency (beacon-core) and add tower (dev) for oneshot.
The path-glob integration tests surfaced a real enforcement gap: an ad-hoc
`read_*('<glob>')` scan resolves to a `SuperListingTable` (and external-table
scans to an `ExternalTable`), neither of which the authz resolver recognized —
so PATH grants/denies were silently a no-op for the most common dataset-query
pattern. Teach `scan_targets` about both wrappers (via new `table_paths()`
accessors) so path enforcement actually applies.
Tests (beacon-core/tests/auth.rs): copy a parquet fixture into the datasets
dir and query it via read_parquet under BEACON_AUTH_ENFORCE=true —
- a PATH grant matches only its prefix (other prefixes denied),
- globs are segment-aware (`*` stops at `/`, `**` recurses),
- DENY ON PATH wins over a broad SELECT grant.
…-only There is now exactly ONE super-user, defined solely by BEACON_ADMIN_USERNAME / BEACON_ADMIN_PASSWORD. It cannot be created, changed, or duplicated through SQL. - AuthContext holds the super-user credential (set at bootstrap) and checks it first in `authenticate` (constant-time, Basic-only), returning a super identity without touching the provider/store. Everyone else — local users and OIDC principals — is always non-super. - The super-user is no longer a role: the reserved `admin` role and the global- ALL -> super inference are removed. `bootstrap_auth` just sets the credential and seeds the (read-only) anonymous user. - Roles are strictly read-only: `grant`/`deny` reject any non-SELECT privilege, so there is no way to mint another super-user or grant write access via a role. - The super-user username is reserved: CREATE/DROP USER on it is rejected. Tests updated across beacon-auth, beacon-core (runtime + integration), and adjusted for the new model; full workspace suite green (671 passed).
…signature PR #318 (merged into main) added this test using the old run_query(query, bool) signature; this branch changed run_query to take an AuthIdentity. Update the test's super-user calls to AuthIdentity::system().
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #321 +/- ##
==========================================
+ Coverage 72.96% 74.30% +1.33%
==========================================
Files 269 280 +11
Lines 34503 36298 +1795
==========================================
+ Hits 25176 26972 +1796
+ Misses 9327 9326 -1
🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds authentication and role-based access control to Beacon: roles with grants on tables and dataset paths (globs), a single config-defined super-user, and pluggable authentication (local username/password + OIDC). All enforcement is opt-in and off by default, so existing deployments are unaffected until
BEACON_AUTH_ENFORCE=true.Built from scratch, modeled on the old #221 but adapted to the current codebase.
Model
BEACON_ADMIN_USERNAME/BEACON_ADMIN_PASSWORD, checked directly at auth time (constant-time, Basic-only). It is not a stored user or role, so it cannot be created, changed, duplicated, or spoofed (OIDC bearer is never super) through SQL.GRANT/DENYaccept onlySELECT; write + auth-management is reserved to the super-user.Privilege×Table | Path-glob | All), evaluated deny-wins, default-deny, with segment-aware path globs (example/*≠example_2/...,**crosses/).What's included
beacon-authcrate: role model,Credential/AuthProvider/UserDirectorytraits,AuthContext/AuthIdentity, argon2 passwords, providers —BasicAuthProvider(in-memory),SqliteAuthProvider/SqliteStore(persistent),OidcAuthProvider(JWT/JWKS, configurable claims),CompositeAuthProvider(Basic→local, Bearer→OIDC).run_query/explain_*take anAuthIdentity; read enforcement instatement_plan/authz.rs(walksTableScans → table/path targets, requiresSELECT); writes/DDL keep the super-user gate.CREATE/DROP USER/ROLE,GRANT ROLE … TO USER,GRANT/DENY <priv> [ON TABLE t | ON PATH 'glob' | ON ALL] TO ROLE,REVOKE [DENY] … FROM ROLE(super-user only).resolve_identityHTTP middleware (Basic/Bearer → identity), super-user-gated admin routes; Flight SQL resolves identity via the runtime.AuthConfig{anonymous_enabled, enforce},OidcConfig. New pinned deps:rusqlite,jsonwebtoken,reqwest(rustls),argon2.Config
Example:
Tests
Full workspace suite green (671 passed, 0 failed). New coverage:
BEACON_AUTH_ENFORCE=true(table + path-glob grants, deny-wins, revoke, anonymous denial, super-user bypass, enforce-off compat, information_schema exemption), escalation/DDL gating, reserved super-user.The path-glob tests caught (and fixed) a real gap:
read_*scans (SuperListingTable) were bypassing path enforcement.Notes
BEACON_OIDC_*at the realm (leave audience empty, roles claimrealm_access.roles) and map admin users to roles you grant locally.