Skip to content

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

Merged
sushmasreedharan-tableau merged 10 commits into
tableau:developmentfrom
sushmasreedharan-tableau:dev/sreedharan/w-24145098-wif-admin-apis
Oct 21, 2026
Merged

sushmasreedharan-tableau merged 10 commits into
tableau:developmentfrom
sushmasreedharan-tableau:dev/sreedharan/w-24145098-wif-admin-apis

Conversation

@sushmasreedharan-tableau

@sushmasreedharan-tableau sushmasreedharan-tableau commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Client-side wrapper for the Tableau Server 2026.3 Workload Identity Federation (WIF) admin REST API 3.30 at /sites/{siteLuid}/auth-federation/admin[/connectors|/keys].

Adds one new endpoint accessor — server.auth_federation — exposing:

  • Config: get(), enable(token_lifetime_seconds=None), update(token_lifetime_seconds=None), delete()
  • Connectors: create_connector(connector), list_connectors(), get_connector(id), delete_connector(id)
  • Keys: create_key(), activate_key(id), deactivate_key(id), delete_key(id)

Three new exports: AuthFederationItem, AuthFederationConnectorItem, AuthFederationKeyItem.

Wire-shape note

The admin controller (RestApiAuthFederationAdminController) is the only TSC REST controller that binds @RequestBody <SpecificType> instead of @RequestBody TsRequest. For parity, enable(), update(), and create_connector() emit the bare <authFederationAdmin> / <authFederationConnector> element with no <tsRequest> envelope. Every other method follows the usual envelope convention. See the comment at the top of AuthFederationRequest in request_factory.py.

Security

All id-taking methods (get_connector, delete_connector, activate_key, deactivate_key, delete_key) validate the id is a well-formed UUID client-side before any HTTP call — None, empty strings, and non-UUID-shaped targets raise ValueError, preventing path injection. 25 parametrized tests cover this.

Requirements

  • Tableau Server REST API 3.30+ (Tableau Server 2026.3)
  • WorkloadIdentityFederation feature flag enabled for the site (403 otherwise, including on get())
  • Site Administrator role (403 otherwise)

Scope semantics: the four key-management methods are bound to OAuthScopes.NO_ACCESS server-side, which rejects OAuth-scope-restricted sessions (connected-app JWT) with 401. Password and PAT sessions do not carry scopes and are accepted.

Test plan

  • 25 unit tests / 60 parametrized cases pass locally (pytest test/test_auth_federation.py)
  • black --check clean
  • mypy clean on modified files
  • E2E round-trip test in test_e2e/test_auth_federation.py — PAT-signed-in, covers every method, guards against clobbering an already-enabled site. Not run by maintainer — requires a 2026.3 site with the WIF feature flag enabled; reviewer should run or provide site creds.

Deferred follow-ups (not in this PR)

  • Server-side counterpart: change RestApiAuthFederationAdminController to @RequestBody TsRequest for parity with every other TSC REST controller. Would let us drop the bare-body branch in request_factory.AuthFederation.
  • No list-keys route in the server surface, so inactive keys can't be discovered via REST once their kid is forgotten client-side.

@W-24145098

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>
Review follow-up on PR tableau#1894.

- 403/404 docstring and CHANGELOG rewritten per server behavior:
  authorizationCheck() runs requireWifFeatureEnabled first, so every
  method (incl. get()) returns 403 when the per-site WIF feature flag
  is off — even for a site admin. 404 holds only for update() and the
  four connector methods; delete() on a never-enabled site is a no-op;
  create_key() has no enable() gate and leaves an orphan key against
  the per-site key limit; activate/deactivate/delete_key 404 only on
  unknown kid.
- PAT wording corrected: NO_ACCESS rejects only OAuth-scope-restricted
  sessions (e.g. connected-app JWT) with 401. PAT and password sessions
  do not carry scopes and are accepted — this is what the repo's own
  e2e harness relies on.
- delete_key rotation guidance: deactivating the active key with no
  replacement leaves the site unable to mint tokens. Safe order is
  create_key() -> activate_key(new) -> delete_key(old).
