Skip to content

@W-24145098 - Add Workload Identity Federation (WIF) admin REST API - #1894

Open
sushmasreedharan-tableau wants to merge 5 commits into
tableau:developmentfrom
sushmasreedharan-tableau:dev/sreedharan/w-24145098-wif-admin-apis
Open

sushmasreedharan-tableau wants to merge 5 commits into
tableau:developmentfrom
sushmasreedharan-tableau:dev/sreedharan/w-24145098-wif-admin-apis

Conversation

@sushmasreedharan-tableau

Copy link
Copy Markdown
Contributor

Summary

Expose the Tableau Server WIF admin REST surface (REST API 3.30+ / Tableau Server release 2026.3) on the TSC server object as server.auth_federation. Lets a site admin manage the WIF site configuration, signing keys, and connectors from the client, and complements the per-connection WIF attributes delivered in #1887.

Scope stops at the admin surface (/sites/{siteLuid}/auth-federation/admin[/connectors|/keys]). The public .well-known endpoints (/openid-configuration, /jwks.json) and the sign-in path are consumed by Snowflake / GCP / BigQuery directly, not by TSC callers, and are intentionally out of scope for this PR.

New endpoint methods

Admin config (/sites/{siteLuid}/auth-federation/admin)

  • server.auth_federation.get() -> AuthFederationItem
  • server.auth_federation.enable(config) -> AuthFederationItem — idempotent PUT; requires config.enabled=True
  • server.auth_federation.update(config) -> AuthFederationItem — merge-patch; only token_lifetime_seconds is sent on the wire; issuer_url, active_key_id, and connectors are server-assigned and omitted
  • server.auth_federation.delete() -> None — removes the config, every signing key, and every connector on the site

Connectors (.../admin/connectors)

  • server.auth_federation.create_connector(connector) -> AuthFederationConnectorItem
  • server.auth_federation.list_connectors() -> list[AuthFederationConnectorItem]
  • server.auth_federation.get_connector(id) — accepts LUID string or AuthFederationConnectorItem
  • server.auth_federation.delete_connector(id) — accepts LUID string or AuthFederationConnectorItem; idempotent server-side

Keys (.../admin/keys)

  • server.auth_federation.create_key() -> AuthFederationKeyItem — inactive by default; private key is never returned
  • server.auth_federation.activate_key(id) — atomic: deactivates whichever key was previously active
  • server.auth_federation.deactivate_key(id)
  • server.auth_federation.delete_key(id) — HTTP 400 if the key is still active

All methods are version-gated by @api(version="3.30").

New public types

Exported from both tableauserverclient.models and the TSC top level (TSC.X):

  • AuthFederationItem — admin config (enabled, issuer_url, active_key_id, token_lifetime_seconds, connectors)
  • AuthFederationConnectorItem — single connector (luid, connector_class, audience)
  • AuthFederationKeyItem — JWK metadata (kid, kty, alg, use, n, e)

Design notes mirrored from the server

  • enable() enforces enabled=True client-side; the server also rejects enabled=false with HTTP 400 and tells callers to use DELETE instead.
  • update() only emits tokenLifetimeSeconds on the wire. The server silently ignores read-only fields on PATCH; the client does the same to avoid pretending they're mutable.
  • delete_connector() is idempotent server-side (returns 204 even if the connector does not exist); orphan connections that referenced the deleted connector are not cascade-deleted.
  • create_key() sends only the empty <tsRequest/> envelope (POST body is empty on the Java controller side).
  • activate_key() / deactivate_key() / delete_key() take the JWK kid as the URL path parameter — same value the server returns as authFederationKey@kid.

Test plan

  • python -m pytest test/test_auth_federation.py — 22 new tests, all passing
  • python -m pytest test/ — full suite: 977 passed, 1 pre-existing skip, 0 failures
  • python -m mypy tableauserverclient — no issues in 106 source files
  • python -m black tableauserverclient test — formatted
  • Request-body shape asserted via defusedxml parsing of requests_mock history for every PUT / PATCH / POST method (not just string matching)
  • Both "pass a str" and "pass an item" forms covered for every id-taking method
  • EndpointUnavailableError verified when running against a server pinned to 3.29

🤖 Generated with Claude Code

Expose the Tableau Server WIF admin surface (REST API 3.30+ / Tableau
Server release 2026.3) on the TSC server object as
`server.auth_federation`. Site admins can now manage the whole WIF
lifecycle from the client, complementing the per-connection WIF
attributes delivered in PR tableau#1887.

