Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
# Unreleased
# v1.1.0

## Features

### TFE admin identity (SAML / SCIM)
* Added ``client.admin`` nested namespace exposing three TFE-only services: ``client.admin.saml_settings`` (read, update, revoke_idp_cert), ``client.admin.scim_settings`` (read, update, delete), and ``client.admin.scim_tokens`` (list, create, read, delete). All endpoints return ``pytfe.errors.NotFound`` on HCP Terraform (SaaS) — verified live against ``app.terraform.io``.
* Added models: ``AdminSAMLSettings`` / ``AdminSAMLSettingsUpdateOptions``, ``AdminSCIMSettings`` / ``AdminSCIMSettingsUpdateOptions``, ``AdminSCIMToken`` / ``AdminSCIMTokenCreateOptions``, plus ``SAMLProviderType`` and ``SAMLSignatureMethod`` enums.
* ``AdminSCIMSettingsUpdateOptions`` distinguishes "field unset" from "field explicitly set to None" for ``site_admin_group_scim_id``. Pass ``None`` to send JSON ``null`` (unlinking the SCIM site-admin group); omit the kwarg entirely to leave the server value untouched. The omit-vs-explicit-null distinction is preserved end-to-end via a custom ``to_payload()`` that inspects Pydantic's ``model_fields_set``.
* Added typed exceptions ``InvalidSAMLProviderTypeError``, ``InvalidSCIMTokenIDError``, ``RequiredSCIMTokenDescriptionError``.
* The transport-level redacting logger now redacts the wire-format ``private-key`` field (with hyphen) in addition to the existing ``private_key`` (with underscore), so SAML SP private keys cannot leak via ``PYTFE_LOG=debug``. X.509 certificate fields (``idp-cert``, ``certificate``, ``old-idp-cert``) are intentionally NOT redacted because they're public material by design.

### GitHub App installation discovery
* Added ``client.github_app_installations`` resource with ``list`` (supports ``filter[name]`` and ``filter[installation_id]``) and ``read`` methods for looking up GitHub App installations the authenticated user can see. Returns ``GitHubAppInstallation`` records carrying both the HCP-side ``id`` (``ghain-...``) and the GitHub-side numeric ``installation_id``. The actual GitHub App authorisation flow happens through the HCP Terraform UI; this resource is the discovery surface workspace/stack/registry-module VCS configuration consumes.
* Added model ``GitHubAppInstallation``, ``GitHubAppInstallationListOptions``, ``GitHubAppInstallationType``.
* Added typed exception ``InvalidGitHubAppInstallationIDError``.
## Bug Fixes

### Pagination
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ and upstream HCP Terraform API docs.
|---|---|
| Configure the SDK | [Authentication](./docs/authentication.md), [Pagination](./docs/pagination.md), [Logging](./docs/LOGGING.md) |
| API guides | [API index](./docs/api/index.md), [Workspaces](./docs/api/workspaces.md), [Runs/plans/applies](./docs/api/runs-plans-applies.md), [State versions](./docs/api/state-versions.md) |
| Scenario guides | [API-driven run](./docs/scenarios/api-driven-run.md), [State management](./docs/scenarios/state-management.md), [Migrate workspaces and state](./docs/scenarios/migrate-workspaces-and-state.md), [Team access onboarding](./docs/scenarios/team-access-onboarding.md), [No-code provisioning](./docs/scenarios/no-code-provisioning.md) |
| Scenario guides | [API-driven run](./docs/scenarios/api-driven-run.md), [State management](./docs/scenarios/state-management.md), [Migrate workspaces and state](./docs/scenarios/migrate-workspaces-and-state.md), [Team access onboarding](./docs/scenarios/team-access-onboarding.md), [No-code provisioning](./docs/scenarios/no-code-provisioning.md), [TFE identity bootstrap](./docs/scenarios/tfe-identity-bootstrap.md), [TFE admin bootstrap](./docs/scenarios/tfe-admin-bootstrap.md) |
| Operations guides | [Troubleshooting](./docs/troubleshooting.md), [Errors](./docs/errors.md), [Terraform Enterprise](./docs/terraform-enterprise.md) |
| Contribute to the SDK | [CONTRIBUTING](./docs/CONTRIBUTING.md), [ITERATORS](./docs/ITERATORS.md), [MODELS](./docs/MODELS.md), [RESOURCE](./docs/RESOURCE.md) |