- New test_e2e/test_auth_federation.py: PAT-signed-in round trip
  across every method. Primarily a wire-shape regression test for the
  three bare-body routes (PUT/PATCH /admin, POST /admin/connectors)
  that mocked unit tests cannot catch. Guards against clobbering a
  live WIF config and tears down to a clean site on exit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three review nits from Wayne Xie's second pass on PR tableau#1894:

- Realism in the "never-enabled" GET fixture: use the real server defaults
  — token lifetime 7200 (AuthFederationService.java:86) and the actual
  issuer URL shape %s/api/%s/sites/<luid>/auth-federation (:748-766),
  not the fabricated /sites/mySite/... URL and 3600 lifetime.
- UUID-guard matrix: add None to the bad-target list and parametrize over
  {raw, item} so the item-wrapped None path (connector.luid=None /
  key.kid=None) is also covered. Widens bad_target to object.
- update() token_lifetime_seconds is now required: there is no other
  mutable field on the config, so a no-arg call is not a meaningful
  operation. Tightens the matching request_factory signature and drops
  the test that exercised the empty-patch wire shape.

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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The end-to-end test suppresses deletion failures, allowing a passing result despite unsuccessful cleanup.

2 open issues
What changed in this PR

Adds Workload Identity Federation administration to the Tableau Server Client through server.auth_federation, targeting REST API 3.30+.

Changes:

  • Adds configuration, connector, and signing-key management with UUID validation and version gating.
  • Exports three response models and implements the admin controller’s bare-XML request format.
  • Adds mocked unit tests, response fixtures, an end-to-end round-trip test, and usage documentation.
File Description
test/​test_auth_federation.py Tests endpoint behavior, request formats, UUID validation, and version gates.
test/​assets/​auth_federation_update.xml Updated configuration response fixture.
test/​assets/​auth_federation_key_create.xml Public signing-key response fixture.
test/​assets/​auth_federation_get.xml Configuration response with registered connectors.
test/​assets/​auth_federation_enable.xml Enabled configuration response fixture.
test/​assets/​auth_federation_connectors_list.xml Populated connector-list fixture.
test/​assets/​auth_federation_connectors_list_empty.xml Empty connector-list fixture.
test/​assets/​auth_federation_connector_get.xml Single-connector response fixture.
test/​assets/​auth_federation_connector_create.xml Created-connector response fixture.
test_e2e/​test_auth_federation.py Adds a guarded live-site lifecycle test.
tableauserverclient/​server/​server.py Registers server.auth_federation.
tableauserverclient/​server/​request_factory.py Serializes bare XML for configuration and connector requests.
tableauserverclient/​server/​endpoint/​auth_federation_endpoint.py Implements WIF administration methods and validation.
tableauserverclient/​server/​endpoint/​__init__.py Exports the new endpoint class.
tableauserverclient/​models/​auth_federation_item.py Adds configuration, connector, and key response models.
tableauserverclient/​models/​__init__.py Exports the new models.
tableauserverclient/​__init__.py Exposes the models through the public package API.
CHANGELOG.md Documents WIF administration and clarifies the release name.

Review effort: Balanced

Comment thread test_e2e/test_auth_federation.py Outdated
Comment thread tableauserverclient/server/endpoint/auth_federation_endpoint.py Outdated
@jacalata

Copy link
Copy Markdown
Contributor

The copilot suggestions are pretty small but they look like good things to do. Is that server-side TsRequest change actually planned soon?

@sushmasreedharan-tableau

Copy link
Copy Markdown
Contributor Author

The copilot suggestions are pretty small but they look like good things to do. Is that server-side TsRequest change actually planned soon?

Yea I'll create a GUS work item and talk to my team to see when we can pick up the server side changes. I can fix the co-pilot issues

- E2E teardown: let `ServerResponseError` from `auth_federation.delete()`
  propagate instead of swallowing it. A failed teardown left WIF enabled
  on the site, so the next run would hit the fixture's `enabled=True`
  guard and silently skip — the destructive paths stopped being
  exercised without anyone noticing.
- Key-rotation guidance (endpoint docstring, CHANGELOG, and docs): the
  previous `create_key() -> activate_key(new) -> delete_key(old)`
  sequence omitted that the old key must stay in the JWKS until every
  JWT it signed has expired. Deleting it immediately removes it from
  the key set, and verifiers that fetch JWKS afterwards reject the
  still-valid tokens signed with it. New guidance keeps the old key
  inactive (but published) for up to `token_lifetime_seconds` before
  deleting.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
