Skip to content

11 apache polaris planning and implementation - #23

Open
mgdenno wants to merge 64 commits into
mainfrom
11-apache-polaris-planning-and-implementation
Open

11 apache polaris planning and implementation#23
mgdenno wants to merge 64 commits into
mainfrom
11-apache-polaris-planning-and-implementation

Conversation

@mgdenno

@mgdenno mgdenno commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

Part of a larger effort to migrate the catalog stack from iceberg-rest/iceberg-pg to Apache Polaris with Keycloak-driven access control. Companion PRs: rti-international/aws-project-amzifved-infrastructure#72 (per-service IRSA roles), RTIInternational/teehr-fved#75 (deployment wiring), and RTIInternational/teehr#810 (Spark session auth).

Summary

Introduce Apache Polaris as the Iceberg REST catalog, replacing iceberg-rest/iceberg-pg, with Keycloak-driven access control and a broker-mediated auth flow for long-lived Jupyter Spark sessions.

  • Access control: Keycloak group membership → composite realm roles → realm_access.roles JWT claim → Polaris principal-role regex mapper (group-level, no sync needed); polaris-sync-principals-script for individual/table-level grants layered on top.
  • AuthManager/broker flow: TeehrBrokerAuthManager (Spark-side) + new teehr-api broker endpoints (/auth/polaris-session, /auth/polaris-token, /auth/polaris-token/session) mint/rotate short-lived Polaris tokens from an encrypted, Postgres-backed delegated session store, without exposing refresh tokens to notebooks.
  • New/changed components: polaris, polaris-bootstrap, polaris-pg manifests; Trino/Spark/JupyterHub wiring updated for the new catalog; iceberg-rest/iceberg-pg removed.
  • Tests: new Polaris integration test scripts, AuthManager permission-enforcement tests, and a new automated cluster-mode executor auth test (spark-authmanager-executor-enforcement).
  • Security hardening from review: session/user-identity binding on broker token exchange, rate limiting on the session-token endpoint, corrected OpenAPI security schemes.
  • Docs added/corrected (polaris-access-control.md, polaris-broker-api-contract.md, polaris-migration-plan.md) to match actual implementation.

@mgdenno mgdenno linked an issue Jul 17, 2026 that may be closed by this pull request
@mgdenno mgdenno linked an issue Jul 20, 2026 that may be closed by this pull request
mgdenno and others added 27 commits August 18, 2026 17:26
Polaris's /v1/config endpoint echoes the catalog's entire raw
properties map back to every REST client, unconditionally, regardless
of the client's requested X-Iceberg-Access-Delegation mode. Setting
s3.remote-signing-enabled=true here made every client's Iceberg
S3FileIO try to use remote signing, which Polaris doesn't implement
(no /v1/aws/s3/sign route) — breaking clients that correctly ask for
vended-credentials instead. Drop the property; ensure_catalog() will
PUT the corrected properties onto the existing catalog on next run.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The jupyter SA's IAM role trust was recently narrowed to exclude it,
breaking Spark session creation. It was never load-bearing for real
data access: Iceberg reads go through the Polaris broker/AuthManager
(pure OAuth, no AWS creds), and icechunk/gridded reads go through
xpublish-api's own scoped role. Removing the annotation lets the
credential fallback in spark_session_utils.py degrade to anonymous
instead of throwing AssumeRoleWithWebIdentity AccessDenied.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Nothing consumes its credentials anywhere in teehr-cloud-core,
teehr-fved, or teehr — real Spark→Polaris auth goes through the
AuthManager/broker per-user delegated path instead. It was planned
for a headless Prefect batch client-credentials flow but never wired
up, and its lack of a realm role assignment (unlike trino-polaris and
prefect-polaris) would have made it a live footgun if anything ever
did start using it. Removing the client, its secret, and doc
references rather than fixing the missing role.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Stop BROKER_SESSION_SIGNING_SECRET and BROKER_REFRESH_TOKEN_ENCRYPTION_SECRET
  from collapsing to the same value as BROKER_OAUTH_CLIENT_SECRET (and each
  other). New dedicated broker-secrets K8s secret; config.py no longer
  cascades defaults between the three.
- exchange_token_for_polaris_via_broker_session now verifies the request's
  user_id/session_id/realm against the stored session record and rejects
  mismatches, per the binding requirement in polaris-broker-api-contract.md.
- /auth/polaris-token/session is no longer exempt from rate limiting —
  added InMemoryRateLimiter.check_by_key() for paths with no resolved
  AuthIdentity, keyed by client IP.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
spark-authmanager-enforcement (and spark-permission-enforcement) only
exercise driver-only local mode via create_minio_spark_session(), so
neither ever tested whether Spark executors actually get the Polaris
auth env vars AuthManager needs, or can participate in a real
distributed Iceberg write/read. That path was previously only checked
manually via test_executor_auth.py (run by hand in Jupyter, not part
of the automated suite).

