Skip to content

Commit e1fd0b2

Browse files
committed
add support for other admin features like smtp, token
1 parent a4621c3 commit e1fd0b2

16 files changed

Lines changed: 1769 additions & 33 deletions

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ and upstream HCP Terraform API docs.
165165
|---|---|
166166
| Configure the SDK | [Authentication](./docs/authentication.md), [Pagination](./docs/pagination.md), [Logging](./docs/LOGGING.md) |
167167
| 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) |
168-
| 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) |
168+
| 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) |
169169
| Operations guides | [Troubleshooting](./docs/troubleshooting.md), [Errors](./docs/errors.md), [Terraform Enterprise](./docs/terraform-enterprise.md) |
170170
| Contribute to the SDK | [CONTRIBUTING](./docs/CONTRIBUTING.md), [ITERATORS](./docs/ITERATORS.md), [MODELS](./docs/MODELS.md), [RESOURCE](./docs/RESOURCE.md) |
171171

docs/api/admin-identity.md

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,8 +226,45 @@ Two things worth pinning explicitly:
226226
fields like `github-app-installation-id` use the HCP-side `id`, not
227227
the numeric GitHub-side installation ID.
228228

229+
## SMTP settings
230+
231+
Singleton resource. Same admin-only requirements as SAML/SCIM.
232+
233+
| Method | Purpose |
234+
|---|---|
235+
| `client.admin.smtp_settings.read()` | Read current SMTP config (no password). |
236+
| `client.admin.smtp_settings.update(options)` | Partial update; `password` and `test_email_address` are write-only. |
237+
238+
```python
239+
from pytfe.models import AdminSMTPSettingsUpdateOptions, SMTPAuthType
240+
241+
# Read
242+
smtp = client.admin.smtp_settings.read()
243+
print(smtp.enabled, smtp.host, smtp.port, smtp.auth)
244+
245+
# Update (also sends a test email if test_email_address is set)
246+
client.admin.smtp_settings.update(
247+
AdminSMTPSettingsUpdateOptions(
248+
enabled=True,
249+
host="smtp.example.com",
250+
port=587,
251+
sender="noreply@example.com",
252+
auth=SMTPAuthType.LOGIN,
253+
username="smtp-bot",
254+
password="set-by-secret-manager",
255+
test_email_address="ops@example.com",
256+
)
257+
)
258+
```
259+
260+
The `auth` field accepts `SMTPAuthType.NONE`, `PLAIN`, or `LOGIN`.
261+
`password` is sensitive — the transport logger redacts it in debug
262+
output. `test_email_address` is a write-only signal: when supplied on
263+
update, TFE sends a verification email to that address and the field is
264+
not returned on read.
265+
229266
## Token requirements
230267

231-
- SAML / SCIM / SCIM tokens: TFE site-admin user token.
268+
- SAML / SCIM / SCIM tokens / SMTP: TFE site-admin user token.
232269
- GitHub App installations: any user token; the response is scoped to
233270
what that user can see.

docs/api/index.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ column.
9595
| `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) |
9696
| `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) |
9797
| `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) |
98+
| `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) |
9899

99100
## TFE admin (site-admin only)
100101

@@ -106,6 +107,7 @@ HCP Terraform (SaaS).
106107
| `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) |
107108
| `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) |
108109
| `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) |
110+
| `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) |
109111

110112
## Focused guides
111113

