Skip to content

Commit 0abad7b

Browse files
Add organization audit configuration API support
1 parent 26ff34e commit 0abad7b

5 files changed

Lines changed: 443 additions & 0 deletions

File tree

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
#!/usr/bin/env python3
2+
"""Organization audit configuration operations example.
3+
4+
Demonstrates:
5+
1. read() - read organization audit configuration
6+
2. test() - send a test audit event
7+
3. update() - update organization audit configuration
8+
"""
9+
10+
import os
11+
12+
from pytfe import TFEClient, TFEConfig
13+
from pytfe.errors import TFEError
14+
from pytfe.models.organization_audit_configuration import (
15+
OrganizationAuditConfigAuditTrails,
16+
OrganizationAuditConfigurationOptions,
17+
)
18+
19+
20+
def main() -> None:
21+
client = TFEClient(TFEConfig.from_env())
22+
23+
organization_name = os.getenv("TFE_ORG", "example-org")
24+
25+
try:
26+
print("[READ] Reading organization audit configuration")
27+
read_result = client.organization_audit_configurations.read(organization_name)
28+
print(f"[READ] id={read_result.id}, updated_at={read_result.updated_at}")
29+
if read_result.audit_trails is not None:
30+
print(f"[READ] audit_trails_enabled={read_result.audit_trails.enabled}")
31+
32+
print("[TEST] Sending test audit event")
33+
test_result = client.organization_audit_configurations.test(organization_name)
34+
print(f"[TEST] request_id={test_result.request_id}")
35+
36+
print("[UPDATE] Updating organization audit configuration")
37+
options = OrganizationAuditConfigurationOptions(
38+
audit_trails=OrganizationAuditConfigAuditTrails(enabled=True)
39+
)
40+
update_result = client.organization_audit_configurations.update(
41+
organization_name,
42+
options,
43+
)
44+
print(f"[UPDATE] id={update_result.id}, updated_at={update_result.updated_at}")
45+
46+
except TFEError as exc:
47+
print(f"API error: {exc}")
48+
print("Check TFE_TOKEN, TFE_ADDRESS, and TFE_ORG.")
49+
finally:
50+
client.close()
51+
52+
53+
if __name__ == "__main__":
54+
main()

