Repository navigation
Commit 83b8c14
@W-24145098 - Add Workload Identity Federation (WIF) admin REST API (#1894)
* @W-24145098 - Add Workload Identity Federation (WIF) admin REST API
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 #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>
* Document that AuthFederation.update() drops `enabled`
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>
* Align AuthFederation docstrings with monolith server behaviour
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>
* Correct list_connectors() docstring: server returns HTTP 404, not 412
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>
* Address review feedback on AuthFederation docs and tests
- 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>
* Correct 403/404 and PAT semantics; add e2e round-trip test
Review follow-up on PR #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>
* Tighten AuthFederation tests + require update() lifetime arg
Three review nits from Wayne Xie's second pass on PR #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>
* Address Copilot review comments on WIF admin PR
- 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>
* Remove rationale comment from WIF e2e teardown
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>
* Rework key-lifecycle docstrings per PR #1894 review
- 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>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>1 parent 3219c9d commit 83b8c14
18 files changed
Lines changed: 1164 additions & 1 deletion
File tree
- tableauserverclient
- models
- server
- endpoint
- test_e2e
- test
- assets
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | 3 | | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
4 | 57 | | |
5 | 58 | | |
6 | 59 | | |
| |||
29 | 82 | | |
30 | 83 | | |
31 | 84 | | |
32 | | - | |
| 85 | + | |
33 | 86 | | |
34 | 87 | | |
35 | 88 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | 3 | | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
4 | 7 | | |
5 | 8 | | |
6 | 9 | | |
| |||
80 | 83 | | |
81 | 84 | | |
82 | 85 | | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
83 | 89 | | |
84 | 90 | | |
85 | 91 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
1 | 6 | | |
2 | 7 | | |
3 | 8 | | |
| |||
55 | 60 | | |
56 | 61 | | |
57 | 62 | | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
58 | 66 | | |
59 | 67 | | |
60 | 68 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
| 2 | + | |
2 | 3 | | |
3 | 4 | | |
4 | 5 | | |
| |||
35 | 36 | | |
36 | 37 | | |
37 | 38 | | |
| 39 | + | |
38 | 40 | | |
39 | 41 | | |
40 | 42 | | |
| |||
0 commit comments