Skip to content

Commit 760583d

Browse files
committed
feat(gooddata-sdk): [AUTO] Add IP allowlist policy CRUD endpoints to metadata API
1 parent e1d6ad4 commit 760583d

5 files changed

Lines changed: 279 additions & 0 deletions

File tree

packages/gooddata-sdk/src/gooddata_sdk/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,10 @@
109109
CatalogExportTemplate,
110110
CatalogExportTemplateAttributes,
111111
)
112+
from gooddata_sdk.catalog.organization.entity_model.ip_allowlist_policy import (
113+
CatalogIpAllowlistPolicy,
114+
CatalogIpAllowlistPolicyAttributes,
115+
)
112116
from gooddata_sdk.catalog.organization.entity_model.jwk import (
113117
CatalogJwk,
114118
CatalogJwkAttributes,
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# (C) 2026 GoodData Corporation
2+
from __future__ import annotations
3+
4+
from typing import Any
5+
6+
import attrs
7+
8+
9+
@attrs.define(kw_only=True)
10+
class CatalogIpAllowlistPolicyAttributes:
11+
"""Attributes of an IP allowlist policy."""
12+
13+
allowed_sources: list[str] = attrs.field(factory=list)
14+
15+
@classmethod
16+
def from_api(cls, entity: dict[str, Any]) -> CatalogIpAllowlistPolicyAttributes:
17+
raw = entity.get("allowedSources")
18+
return cls(
19+
allowed_sources=list(raw) if raw is not None else [],
20+
)
21+
22+
def to_api(self) -> dict[str, Any]:
23+
return {"allowedSources": self.allowed_sources}
24+
25+
26+
@attrs.define(kw_only=True)
27+
class CatalogIpAllowlistPolicy:
28+
"""Represents an IP allowlist policy entity."""
29+
30+
id: str
31+
attributes: CatalogIpAllowlistPolicyAttributes | None = None
32+
33+
@classmethod
34+
def from_api(cls, entity: dict[str, Any]) -> CatalogIpAllowlistPolicy:
35+
"""Parse from a JSON:API entity data dict (the ``data`` key of a response).
36+
37+
Args:
38+
entity: The ``data`` portion of a JSON:API document, e.g.
39+
``{"id": "...", "type": "ipAllowlistPolicy", "attributes": {...}}``.
40+
41+
Returns:
42+
CatalogIpAllowlistPolicy: The parsed entity.
43+
"""
44+
raw_attrs = entity.get("attributes")
45+
return cls(
46+
id=entity["id"],
47+
attributes=CatalogIpAllowlistPolicyAttributes.from_api(raw_attrs) if raw_attrs is not None else None,
48+
)
49+
50+
def to_api(self) -> dict[str, Any]:
51+
"""Serialise to a JSON:API request document.
52+
53+
Returns:
54+
dict: A JSON:API document suitable for POST/PUT request bodies.
55+
"""
56+
attrs_dict: dict[str, Any] = {}
57+
if self.attributes is not None:
58+
attrs_dict = self.attributes.to_api()
59+
return {
60+
"data": {
61+
"id": self.id,
62+
"type": "ipAllowlistPolicy",
63+
"attributes": attrs_dict,
64+
}
65+
}

packages/gooddata-sdk/src/gooddata_sdk/catalog/organization/service.py

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@
2020

2121
from gooddata_sdk import CatalogDeclarativeExportTemplate, CatalogExportTemplate
2222
from gooddata_sdk.catalog.catalog_service_base import CatalogServiceBase
23+
from gooddata_sdk.catalog.identifier import CatalogAssigneeIdentifier
2324
from gooddata_sdk.catalog.organization.entity_model.directive import CatalogCspDirective
2425
from gooddata_sdk.catalog.organization.entity_model.identity_provider import CatalogIdentityProvider
26+
from gooddata_sdk.catalog.organization.entity_model.ip_allowlist_policy import CatalogIpAllowlistPolicy
2527
from gooddata_sdk.catalog.organization.entity_model.jwk import CatalogJwk, CatalogJwkDocument
2628
from gooddata_sdk.catalog.organization.entity_model.llm_provider import (
2729
CatalogLlmProvider,
@@ -628,6 +630,121 @@ def delete_llm_provider(self, id: str) -> None:
628630
"""
629631
self._entities_api.delete_entity_llm_providers(id, _check_return_type=False)
630632

633+
# IP Allowlist Policy APIs
634+
635+
def get_ip_allowlist_policy(self, policy_id: str) -> CatalogIpAllowlistPolicy:
636+
"""Get an IP allowlist policy by ID.
637+
638+
Args:
639+
policy_id (str): IP allowlist policy identifier.
640+
641+
Returns:
642+
CatalogIpAllowlistPolicy: The retrieved policy.
643+
"""
644+
response = self._client._do_json_request(
645+
"GET",
646+
f"api/v1/entities/ipAllowlistPolicies/{policy_id}",
647+
)
648+
response.raise_for_status()
649+
return CatalogIpAllowlistPolicy.from_api(response.json()["data"])
650+
651+
def list_ip_allowlist_policies(self) -> list[CatalogIpAllowlistPolicy]:
652+
"""List all IP allowlist policies in the organization.
653+
654+
Returns:
655+
list[CatalogIpAllowlistPolicy]: List of IP allowlist policies.
656+
"""
657+
response = self._client._do_json_request(
658+
"GET",
659+
"api/v1/entities/ipAllowlistPolicies",
660+
)
661+
response.raise_for_status()
662+
return [CatalogIpAllowlistPolicy.from_api(item) for item in response.json().get("data", [])]
663+
664+
def create_ip_allowlist_policy(self, policy: CatalogIpAllowlistPolicy) -> CatalogIpAllowlistPolicy:
665+
"""Create a new IP allowlist policy.
666+
667+
Args:
668+
policy (CatalogIpAllowlistPolicy): The policy to create.
669+
670+
Returns:
671+
CatalogIpAllowlistPolicy: The created policy as returned by the server.
672+
"""
673+
response = self._client._do_json_request(
674+
"POST",
675+
"api/v1/entities/ipAllowlistPolicies",
676+
json_body=policy.to_api(),
677+
)
678+
response.raise_for_status()
679+
return CatalogIpAllowlistPolicy.from_api(response.json()["data"])
680+
681+
def update_ip_allowlist_policy(self, policy: CatalogIpAllowlistPolicy) -> CatalogIpAllowlistPolicy:
682+
"""Replace an existing IP allowlist policy (full PUT).
683+
684+
Args:
685+
policy (CatalogIpAllowlistPolicy): The policy with updated values.
686+
687+
Returns:
688+
CatalogIpAllowlistPolicy: The updated policy as returned by the server.
689+
"""
690+
response = self._client._do_json_request(
691+
"PUT",
692+
f"api/v1/entities/ipAllowlistPolicies/{policy.id}",
693+
json_body=policy.to_api(),
694+
)
695+
response.raise_for_status()
696+
return CatalogIpAllowlistPolicy.from_api(response.json()["data"])
697+
698+
def delete_ip_allowlist_policy(self, policy_id: str) -> None:
699+
"""Delete an IP allowlist policy.
700+
701+
Args:
702+
policy_id (str): IP allowlist policy identifier.
703+
"""
704+
response = self._client._do_json_request(
705+
"DELETE",
706+
f"api/v1/entities/ipAllowlistPolicies/{policy_id}",
707+
)
708+
response.raise_for_status()
709+
710+
def add_targets_to_ip_allowlist_policy(
711+
self,
712+
policy_id: str,
713+
targets: list[CatalogAssigneeIdentifier],
714+
) -> None:
715+
"""Add user or user-group targets to an IP allowlist policy.
716+
717+
Args:
718+
policy_id (str): IP allowlist policy identifier.
719+
targets (list[CatalogAssigneeIdentifier]): Users or user groups to add.
720+
"""
721+
request_body = {"targets": [{"id": t.id, "type": t.type} for t in targets]}
722+
response = self._client._do_json_request(
723+
"POST",
724+
f"api/v1/actions/ipAllowlistPolicies/{policy_id}/addTargets",
725+
json_body=request_body,
726+
)
727+
response.raise_for_status()
728+
729+
def remove_targets_from_ip_allowlist_policy(
730+
self,
731+
policy_id: str,
732+
targets: list[CatalogAssigneeIdentifier],
733+
) -> None:
734+
"""Remove user or user-group targets from an IP allowlist policy.
735+
736+
Args:
737+
policy_id (str): IP allowlist policy identifier.
738+
targets (list[CatalogAssigneeIdentifier]): Users or user groups to remove.
739+
"""
740+
request_body = {"targets": [{"id": t.id, "type": t.type} for t in targets]}
741+
response = self._client._do_json_request(
742+
"POST",
743+
f"api/v1/actions/ipAllowlistPolicies/{policy_id}/removeTargets",
744+
json_body=request_body,
745+
)
746+
response.raise_for_status()
747+
631748
# Layout APIs
632749

633750
def get_declarative_notification_channels(self) -> list[CatalogDeclarativeNotificationChannel]:

packages/gooddata-sdk/src/gooddata_sdk/client.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,55 @@ def __init__(
9191
self._ai_lake_api = apis.AILakeApi(self._api_client)
9292
self._executions_cancellable = executions_cancellable
9393

94+
def _do_json_request(
95+
self,
96+
method: str,
97+
endpoint: str,
98+
json_body: dict | None = None,
99+
) -> requests.Response:
100+
"""Perform a JSON:API HTTP request.
101+
102+
Used as a low-level escape hatch when the generated API client does not
103+
yet have the endpoint models (e.g. immediately after a new endpoint is
104+
added to the OpenAPI spec but before the client is regenerated).
105+
106+
The standard GoodData request headers (Authorization, X-Requested-With,
107+
X-GDC-VALIDATE-RELATIONS) are added automatically. Any custom headers
108+
provided at client construction time are merged in as well.
109+
110+
Args:
111+
method (str): HTTP method string, e.g. ``"GET"``, ``"POST"``,
112+
``"PUT"``, ``"DELETE"``.
113+
endpoint (str): API endpoint path without a leading slash, e.g.
114+
``"api/v1/entities/ipAllowlistPolicies"``.
115+
json_body (dict | None): Optional dict to be serialised as the JSON
116+
request body. When given, ``Content-Type`` is set to
117+
``application/vnd.gooddata.api+json``.
118+
119+
Returns:
120+
requests.Response: The HTTP response. The caller is responsible
121+
for checking the status code (e.g. ``response.raise_for_status()``).
122+
"""
123+
if not self._hostname.endswith("/"):
124+
endpoint = f"/{endpoint}"
125+
126+
headers: dict[str, str] = {
127+
"Authorization": f"Bearer {self._token}",
128+
"Accept": "application/vnd.gooddata.api+json",
129+
"X-Requested-With": "XMLHttpRequest",
130+
"X-GDC-VALIDATE-RELATIONS": "true",
131+
}
132+
if json_body is not None:
133+
headers["Content-Type"] = "application/vnd.gooddata.api+json"
134+
headers.update(self._custom_headers)
135+
136+
return requests.request(
137+
method=method,
138+
url=f"{self._hostname}{endpoint}",
139+
headers=headers,
140+
json=json_body,
141+
)
142+
94143
def _do_post_request(
95144
self,
96145
data: bytes,

packages/gooddata-sdk/tests/catalog/test_catalog_organization.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
from gooddata_sdk import (
88
CatalogCspDirective,
99
CatalogDeclarativeNotificationChannel,
10+
CatalogIpAllowlistPolicy,
11+
CatalogIpAllowlistPolicyAttributes,
1012
CatalogJwk,
1113
CatalogOrganization,
1214
CatalogOrganizationSetting,
@@ -563,3 +565,45 @@ def test_layout_notification_channels(test_config, snapshot_notification_channel
563565
# sdk.catalog_organization.put_declarative_identity_providers([])
564566
# idps = sdk.catalog_organization.get_declarative_identity_providers()
565567
# assert len(idps) == 0
568+
569+
570+
@gd_vcr.use_cassette(str(_fixtures_dir / "test_ip_allowlist_policy_crud.yaml"))
571+
def test_ip_allowlist_policy_crud(test_config):
572+
"""Verify full CRUD lifecycle for IP allowlist policies.
573+
574+
Creates a policy, reads it back, updates the allowed sources, verifies
575+
the list endpoint includes it, then removes it in the finally block so
576+
the staging environment is always restored.
577+
"""
578+
sdk = GoodDataSdk.create(host_=test_config["host"], token_=test_config["token"])
579+
policy_id = "sdk-test-ip-policy"
580+
581+
policy = CatalogIpAllowlistPolicy(
582+
id=policy_id,
583+
attributes=CatalogIpAllowlistPolicyAttributes(
584+
allowed_sources=["192.168.1.0/24"],
585+
),
586+
)
587+
try:
588+
created = sdk.catalog_organization.create_ip_allowlist_policy(policy)
589+
assert created.id == policy_id
590+
assert created.attributes is not None
591+
assert "192.168.1.0/24" in created.attributes.allowed_sources
592+
593+
retrieved = sdk.catalog_organization.get_ip_allowlist_policy(policy_id)
594+
assert retrieved.id == policy_id
595+
596+
all_policies = sdk.catalog_organization.list_ip_allowlist_policies()
597+
assert any(p.id == policy_id for p in all_policies)
598+
599+
updated_policy = CatalogIpAllowlistPolicy(
600+
id=policy_id,
601+
attributes=CatalogIpAllowlistPolicyAttributes(
602+
allowed_sources=["10.0.0.0/8"],
603+
),
604+
)
605+
updated = sdk.catalog_organization.update_ip_allowlist_policy(updated_policy)
606+
assert updated.attributes is not None
607+
assert "10.0.0.0/8" in updated.attributes.allowed_sources
608+
finally:
609+
safe_delete(sdk.catalog_organization.delete_ip_allowlist_policy, policy_id)

0 commit comments

Comments
 (0)