Adds spark_authmanager_executor_test.py, adapted from that manual
script but with proper PASS/FAIL assertions and exit codes matching
the other Garden test scripts' convention, and without the
list_tables()/configurations table reads the manual version had --
those depend on warehouse seed data that may or may not exist when
this runs as an isolated automated test; this one only touches a
table it creates and drops itself. Uses create_spark_session()
directly rather than create_minio_spark_session(), since the MinIO
endpoint/path-style env vars are already set on this test container
(matching how the real jupyter/spark pods pick them up) and go
through the catalog-level S3 config path, not the Hadoop-level one
the MinIO wrapper additionally sets (which Iceberg's S3FileIO doesn't
use anyway).

Not yet confirmed whether the test container's service account can
actually create executor pods (RBAC for that is granted to the
`spark` ServiceAccount; unclear if/how the test container's default
SA gets equivalent permission) -- first run against the KinD cluster
will tell us.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
create_spark_session(start_spark_cluster=True) reads
os.environ["TEEHR_SPARK_IMAGE"] with no fallback when executor_image
isn't passed explicitly. jupyterhub's garden.yaml sets this from
build.teehr-spark-executor-image's output; the new Test action didn't,
since it's a different container than the jupyter pod.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ault

- Garden's type: container action schema has no serviceAccountName
  field (confirmed via its own "key not allowed" validation error,
  which lists every supported key). Switched to type: kubernetes-pod
  with a full podSpec, which does support it -- confirmed against a
  real KinD cluster that executor pods only get created successfully
  once this test container runs as the jupyter SA (the default SA in
  this namespace has no RBAC to create/list/watch pods, services, or
  PVCs; that's scoped to the jupyter/spark SAs specifically).
- create_spark_session() (used here instead of
  create_minio_spark_session()) defaults remote_warehouse_dir to ""
  when REMOTE_WAREHOUSE_IDENTIFIER isn't set, unlike the minio wrapper
  which defaults it to the realm name -- Polaris's REST catalog
  rejects CREATE TABLE with "Please specify a warehouse" when it's
  empty. Set REMOTE_WAREHOUSE_IDENTIFIER=teehr explicitly to match.

Confirmed passing end to end against a real KinD cluster: executor
pods created, auth env vars (POLARIS_DEFAULT_REALM,
POLARIS_BROKER_SESSION_TOKEN) present on all partitions, distributed
write/read/drop of a 200k-row Iceberg table succeeded.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ession

create_minio_spark_session() defaulted remote_warehouse_dir to the
realm name when REMOTE_WAREHOUSE_IDENTIFIER wasn't set; plain
create_spark_session() defaults it to "" instead, which Polaris's REST
catalog rejects with "Malformed request: Please specify a warehouse"
on any catalog read/write. Both test scripts already set
POLARIS_DEFAULT_REALM=teehr but not REMOTE_WAREHOUSE_IDENTIFIER, which
only surfaced once they stopped going through the minio wrapper.

Confirmed passing against a real KinD cluster: spark-permission-enforcement
(admin/poweruser/user) and spark-authmanager-enforcement (admin/poweruser/user)
both PASSED end to end.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…s-control doc

- /auth/polaris-session's OpenAPI schema advertised ApiKeyAuth like the
  generic default, but the route requires identity.auth_type == "jwt"
  same as /auth/polaris-token, which already has this override. Added
  the matching security override.
- docs/polaris-broker-api-contract.md documented a flat error payload
  shape; FastAPI actually wraps every HTTPException(detail=...) under
  a top-level "detail" key with no unwrapping handler registered.
  Updated the doc to match reality rather than changing response
  behavior (unknown whether any real consumer depends on the
  documented flat shape).
- polaris-sync-principals-script.yaml's sync_principals.py had two
  redundant paths granting principal roles: one from the user's
  effective realm roles (matching Polaris's own regex mapper input
  exactly), and a second, less complete one hardcoded against raw
  Keycloak group names/paths -- including a hardcoded plural/singular
  special case (iceberg-catalog-admins group -> iceberg-catalog-admin
  role) that only worked by coincidence for the groups that exist
  today. Removed the group-based path entirely; the realm-role-based
  path already covers everything correctly (composite roles included)
  since it mirrors exactly what Polaris's regex reads from the JWT.
- docs/polaris-access-control.md described Polaris reading a `groups`
  claim and matching plural/path-prefixed group names directly. The
  actual mechanism (polaris/manifests/polaris-config.yaml.tpl) reads
  realm_access.roles, populated via each Keycloak group's composite
  realmRoles mapping (keycloak-bootstrap/manifests/realm-configmap.yaml.tpl)
  -- a two-step chain the doc didn't mention. Rewrote to describe both
  steps accurately, including the plural-group/singular-role naming
  mismatch for iceberg-catalog-admins.

Verified against a real KinD cluster: /openapi.json confirms
/auth/polaris-session no longer advertises ApiKeyAuth; re-ran
spark-permission-enforcement after redeploying the updated
sync_principals.py -- still PASSED for all 3 users (admin/poweruser/user),
confirming the realm-role-only path grants identical permissions to
what the removed group-based path did.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@mgdenno
mgdenno requested review from jbkolze and samlamont August 28, 2026 20:15
@mgdenno
mgdenno marked this pull request as ready for review September 1, 2026 21:47
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.

Implement Polaris Apache Polaris planning and implementation

2 participants