Skip to content

Commit 64dec85

Browse files
committed
feat(gooddata-sdk): [AUTO] Add IP allowlist policy CRUD endpoints to metadata API
1 parent 38b0798 commit 64dec85

4 files changed

Lines changed: 268 additions & 0 deletions

File tree

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@
109109
CatalogExportTemplate,
110110
CatalogExportTemplateAttributes,
111111
)
112+
from gooddata_sdk.catalog.organization.entity_model.ip_allowlist_policy import CatalogIpAllowlistPolicy
112113
from gooddata_sdk.catalog.organization.entity_model.jwk import (
113114
CatalogJwk,
114115
CatalogJwkAttributes,
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# (C) 2026 GoodData Corporation
2+
from __future__ import annotations
3+
4+
import json
5+
from typing import Any
6+
7+
import attrs
8+
from attrs import define
9+
10+
from gooddata_sdk.catalog.base import Base
11+
12+
13+
@define(kw_only=True)
14+
class CatalogIpAllowlistPolicy(Base):
15+
"""Represents an IP allowlist policy entity."""
16+
17+
id: str
18+
allowed_sources: list[str] = attrs.field(factory=list)
19+
users: list[str] = attrs.field(factory=list)
20+
user_groups: list[str] = attrs.field(factory=list)
21+
22+
@staticmethod
23+
def client_class() -> type:
24+
return NotImplemented # type: ignore[return-value]
25+
26+
@classmethod
27+
def from_api(cls, entity: dict[str, Any]) -> CatalogIpAllowlistPolicy:
28+
attrs_raw = entity.get("attributes") or {}
29+
users_raw = attrs_raw.get("users") or []
30+
user_groups_raw = attrs_raw.get("userGroups") or []
31+
return cls(
32+
id=entity["id"],
33+
allowed_sources=attrs_raw.get("allowedSources") or [],
34+
users=[u["id"] if isinstance(u, dict) else u for u in users_raw],
35+
user_groups=[g["id"] if isinstance(g, dict) else g for g in user_groups_raw],
36+
)
37+
38+
def to_api_dict(self) -> dict[str, Any]:
39+
"""Serialize to JSON API document dict for POST/PUT requests."""
40+
attributes: dict[str, Any] = {
41+
"allowedSources": self.allowed_sources,
42+
}
43+
if self.users:
44+
attributes["users"] = [{"id": uid, "type": "user"} for uid in self.users]
45+
if self.user_groups:
46+
attributes["userGroups"] = [{"id": gid, "type": "userGroup"} for gid in self.user_groups]
47+
return {
48+
"data": {
49+
"id": self.id,
50+
"type": "ipAllowlistPolicy",
51+
"attributes": attributes,
52+
}
53+
}
54+
55+
def to_api_json_bytes(self) -> bytes:
56+
"""Serialize to JSON bytes for HTTP request body."""
57+
return json.dumps(self.to_api_dict()).encode("utf-8")

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

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import functools
55
from typing import Any, Literal
66

7+
import requests as _requests
78
from gooddata_api_client.exceptions import NotFoundException
89
from gooddata_api_client.model.declarative_export_templates import DeclarativeExportTemplates
910
from gooddata_api_client.model.declarative_notification_channels import DeclarativeNotificationChannels
@@ -22,6 +23,7 @@
2223
from gooddata_sdk.catalog.catalog_service_base import CatalogServiceBase
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,
@@ -35,6 +37,9 @@
3537
from gooddata_sdk.client import GoodDataApiClient
3638
from gooddata_sdk.utils import load_all_entities, load_all_entities_dict
3739

40+
_IP_ALLOWLIST_BASE_PATH = "/api/v1/entities/ipAllowlistPolicies"
41+
_IP_ALLOWLIST_ACTIONS_PATH = "/api/v1/actions/ipAllowlistPolicies"
42+
3843
# Org-level setting controlling which HLL function family calcique uses when
3944
# generating SQL over HLL synopses. `Native` (default) emits StarRocks-native
4045
# `HLL_*` functions; `Presto` emits the Presto-compatible HLL function family
@@ -628,6 +633,172 @@ def delete_llm_provider(self, id: str) -> None:
628633
"""
629634
self._entities_api.delete_entity_llm_providers(id, _check_return_type=False)
630635

636+
# IP Allowlist Policy CRUD (entity endpoints not yet in generated client)
637+
638+
def _ip_api_call(
639+
self,
640+
method: str,
641+
path: str,
642+
body: dict[str, Any] | None = None,
643+
) -> dict[str, Any]:
644+
"""Make an authenticated HTTP request to an IP allowlist policy endpoint.
645+
646+
Uses requests directly (mirroring ``GoodDataApiClient._do_post_request``)
647+
so that we are not dependent on the generated client having typed wrappers
648+
for these endpoints. Returns the parsed JSON body, or an empty dict for
649+
responses without a body (e.g. 204 No Content on DELETE).
650+
"""
651+
hostname: str = self._client._hostname # type: ignore[attr-defined]
652+
token: str = self._client._token # type: ignore[attr-defined]
653+
654+
prefix = "" if hostname.endswith("/") else "/"
655+
url = f"{hostname}{prefix}{path.lstrip('/')}"
656+
657+
headers: dict[str, str] = {
658+
"Content-Type": "application/vnd.gooddata.api+json",
659+
"Authorization": f"Bearer {token}",
660+
"Accept": "application/vnd.gooddata.api+json",
661+
}
662+
kwargs: dict[str, Any] = {"headers": headers}
663+
if body is not None:
664+
kwargs["json"] = body
665+
666+
response = _requests.request(method, url, **kwargs)
667+
response.raise_for_status()
668+
if response.content:
669+
return response.json() # type: ignore[no-any-return]
670+
return {}
671+
672+
def list_ip_allowlist_policies(self) -> list[CatalogIpAllowlistPolicy]:
673+
"""Return all IP allowlist policies in the organization.
674+
675+
Returns:
676+
list[CatalogIpAllowlistPolicy]:
677+
List of IP allowlist policies.
678+
"""
679+
all_items: list[CatalogIpAllowlistPolicy] = []
680+
page = 0
681+
page_size = 500
682+
while True:
683+
raw = self._ip_api_call(
684+
"GET",
685+
f"{_IP_ALLOWLIST_BASE_PATH}?page={page}&size={page_size}",
686+
)
687+
data = raw.get("data") or []
688+
all_items.extend(CatalogIpAllowlistPolicy.from_api(item) for item in data)
689+
if len(data) < page_size:
690+
break
691+
page += 1
692+
return all_items
693+
694+
def get_ip_allowlist_policy(self, policy_id: str) -> CatalogIpAllowlistPolicy:
695+
"""Get an individual IP allowlist policy.
696+
697+
Args:
698+
policy_id (str):
699+
IP allowlist policy identifier.
700+
701+
Returns:
702+
CatalogIpAllowlistPolicy:
703+
The requested IP allowlist policy.
704+
"""
705+
raw = self._ip_api_call("GET", f"{_IP_ALLOWLIST_BASE_PATH}/{policy_id}")
706+
return CatalogIpAllowlistPolicy.from_api(raw["data"])
707+
708+
def create_ip_allowlist_policy(self, policy: CatalogIpAllowlistPolicy) -> CatalogIpAllowlistPolicy:
709+
"""Create a new IP allowlist policy.
710+
711+
Args:
712+
policy (CatalogIpAllowlistPolicy):
713+
IP allowlist policy to create.
714+
715+
Returns:
716+
CatalogIpAllowlistPolicy:
717+
Created IP allowlist policy.
718+
"""
719+
raw = self._ip_api_call("POST", _IP_ALLOWLIST_BASE_PATH, body=policy.to_api_dict())
720+
return CatalogIpAllowlistPolicy.from_api(raw["data"])
721+
722+
def update_ip_allowlist_policy(self, policy: CatalogIpAllowlistPolicy) -> CatalogIpAllowlistPolicy:
723+
"""Update an existing IP allowlist policy.
724+
725+
Args:
726+
policy (CatalogIpAllowlistPolicy):
727+
IP allowlist policy with updated fields.
728+
729+
Returns:
730+
CatalogIpAllowlistPolicy:
731+
Updated IP allowlist policy.
732+
733+
Raises:
734+
ValueError:
735+
IP allowlist policy does not exist.
736+
"""
737+
raw = self._ip_api_call(
738+
"PUT",
739+
f"{_IP_ALLOWLIST_BASE_PATH}/{policy.id}",
740+
body=policy.to_api_dict(),
741+
)
742+
return CatalogIpAllowlistPolicy.from_api(raw["data"])
743+
744+
def delete_ip_allowlist_policy(self, policy_id: str) -> None:
745+
"""Delete an IP allowlist policy.
746+
747+
Args:
748+
policy_id (str):
749+
IP allowlist policy identifier.
750+
751+
Returns:
752+
None
753+
"""
754+
self._ip_api_call("DELETE", f"{_IP_ALLOWLIST_BASE_PATH}/{policy_id}")
755+
756+
def add_targets_to_ip_allowlist_policy(
757+
self,
758+
policy_id: str,
759+
targets: list[dict[str, str]],
760+
) -> None:
761+
"""Add targets to an IP allowlist policy.
762+
763+
Args:
764+
policy_id (str):
765+
IP allowlist policy identifier.
766+
targets (list[dict[str, str]]):
767+
List of targets to add. Each target is a dict with ``id`` and
768+
``type`` keys (e.g. ``{"id": "user1", "type": "user"}``).
769+
770+
Returns:
771+
None
772+
"""
773+
self._ip_api_call(
774+
"POST",
775+
f"{_IP_ALLOWLIST_ACTIONS_PATH}/{policy_id}/addTargets",
776+
body={"targets": targets},
777+
)
778+
779+
def remove_targets_from_ip_allowlist_policy(
780+
self,
781+
policy_id: str,
782+
targets: list[dict[str, str]],
783+
) -> None:
784+
"""Remove targets from an IP allowlist policy.
785+
786+
Args:
787+
policy_id (str):
788+
IP allowlist policy identifier.
789+
targets (list[dict[str, str]]):
790+
List of targets to remove. Each target is a dict with ``id``
791+
and ``type`` keys (e.g. ``{"id": "user1", "type": "user"}``).
792+
793+
Returns:
794+
None
795+
"""
796+
self._ip_api_call(
797+
"POST",
798+
f"{_IP_ALLOWLIST_ACTIONS_PATH}/{policy_id}/removeTargets",
799+
body={"targets": targets},
800+
)
801+
631802
# Layout APIs
632803

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

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

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from gooddata_sdk import (
88
CatalogCspDirective,
99
CatalogDeclarativeNotificationChannel,
10+
CatalogIpAllowlistPolicy,
1011
CatalogJwk,
1112
CatalogOrganization,
1213
CatalogOrganizationSetting,
@@ -563,3 +564,41 @@ def test_layout_notification_channels(test_config, snapshot_notification_channel
563564
# sdk.catalog_organization.put_declarative_identity_providers([])
564565
# idps = sdk.catalog_organization.get_declarative_identity_providers()
565566
# assert len(idps) == 0
567+
568+
569+
@gd_vcr.use_cassette(str(_fixtures_dir / "ip_allowlist_policy_crud.yaml"))
570+
def test_ip_allowlist_policy_crud(test_config):
571+
"""Integration test: create, read, update, and delete an IP allowlist policy."""
572+
sdk = GoodDataSdk.create(host_=test_config["host"], token_=test_config["token"])
573+
574+
policy_id = "test-ip-allowlist-policy"
575+
policy = CatalogIpAllowlistPolicy(
576+
id=policy_id,
577+
allowed_sources=["192.168.1.0/24", "10.0.0.1"],
578+
)
579+
580+
try:
581+
# Create
582+
created = sdk.catalog_organization.create_ip_allowlist_policy(policy)
583+
assert created.id == policy_id
584+
assert "192.168.1.0/24" in created.allowed_sources
585+
586+
# Get
587+
fetched = sdk.catalog_organization.get_ip_allowlist_policy(policy_id)
588+
assert fetched.id == policy_id
589+
assert set(fetched.allowed_sources) == set(policy.allowed_sources)
590+
591+
# Update
592+
updated_policy = CatalogIpAllowlistPolicy(
593+
id=policy_id,
594+
allowed_sources=["10.0.0.0/8"],
595+
)
596+
updated = sdk.catalog_organization.update_ip_allowlist_policy(updated_policy)
597+
assert updated.id == policy_id
598+
assert updated.allowed_sources == ["10.0.0.0/8"]
599+
600+
# List
601+
all_policies = sdk.catalog_organization.list_ip_allowlist_policies()
602+
assert any(p.id == policy_id for p in all_policies)
603+
finally:
604+
safe_delete(sdk.catalog_organization.delete_ip_allowlist_policy, policy_id)

0 commit comments

Comments
 (0)