Expand Down
270 changes: 270 additions & 0 deletions docs/api/admin-identity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
# Admin identity (SAML / SCIM) and GitHub App installations

pyTFE exposes the Terraform Enterprise admin identity APIs (SAML
settings, SCIM settings, SCIM tokens) under a nested namespace,
`client.admin.*`, and the read-only GitHub App installation discovery
API as a top-level `client.github_app_installations` resource.

| Service | TFE-only | Purpose |
|---|---|---|
| `client.admin.saml_settings` | yes | Read / update the org's SAML config; revoke previous IdP cert |
| `client.admin.scim_settings` | yes | Enable / pause SCIM, manage site-admin group mapping |
| `client.admin.scim_tokens` | yes | List / create / read / delete SCIM provisioning tokens |
| `client.github_app_installations` | no | Look up GitHub App installations visible to the caller |

Upstream docs:

- SAML settings: https://developer.hashicorp.com/terraform/enterprise/api-docs/admin/settings
- SCIM settings: https://developer.hashicorp.com/terraform/enterprise/api-docs/admin/scim-settings
- SCIM tokens: https://developer.hashicorp.com/terraform/enterprise/api-docs/admin/scim-tokens
- GitHub App installations: https://developer.hashicorp.com/terraform/enterprise/api-docs/github-app-installations

Examples:

- [`admin_identity.py`](../../examples/admin_identity.py)
- [`github_app_installations.py`](../../examples/github_app_installations.py)

## Why the nested namespace

The TFE admin APIs require **site-admin** permission, are not available
on HCP Terraform (SaaS), and live under a distinct `/api/v2/admin/...`
URL prefix. Grouping them under `client.admin.*` makes that boundary
visible at the call site — when you see `client.admin.foo()` you know
this is admin work, not a regular organisation operation. Calls against
HCP Terraform return `404`, surfaced as `pytfe.errors.NotFound`.

## SAML settings

Singleton resource: the organisation has exactly one SAML config.

| Method | Purpose |
|---|---|
| `client.admin.saml_settings.read()` | Read current SAML config. |
| `client.admin.saml_settings.update(options)` | Partial update — only fields you set are sent. |
| `client.admin.saml_settings.revoke_idp_cert()` | Promote the new IdP cert and clear the old one. |

```python
from pytfe import TFEClient
from pytfe.models import (
AdminSAMLSettingsUpdateOptions,
SAMLProviderType,
SAMLSignatureMethod,
)

client = TFEClient()

# Read
saml = client.admin.saml_settings.read()
print(saml.enabled, saml.provider_type, saml.sso_endpoint_url)

# Update (partial — only the listed fields are sent)
saml = client.admin.saml_settings.update(
AdminSAMLSettingsUpdateOptions(
enabled=True,
idp_cert="-----BEGIN CERTIFICATE-----\n...",
sso_endpoint_url="https://idp.example.com/sso",
slo_endpoint_url="https://idp.example.com/slo",
provider_type=SAMLProviderType.OKTA,
authn_requests_signed=True,
signature_signing_method=SAMLSignatureMethod.SHA256,
)
)
```

### Rotating the IdP certificate

```python
# 1. Push the new cert via update. The old cert stays in place during
# the rotation window so in-flight sessions keep working.
client.admin.saml_settings.update(
AdminSAMLSettingsUpdateOptions(
idp_cert="-----BEGIN CERTIFICATE-----\nNEW...",
)
)

# 2. Drain in-flight SSO sessions / verify the new cert works.

# 3. Revoke the old cert.
client.admin.saml_settings.revoke_idp_cert()
```

### Sensitive fields

`private_key` (wire name: `private-key`) is sensitive. The transport-
level debug logger redacts that key in both `snake_case` and
hyphenated forms before anything reaches the log. Certificate material
(`idp-cert`, `certificate`, `old-idp-cert`) is **not** redacted —
those are public X.509 blobs by design.

## SCIM settings

Singleton resource. Three operations: read, update (PATCH), delete.

| Method | Purpose |
|---|---|
| `client.admin.scim_settings.read()` | Read current SCIM config. |
| `client.admin.scim_settings.update(options)` | Partial update; see omit-vs-null note below. |
| `client.admin.scim_settings.delete()` | Disable SCIM. PATCH cannot set `enabled=False`; use this instead. |

