|
2 | 2 | from __future__ import annotations |
3 | 3 |
|
4 | 4 | import functools |
| 5 | +import json |
5 | 6 | from typing import Any, Literal |
| 7 | +from urllib.parse import urlparse |
6 | 8 |
|
7 | 9 | from gooddata_api_client.exceptions import NotFoundException |
8 | 10 | from gooddata_api_client.model.declarative_export_templates import DeclarativeExportTemplates |
|
22 | 24 | from gooddata_sdk.catalog.catalog_service_base import CatalogServiceBase |
23 | 25 | from gooddata_sdk.catalog.organization.entity_model.directive import CatalogCspDirective |
24 | 26 | from gooddata_sdk.catalog.organization.entity_model.identity_provider import CatalogIdentityProvider |
| 27 | +from gooddata_sdk.catalog.organization.entity_model.ip_allowlist_policy import ( |
| 28 | + CatalogIpAllowlistPolicy, |
| 29 | + CatalogIpAllowlistPolicyTargets, |
| 30 | +) |
25 | 31 | from gooddata_sdk.catalog.organization.entity_model.jwk import CatalogJwk, CatalogJwkDocument |
26 | 32 | from gooddata_sdk.catalog.organization.entity_model.llm_provider import ( |
27 | 33 | CatalogLlmProvider, |
|
45 | 51 | HLL_TYPE_SETTING_ID = "hyperLogLogType" |
46 | 52 | HLL_TYPE_SETTING_TYPE = "HLL_TYPE" |
47 | 53 |
|
| 54 | +_IP_ALLOWLIST_ENTITIES_BASE = "api/v1/entities/ipAllowlistPolicies" |
| 55 | +_IP_ALLOWLIST_ACTIONS_BASE = "api/v1/actions/ipAllowlistPolicies" |
| 56 | +_JSON_CONTENT_TYPE = "application/json" |
| 57 | + |
48 | 58 |
|
49 | 59 | class CatalogOrganizationService(CatalogServiceBase): |
50 | 60 | def __init__(self, api_client: GoodDataApiClient) -> None: |
@@ -628,6 +638,150 @@ def delete_llm_provider(self, id: str) -> None: |
628 | 638 | """ |
629 | 639 | self._entities_api.delete_entity_llm_providers(id, _check_return_type=False) |
630 | 640 |
|
| 641 | + # IP Allowlist Policy APIs |
| 642 | + |
| 643 | + @staticmethod |
| 644 | + def _ip_policy_to_body(policy: CatalogIpAllowlistPolicy) -> bytes: |
| 645 | + """Serialize a policy to a JSON:API request body.""" |
| 646 | + data: dict[str, Any] = { |
| 647 | + "type": "ipAllowlistPolicy", |
| 648 | + "id": policy.id, |
| 649 | + "attributes": {"allowedSources": policy.allowed_sources}, |
| 650 | + } |
| 651 | + rels: dict[str, Any] = {} |
| 652 | + if policy.users: |
| 653 | + rels["users"] = {"data": [{"id": u.id, "type": u.type} for u in policy.users]} |
| 654 | + if policy.user_groups: |
| 655 | + rels["userGroups"] = {"data": [{"id": g.id, "type": g.type} for g in policy.user_groups]} |
| 656 | + if rels: |
| 657 | + data["relationships"] = rels |
| 658 | + return json.dumps({"data": data}).encode("utf-8") |
| 659 | + |
| 660 | + def get_ip_allowlist_policy(self, policy_id: str) -> CatalogIpAllowlistPolicy: |
| 661 | + """Get an IP allowlist policy by ID. |
| 662 | +
|
| 663 | + Args: |
| 664 | + policy_id (str): Policy identifier. |
| 665 | +
|
| 666 | + Returns: |
| 667 | + CatalogIpAllowlistPolicy: Retrieved policy. |
| 668 | +
|
| 669 | + Raises: |
| 670 | + NotFoundException: Policy does not exist. |
| 671 | + """ |
| 672 | + response = self._client._do_get_request(f"{_IP_ALLOWLIST_ENTITIES_BASE}/{policy_id}") |
| 673 | + if response.status_code == 404: |
| 674 | + raise NotFoundException(status=404, reason=response.reason) |
| 675 | + response.raise_for_status() |
| 676 | + return CatalogIpAllowlistPolicy.from_api(response.json()["data"]) |
| 677 | + |
| 678 | + def list_ip_allowlist_policies(self) -> list[CatalogIpAllowlistPolicy]: |
| 679 | + """Return all IP allowlist policies in the organization. |
| 680 | +
|
| 681 | + Follows JSON:API ``links.next`` pagination transparently. |
| 682 | +
|
| 683 | + Returns: |
| 684 | + list[CatalogIpAllowlistPolicy]: All policies. |
| 685 | + """ |
| 686 | + policies: list[CatalogIpAllowlistPolicy] = [] |
| 687 | + endpoint = _IP_ALLOWLIST_ENTITIES_BASE |
| 688 | + while True: |
| 689 | + response = self._client._do_get_request(endpoint) |
| 690 | + response.raise_for_status() |
| 691 | + body = response.json() |
| 692 | + policies.extend(CatalogIpAllowlistPolicy.from_api(item) for item in body["data"]) |
| 693 | + next_url = (body.get("links") or {}).get("next") |
| 694 | + if not next_url: |
| 695 | + break |
| 696 | + parsed = urlparse(str(next_url)) |
| 697 | + endpoint = parsed.path.lstrip("/") |
| 698 | + if parsed.query: |
| 699 | + endpoint = f"{endpoint}?{parsed.query}" |
| 700 | + return policies |
| 701 | + |
| 702 | + def create_ip_allowlist_policy(self, policy: CatalogIpAllowlistPolicy) -> CatalogIpAllowlistPolicy: |
| 703 | + """Create a new IP allowlist policy. |
| 704 | +
|
| 705 | + Args: |
| 706 | + policy (CatalogIpAllowlistPolicy): Policy to create. |
| 707 | +
|
| 708 | + Returns: |
| 709 | + CatalogIpAllowlistPolicy: Newly created policy as returned by the server. |
| 710 | + """ |
| 711 | + response = self._client._do_post_request( |
| 712 | + data=self._ip_policy_to_body(policy), |
| 713 | + endpoint=_IP_ALLOWLIST_ENTITIES_BASE, |
| 714 | + content_type=_JSON_CONTENT_TYPE, |
| 715 | + ) |
| 716 | + response.raise_for_status() |
| 717 | + return CatalogIpAllowlistPolicy.from_api(response.json()["data"]) |
| 718 | + |
| 719 | + def update_ip_allowlist_policy(self, policy: CatalogIpAllowlistPolicy) -> CatalogIpAllowlistPolicy: |
| 720 | + """Replace an existing IP allowlist policy (full PUT). |
| 721 | +
|
| 722 | + Args: |
| 723 | + policy (CatalogIpAllowlistPolicy): Updated policy object. |
| 724 | +
|
| 725 | + Returns: |
| 726 | + CatalogIpAllowlistPolicy: Policy as returned by the server after update. |
| 727 | +
|
| 728 | + Raises: |
| 729 | + NotFoundException: Policy does not exist. |
| 730 | + """ |
| 731 | + response = self._client._do_put_request( |
| 732 | + data=self._ip_policy_to_body(policy), |
| 733 | + endpoint=f"{_IP_ALLOWLIST_ENTITIES_BASE}/{policy.id}", |
| 734 | + content_type=_JSON_CONTENT_TYPE, |
| 735 | + ) |
| 736 | + if response.status_code == 404: |
| 737 | + raise NotFoundException(status=404, reason=response.reason) |
| 738 | + response.raise_for_status() |
| 739 | + return CatalogIpAllowlistPolicy.from_api(response.json()["data"]) |
| 740 | + |
| 741 | + def delete_ip_allowlist_policy(self, policy_id: str) -> None: |
| 742 | + """Delete an IP allowlist policy. |
| 743 | +
|
| 744 | + Args: |
| 745 | + policy_id (str): Policy identifier. |
| 746 | +
|
| 747 | + Raises: |
| 748 | + ValueError: Policy does not exist. |
| 749 | + """ |
| 750 | + response = self._client._do_delete_request(f"{_IP_ALLOWLIST_ENTITIES_BASE}/{policy_id}") |
| 751 | + if response.status_code == 404: |
| 752 | + raise ValueError(f"Cannot delete IP allowlist policy {policy_id!r}. This policy does not exist.") |
| 753 | + response.raise_for_status() |
| 754 | + |
| 755 | + def add_targets_to_ip_allowlist_policy(self, policy_id: str, targets: CatalogIpAllowlistPolicyTargets) -> None: |
| 756 | + """Add users or user-groups to an existing IP allowlist policy. |
| 757 | +
|
| 758 | + Args: |
| 759 | + policy_id (str): Policy identifier. |
| 760 | + targets (CatalogIpAllowlistPolicyTargets): Targets to add. |
| 761 | + """ |
| 762 | + body = json.dumps({"targets": [{"id": t.id, "type": t.type} for t in targets.targets]}).encode("utf-8") |
| 763 | + response = self._client._do_post_request( |
| 764 | + data=body, |
| 765 | + endpoint=f"{_IP_ALLOWLIST_ACTIONS_BASE}/{policy_id}/addTargets", |
| 766 | + content_type=_JSON_CONTENT_TYPE, |
| 767 | + ) |
| 768 | + response.raise_for_status() |
| 769 | + |
| 770 | + def remove_targets_from_ip_allowlist_policy(self, policy_id: str, targets: CatalogIpAllowlistPolicyTargets) -> None: |
| 771 | + """Remove users or user-groups from an existing IP allowlist policy. |
| 772 | +
|
| 773 | + Args: |
| 774 | + policy_id (str): Policy identifier. |
| 775 | + targets (CatalogIpAllowlistPolicyTargets): Targets to remove. |
| 776 | + """ |
| 777 | + body = json.dumps({"targets": [{"id": t.id, "type": t.type} for t in targets.targets]}).encode("utf-8") |
| 778 | + response = self._client._do_post_request( |
| 779 | + data=body, |
| 780 | + endpoint=f"{_IP_ALLOWLIST_ACTIONS_BASE}/{policy_id}/removeTargets", |
| 781 | + content_type=_JSON_CONTENT_TYPE, |
| 782 | + ) |
| 783 | + response.raise_for_status() |
| 784 | + |
631 | 785 | # Layout APIs |
632 | 786 |
|
633 | 787 | def get_declarative_notification_channels(self) -> list[CatalogDeclarativeNotificationChannel]: |
|
0 commit comments