From b58f54723e783769e298f81002dc90246ab0da9b Mon Sep 17 00:00:00 2001 From: paulnoirel <87332996+paulnoirel@users.noreply.github.com> Date: Thu, 29 May 2025 16:48:32 +0100 Subject: [PATCH] Add creation of IAM integrations --- .../src/labelbox/schema/iam_integration.py | 439 +++++++++++++++++- .../src/labelbox/schema/organization.py | 34 +- .../integration/test_delegated_access.py | 359 +++++++++++++- 3 files changed, 812 insertions(+), 20 deletions(-) diff --git a/libs/labelbox/src/labelbox/schema/iam_integration.py b/libs/labelbox/src/labelbox/schema/iam_integration.py index cb5309929..762ac0935 100644 --- a/libs/labelbox/src/labelbox/schema/iam_integration.py +++ b/libs/labelbox/src/labelbox/schema/iam_integration.py @@ -1,44 +1,104 @@ from dataclasses import dataclass +from typing import Optional, Union, Dict, Any, TYPE_CHECKING from labelbox.utils import snake_case from labelbox.orm.db_object import DbObject from labelbox.orm.model import Field +if TYPE_CHECKING: + from labelbox import Client + + +class IAMIntegrationProvider: + """Constants for IAM integration providers.""" + + Aws = "AWS" + Gcp = "GCP" + Azure = "Azure" + @dataclass class AwsIamIntegrationSettings: - role_arn: str + """Settings for AWS IAM integration. + + Attributes: + role_arn: AWS role ARN + read_bucket: Optional read bucket name + """ + + role_arn: Optional[str] = None + read_bucket: Optional[str] = None @dataclass class GcpIamIntegrationSettings: - service_account_email_id: str - read_bucket: str + """Settings for GCP IAM integration. + Attributes: + service_account_email_id: GCP service account email ID + read_bucket: GCP read bucket name + """ -class IAMIntegration(DbObject): - """Represents an IAM integration for delegated access + service_account_email_id: Optional[str] = None + read_bucket: Optional[str] = None + + +@dataclass +class AzureIamIntegrationSettings: + """Settings for Azure IAM integration. Attributes: - name (str) - updated_at (datetime) - created_at (datetime) - provider (str) - valid (bool) - last_valid_at (datetime) - is_org_default (boolean) + read_container_url: Azure container URL + tenant_id: Azure tenant ID + client_id: Azure client ID + client_secret: Azure client secret + """ + + read_container_url: Optional[str] = None + tenant_id: Optional[str] = None + client_id: Optional[str] = None + client_secret: Optional[str] = None + +class IAMIntegration(DbObject): + """Represents an IAM integration for delegated access. + + Attributes: + settings: Provider-specific settings for the integration + name: Name of the integration + created_at: When the integration was created + updated_at: When the integration was last updated + provider: The cloud provider (e.g., "AWS", "GCP", "Azure") + valid: Whether the integration is valid + last_valid_at: When the integration was last validated + is_org_default: Whether this is the default integration for the organization """ - def __init__(self, client, data): + settings: Optional[ + Union[ + AwsIamIntegrationSettings, + GcpIamIntegrationSettings, + AzureIamIntegrationSettings, + ] + ] = None + + def __init__(self, client: "Client", data: Dict[str, Any]) -> None: + """Initialize an IAM integration. + + Args: + client: The Labelbox client + data: The integration data from the API + """ settings = data.pop("settings", None) if settings is not None: - type_name = settings.pop("__typename") + type_name = settings.pop("__typename", None) settings = {snake_case(k): v for k, v in settings.items()} - if type_name == "GcpIamIntegrationSettings": - self.settings = GcpIamIntegrationSettings(**settings) - elif type_name == "AwsIamIntegrationSettings": + if type_name == "AwsIamIntegrationSettings": self.settings = AwsIamIntegrationSettings(**settings) + elif type_name == "GcpIamIntegrationSettings": + self.settings = GcpIamIntegrationSettings(**settings) + elif type_name == "AzureIamIntegrationSettings": + self.settings = AzureIamIntegrationSettings(**settings) else: self.settings = None else: @@ -54,3 +114,348 @@ def __init__(self, client, data): valid = Field.Boolean("valid") last_valid_at = Field.DateTime("last_valid_at") is_org_default = Field.Boolean("is_org_default") + + @staticmethod + def create( + client: "Client", + name: str, + settings: Union[ + AwsIamIntegrationSettings, + GcpIamIntegrationSettings, + AzureIamIntegrationSettings, + ], + ) -> "IAMIntegration": + """Creates a new IAM integration. + + Args: + client: The Labelbox client + name: Name of the integration + settings: Provider-specific settings for the integration + + Returns: + The created IAM integration + + Raises: + ValueError: If unsupported settings type is provided + """ + if isinstance(settings, AwsIamIntegrationSettings): + return IAMIntegration.create_aws_integration( + client, + name=name, + role_arn=settings.role_arn or "", + read_bucket=settings.read_bucket, + ) + elif isinstance(settings, GcpIamIntegrationSettings): + return IAMIntegration.create_gcp_integration( + client, + name=name, + read_bucket=settings.read_bucket or "", + ) + elif isinstance(settings, AzureIamIntegrationSettings): + return IAMIntegration.create_azure_integration( + client, + name=name, + read_container_url=settings.read_container_url or "", + tenant_id=settings.tenant_id or "", + ) + else: + raise ValueError( + f"Unsupported settings type for integration creation: {type(settings).__name__}" + ) + + @staticmethod + def create_aws_integration( + client: "Client", + name: str, + role_arn: str, + read_bucket: Optional[str] = None, + ) -> "IAMIntegration": + """Creates a new AWS IAM integration. + + Args: + client: The Labelbox client + name: Name of the integration + role_arn: AWS role ARN + read_bucket: Optional read bucket name + + Returns: + The created AWS IAM integration + """ + query_str = """ + mutation CreateAwsIamIntegrationPyApi($data: AwsIamIntegrationCreateInput!) { + createAwsIamIntegration(data: $data) { + id + name + createdAt + updatedAt + provider + valid + lastValidAt + isOrgDefault + settings { + __typename + ... on AwsIamIntegrationSettings { + roleArn + readBucket + } + } + } + } + """ + params = { + "data": { + "name": name, + "roleArn": role_arn, + "readBucket": read_bucket, + } + } + res = client.execute(query_str, params) + return IAMIntegration(client, res["createAwsIamIntegration"]) + + @staticmethod + def create_gcp_integration( + client: "Client", name: str, read_bucket: str + ) -> "IAMIntegration": + """Creates a new GCP IAM integration. + + Args: + client: The Labelbox client + name: Name of the integration + read_bucket: GCP read bucket name + + Returns: + The created GCP IAM integration + """ + query_str = """ + mutation CreateGcpIamIntegrationPyApi($data: GcpIamIntegrationCreateInput!) { + createGcpIamIntegration(data: $data) { + id + name + createdAt + updatedAt + provider + valid + lastValidAt + isOrgDefault + settings { + __typename + ... on GcpIamIntegrationSettings { + serviceAccountEmailId + readBucket + } + } + } + } + """ + params = {"data": {"name": name, "readBucket": read_bucket}} + res = client.execute(query_str, params) + return IAMIntegration(client, res["createGcpIamIntegration"]) + + @staticmethod + def create_azure_integration( + client: "Client", name: str, read_container_url: str, tenant_id: str + ) -> "IAMIntegration": + """Creates a new Azure IAM integration. + + Args: + client: The Labelbox client + name: Name of the integration + read_container_url: Azure container URL + tenant_id: Azure tenant ID + + Returns: + The created Azure IAM integration + """ + query_str = """ + mutation CreateAzureIamIntegrationPyApi($data: AzureIamIntegrationCreateInput!) { + createAzureIamIntegration(data: $data) { + id + name + createdAt + updatedAt + provider + valid + lastValidAt + isOrgDefault + settings { + __typename + ... on AzureIamIntegrationSettings { + readContainerUrl + tenantId + } + } + } + } + """ + params = { + "data": { + "name": name, + "readContainerUrl": read_container_url, + "tenantId": tenant_id, + } + } + res = client.execute(query_str, params) + return IAMIntegration(client, res["createAzureIamIntegration"]) + + def update( + self, + name: Optional[str] = None, + settings: Optional[ + Union[ + AwsIamIntegrationSettings, + GcpIamIntegrationSettings, + AzureIamIntegrationSettings, + ] + ] = None, + ) -> None: + """Updates an existing IAM integration. + + Args: + name: Optional new name for the integration + settings: Optional new provider-specific settings for the integration + + Raises: + ValueError: If current integration settings are missing or if settings type + doesn't match the integration provider + """ + current_settings = settings or self.settings + + if not current_settings: + raise ValueError("Current integration settings are missing.") + + if self.provider == "AWS": + if not isinstance(current_settings, AwsIamIntegrationSettings): + raise ValueError( + "Expected AwsIamIntegrationSettings for AWS provider." + ) + self.update_aws_integration( + name=name or self.name, + role_arn=current_settings.role_arn or "", + read_bucket=current_settings.read_bucket, + ) + elif self.provider == "GCP": + if not isinstance(current_settings, GcpIamIntegrationSettings): + raise ValueError( + "Expected GcpIamIntegrationSettings for GCP provider." + ) + self.update_gcp_integration( + name=name or self.name, + read_bucket=current_settings.read_bucket or "", + ) + elif self.provider == "Azure": + if not isinstance(current_settings, AzureIamIntegrationSettings): + raise ValueError( + "Expected AzureIamIntegrationSettings for Azure provider." + ) + self.update_azure_integration( + name=name or self.name, + read_container_url=current_settings.read_container_url or "", + tenant_id=current_settings.tenant_id or "", + ) + else: + raise ValueError(f"Unsupported provider: {self.provider}") + + def update_aws_integration( + self, name: str, role_arn: str, read_bucket: Optional[str] = None + ) -> None: + """Updates an existing AWS IAM integration. + + Args: + name: New name for the integration + role_arn: New AWS role ARN + read_bucket: New read bucket name + """ + query_str = """ + mutation UpdateAwsIamIntegrationPyApi($data: AwsIamIntegrationUpdateInput!, $where: WhereUniqueIdInput!) { + updateAwsIamIntegration(data: $data, where: $where) { id } + } + """ + params = { + "data": { + "name": name, + "roleArn": role_arn, + "readBucket": read_bucket, + }, + "where": {"id": self.uid}, + } + self.client.execute(query_str, params) + + def update_gcp_integration(self, name: str, read_bucket: str) -> None: + """Updates an existing GCP IAM integration. + + Args: + name: New name for the integration + read_bucket: New read bucket name + """ + query_str = """ + mutation UpdateGcpIamIntegrationPyApi($data: GcpIamIntegrationUpdateInput!, $where: WhereUniqueIdInput!) { + updateGcpIamIntegration(data: $data, where: $where) { id } + } + """ + params = { + "data": {"name": name, "readBucket": read_bucket}, + "where": {"id": self.uid}, + } + self.client.execute(query_str, params) + + def update_azure_integration( + self, + name: str, + read_container_url: str, + tenant_id: str, + ) -> None: + """Updates an existing Azure IAM integration. + + Args: + name: New name for the integration + read_container_url: New Azure container URL + tenant_id: New Azure tenant ID + + Note: + Client credentials (client_id, client_secret) cannot be updated + through the update API for security reasons. + """ + query_str = """ + mutation UpdateAzureIamIntegrationPyApi($data: AzureIamIntegrationUpdateInput!, $where: WhereUniqueIdInput!) { + updateAzureIamIntegration(data: $data, where: $where) { id } + } + """ + params = { + "data": { + "name": name, + "readContainerUrl": read_container_url, + "tenantId": tenant_id, + }, + "where": {"id": self.uid}, + } + + self.client.execute(query_str, params) + + def validate(self) -> Dict[str, Any]: + """Validates the IAM integration. + + Returns: + Dict containing validation results with the following keys: + - valid: Whether the integration is valid + - checks: List of validation checks with their results + """ + query_str = """ + mutation ValidateIamIntegrationPyApi($where: WhereUniqueIdInput!) { + validateIamIntegration(where: $where) { + valid + checks { name success message } + } + } + """ + params = {"where": {"id": self.uid}} + return self.client.execute(query_str, params)["validateIamIntegration"] + + def set_as_default(self) -> None: + """Sets this integration as the default for the organization.""" + query_str = """ + mutation SetDefaultIamIntegrationPyApi($where: WhereUniqueIdInput!) { + setDefaultIamIntegration(where: $where) { id } + } + """ + params = {"where": {"id": self.uid}} + self.client.execute(query_str, params, experimental=True) diff --git a/libs/labelbox/src/labelbox/schema/organization.py b/libs/labelbox/src/labelbox/schema/organization.py index cd4c24ada..8d15cc1b2 100644 --- a/libs/labelbox/src/labelbox/schema/organization.py +++ b/libs/labelbox/src/labelbox/schema/organization.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING, Dict, List, Optional +from typing import TYPE_CHECKING, Dict, List, Optional, Union from lbox.exceptions import LabelboxError @@ -8,10 +8,15 @@ from labelbox.schema.invite import InviteLimit from labelbox.schema.resource_tag import ResourceTag from labelbox.pagination import PaginatedCollection +from labelbox.schema.iam_integration import ( + AwsIamIntegrationSettings, + GcpIamIntegrationSettings, + AzureIamIntegrationSettings, + IAMIntegration, +) if TYPE_CHECKING: from labelbox import ( - IAMIntegration, Invite, InviteLimit, ProjectRole, @@ -245,6 +250,31 @@ def get_default_iam_integration(self) -> Optional["IAMIntegration"]: None if not len(default_integration) else default_integration.pop() ) + def create_iam_integration( + self, + name: str, + settings: Union[ + AwsIamIntegrationSettings, + GcpIamIntegrationSettings, + AzureIamIntegrationSettings, + ], + ) -> "IAMIntegration": + """Creates a new IAM integration for the organization. + + Args: + settings: Provider-specific settings for the integration: + - AwsIamIntegrationSettings: name, role_arn, read_bucket (optional) + - GcpIamIntegrationSettings: name, read_bucket + - AzureIamIntegrationSettings: name, read_container_url, tenant_id + + Returns: + IAMIntegration: The created integration + + Raises: + ValueError: If unsupported settings type is provided + """ + return IAMIntegration.create(self.client, name, settings) + def get_invites(self) -> PaginatedCollection: """ Retrieves all invites for this organization. diff --git a/libs/labelbox/tests/integration/test_delegated_access.py b/libs/labelbox/tests/integration/test_delegated_access.py index 0e6422b08..6e3d4c95b 100644 --- a/libs/labelbox/tests/integration/test_delegated_access.py +++ b/libs/labelbox/tests/integration/test_delegated_access.py @@ -1,10 +1,367 @@ import os +import uuid +from typing import Optional import requests import pytest -import uuid from labelbox import Client +from labelbox.schema.iam_integration import ( + AwsIamIntegrationSettings, + GcpIamIntegrationSettings, + AzureIamIntegrationSettings, +) + + +def delete_iam_integration(client, iam_integration_id: str): + """Helper function to delete an IAM integration using GraphQL mutation.""" + mutation = """mutation DeleteIamIntegrationPyApi($id: ID!) { + deleteIamIntegration(where: { id: $id }) + }""" + params = {"id": iam_integration_id} + client.execute(mutation, params, experimental=True) + + +@pytest.fixture +def test_integration_name() -> str: + """Returns a unique name for test integrations.""" + return f"test-integration-{uuid.uuid4()}" + + +@pytest.fixture +def aws_integration( + client, test_integration_name +) -> Optional["IAMIntegration"]: + """Creates a test AWS integration and cleans it up after the test.""" + settings = AwsIamIntegrationSettings( + role_arn="arn:aws:iam::000000000000:role/temporary", + read_bucket="test-bucket", + ) + integration = client.get_organization().create_iam_integration( + name=test_integration_name, + settings=settings, + ) + yield integration + # Proper cleanup using delete mutation + delete_iam_integration(client, integration.uid) + + +@pytest.fixture +def gcp_integration( + client, test_integration_name +) -> Optional["IAMIntegration"]: + """Creates a test GCP integration and cleans it up after the test.""" + settings = GcpIamIntegrationSettings( + read_bucket="gs://test-bucket", + ) + integration = client.get_organization().create_iam_integration( + name=test_integration_name, + settings=settings, + ) + yield integration + # Proper cleanup using delete mutation + delete_iam_integration(client, integration.uid) + + +@pytest.fixture +def azure_integration( + client, test_integration_name +) -> Optional["IAMIntegration"]: + """Creates a test Azure integration and cleans it up after the test.""" + settings = AzureIamIntegrationSettings( + read_container_url="https://test.blob.core.windows.net/test", + tenant_id="test-tenant", + ) + integration = client.get_organization().create_iam_integration( + name=test_integration_name, + settings=settings, + ) + yield integration + # Proper cleanup using delete mutation + delete_iam_integration(client, integration.uid) + + +def test_create_aws_integration(client, test_integration_name): + """Test creating an AWS IAM integration.""" + settings = AwsIamIntegrationSettings( + role_arn="arn:aws:iam::000000000000:role/temporary", + read_bucket="test-bucket", + ) + integration = client.get_organization().create_iam_integration( + name=test_integration_name, settings=settings + ) + + try: + assert integration.name == test_integration_name + assert integration.provider == "AWS" + assert isinstance(integration.settings, AwsIamIntegrationSettings) + assert integration.settings.role_arn == settings.role_arn + assert integration.settings.read_bucket == settings.read_bucket + finally: + # Ensure cleanup even if assertions fail + delete_iam_integration(client, integration.uid) + + +def test_create_gcp_integration(client, test_integration_name): + """Test creating a GCP IAM integration.""" + settings = GcpIamIntegrationSettings(read_bucket="gs://test-bucket") + integration = client.get_organization().create_iam_integration( + name=test_integration_name, settings=settings + ) + + try: + assert integration.name == test_integration_name + assert integration.provider == "GCP" + assert isinstance(integration.settings, GcpIamIntegrationSettings) + assert integration.settings.read_bucket == settings.read_bucket + finally: + # Ensure cleanup even if assertions fail + delete_iam_integration(client, integration.uid) + + +def test_create_azure_integration(client, test_integration_name): + """Test creating an Azure IAM integration.""" + settings = AzureIamIntegrationSettings( + read_container_url="https://test.blob.core.windows.net/test", + tenant_id="test-tenant", + ) + integration = client.get_organization().create_iam_integration( + name=test_integration_name, settings=settings + ) + + try: + assert integration.name == test_integration_name + assert integration.provider == "Azure" + assert isinstance(integration.settings, AzureIamIntegrationSettings) + assert ( + integration.settings.read_container_url + == settings.read_container_url + ) + assert integration.settings.tenant_id == settings.tenant_id + finally: + # Ensure cleanup even if assertions fail + delete_iam_integration(client, integration.uid) + + +def test_update_aws_integration(client, test_integration_name): + """Test updating an AWS IAM integration.""" + # Create initial integration + settings = AwsIamIntegrationSettings( + role_arn="arn:aws:iam::000000000000:role/temporary", + read_bucket="test-bucket", + ) + integration = client.get_organization().create_iam_integration( + name=test_integration_name, settings=settings + ) + + try: + # Update integration + new_settings = AwsIamIntegrationSettings( + role_arn="arn:aws:iam::111111111111:role/updated", + read_bucket="updated-bucket", + ) + integration.update( + name=f"updated-{test_integration_name}", settings=new_settings + ) + + # Verify update - find the specific integration by ID + updated_integration = None + for iam_int in client.get_organization().get_iam_integrations(): + if iam_int.uid == integration.uid: + updated_integration = iam_int + break + + assert updated_integration is not None + assert updated_integration.name == f"updated-{test_integration_name}" + # Note: Settings may not be returned immediately after update + finally: + # Ensure cleanup even if assertions fail + delete_iam_integration(client, integration.uid) + + +def test_update_gcp_integration(client, test_integration_name): + """Test updating a GCP IAM integration.""" + # Create initial integration + settings = GcpIamIntegrationSettings(read_bucket="gs://test-bucket") + integration = client.get_organization().create_iam_integration( + name=test_integration_name, settings=settings + ) + + try: + # Update integration + new_settings = GcpIamIntegrationSettings( + read_bucket="gs://updated-bucket" + ) + integration.update( + name=f"updated-{test_integration_name}", settings=new_settings + ) + + # Verify update - find the specific integration by ID + updated_integration = None + for iam_int in client.get_organization().get_iam_integrations(): + if iam_int.uid == integration.uid: + updated_integration = iam_int + break + + assert updated_integration is not None + assert updated_integration.name == f"updated-{test_integration_name}" + # Note: Settings may not be returned immediately after update + finally: + # Ensure cleanup even if assertions fail + delete_iam_integration(client, integration.uid) + + +def test_update_azure_integration(client, test_integration_name): + """Test updating an Azure IAM integration.""" + # Create initial integration + settings = AzureIamIntegrationSettings( + read_container_url="https://test.blob.core.windows.net/test", + tenant_id="test-tenant", + ) + integration = client.get_organization().create_iam_integration( + name=test_integration_name, settings=settings + ) + + try: + # Update integration + new_settings = AzureIamIntegrationSettings( + read_container_url="https://updated.blob.core.windows.net/test", + tenant_id="updated-tenant", + ) + integration.update( + name=f"updated-{test_integration_name}", settings=new_settings + ) + + # Verify update - find the specific integration by ID + updated_integration = None + for iam_int in client.get_organization().get_iam_integrations(): + if iam_int.uid == integration.uid: + updated_integration = iam_int + break + + assert updated_integration is not None + assert updated_integration.name == f"updated-{test_integration_name}" + # Note: Settings may not be returned immediately after update + finally: + # Ensure cleanup even if assertions fail + delete_iam_integration(client, integration.uid) + + +def test_update_azure_integration_with_credentials( + client, test_integration_name +): + """Test updating an Azure IAM integration including credentials.""" + # Create initial integration without credentials + settings = AzureIamIntegrationSettings( + read_container_url="https://test.blob.core.windows.net/test", + tenant_id="test-tenant", + ) + integration = client.get_organization().create_iam_integration( + name=test_integration_name, settings=settings + ) + + try: + # Update integration - note: credentials are not supported in updates + new_settings = AzureIamIntegrationSettings( + read_container_url="https://updated.blob.core.windows.net/test", + tenant_id="updated-tenant", + # Note: client_id and client_secret are not supported in update operations + ) + integration.update( + name=f"updated-{test_integration_name}", settings=new_settings + ) + + # Verify update (Note: credentials are not returned for security reasons) + updated_integration = client.get_organization().get_iam_integrations()[ + 0 + ] + assert updated_integration.name == f"updated-{test_integration_name}" + # Note: Settings might not be returned for security reasons + finally: + # Ensure cleanup even if assertions fail + delete_iam_integration(client, integration.uid) + + +def test_validate_integration_format(client): + """Test that validate returns a result with the correct format.""" + # Get any existing integration or create a test one + integrations = client.get_organization().get_iam_integrations() + if not integrations: + pytest.skip("No IAM integrations available for testing") + + integration = integrations[0] + result = integration.validate() + + # Verify the result structure + assert isinstance(result, dict) + assert "valid" in result + assert isinstance(result["valid"], bool) + assert "checks" in result + assert isinstance(result["checks"], list) + + # Verify each check's structure + for check in result["checks"]: + assert isinstance(check, dict) + assert "name" in check + assert isinstance(check["name"], str) + assert "success" in check + assert isinstance(check["success"], bool) + assert "message" in check + assert isinstance(check["message"], str) + + +def test_validate_with_additional_checks(client): + """Test validate with additional cloud buckets validation.""" + # Get any existing integration or create a test one + integrations = client.get_organization().get_iam_integrations() + if not integrations: + pytest.skip("No IAM integrations available for testing") + + integration = integrations[0] + result = integration.validate() + + # Verify the result structure + assert isinstance(result, dict) + assert "valid" in result + assert isinstance(result["valid"], bool) + assert "checks" in result + assert isinstance(result["checks"], list) + + +def test_set_as_default(client, test_integration_name): + """Test setting an integration as default.""" + # Save the original default integration + original_default = client.get_organization().get_default_iam_integration() + + integration = None + try: + # Create an integration + settings = AwsIamIntegrationSettings( + role_arn="arn:aws:iam::000000000000:role/temporary", + read_bucket="test-bucket", + ) + integration = client.get_organization().create_iam_integration( + name=test_integration_name, settings=settings + ) + + # Set as default + integration.set_as_default() + + # Verify it's now the default + default_integration = ( + client.get_organization().get_default_iam_integration() + ) + assert default_integration is not None + assert default_integration.uid == integration.uid + assert default_integration.is_org_default + + finally: + # Restore the original default integration + if original_default is not None: + original_default.set_as_default() + # Clean up the created integration + if integration is not None: + delete_iam_integration(client, integration.uid) @pytest.mark.skip(