```python
from pytfe.models import AdminSCIMSettingsUpdateOptions

scim = client.admin.scim_settings.read()
print(scim.enabled, scim.paused, scim.site_admin_group_display_name)

# Pause SCIM provisioning without disabling it
client.admin.scim_settings.update(AdminSCIMSettingsUpdateOptions(paused=True))

# Disable SCIM entirely (note: does NOT revoke site-admin access
# already granted by SCIM to existing users)
client.admin.scim_settings.delete()
```

### The omit-vs-explicit-null rule for `site_admin_group_scim_id`

This field has three meaningful states the SDK preserves end-to-end:

| Caller intent | How to express it | What goes on the wire |
|---|---|---|
| Don't touch the server value | Omit the kwarg entirely | Field is not in the request body |
| Set the mapping to a specific group | `site_admin_group_scim_id="g-1"` | `{"site-admin-group-scim-id": "g-1"}` |
| Unlink the SCIM site-admin group | `site_admin_group_scim_id=None` | `{"site-admin-group-scim-id": null}` |

Pydantic's normal `exclude_none=True` flattens "omit" and "explicit
None" together. To preserve the distinction this update options model
overrides serialization via `to_payload()`, which inspects
`model_fields_set` to tell the two cases apart. You call the resource
method the same way; the distinction is handled internally.

```python
# Omit — server keeps existing mapping
client.admin.scim_settings.update(
AdminSCIMSettingsUpdateOptions(paused=False)
)

# Explicit None — unlink the SCIM group from site-admin
client.admin.scim_settings.update(
AdminSCIMSettingsUpdateOptions(site_admin_group_scim_id=None)
)
```

## SCIM tokens

| Method | Purpose |
|---|---|
| `client.admin.scim_tokens.list()` | Iterate existing SCIM tokens (without their plaintext values). |
| `client.admin.scim_tokens.create(options)` | Mint a new SCIM token. The plaintext value is on the response — capture it now. |
| `client.admin.scim_tokens.read(scim_token_id)` | Read a single token's metadata. |
| `client.admin.scim_tokens.delete(scim_token_id)` | Revoke a SCIM token. |

```python
from pytfe.models import AdminSCIMTokenCreateOptions

# Mint a token — capture .token from the response, you won't see it again
new = client.admin.scim_tokens.create(
AdminSCIMTokenCreateOptions(description="okta-scim-bot")
)
print(new.id, new.token) # token is None on every subsequent read

for tok in client.admin.scim_tokens.list():
print(tok.id, tok.description, tok.last_used_at)

client.admin.scim_tokens.delete("at-...")
```

Two operational notes worth knowing:

- The `description` field is technically optional in the upstream API,
but the SDK rejects an empty description at the resource layer
(raises `RequiredSCIMTokenDescriptionError`). Audit logs are
unreadable without one.
- The DELETE path is `/api/v2/admin/scim-tokens/{id}` (the admin
namespace), not the generic `/api/v2/authentication-tokens/{id}`
path used by other token types.

## GitHub App installations

Read-only lookup. Use these endpoints to discover the
`github-app-installation-id` value that workspace, stack, and
registry-module VCS configuration takes.

| Method | Purpose |
|---|---|
| `client.github_app_installations.list(options=None)` | Iterate installations visible to the caller. |
| `client.github_app_installations.read(github_app_installation_id)` | Read one by HCP-side ID. |

```python
from pytfe.models import GitHubAppInstallationListOptions

# Find all installations
for app in client.github_app_installations.list():
print(app.id, app.name, app.installation_id, app.installation_type)

# Filter by GitHub org / login name
for app in client.github_app_installations.list(
GitHubAppInstallationListOptions(name="my-org")
):
print(app.id, app.installation_url)

# Filter by GitHub-side numeric installation ID (NOT the HCP `id`)
for app in client.github_app_installations.list(
GitHubAppInstallationListOptions(installation_id=54810170)
):
print(app.id)

# Read a specific installation by its HCP-side ID
app = client.github_app_installations.read("ghain-abc123")
```

Two things worth pinning explicitly:

- The read URL uses the singular path segment: `/api/v2/github-app/installation/{id}`,
not the plural `installations`. The list URL uses the plural. This
is the upstream contract — the SDK mirrors it.
- `installation_id` (the GitHub-side numeric ID) is distinct from `id`
(HCP Terraform's internal string ID, e.g. `ghain-...`). VCS-config
fields like `github-app-installation-id` use the HCP-side `id`, not
the numeric GitHub-side installation ID.

## SMTP settings

Singleton resource. Same admin-only requirements as SAML/SCIM.

| Method | Purpose |
|---|---|
| `client.admin.smtp_settings.read()` | Read current SMTP config (no password). |
| `client.admin.smtp_settings.update(options)` | Partial update; `password` and `test_email_address` are write-only. |

```python
from pytfe.models import AdminSMTPSettingsUpdateOptions, SMTPAuthType

# Read
smtp = client.admin.smtp_settings.read()
print(smtp.enabled, smtp.host, smtp.port, smtp.auth)

# Update (also sends a test email if test_email_address is set)
client.admin.smtp_settings.update(
AdminSMTPSettingsUpdateOptions(
enabled=True,
host="smtp.example.com",
port=587,
sender="noreply@example.com",
auth=SMTPAuthType.LOGIN,
username="smtp-bot",
password="set-by-secret-manager",
test_email_address="ops@example.com",
)
)
```

The `auth` field accepts `SMTPAuthType.NONE`, `PLAIN`, or `LOGIN`.
`password` is sensitive — the transport logger redacts it in debug
output. `test_email_address` is a write-only signal: when supplied on
update, TFE sends a verification email to that address and the field is
not returned on read.

## Token requirements

- SAML / SCIM / SCIM tokens / SMTP: TFE site-admin user token.
- GitHub App installations: any user token; the response is scoped to
what that user can see.
16 changes: 16 additions & 0 deletions docs/api/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,20 @@ column.
| `client.explorer` | `Explorer` | query and saved-view helpers | [explorer.py](../../examples/explorer.py) | [Explorer](https://developer.hashicorp.com/terraform/cloud-docs/api-docs/explorer) |
| `client.stacks` | `Stacks` | `list`, `read`, `create`, `update`, `delete`, `force_delete`, VCS fetch | [stack.py](../../examples/stack.py) | [Stacks](https://developer.hashicorp.com/terraform/cloud-docs/api-docs/stacks) |
| `client.stack_configurations` | `StackConfigurations` | `list`, `read`, `create` | [stack_configuration.py](../../examples/stack_configuration.py) | [Stacks](https://developer.hashicorp.com/terraform/cloud-docs/api-docs/stacks) |
| `client.github_app_installations` | `GitHubAppInstallations` | `list`, `read` | [github_app_installations.py](../../examples/github_app_installations.py) | [GitHub App installations](https://developer.hashicorp.com/terraform/enterprise/api-docs/github-app-installations) |
| `client.organization_token_ttl_policies` | `OrganizationTokenTTLPolicies` | `list`, `update`, `reset_to_defaults` | [org_token_ttl.py](../../examples/org_token_ttl.py) | [Org token TTL settings](https://developer.hashicorp.com/terraform/cloud-docs/users-teams-organizations/organizations/settings#api-tokens) |

## TFE admin (site-admin only)

These endpoints require TFE site-admin permission and return `404` on
HCP Terraform (SaaS).

| Client attribute | Resource class | Common methods | Example | Upstream API docs |
|---|---|---|---|---|
| `client.admin.saml_settings` | `_AdminSAMLSettings` | `read`, `update`, `revoke_idp_cert` | [admin_identity.py](../../examples/admin_identity.py) | [SAML settings](https://developer.hashicorp.com/terraform/enterprise/api-docs/admin/settings) |
| `client.admin.scim_settings` | `_AdminSCIMSettings` | `read`, `update`, `delete` | [admin_identity.py](../../examples/admin_identity.py) | [SCIM settings](https://developer.hashicorp.com/terraform/enterprise/api-docs/admin/scim-settings) |
| `client.admin.scim_tokens` | `_AdminSCIMTokens` | `list`, `create`, `read`, `delete` | [admin_identity.py](../../examples/admin_identity.py) | [SCIM tokens](https://developer.hashicorp.com/terraform/enterprise/api-docs/admin/scim-tokens) |
| `client.admin.smtp_settings` | `_AdminSMTPSettings` | `read`, `update` | [admin_smtp.py](../../examples/admin_smtp.py) | [SMTP settings](https://developer.hashicorp.com/terraform/enterprise/api-docs/admin/settings) |

## Focused guides

Expand All @@ -105,3 +119,5 @@ column.
- [policies.md](policies.md)
- [run-tasks.md](run-tasks.md)
- [no-code-provisioning.md](no-code-provisioning.md)
- [admin-identity.md](admin-identity.md)
- [organization-defaults-and-token-ttl.md](organization-defaults-and-token-ttl.md)
Loading
Loading