New endpoint methods on `server.auth_federation`:

Admin config (`/sites/{siteLuid}/auth-federation/admin`)
- get() -> AuthFederationItem
- enable(config) -> AuthFederationItem   (idempotent PUT; requires enabled=True)
- update(config) -> AuthFederationItem   (merge-patch; only
  `token_lifetime_seconds` is mutable - `issuer_url`, `active_key_id`,
  and `connectors` are read-only and never sent on the wire)
- delete()                               (deletes config + keys + connectors)

Connectors (`.../admin/connectors`)
- create_connector(connector) -> AuthFederationConnectorItem
- list_connectors() -> list[AuthFederationConnectorItem]
- get_connector(id)    (accepts luid string or item)
- delete_connector(id) (accepts luid string or item; idempotent)

Keys (`.../admin/keys`)
- create_key() -> AuthFederationKeyItem   (inactive by default; private
  key is never returned)
- activate_key(id)    (atomic; deactivates whatever key was active)
- deactivate_key(id)
- delete_key(id)      (400 if the key is still active)

Three new public models exported from both `tableauserverclient.models`
and the TSC top-level: `AuthFederationItem`, `AuthFederationConnectorItem`,
`AuthFederationKeyItem`. All methods version-gated by @api(version="3.30").

Includes 22 unit tests under `test/test_auth_federation.py` covering the
happy path for every method, request-body shape (via defusedxml parsing
of `request_history[0].text`) for PUT/PATCH/POST verbs, the
read-only-field hygiene of `update()`, the enabled=True guard on
`enable()`, the required-field guards on `create_connector`, the
list-connectors empty case, both the "pass a str" and "pass an item"
forms of every id-taking method, and the EndpointUnavailableError raised
when running against a server older than 3.30.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Server-side AuthFederationAdminAppService.updateConfig rejects any
non-null `enabled` on PATCH with HTTP 400 and tells callers to use
enable() / delete() to change state. The client's update_req already
only sends `tokenLifetimeSeconds`, but the docstring listed only
issuer_url, active_key_id, and connectors as "silently ignored",
leaving `enabled` as a hidden footgun. Add it to the list and point
callers at the correct methods.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three docstring claims didn't match what the server actually does per
AuthFederationService.java and RestApiAuthFederationKeysAppService.java:

* enable(): did not mention that AuthFederationService.enableWif
  auto-generates one active signing key on first enable
  (AuthFederationService.java:160-164). A caller doesn't need to
  immediately create_key + activate_key — the returned item's
  active_key_id is already populated.

* list_connectors(): claimed empty-list on "WIF not enabled". The
  server's requireWifEnabled (AuthFederationService.java:226-233)
  throws WIF_NOT_ENABLED_FOR_SITE / FAILED_PRECONDITION, which the
  client surfaces as ServerResponseError. Only an empty configured
  site returns [].

* create_key(): claimed 409 for the max-keys-exceeded case. The server
  throws BadRequestRestException (HTTP 400), not 409
  (RestApiAuthFederationKeysAppService.java:44-48).

Docstring-only; no code or test changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
RestApiAuthFederationAdminAppService.listConnectors intercepts
TableauStatusCode.FAILED_PRECONDITION from the service-layer
requireWifEnabled check and re-throws configNotFound (HTTP 404) with
AUTH_FEDERATION_NOT_FOUND -- see RestApiAuthFederationAdminAppService.java:157-162.
The service-layer behavior cited in the previous commit does not reach
the wire; callers see 404.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Reword PAT/OAuth-scope guidance: the four key methods are bound to
  OAuthScopes.NO_ACCESS server-side, so every OAuth-scoped session
  (including every PAT) is rejected unconditionally. Authenticate with
  a username/password session instead. Reflected in CHANGELOG and the
  class docstring.
- Drop the misleading "Snowflake" case-sensitivity counter-example in
  create_connector; just state that class names are case-sensitive and
  must be the exact lowercase name.
- Note in CHANGELOG that delete_key fails with 400 on an active key;
  deactivate first.
- Replace vague "server rejects the request" in the class docstring
  with the specific HTTP 404 the server returns when WIF has never
  been enabled.
- Make the _coerce_uuid wording in CHANGELOG explicit: None, empty
  strings, and non-UUID-shaped targets are rejected client-side.
- Add a comment in test_auth_federation_update_empty_patch noting that
  server-side binding of an empty <authFederationAdmin/> must be
  covered by an integration test, not this unit test.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

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