From 1f76233c5d41a44d7108432ec861ce99b8f2e52b Mon Sep 17 00:00:00 2001 From: anders-albert Date: Thu, 12 Dec 2024 08:01:54 +0100 Subject: [PATCH 1/5] feat: added service account data classes --- cognite/client/data_classes/iam.py | 150 ++++++++++++++++++++++++++++- 1 file changed, 149 insertions(+), 1 deletion(-) diff --git a/cognite/client/data_classes/iam.py b/cognite/client/data_classes/iam.py index b9694f5c1b..2b2bb8b866 100644 --- a/cognite/client/data_classes/iam.py +++ b/cognite/client/data_classes/iam.py @@ -2,7 +2,7 @@ from abc import ABC from collections.abc import Iterable -from typing import TYPE_CHECKING, Any, Literal, TypeAlias, cast +from typing import TYPE_CHECKING, Any, Literal, NoReturn, TypeAlias, cast from typing_extensions import Self @@ -489,3 +489,151 @@ def dump(self, camel_case: bool = True) -> dict[str, Any]: @classmethod def _load(cls, resource: dict, cognite_client: CogniteClient | None = None) -> ClientCredentials: return cls(client_id=resource["clientId"], client_secret=resource["clientSecret"]) + + +class ServiceAccountCore(WriteableCogniteResource["ServiceAccountWrite"], ABC): + def __init__(self, name: str | None = None, external_id: str | None = None, description: str | None = None) -> None: + self.name = name + self.external_id = external_id + self.description = description + + +class ServiceAccountWrite(ServiceAccountCore): + """A service account. + + This is the write/request format of the service account dto. + + Args: + name (str | None): Human-readable name of a service account + external_id (str | None): The external ID provided by the client. Must be unique for the resource type. + description (str | None): Longer description of a service account + + """ + + def __init__(self, name: str | None = None, external_id: str | None = None, description: str | None = None) -> None: + super().__init__( + name=name, + external_id=external_id, + description=description, + ) + + def as_write(self) -> ServiceAccountWrite: + return self + + +class ServiceAccount(ServiceAccountCore): + """A service account. + + This is the read/response format of the service account. + + Args: + id (str | None): Unique identifier of a service account + external_id (str | None): The external ID provided by the client. Must be unique for the resource type. + name (str | None): Human-readable name of a service account + description (str | None): Longer description of a service account + created_by (str | None): The ID of an organization user + created_time (int | None): The number of milliseconds since 00:00:00 Thursday, 1 January 1970, Coordinated Universal Time (UTC), minus leap seconds. + last_updated_time (int | None): The number of milliseconds since 00:00:00 Thursday, 1 January 1970, Coordinated Universal Time (UTC), minus leap seconds. + + """ + + def __init__( + self, + id: str | None = None, + external_id: str | None = None, + name: str | None = None, + description: str | None = None, + created_by: str | None = None, + created_time: int | None = None, + last_updated_time: int | None = None, + ) -> None: + super().__init__( + name=name, + external_id=external_id, + description=description, + ) + self.id = id + self.created_by = created_by + self.created_time = created_time + self.last_updated_time = last_updated_time + + def as_write(self) -> ServiceAccountWrite: + return ServiceAccountWrite( + external_id=self.external_id, + description=self.description, + name=self.name, + ) + + +class ServiceAccountWriteList(CogniteResourceList[ServiceAccountWrite]): + _RESOURCE = ServiceAccountWrite + + +class ServiceAccountList(WriteableCogniteResourceList[ServiceAccountWrite, ServiceAccount]): + _RESOURCE = ServiceAccount + + def as_write(self) -> ServiceAccountWriteList: + return ServiceAccountWriteList([item.as_write() for item in self.data]) + + +class ServiceAccountSecretCore(WriteableCogniteResource["ServiceAccountSecretWrite"], ABC): ... + + +class ServiceAccountSecretWrite(ServiceAccountSecretCore): + """A service account secret. + + This is the write/request format of the service account secret dto. + + Args: + expires_in_seconds (int | None): The number of seconds until the secret expires. The maximum value is 180 days. + + """ + + def __init__(self, expires_in_seconds: int | None = None) -> None: + self.expires_in_seconds = expires_in_seconds + + def as_write(self) -> ServiceAccountSecretWrite: + return self + + +class ServiceAccountSecret(ServiceAccountSecretCore): + """A service account secret. + + This is the read/response format of the service account secret dto. + + Args: + id (int | None): Unique identifier of a service account secret + client_id (str | None): Unique identifier of a service account + expiration_time (int | None): The number of milliseconds since 00:00:00 Thursday, 1 January 1970, Coordinated Universal Time (UTC), minus leap seconds. + created_time (int | None): The number of milliseconds since 00:00:00 Thursday, 1 January 1970, Coordinated Universal Time (UTC), minus leap seconds. + last_token_issuance_time (str | None): The time when this secret was last used to issue a CDF access token. + + """ + + def __init__( + self, + id: int | None = None, + client_id: str | None = None, + expiration_time: int | None = None, + created_time: int | None = None, + last_token_issuance_time: str | None = None, + ) -> None: + self.id = id + self.client_id = client_id + self.expiration_time = expiration_time + self.created_time = created_time + self.last_token_issuance_time = last_token_issuance_time + + def as_write(self) -> NoReturn: + raise TypeError(f"{type(self).__name__} cannot be converted to a write object") + + +class ServiceAccountSecretDtoWriteList(CogniteResourceList[ServiceAccountSecretWrite]): + _RESOURCE = ServiceAccountSecretWrite + + +class ServiceAccountSecretDtoList(WriteableCogniteResourceList[ServiceAccountSecretWrite, ServiceAccountSecret]): + _RESOURCE = ServiceAccountSecret + + def as_write(self) -> NoReturn: + raise TypeError(f"{type(self).__name__} cannot be converted to a write object") From 708c5362aba8437bf4f45083e2fe91bfdcb2b3c0 Mon Sep 17 00:00:00 2001 From: anders-albert Date: Thu, 12 Dec 2024 08:23:54 +0100 Subject: [PATCH 2/5] feat: first pass of API --- cognite/client/_api/iam.py | 391 ++++++++++++++++++++++++++++- cognite/client/data_classes/iam.py | 8 +- 2 files changed, 396 insertions(+), 3 deletions(-) diff --git a/cognite/client/_api/iam.py b/cognite/client/_api/iam.py index 9fbb280be6..195bdbda3b 100644 --- a/cognite/client/_api/iam.py +++ b/cognite/client/_api/iam.py @@ -1,7 +1,7 @@ from __future__ import annotations import warnings -from collections.abc import Iterable, Sequence +from collections.abc import Iterable, Iterator, Sequence from itertools import groupby from operator import itemgetter from typing import TYPE_CHECKING, Any, Literal, TypeAlias, cast, overload @@ -36,11 +36,20 @@ from cognite.client.data_classes.iam import ( GroupWrite, SecurityCategoryWrite, + ServiceAccount, + ServiceAccountList, + ServiceAccountSecret, + ServiceAccountSecretList, + ServiceAccountUpdate, + ServiceAccountWrite, SessionStatus, SessionType, TokenInspection, ) +from cognite.client.utils._auxiliary import interpolate_and_url_encode +from cognite.client.utils._experimental import FeaturePreviewWarning from cognite.client.utils._identifier import IdentifierSequence +from cognite.client.utils.useful_types import SequenceNotStr if TYPE_CHECKING: from cognite.client import CogniteClient @@ -653,3 +662,383 @@ def list(self, status: SessionStatus | None = None, limit: int = DEFAULT_LIMIT_R """ filter = {"status": status.upper()} if status is not None else None return self._list(list_cls=SessionList, resource_cls=Session, method="GET", filter=filter, limit=limit) + + +class ServiceAccountsAPI(APIClient): + _RESOURCE_PATH = "/api/v1/orgs/{}/serviceaccounts" + + def __init__(self, config: ClientConfig, api_version: str | None, cognite_client: CogniteClient) -> None: + super().__init__(config, api_version, cognite_client) + self._warning = FeaturePreviewWarning( + api_maturity="alpha", sdk_maturity="alpha", feature_name="ServiceAccounts" + ) + self.secrets = ServiceAccountSecretsAPI(config, api_version, cognite_client, self._warning) + + @overload + def __call__( + self, + chunk_size: None = None, + limit: int | None = None, + ) -> Iterator[ServiceAccount]: ... + + @overload + def __call__( + self, + chunk_size: int, + limit: int | None = None, + ) -> Iterator[ServiceAccountList]: ... + + def __call__( + self, + chunk_size: int | None = None, + limit: int | None = None, + ) -> Iterator[ServiceAccount] | Iterator[ServiceAccountList]: + """Iterate over service accounts + + Fetches service account dto as they are iterated over, so you keep a limited number of service accounts in memory. + + Args: + chunk_size (int | None): Number of service accounts to return in each chunk. Defaults to yielding one service account dto a time. + limit (int | None): Maximum number of service accounts to return. Defaults to return all. + + Returns: + Iterator[ServiceAccountDto] | Iterator[ServiceAccountDtoList]: yields ServiceAccountDto one by one if chunk_size is not specified, else ServiceAccountDtoList objects. + """ + self._warning.warn() + + return self._list_generator( + list_cls=ServiceAccountList, + resource_cls=ServiceAccount, + method="GET", + chunk_size=chunk_size, + limit=limit, + headers={"cdf-version": "alpha"}, + ) + + def __iter__(self) -> Iterator[ServiceAccount]: + """Iterate over service accounts + + Fetches service accounts as they are iterated over, so you keep a + limited number of service accounts in memory. + + Returns: + Iterator[ServiceAccountDto]: yields service account dto one by one. + """ + return self() + + @overload + def create(self, org: str, item: ServiceAccountWrite) -> ServiceAccount: ... + + @overload + def create(self, org: str, item: Sequence[ServiceAccountWrite]) -> ServiceAccountList: ... + + def create( + self, + org: str, + item: ServiceAccountWrite | Sequence[ServiceAccountWrite], + ) -> ServiceAccount | ServiceAccountList: + """`Create service accounts `_ + + Create service accounts in an organization. + + #### Access control + Requires the caller to be an admin in the + target organization. + + Args: + org (str): ID of an organization + item (ServiceAccountWrite | Sequence[ServiceAccountWrite]): Service account or list of service accounts to create. + + Returns: + ServiceAccount | ServiceAccountList: The created service account or service accounts. + + Examples: + + + + """ + self._warning.warn() + + return self._create_multiple( + resource_path=interpolate_and_url_encode(self._RESOURCE_PATH, org), + list_cls=ServiceAccountList, + resource_cls=ServiceAccount, + items=item, + input_resource_cls=ServiceAccountWrite, + headers={"cdf-version": "alpha"}, + ) + + @overload + def update(self, org: str, item: ServiceAccountWrite | ServiceAccountUpdate) -> ServiceAccount: ... + + @overload + def update(self, org: str, item: Sequence[ServiceAccount | ServiceAccountUpdate]) -> ServiceAccountList: ... + + def update( + self, + org: str, + item: ServiceAccountWrite | ServiceAccountUpdate | Sequence[ServiceAccount | ServiceAccountUpdate], + ) -> ServiceAccount | ServiceAccountList: + """`Update service accounts `_ + + Update service accounts in an organization. + + #### Access control + Requires the caller to be an admin in the + target organization. + + Args: + org (str): ID of an organization + item (ServiceAccountWrite | ServiceAccountUpdate | Sequence[ServiceAccount | ServiceAccountUpdate]): Service account or list of service accounts to update. + + Returns: + ServiceAccount | ServiceAccountList: The updated service account or service accounts. + + Examples: + + + + """ + self._warning.warn() + + return self._update_multiple( + resource_path=interpolate_and_url_encode(self._RESOURCE_PATH, org), + list_cls=ServiceAccountList, + resource_cls=ServiceAccount, + items=item, + input_resource_cls=ServiceAccountWrite, + headers={"cdf-version": "alpha"}, + ) + + def delete( + self, org: str, id: int | Sequence[id] | None = None, external_id: str | SequenceNotStr[str] | None = None + ) -> None: + """`Delete service accounts `_ + + Delete service accounts in an organization. All secrets associated with the service accounts will be deleted + as well. + + #### Access control + Requires the caller to be an admin in the target organization. + + Args: + org (str): ID of an organization + id (int | Sequence[id] | NOne): ID or list of IDs of service accounts to delete. + external_id (str | SequenceNotStr[str] | None): External ID or list of external IDs of service accounts to delete. + + Examples: + + + + """ + self._warning.warn() + + self._delete_multiple( + resource_path=interpolate_and_url_encode(self._RESOURCE_PATH, org), + identifiers=IdentifierSequence.load(ids=id, external_ids=external_id), + wrap_ids=False, + headers={"cdf-version": "alpha"}, + ) + + def list(self, org: str, limit: int | None = DEFAULT_LIMIT_READ) -> ServiceAccount | ServiceAccountList: + """`List service accounts `_ + + List service accounts in an organization. + + #### Access control + Requires the caller to be logged into the target + organization. + + Args: + org (str): ID of an organization + limit (int | None): Max number of service accounts to return. Defaults to 25. Set to -1, float("inf") or None to return all items. + + Returns: + ServiceAccount | ServiceAccountList: A service account or list of service accounts. + + Examples: + + + + """ + self._warning.warn() + + return self._list( + resource_path=interpolate_and_url_encode(self._RESOURCE_PATH, org), + list_cls=ServiceAccountList, + resource_cls=ServiceAccount, + method="GET", + limit=limit, + headers={"cdf-version": "alpha"}, + ) + + +class ServiceAccountSecretsAPI(APIClient): + _RESOURCE_PATH = "/api/v1/orgs/{}/serviceaccounts/{}/secrets" + + def __init__( + self, + config: ClientConfig, + api_version: str | None, + cognite_client: CogniteClient, + warning: FeaturePreviewWarning, + ) -> None: + super().__init__(config, api_version, cognite_client) + self._warning = warning + + @overload + def __call__( + self, + chunk_size: None = None, + limit: int | None = None, + ) -> Iterator[ServiceAccountSecret]: ... + + @overload + def __call__( + self, + chunk_size: int, + limit: int | None = None, + ) -> Iterator[ServiceAccountSecretList]: ... + + def __call__( + self, + chunk_size: int | None = None, + limit: int | None = None, + ) -> Iterator[ServiceAccountSecret] | Iterator[ServiceAccountSecretList]: + """Iterate over service account secrets + + Fetches service account secret dto as they are iterated over, so you keep a limited number of service account secrets in memory. + + Args: + chunk_size (int | None): Number of service account secretss to return in each chunk. Defaults to yielding one service account secret dto a time. + limit (int | None): Maximum number of service account secrets to return. Defaults to return all. + + Returns: + Iterator[ServiceAccountSecretDto] | Iterator[ServiceAccountSecretDtoList]: yields ServiceAccountSecretDto one by one if chunk_size is not specified, else ServiceAccountSecretDtoList objects. + """ + self._warning.warn() + + return self._list_generator( + list_cls=ServiceAccountSecretList, + resource_cls=ServiceAccountSecret, + method="GET", + chunk_size=chunk_size, + limit=limit, + headers={"cdf-version": "alpha"}, + ) + + def __iter__(self) -> Iterator[ServiceAccountSecret]: + """Iterate over service account secretss + + Fetches service account secrets as they are iterated over, so you keep a + limited number of service account secrets in memory. + + Returns: + Iterator[ServiceAccountSecretDto]: yields service account secret dto one by one. + """ + return self() + + @overload + def create(self, org: str, client_id: str, item: ServiceAccountWrite) -> ServiceAccountSecret: ... + + @overload + def create(self, org: str, client_id: str, item: Sequence[ServiceAccountWrite]) -> ServiceAccountSecretList: ... + + def create( + self, org: str, client_id: str, item: ServiceAccountWrite | Sequence[ServiceAccountWrite] + ) -> ServiceAccountSecret | ServiceAccountSecretList: + """`Create a service account secret `_ + + Create a secret for a service account. + + This is the only time when the client secret will be shown. Make sure to store it securely. + + #### Access control + Requires the caller to be an admin in the target organization. + + Args: + org (str): ID of an organization + client_id (str): None + item (ServiceAccountWrite | Sequence[ServiceAccountWrite]): Service account or list of service accounts to create. + + Returns: + ServiceAccountSecret | ServiceAccountSecretList: The created service account secret or service account secrets. + + Examples: + + + + """ + self._warning.warn() + + return self._create_multiple( + resource_path=interpolate_and_url_encode(self._RESOURCE_PATH, org, client_id), + list_cls=ServiceAccountSecretList, + resource_cls=ServiceAccountSecret, + items=item, + input_resource_cls=ServiceAccountWrite, + headers={"cdf-version": "alpha"}, + ) + + def delete(self, org: str, client_id: str, id: int | Sequence[int]) -> None: + """`Delete service account secrets `_ + + Delete secrets for a service account. + + #### Access control + Requires the caller to be an admin in the target + organization. + + Args: + org (str): ID of an organization + client_id (str): None + id (int | Sequence[int]): ID or list of IDs of service account secrets to delete. + + Examples: + + + + """ + self._warning.warn() + + self._delete_multiple( + resource_path=interpolate_and_url_encode(self._RESOURCE_PATH, org, client_id), + identifiers=IdentifierSequence.load(ids=id), + wrap_ids=False, + headers={"cdf-version": "alpha"}, + ) + + def list( + self, + org: str, + client_id: str, + ) -> ServiceAccountSecretList: + """`List service account secrets `_ + + List secrets for a service account. + + #### Access control + Requires the caller to be an admin in the target + organization. + + Args: + org (str): ID of an organization + client_id (str): Unique identifier of a service account + + Returns: + ServiceAccountSecretList: A list of service account secrets. + + Examples: + + + + """ + self._warning.warn() + + return self._list( + resource_path=interpolate_and_url_encode(self._RESOURCE_PATH, org, client_id), + list_cls=ServiceAccountSecretList, + resource_cls=ServiceAccountSecret, + method="GET", + headers={"cdf-version": "alpha"}, + ) diff --git a/cognite/client/data_classes/iam.py b/cognite/client/data_classes/iam.py index 2b2bb8b866..930bda59b5 100644 --- a/cognite/client/data_classes/iam.py +++ b/cognite/client/data_classes/iam.py @@ -10,6 +10,7 @@ CogniteResource, CogniteResourceList, CogniteResponse, + CogniteUpdate, IdTransformerMixin, InternalIdTransformerMixin, NameTransformerMixin, @@ -565,6 +566,9 @@ def as_write(self) -> ServiceAccountWrite: ) +class ServiceAccountUpdate(CogniteUpdate): ... + + class ServiceAccountWriteList(CogniteResourceList[ServiceAccountWrite]): _RESOURCE = ServiceAccountWrite @@ -628,11 +632,11 @@ def as_write(self) -> NoReturn: raise TypeError(f"{type(self).__name__} cannot be converted to a write object") -class ServiceAccountSecretDtoWriteList(CogniteResourceList[ServiceAccountSecretWrite]): +class ServiceAccountSecretWriteList(CogniteResourceList[ServiceAccountSecretWrite]): _RESOURCE = ServiceAccountSecretWrite -class ServiceAccountSecretDtoList(WriteableCogniteResourceList[ServiceAccountSecretWrite, ServiceAccountSecret]): +class ServiceAccountSecretList(WriteableCogniteResourceList[ServiceAccountSecretWrite, ServiceAccountSecret]): _RESOURCE = ServiceAccountSecret def as_write(self) -> NoReturn: From 83f1a66968482eb35f90d2bd63a35873b74efb03 Mon Sep 17 00:00:00 2001 From: anders-albert Date: Thu, 12 Dec 2024 08:34:42 +0100 Subject: [PATCH 3/5] fix: implemented update class --- cognite/client/_api/iam.py | 25 +++++++++++++----------- cognite/client/data_classes/iam.py | 31 +++++++++++++++++++++++++++++- 2 files changed, 44 insertions(+), 12 deletions(-) diff --git a/cognite/client/_api/iam.py b/cognite/client/_api/iam.py index 195bdbda3b..221bf18d0e 100644 --- a/cognite/client/_api/iam.py +++ b/cognite/client/_api/iam.py @@ -695,14 +695,14 @@ def __call__( ) -> Iterator[ServiceAccount] | Iterator[ServiceAccountList]: """Iterate over service accounts - Fetches service account dto as they are iterated over, so you keep a limited number of service accounts in memory. + Fetches service accounts as they are iterated over, so you keep a limited number of service accounts in memory. Args: - chunk_size (int | None): Number of service accounts to return in each chunk. Defaults to yielding one service account dto a time. + chunk_size (int | None): Number of service accounts to return in each chunk. Defaults to yielding one service account a time. limit (int | None): Maximum number of service accounts to return. Defaults to return all. Returns: - Iterator[ServiceAccountDto] | Iterator[ServiceAccountDtoList]: yields ServiceAccountDto one by one if chunk_size is not specified, else ServiceAccountDtoList objects. + Iterator[ServiceAccount] | Iterator[ServiceAccountList]: yields ServiceAccount one by one if chunk_size is not specified, else ServiceAccountList objects. """ self._warning.warn() @@ -722,7 +722,7 @@ def __iter__(self) -> Iterator[ServiceAccount]: limited number of service accounts in memory. Returns: - Iterator[ServiceAccountDto]: yields service account dto one by one. + Iterator[ServiceAccount]: yields service account one by one. """ return self() @@ -778,6 +778,7 @@ def update( self, org: str, item: ServiceAccountWrite | ServiceAccountUpdate | Sequence[ServiceAccount | ServiceAccountUpdate], + mode: Literal["replace_ignore_null", "patch", "replace"] = "replace_ignore_null", ) -> ServiceAccount | ServiceAccountList: """`Update service accounts `_ @@ -790,6 +791,7 @@ def update( Args: org (str): ID of an organization item (ServiceAccountWrite | ServiceAccountUpdate | Sequence[ServiceAccount | ServiceAccountUpdate]): Service account or list of service accounts to update. + mode (Literal["replace_ignore_null", "patch", "replace"]): The update mode to use. Defaults to 'replace_ignore_null'. Returns: ServiceAccount | ServiceAccountList: The updated service account or service accounts. @@ -805,13 +807,14 @@ def update( resource_path=interpolate_and_url_encode(self._RESOURCE_PATH, org), list_cls=ServiceAccountList, resource_cls=ServiceAccount, + update_cls=ServiceAccountUpdate, items=item, - input_resource_cls=ServiceAccountWrite, + mode=mode, headers={"cdf-version": "alpha"}, ) def delete( - self, org: str, id: int | Sequence[id] | None = None, external_id: str | SequenceNotStr[str] | None = None + self, org: str, id: int | Sequence[int] | None = None, external_id: str | SequenceNotStr[str] | None = None ) -> None: """`Delete service accounts `_ @@ -823,7 +826,7 @@ def delete( Args: org (str): ID of an organization - id (int | Sequence[id] | NOne): ID or list of IDs of service accounts to delete. + id (int | Sequence[int] | None): ID or list of IDs of service accounts to delete. external_id (str | SequenceNotStr[str] | None): External ID or list of external IDs of service accounts to delete. Examples: @@ -907,14 +910,14 @@ def __call__( ) -> Iterator[ServiceAccountSecret] | Iterator[ServiceAccountSecretList]: """Iterate over service account secrets - Fetches service account secret dto as they are iterated over, so you keep a limited number of service account secrets in memory. + Fetches service account secret as they are iterated over, so you keep a limited number of service account secrets in memory. Args: - chunk_size (int | None): Number of service account secretss to return in each chunk. Defaults to yielding one service account secret dto a time. + chunk_size (int | None): Number of service account secretss to return in each chunk. Defaults to yielding one service account secret a time. limit (int | None): Maximum number of service account secrets to return. Defaults to return all. Returns: - Iterator[ServiceAccountSecretDto] | Iterator[ServiceAccountSecretDtoList]: yields ServiceAccountSecretDto one by one if chunk_size is not specified, else ServiceAccountSecretDtoList objects. + Iterator[ServiceAccountSecret] | Iterator[ServiceAccountSecretList]: yields ServiceAccountSecret one by one if chunk_size is not specified, else ServiceAccountSecretList objects. """ self._warning.warn() @@ -934,7 +937,7 @@ def __iter__(self) -> Iterator[ServiceAccountSecret]: limited number of service account secrets in memory. Returns: - Iterator[ServiceAccountSecretDto]: yields service account secret dto one by one. + Iterator[ServiceAccountSecret]: yields service account secret one by one. """ return self() diff --git a/cognite/client/data_classes/iam.py b/cognite/client/data_classes/iam.py index 930bda59b5..7336ef3c1e 100644 --- a/cognite/client/data_classes/iam.py +++ b/cognite/client/data_classes/iam.py @@ -7,6 +7,7 @@ from typing_extensions import Self from cognite.client.data_classes._base import ( + CognitePrimitiveUpdate, CogniteResource, CogniteResourceList, CogniteResponse, @@ -14,6 +15,7 @@ IdTransformerMixin, InternalIdTransformerMixin, NameTransformerMixin, + PropertySpec, WriteableCogniteResource, WriteableCogniteResourceList, ) @@ -566,7 +568,34 @@ def as_write(self) -> ServiceAccountWrite: ) -class ServiceAccountUpdate(CogniteUpdate): ... +class ServiceAccountUpdate(CogniteUpdate): + class _NullableStringServiceAccountUpdate(CognitePrimitiveUpdate): + def set(self, value: str | None) -> ServiceAccountUpdate: + return self._set(value) + + class _StringServiceAccountUpdate(CognitePrimitiveUpdate): + def set(self, value: str) -> ServiceAccountUpdate: + return self._set(value) + + @property + def name(self) -> _StringServiceAccountUpdate: + return ServiceAccountUpdate._StringServiceAccountUpdate(self, "name") + + @property + def description(self) -> _NullableStringServiceAccountUpdate: + return ServiceAccountUpdate._NullableStringServiceAccountUpdate(self, "description") + + @property + def external_id(self) -> _NullableStringServiceAccountUpdate: + return ServiceAccountUpdate._NullableStringServiceAccountUpdate(self, "externalId") + + @classmethod + def _get_update_properties(cls, item: CogniteResource | None = None) -> list[PropertySpec]: + return [ + PropertySpec("name", is_nullable=False), + PropertySpec("description", is_nullable=True), + PropertySpec("external_id", is_nullable=True), + ] class ServiceAccountWriteList(CogniteResourceList[ServiceAccountWrite]): From 5c546cec193bffb62aae61ba7103631a7cbde138 Mon Sep 17 00:00:00 2001 From: anders-albert Date: Thu, 12 Dec 2024 08:36:55 +0100 Subject: [PATCH 4/5] feat: connect service account --- cognite/client/_api/iam.py | 1 + cognite/client/testing.py | 12 +++++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/cognite/client/_api/iam.py b/cognite/client/_api/iam.py index 221bf18d0e..8332f4f200 100644 --- a/cognite/client/_api/iam.py +++ b/cognite/client/_api/iam.py @@ -114,6 +114,7 @@ def __init__(self, config: ClientConfig, api_version: str | None, cognite_client self.user_profiles = UserProfilesAPI(config, api_version, cognite_client) # TokenAPI only uses base_url, so we pass `api_version=None`: self.token = TokenAPI(config, api_version=None, cognite_client=cognite_client) + self.service_accounts = ServiceAccountsAPI(config, api_version, cognite_client) @staticmethod def compare_capabilities( diff --git a/cognite/client/testing.py b/cognite/client/testing.py index c666f34a13..a20bbaa78d 100644 --- a/cognite/client/testing.py +++ b/cognite/client/testing.py @@ -35,7 +35,15 @@ from cognite.client._api.hosted_extractors.jobs import JobsAPI from cognite.client._api.hosted_extractors.mappings import MappingsAPI from cognite.client._api.hosted_extractors.sources import SourcesAPI -from cognite.client._api.iam import IAMAPI, GroupsAPI, SecurityCategoriesAPI, SessionsAPI, TokenAPI +from cognite.client._api.iam import ( + IAMAPI, + GroupsAPI, + SecurityCategoriesAPI, + ServiceAccountsAPI, + ServiceAccountSecretsAPI, + SessionsAPI, + TokenAPI, +) from cognite.client._api.labels import LabelsAPI from cognite.client._api.postgres_gateway import PostgresGatewaysAPI from cognite.client._api.postgres_gateway.tables import TablesAPI as PostgresTablesAPI @@ -129,6 +137,8 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: self.iam = MagicMock(spec=IAMAPI) self.iam.groups = MagicMock(spec_set=GroupsAPI) self.iam.security_categories = MagicMock(spec_set=SecurityCategoriesAPI) + self.iam.service_accounts = MagicMock(spec=ServiceAccountsAPI) + self.iam.service_accounts.secrets = MagicMock(spec_set=ServiceAccountSecretsAPI) self.iam.sessions = MagicMock(spec_set=SessionsAPI) self.iam.user_profiles = MagicMock(spec_set=UserProfilesAPI) self.iam.token = MagicMock(spec_set=TokenAPI) From 42b7739805121d1848e37c1596b6c5df39d11fda Mon Sep 17 00:00:00 2001 From: anders-albert Date: Thu, 12 Dec 2024 08:45:14 +0100 Subject: [PATCH 5/5] tests: added full test --- tests/tests_integration/test_api/test_iam.py | 45 ++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/tests/tests_integration/test_api/test_iam.py b/tests/tests_integration/test_api/test_iam.py index a992bc4c16..462d8cf48a 100644 --- a/tests/tests_integration/test_api/test_iam.py +++ b/tests/tests_integration/test_api/test_iam.py @@ -7,6 +7,13 @@ from cognite.client import CogniteClient from cognite.client.data_classes import CreatedSession, Group, GroupList, SecurityCategory from cognite.client.data_classes.capabilities import EventsAcl, ProjectCapabilityList +from cognite.client.data_classes.iam import ( + ServiceAccount, + ServiceAccountSecret, + ServiceAccountSecretWrite, + ServiceAccountUpdate, + ServiceAccountWrite, +) from cognite.client.utils._text import random_string @@ -87,3 +94,41 @@ def test_create_retrieve_and_revoke(self, cognite_client: CogniteClient) -> None if created: revoked = cognite_client.iam.sessions.revoke(created.id) assert created.id == revoked.id + + +class TestServiceAccountsAPI: + def test_create_update_retrieve_delete(self, cognite_client: CogniteClient) -> None: + org = "pytest_org" + item = ServiceAccountWrite( + name="test_" + random_string(10), + external_id=f"test_{random_string(10)}", + description="Original description", + ) + + created: ServiceAccount | None = None + secret: ServiceAccountSecret | None = None + try: + created = cognite_client.iam.service_accounts.create(org, item) + assert created.as_write().dump() == item.dump() + + update = ServiceAccountUpdate(id=created.id).description.set("Updated description") + + updated = cognite_client.iam.service_accounts.update(org, update) + assert updated.description == "Updated description" + + retrieved = cognite_client.iam.service_accounts.retrieve(created.id) + assert retrieved.as_write().dump() == updated.as_write().dump() + + secret = cognite_client.iam.service_accounts.secrets.create( + org, created.id, ServiceAccountSecretWrite(expires_in_seconds=3600) + ) + assert secret.id is not None + + listed = cognite_client.iam.service_accounts.secrets.list(org, created.id) + assert len(listed) == 1 + assert listed[0].id == secret.id + finally: + if created: + if secret: + cognite_client.iam.service_accounts.secrets.delete(org, created.id, secret.id) + cognite_client.iam.service_accounts.delete(created.id)