sushmasreedharan-tableau added a commit to sushmasreedharan-tableau/server-client-python that referenced this pull request Oct 21, 2026
The code PR's docstring + CHANGELOG now clarify that the old key must be
retained in the site's JWKS until every JWT it signed has expired, and
only then deleted. The previous `create_key() -> activate_key(new) ->
delete_key(old)` sequence skipped that retain step; deleting the old key
immediately removes it from the JWKS and verifiers that fetch the key
set afterwards reject the still-valid tokens signed with it. Updated
the `auth_federation.delete_key` section here to match.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The behavior is self-evident from a bare `delete()` with no try/except;
the rationale belongs in the PR / commit history, not the test body.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread tableauserverclient/server/endpoint/auth_federation_endpoint.py
Comment thread tableauserverclient/server/endpoint/auth_federation_endpoint.py Outdated
- deactivate_key: absorb the "deactivate on the active key with no
  replacement leaves the site unable to mint" warning, which belongs
  on deactivate_key itself (not delete_key).
- delete_key: drop the create_key / activate_key chain from the
  rotation guidance. Reframe the docstring around what distinguishes
  delete from deactivate: both stop new signing with the key, but
  delete also pulls it from the JWKS, so still-unexpired tokens signed
  with it stop verifying. Rotation guidance now reduces to "keep the
  old key deactivated-but-published until its tokens expire, then
  delete".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
sushmasreedharan-tableau added a commit to sushmasreedharan-tableau/server-client-python that referenced this pull request Oct 21, 2026
Code PR dropped the activate_key chain from `delete_key` and refocused
the docstring on the delete-vs-deactivate distinction (both stop new
signing with the key, but delete also pulls it from the JWKS, breaking
verification for still-unexpired tokens). Mirror that framing here.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@sushmasreedharan-tableau
sushmasreedharan-tableau merged commit 83b8c14 into tableau:development Oct 21, 2026
31 checks passed
sushmasreedharan-tableau added a commit that referenced this pull request Oct 23, 2026
* @W-24145098 - docs: document WIF admin REST API endpoints

Documents the new server.auth_federation endpoint added in PR #1894 on the
tableau/server-client-python main branch.

- New top-level section 'Auth Federation' in docs/api-ref.md, between
  'Authentication' and 'Connections'. Documents:
  - AuthFederationItem, AuthFederationConnectorItem, AuthFederationKeyItem
    class attribute tables.
  - 12 endpoint methods: get, enable, update, delete; create_connector,
    list_connectors, get_connector, delete_connector; create_key,
    activate_key, deactivate_key, delete_key.
  - Signature, parameters, return type, and example per method.
  - Server behavior notes callers can't derive from the signature:
    per-method 403/404 semantics, PAT/scope acceptance, orphan-key risk
    when create_key runs before enable, safe-rotation order for keys,
    and client-side UUID validation that raises ValueError on malformed
    ids before any HTTP call.
- _includes/docs_menu.html: adds an 'Auth Federation' entry in the API
  Reference sidebar between 'Authentication' and 'Connections'.

Requires REST API 3.30+ (Tableau Server release 26.3) and the WIF
feature enabled on the site; the server returns 403 otherwise, including
on get().

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

* Match updated key-rotation guidance from PR #1894

The code PR's docstring + CHANGELOG now clarify that the old key must be
retained in the site's JWKS until every JWT it signed has expired, and
only then deleted. The previous `create_key() -> activate_key(new) ->
delete_key(old)` sequence skipped that retain step; deleting the old key
immediately removes it from the JWKS and verifiers that fetch the key
set afterwards reject the still-valid tokens signed with it. Updated
the `auth_federation.delete_key` section here to match.

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

* Match delete_key docstring reframe from PR #1894

Code PR dropped the activate_key chain from `delete_key` and refocused
the docstring on the delete-vs-deactivate distinction (both stop new
signing with the key, but delete also pulls it from the JWKS, breaking
verification for still-unexpired tokens). Mirror that framing here.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

3 participants