src/pytfe/client.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from .resources.notification_configuration import NotificationConfigurations
1313
from .resources.oauth_client import OAuthClients
1414
from .resources.oauth_token import OAuthTokens
15+
from .resources.organization_audit_configuration import OrganizationAuditConfigurations
1516
from .resources.organization_membership import OrganizationMemberships
1617
from .resources.organization_token import OrganizationTokens
1718
from .resources.organizations import Organizations
@@ -75,6 +76,9 @@ def __init__(self, config: TFEConfig | None = None):
7576
self.plans = Plans(self._transport)
7677
self.organizations = Organizations(self._transport)
7778
self.organization_memberships = OrganizationMemberships(self._transport)
79+
self.organization_audit_configurations = OrganizationAuditConfigurations(
80+
self._transport
81+
)
7882
self.users = Users(self._transport)
7983
self.organization_tokens = OrganizationTokens(self._transport)
8084
self.projects = Projects(self._transport)
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
from __future__ import annotations
2+
3+
from datetime import datetime
4+
5+
from pydantic import BaseModel, ConfigDict, Field
6+
7+
from .organization import Organization
8+
9+
10+
class OrganizationAuditConfigAuditTrails(BaseModel):
11+
"""Audit Trails configuration."""
12+
13+
model_config = ConfigDict(extra="forbid")
14+
15+
enabled: bool = Field(..., description="Whether Audit Trails is enabled")
16+
17+
18+
class OrganizationAuditConfigAuditStreaming(BaseModel):
19+
"""HCP Audit Log Streaming configuration."""
20+
21+
model_config = ConfigDict(populate_by_name=True, extra="forbid")
22+
23+
enabled: bool = Field(..., description="Whether HCP Audit Log Streaming is enabled")
24+
organization_id: str | None = Field(None, alias="organization-id")
25+
use_default_organization: bool = Field(
26+
...,
27+
alias="use-default-organization",
28+
)
29+
30+
31+
class OrganizationAuditConfigPermissions(BaseModel):
32+
"""Permissions for managing audit configuration."""
33+
34+
model_config = ConfigDict(populate_by_name=True, extra="forbid")
35+
36+
can_enable_hcp_audit_log_streaming: bool = Field(
37+
...,
38+
alias="can-enable-hcp-audit-log-streaming",
39+
)
40+
can_set_hcp_audit_log_streaming_organization: bool = Field(
41+
...,
42+
alias="can-set-hcp-audit-log-streaming-organization-id",
43+
)
44+
can_use_default_audit_log_streaming_organization: bool = Field(
45+
...,
46+
alias="can-use-default-audit-log-streaming-organization",
47+
)
48+
49+
50+
class OrganizationAuditConfigTimestamps(BaseModel):
51+
"""Timestamp fields for organization audit configuration events."""
52+
53+
model_config = ConfigDict(populate_by_name=True, extra="forbid")
54+
55+
audit_trails_disabled_at: datetime | None = Field(
56+
None,
57+
alias="audit-trails-disabled-at",
58+
)
59+
audit_trails_enabled_at: datetime | None = Field(
60+
None,
61+
alias="audit-trails-enabled-at",
62+
)
63+
audit_trails_last_failure: datetime | None = Field(
64+
None,
65+
alias="audit-trails-last-failure",
66+
)
67+
audit_trails_last_success: datetime | None = Field(
68+
None,
69+
alias="audit-trails-last-success",
70+
)
71+
hcp_audit_log_streaming_disabled_at: datetime | None = Field(
72+
None,
73+
alias="hcp-audit-log-streaming-disabled-at",
74+
)
75+
hcp_audit_log_streaming_enabled_at: datetime | None = Field(
76+
None,
77+
alias="hcp-audit-log-streaming-enabled-at",
78+
)
79+
hcp_audit_log_streaming_last_failure: datetime | None = Field(
80+
None,
81+
alias="hcp-audit-log-streaming-last-failure",
82+
)
83+
hcp_audit_log_streaming_last_success: datetime | None = Field(
84+
None,
85+
alias="hcp-audit-log-streaming-last-success",
86+
)
87+
88+
89+
class OrganizationAuditConfiguration(BaseModel):
90+
"""Organization audit configuration resource."""
91+
92+
model_config = ConfigDict(populate_by_name=True, extra="forbid")
93+
94+
id: str
95+
audit_trails: OrganizationAuditConfigAuditTrails | None = Field(
96+
None,
97+
alias="audit-trails",
98+
)
99+
hcp_audit_log_streaming: OrganizationAuditConfigAuditStreaming | None = Field(
100+
None,
101+
alias="hcp-audit-log-streaming",
102+
)
103+
permissions: OrganizationAuditConfigPermissions | None = None
104+
timestamps: OrganizationAuditConfigTimestamps | None = None
105+
updated_at: datetime | None = Field(None, alias="updated-at")
106+
organization: Organization | None = None
107+
108+
109+
class OrganizationAuditConfigurationTest(BaseModel):
110+
"""Result payload for sending a test audit event."""
111+
112+
model_config = ConfigDict(populate_by_name=True, extra="forbid")
113+
114+
request_id: str | None = Field(None, alias="request-id")
115+
116+
117+
class OrganizationAuditConfigurationOptions(BaseModel):
118+
"""Options for updating organization audit configuration."""
119+
120+
model_config = ConfigDict(populate_by_name=True, extra="forbid")
121+
122+
audit_trails: OrganizationAuditConfigAuditTrails | None = Field(
123+
None,
124+
alias="audit-trails",
125+
)
126+
hcp_audit_log_streaming: OrganizationAuditConfigAuditStreaming | None = Field(
127+
None,
128+
alias="hcp-audit-log-streaming",
129+
)
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
from __future__ import annotations
2+
3+
from typing import Any
4+
from urllib.parse import quote
5+
6+
from ..errors import ERR_INVALID_ORG
7+
from ..models.organization import Organization
8+
from ..models.organization_audit_configuration import (
9+
OrganizationAuditConfiguration,
10+
OrganizationAuditConfigurationOptions,
11+
OrganizationAuditConfigurationTest,
12+
)
13+
from ..utils import valid_string_id
14+
from ._base import _Service
15+
16+
17+
class OrganizationAuditConfigurations(_Service):
18+
"""Organization audit configuration service."""
19+
20+
def read(self, organization: str) -> OrganizationAuditConfiguration:
21+
"""Read an organization's audit configuration by organization name."""
22+
if not valid_string_id(organization):
23+
raise ValueError(ERR_INVALID_ORG)
24+
25+
path = f"/api/v2/organizations/{quote(organization)}/audit-configuration"
26+
response = self.t.request("GET", path)
27+
payload = response.json() or {}
28+
data = payload.get("data")
29+
if not isinstance(data, dict):
30+
raise ValueError("Invalid response format")
31+
32+
return self._parse_audit_configuration(data)
33+
34+
def test(self, organization: str) -> OrganizationAuditConfigurationTest:
35+
"""Send a test audit event for an organization."""
36+
if not valid_string_id(organization):
37+
raise ValueError(ERR_INVALID_ORG)
38+
39+
path = f"/api/v2/organizations/{quote(organization)}/audit-configuration/test"
40+
response = self.t.request("POST", path)
41+
payload = response.json() or {}
42+
43+
if isinstance(payload, dict) and "request-id" in payload:
44+
return OrganizationAuditConfigurationTest.model_validate(payload)
45+
46+
data = payload.get("data") if isinstance(payload, dict) else None
47+
if isinstance(data, dict):
48+
if "request-id" in data:
49+
return OrganizationAuditConfigurationTest.model_validate(data)
50+
attrs = data.get("attributes")
51+
if isinstance(attrs, dict) and "request-id" in attrs:
52+
return OrganizationAuditConfigurationTest.model_validate(attrs)
53+
54+
return OrganizationAuditConfigurationTest(request_id=None)
55+
56+
def update(
57+
self,
58+
organization: str,
59+
options: OrganizationAuditConfigurationOptions,
60+
) -> OrganizationAuditConfiguration:
61+
"""Update an organization's audit configuration."""
62+
if not valid_string_id(organization):
63+
raise ValueError(ERR_INVALID_ORG)
64+
65+
attrs = options.model_dump(by_alias=True, exclude_none=True)
66+
body: dict[str, Any] = {
67+
"data": {
68+
"type": "audit-configurations",
69+
"attributes": attrs,
70+
}
71+
}
72+
73+
path = f"/api/v2/organizations/{quote(organization)}/audit-configuration"
74+
response = self.t.request("PATCH", path, json_body=body)
75+
payload = response.json() or {}
76+
data = payload.get("data")
77+
if not isinstance(data, dict):
78+
raise ValueError("Invalid response format")
79+
80+
return self._parse_audit_configuration(data)
81+
82+
def _parse_audit_configuration(self, data: dict[str, Any]) -> OrganizationAuditConfiguration:
83+
attrs = data.get("attributes", {})
84+
relationships = data.get("relationships", {})
85+
86+
org = None
87+
org_data = relationships.get("organization", {}).get("data")
88+
if isinstance(org_data, dict):
89+
org = Organization(id=org_data.get("id"))
90+
91+
return OrganizationAuditConfiguration.model_validate(
92+
{
93+
"id": data.get("id", ""),
94+
"audit-trails": attrs.get("audit-trails"),
95+
"hcp-audit-log-streaming": attrs.get("hcp-audit-log-streaming"),
96+
"permissions": attrs.get("permissions"),
97+
"timestamps": attrs.get("timestamps"),
98+
"updated-at": attrs.get("updated-at"),
99+
"organization": org,
100+
}
101+
)

0 commit comments

Comments
 (0)