@@ -118,3 +120,4 @@ HCP Terraform (SaaS).
118120
- [run-tasks.md](run-tasks.md)
119121
- [no-code-provisioning.md](no-code-provisioning.md)
120122
- [admin-identity.md](admin-identity.md)
123+
- [organization-defaults-and-token-ttl.md](organization-defaults-and-token-ttl.md)
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
# Organisation defaults and API-token TTL policy
2+
3+
Two closely-related per-organisation knobs that both live alongside
4+
the existing `client.organizations` resource:
5+
6+
- **Default execution mode + default agent pool** — what new workspaces
7+
inherit. Exposed via three focused methods on `client.organizations`:
8+
`read_default_settings`, `update_default_settings`,
9+
`reset_default_settings`.
10+
- **API-token max TTL** — how long org/team/user/audit tokens minted in
11+
the organisation are allowed to live. Exposed on a dedicated resource:
12+
`client.organization_token_ttl_policies`.
13+
14+
Both are available on HCP Terraform and on Terraform Enterprise. Neither
15+
requires site-admin permissions; org-owner permissions are sufficient.
16+
17+
Upstream docs:
18+
19+
- Organisations API: https://developer.hashicorp.com/terraform/cloud-docs/api-docs/organizations
20+
- Organisation settings (max TTL): https://developer.hashicorp.com/terraform/cloud-docs/users-teams-organizations/organizations/settings#api-tokens
21+
22+
Examples:
23+
24+
- [`admin_smtp.py`](../../examples/admin_smtp.py) (SMTP — TFE-only; not relevant here but in the same bootstrap scenario)
25+
- [`org_token_ttl.py`](../../examples/org_token_ttl.py)
26+
27+
## Default execution mode + default agent pool
28+
29+
| Method | Purpose |
30+
|---|---|
31+
| `client.organizations.read_default_settings(org)` | Read default execution mode + default agent pool. |
32+
| `client.organizations.update_default_settings(org, options)` | Partial update — see omit-vs-explicit-null rule below. |
33+
| `client.organizations.reset_default_settings(org)` | Convenience: reset to `remote` execution and clear the default agent pool. |
34+
35+
```python
36+
from pytfe import TFEClient
37+
from pytfe.models import OrganizationDefaultSettingsUpdateOptions
38+
39+
client = TFEClient()
40+
41+
# Read
42+
defaults = client.organizations.read_default_settings("my-org")
43+
print(defaults.default_execution_mode, defaults.default_agent_pool_id)
44+
45+
# Switch to agent execution and pin the default pool
46+
client.organizations.update_default_settings(
47+
"my-org",
48+
OrganizationDefaultSettingsUpdateOptions(
49+
default_execution_mode="agent",
50+
default_agent_pool_id="apool-abc123",
51+
),
52+
)
53+
54+
# Reset to remote, clearing the agent pool
55+
client.organizations.reset_default_settings("my-org")
56+
```
57+
58+
### Cross-field validation
59+
60+
`OrganizationDefaultSettingsUpdateOptions` rejects at construction time
61+
the combination "specify a pool id while explicitly asking for a
62+
non-agent execution mode":
63+
64+
```python
65+
# Raises pydantic.ValidationError immediately — no API call.
66+
OrganizationDefaultSettingsUpdateOptions(
67+
default_execution_mode="remote",
68+
default_agent_pool_id="apool-abc123",
69+
)
70+
```
71+
72+
This mirrors the upstream rule and surfaces the mistake locally rather
73+
than as an opaque server-side 422.
74+
75+
### Omit vs explicit `None` for `default_agent_pool_id`
76+
77+
Like SCIM settings, the agent pool field distinguishes three caller
78+
intents end-to-end:
79+
80+
| Caller intent | How to express it | What goes on the wire |
81+
|---|---|---|
82+
| Don't touch the server value | Omit the kwarg entirely | Field is not in the request body |
83+
| Set the pool to a specific id | `default_agent_pool_id="apool-1"` | `{"default-agent-pool-id": "apool-1"}` |
84+
| Clear the pool | `default_agent_pool_id=None` | `{"default-agent-pool-id": null}` |
85+
86+
The `to_payload()` method on the options inspects
87+
`model_fields_set` to preserve this distinction — Pydantic's
88+
`exclude_none=True` would otherwise flatten "omit" and "explicit None"
89+
together.
90+
91+
### What about the broader `client.organizations.update`?
92+
93+
The existing `OrganizationUpdateOptions` has also been fixed (this same
94+
release) so its `default_execution_mode`, `default_agent_pool_id`, and
95+
`max_ttl_enabled` fields now serialise with the correct hyphenated JSON
96+
wire names. Previously they were emitted as snake_case and silently
97+
ignored by the server. If you were calling `client.organizations.update`
98+
with those fields and seeing no effect, this fixes it.
99+
100+
## API-token TTL policy
101+
102+
The org enforces a per-token-type maximum lifetime when
103+
`max_ttl_enabled=True` on the parent organisation. The per-token-type
104+
values live on a separate resource:
105+
106+
| Method | Purpose |
107+
|---|---|
108+
| `client.organization_token_ttl_policies.list(org)` | Iterate current policies. |
109+
| `client.organization_token_ttl_policies.update(org, options)` | PATCH a partial set; at least one field required. |
110+
| `client.organization_token_ttl_policies.reset_to_defaults(org)` | Reset all four token types to the documented 2-year default. |
111+
112+
```python
113+
from pytfe.models import OrgTokenTTLPolicyUpdateOptions, DEFAULT_MAX_TTL_MS
114+
115+
# List
116+
for policy in client.organization_token_ttl_policies.list("my-org"):
117+
print(policy.token_type, policy.max_ttl_ms)
118+
119+
# Update some token types — accepts integers (raw ms) OR duration strings
120+
client.organization_token_ttl_policies.update(
121+
"my-org",
122+
OrgTokenTTLPolicyUpdateOptions(
123+
organization="2y", # duration string
124+
team="30d", # duration string
125+
user=DEFAULT_MAX_TTL_MS, # raw ms
126+
# audit_trails omitted -> server keeps existing value
127+
),
128+
)
129+
130+
# Reset everything to the 2-year default
131+
client.organization_token_ttl_policies.reset_to_defaults("my-org")
132+
```
133+
134+
### Duration parser
135+
136+
`parse_ttl_to_ms()` accepts the same suffixes the Terraform provider
137+
does:
138+
139+
| Suffix | Meaning |
140+
|---|---|
141+
| `ms` | milliseconds |
142+
| `s` | seconds |
143+
| `m` | minutes |
144+
| `h` | hours |
145+
| `d` | days |
146+
| `w` | weeks (7 days) |
147+
| `mo` | months (approximated as 30 days) |
148+
| `y` | years (365 days) |
149+
150+
```python
151+
from pytfe.models import parse_ttl_to_ms
152+
153+
parse_ttl_to_ms("2y") # -> 63_072_000_000
154+
parse_ttl_to_ms("30d") # -> 2_592_000_000
155+
parse_ttl_to_ms("6mo") # -> 15_552_000_000
156+
parse_ttl_to_ms("1h") # -> 3_600_000
157+
```
158+
159+
Use exact day counts (e.g. `"90d"`) when you need precision; months are
160+
approximated as 30 days.
161+
162+
### Important: `audit_trails` token type spelling
163+
164+
The TTL policy API uses `audit_trails` (with an UNDERSCORE) for the
165+
audit-trail policy entry. This is **deliberately different** from the
166+
audit-trail token *creation* endpoint elsewhere in the API which uses
167+
`audit-trails` (with a HYPHEN). The `TokenPolicyType.AUDIT_TRAILS` enum
168+
member preserves the TTL-specific spelling exactly:
169+
170+
```python
171+
from pytfe.models import TokenPolicyType
172+
TokenPolicyType.AUDIT_TRAILS.value # -> "audit_trails"
173+
```
174+
175+
If you copy a token-type string from another part of the API into a TTL
176+
policy call, the server will reject it. The SDK enforces the correct
177+
value at construction time via the enum.
178+
179+
### Empty-update guard
180+
181+
Building an `OrgTokenTTLPolicyUpdateOptions` with no fields and calling
182+
`update()` raises `pytfe.errors.RequiredFieldMissing` **before** any
183+
HTTP request is made:
184+
185+
```python
186+
client.organization_token_ttl_policies.update(
187+
"my-org",
188+
OrgTokenTTLPolicyUpdateOptions(), # no fields
189+
)
190+
# RequiredFieldMissing: OrgTokenTTLPolicyUpdateOptions requires at
191+
# least one of organization, team, user, or audit_trails to be set.
192+
```
193+
194+
This guards against accidental no-op calls that would otherwise hit the
195+
server and either silently succeed (changing nothing) or fail with a
196+
shape error.
197+
198+
## Operational notes
199+
200+
- **Pair `max_ttl_enabled` with policies.** The TTL policy values are
201+
only enforced when the org's `max_ttl_enabled` is true. Flip that on
202+
with `client.organizations.update(org, OrganizationUpdateOptions(max_ttl_enabled=True))`.
203+
- **Reducing TTL doesn't invalidate existing tokens.** Tokens issued
204+
before a policy change keep their original expiration. Plan rotations
205+
accordingly.
206+
- **HCP Terraform vs TFE.** Both surfaces are available on both
207+
platforms (this is not a TFE-only feature, unlike the SAML/SCIM/SMTP
208+
admin endpoints). Documented version gates are not enforced
209+
client-side; the server returns the authoritative error if a feature
210+
isn't available.

0 commit comments

Comments
 (0)