|
| 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