Skip to content

Commit 07eb9a7

Browse files
authored
feat(IAM Identity): add apikey expiration (#314)
Signed-off-by: Hari K Arla <[email protected]>
1 parent f6db961 commit 07eb9a7

File tree

3 files changed

+146
-186
lines changed

3 files changed

+146
-186
lines changed

ibm_platform_services/iam_identity_v1.py

Lines changed: 79 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# coding: utf-8
22

3-
# (C) Copyright IBM Corp. 2025.
3+
# (C) Copyright IBM Corp. 2026.
44
#
55
# Licensed under the Apache License, Version 2.0 (the "License");
66
# you may not use this file except in compliance with the License.
@@ -919,6 +919,7 @@ def create_api_key(
919919
store_value: Optional[bool] = None,
920920
support_sessions: Optional[bool] = None,
921921
action_when_leaked: Optional[str] = None,
922+
expires_at: Optional[str] = None,
922923
entity_lock: Optional[str] = None,
923924
entity_disable: Optional[str] = None,
924925
**kwargs,
@@ -954,6 +955,8 @@ def create_api_key(
954955
access, delete or rotate the API key. Available only for user API keys.
955956
:param str action_when_leaked: (optional) Defines the action to take when
956957
API key is leaked, valid values are 'none', 'disable' and 'delete'.
958+
:param str expires_at: (optional) Date and time when the API key becomes
959+
invalid, ISO 8601 datetime in the format 'yyyy-MM-ddTHH:mm+0000'.
957960
:param str entity_lock: (optional) Indicates if the API key is locked for
958961
further write operations. False by default.
959962
:param str entity_disable: (optional) Indicates if the API key is disabled.
@@ -987,6 +990,7 @@ def create_api_key(
987990
'store_value': store_value,
988991
'support_sessions': support_sessions,
989992
'action_when_leaked': action_when_leaked,
993+
'expires_at': expires_at,
990994
}
991995
data = {k: v for (k, v) in data.items() if v is not None}
992996
data = json.dumps(data)
@@ -1127,6 +1131,7 @@ def update_api_key(
11271131
description: Optional[str] = None,
11281132
support_sessions: Optional[bool] = None,
11291133
action_when_leaked: Optional[str] = None,
1134+
expires_at: Optional[str] = None,
11301135
**kwargs,
11311136
) -> DetailedResponse:
11321137
"""
@@ -1155,6 +1160,8 @@ def update_api_key(
11551160
access, delete or rotate the API key. Available only for user API keys.
11561161
:param str action_when_leaked: (optional) Defines the action to take when
11571162
API key is leaked, valid values are 'none', 'disable' and 'delete'.
1163+
:param str expires_at: (optional) Date and time when the API key becomes
1164+
invalid, ISO 8601 datetime in the format 'yyyy-MM-ddTHH:mm+0000'.
11581165
:param dict headers: A `dict` containing the request headers
11591166
:return: A `DetailedResponse` containing the result, headers and HTTP status code.
11601167
:rtype: DetailedResponse with `dict` result representing a `ApiKey` object
@@ -1179,6 +1186,7 @@ def update_api_key(
11791186
'description': description,
11801187
'support_sessions': support_sessions,
11811188
'action_when_leaked': action_when_leaked,
1189+
'expires_at': expires_at,
11821190
}
11831191
data = {k: v for (k, v) in data.items() if v is not None}
11841192
data = json.dumps(data)
@@ -7750,6 +7758,8 @@ class ApiKey:
77507758
delete or rotate the API key. Available only for user API keys.
77517759
:param str action_when_leaked: (optional) Defines the action to take when API
77527760
key is leaked, valid values are 'none', 'disable' and 'delete'.
7761+
:param str expires_at: (optional) Date and time when the API key becomes
7762+
invalid, ISO 8601 datetime in the format 'yyyy-MM-ddTHH:mm+0000'.
77537763
:param str description: (optional) The optional description of the API key. The
77547764
'description' property is only available if a description was provided during a
77557765
create of an API key.
@@ -7783,6 +7793,7 @@ def __init__(
77837793
modified_at: Optional[datetime] = None,
77847794
support_sessions: Optional[bool] = None,
77857795
action_when_leaked: Optional[str] = None,
7796+
expires_at: Optional[str] = None,
77867797
description: Optional[str] = None,
77877798
history: Optional[List['EnityHistoryRecord']] = None,
77887799
activity: Optional['Activity'] = None,
@@ -7826,6 +7837,8 @@ def __init__(
78267837
access, delete or rotate the API key. Available only for user API keys.
78277838
:param str action_when_leaked: (optional) Defines the action to take when
78287839
API key is leaked, valid values are 'none', 'disable' and 'delete'.
7840+
:param str expires_at: (optional) Date and time when the API key becomes
7841+
invalid, ISO 8601 datetime in the format 'yyyy-MM-ddTHH:mm+0000'.
78297842
:param str description: (optional) The optional description of the API key.
78307843
The 'description' property is only available if a description was provided
78317844
during a create of an API key.
@@ -7844,6 +7857,7 @@ def __init__(
78447857
self.name = name
78457858
self.support_sessions = support_sessions
78467859
self.action_when_leaked = action_when_leaked
7860+
self.expires_at = expires_at
78477861
self.description = description
78487862
self.iam_id = iam_id
78497863
self.account_id = account_id
@@ -7889,6 +7903,8 @@ def from_dict(cls, _dict: Dict) -> 'ApiKey':
78897903
args['support_sessions'] = support_sessions
78907904
if (action_when_leaked := _dict.get('action_when_leaked')) is not None:
78917905
args['action_when_leaked'] = action_when_leaked
7906+
if (expires_at := _dict.get('expires_at')) is not None:
7907+
args['expires_at'] = expires_at
78927908
if (description := _dict.get('description')) is not None:
78937909
args['description'] = description
78947910
if (iam_id := _dict.get('iam_id')) is not None:
@@ -7944,6 +7960,8 @@ def to_dict(self) -> Dict:
79447960
_dict['support_sessions'] = self.support_sessions
79457961
if hasattr(self, 'action_when_leaked') and self.action_when_leaked is not None:
79467962
_dict['action_when_leaked'] = self.action_when_leaked
7963+
if hasattr(self, 'expires_at') and self.expires_at is not None:
7964+
_dict['expires_at'] = self.expires_at
79477965
if hasattr(self, 'description') and self.description is not None:
79487966
_dict['description'] = self.description
79497967
if hasattr(self, 'iam_id') and self.iam_id is not None:
@@ -8007,6 +8025,10 @@ class ApiKeyInsideCreateServiceIdRequest:
80078025
key value is retrievable in the future by using the Get details of an API key
80088026
request. If you create an API key for a user, you must specify `false` or omit
80098027
the value. We don't allow storing of API keys for users.
8028+
:param str action_when_leaked: (optional) Defines the action to take when API
8029+
key is leaked, valid values are 'none', 'disable' and 'delete'.
8030+
:param str expires_at: (optional) Date and time when the API key becomes
8031+
invalid, ISO 8601 datetime in the format 'yyyy-MM-ddTHH:mm+0000'.
80108032
"""
80118033

80128034
def __init__(
@@ -8016,6 +8038,8 @@ def __init__(
80168038
description: Optional[str] = None,
80178039
apikey: Optional[str] = None,
80188040
store_value: Optional[bool] = None,
8041+
action_when_leaked: Optional[str] = None,
8042+
expires_at: Optional[str] = None,
80198043
) -> None:
80208044
"""
80218045
Initialize a ApiKeyInsideCreateServiceIdRequest object.
@@ -8037,11 +8061,17 @@ def __init__(
80378061
API key value is retrievable in the future by using the Get details of an
80388062
API key request. If you create an API key for a user, you must specify
80398063
`false` or omit the value. We don't allow storing of API keys for users.
8064+
:param str action_when_leaked: (optional) Defines the action to take when
8065+
API key is leaked, valid values are 'none', 'disable' and 'delete'.
8066+
:param str expires_at: (optional) Date and time when the API key becomes
8067+
invalid, ISO 8601 datetime in the format 'yyyy-MM-ddTHH:mm+0000'.
80408068
"""
80418069
self.name = name
80428070
self.description = description
80438071
self.apikey = apikey
80448072
self.store_value = store_value
8073+
self.action_when_leaked = action_when_leaked
8074+
self.expires_at = expires_at
80458075

80468076
@classmethod
80478077
def from_dict(cls, _dict: Dict) -> 'ApiKeyInsideCreateServiceIdRequest':
@@ -8057,6 +8087,10 @@ def from_dict(cls, _dict: Dict) -> 'ApiKeyInsideCreateServiceIdRequest':
80578087
args['apikey'] = apikey
80588088
if (store_value := _dict.get('store_value')) is not None:
80598089
args['store_value'] = store_value
8090+
if (action_when_leaked := _dict.get('action_when_leaked')) is not None:
8091+
args['action_when_leaked'] = action_when_leaked
8092+
if (expires_at := _dict.get('expires_at')) is not None:
8093+
args['expires_at'] = expires_at
80608094
return cls(**args)
80618095

80628096
@classmethod
@@ -8075,6 +8109,10 @@ def to_dict(self) -> Dict:
80758109
_dict['apikey'] = self.apikey
80768110
if hasattr(self, 'store_value') and self.store_value is not None:
80778111
_dict['store_value'] = self.store_value
8112+
if hasattr(self, 'action_when_leaked') and self.action_when_leaked is not None:
8113+
_dict['action_when_leaked'] = self.action_when_leaked
8114+
if hasattr(self, 'expires_at') and self.expires_at is not None:
8115+
_dict['expires_at'] = self.expires_at
80788116
return _dict
80798117

80808118
def _to_dict(self):
@@ -9634,90 +9672,6 @@ def __ne__(self, other: 'MfaEnrollmentTypeStatus') -> bool:
96349672
return not self == other
96359673

96369674

9637-
class MfaEnrollments:
9638-
"""
9639-
MfaEnrollments.
9640-
9641-
:param str effective_mfa_type: currently effective mfa type i.e. id_based_mfa or
9642-
account_based_mfa.
9643-
:param IdBasedMfaEnrollment id_based_mfa: (optional)
9644-
:param AccountBasedMfaEnrollment account_based_mfa: (optional)
9645-
"""
9646-
9647-
def __init__(
9648-
self,
9649-
effective_mfa_type: str,
9650-
*,
9651-
id_based_mfa: Optional['IdBasedMfaEnrollment'] = None,
9652-
account_based_mfa: Optional['AccountBasedMfaEnrollment'] = None,
9653-
) -> None:
9654-
"""
9655-
Initialize a MfaEnrollments object.
9656-
9657-
:param str effective_mfa_type: currently effective mfa type i.e.
9658-
id_based_mfa or account_based_mfa.
9659-
:param IdBasedMfaEnrollment id_based_mfa: (optional)
9660-
:param AccountBasedMfaEnrollment account_based_mfa: (optional)
9661-
"""
9662-
self.effective_mfa_type = effective_mfa_type
9663-
self.id_based_mfa = id_based_mfa
9664-
self.account_based_mfa = account_based_mfa
9665-
9666-
@classmethod
9667-
def from_dict(cls, _dict: Dict) -> 'MfaEnrollments':
9668-
"""Initialize a MfaEnrollments object from a json dictionary."""
9669-
args = {}
9670-
if (effective_mfa_type := _dict.get('effective_mfa_type')) is not None:
9671-
args['effective_mfa_type'] = effective_mfa_type
9672-
else:
9673-
raise ValueError('Required property \'effective_mfa_type\' not present in MfaEnrollments JSON')
9674-
if (id_based_mfa := _dict.get('id_based_mfa')) is not None:
9675-
args['id_based_mfa'] = IdBasedMfaEnrollment.from_dict(id_based_mfa)
9676-
if (account_based_mfa := _dict.get('account_based_mfa')) is not None:
9677-
args['account_based_mfa'] = AccountBasedMfaEnrollment.from_dict(account_based_mfa)
9678-
return cls(**args)
9679-
9680-
@classmethod
9681-
def _from_dict(cls, _dict):
9682-
"""Initialize a MfaEnrollments object from a json dictionary."""
9683-
return cls.from_dict(_dict)
9684-
9685-
def to_dict(self) -> Dict:
9686-
"""Return a json dictionary representing this model."""
9687-
_dict = {}
9688-
if hasattr(self, 'effective_mfa_type') and self.effective_mfa_type is not None:
9689-
_dict['effective_mfa_type'] = self.effective_mfa_type
9690-
if hasattr(self, 'id_based_mfa') and self.id_based_mfa is not None:
9691-
if isinstance(self.id_based_mfa, dict):
9692-
_dict['id_based_mfa'] = self.id_based_mfa
9693-
else:
9694-
_dict['id_based_mfa'] = self.id_based_mfa.to_dict()
9695-
if hasattr(self, 'account_based_mfa') and self.account_based_mfa is not None:
9696-
if isinstance(self.account_based_mfa, dict):
9697-
_dict['account_based_mfa'] = self.account_based_mfa
9698-
else:
9699-
_dict['account_based_mfa'] = self.account_based_mfa.to_dict()
9700-
return _dict
9701-
9702-
def _to_dict(self):
9703-
"""Return a json dictionary representing this model."""
9704-
return self.to_dict()
9705-
9706-
def __str__(self) -> str:
9707-
"""Return a `str` version of this MfaEnrollments object."""
9708-
return json.dumps(self.to_dict(), indent=2)
9709-
9710-
def __eq__(self, other: 'MfaEnrollments') -> bool:
9711-
"""Return `true` when self and other are equal, false otherwise."""
9712-
if not isinstance(other, self.__class__):
9713-
return False
9714-
return self.__dict__ == other.__dict__
9715-
9716-
def __ne__(self, other: 'MfaEnrollments') -> bool:
9717-
"""Return `true` when self and other are not equal, false otherwise."""
9718-
return not self == other
9719-
9720-
97219675
class PolicyTemplateReference:
97229676
"""
97239677
Metadata for external access policy.
@@ -14267,14 +14221,19 @@ class UserReportMfaEnrollmentStatus:
1426714221
:param str name: (optional) Name of the user.
1426814222
:param str username: Username of the user.
1426914223
:param str email: (optional) Email of the user.
14270-
:param MfaEnrollments enrollments:
14224+
:param str effective_mfa_type: currently effective mfa type i.e. id_based_mfa or
14225+
account_based_mfa.
14226+
:param IdBasedMfaEnrollment id_based_mfa:
14227+
:param AccountBasedMfaEnrollment account_based_mfa:
1427114228
"""
1427214229

1427314230
def __init__(
1427414231
self,
1427514232
iam_id: str,
1427614233
username: str,
14277-
enrollments: 'MfaEnrollments',
14234+
effective_mfa_type: str,
14235+
id_based_mfa: 'IdBasedMfaEnrollment',
14236+
account_based_mfa: 'AccountBasedMfaEnrollment',
1427814237
*,
1427914238
name: Optional[str] = None,
1428014239
email: Optional[str] = None,
@@ -14284,15 +14243,20 @@ def __init__(
1428414243

1428514244
:param str iam_id: IAMid of the user.
1428614245
:param str username: Username of the user.
14287-
:param MfaEnrollments enrollments:
14246+
:param str effective_mfa_type: currently effective mfa type i.e.
14247+
id_based_mfa or account_based_mfa.
14248+
:param IdBasedMfaEnrollment id_based_mfa:
14249+
:param AccountBasedMfaEnrollment account_based_mfa:
1428814250
:param str name: (optional) Name of the user.
1428914251
:param str email: (optional) Email of the user.
1429014252
"""
1429114253
self.iam_id = iam_id
1429214254
self.name = name
1429314255
self.username = username
1429414256
self.email = email
14295-
self.enrollments = enrollments
14257+
self.effective_mfa_type = effective_mfa_type
14258+
self.id_based_mfa = id_based_mfa
14259+
self.account_based_mfa = account_based_mfa
1429614260

1429714261
@classmethod
1429814262
def from_dict(cls, _dict: Dict) -> 'UserReportMfaEnrollmentStatus':
@@ -14310,10 +14274,22 @@ def from_dict(cls, _dict: Dict) -> 'UserReportMfaEnrollmentStatus':
1431014274
raise ValueError('Required property \'username\' not present in UserReportMfaEnrollmentStatus JSON')
1431114275
if (email := _dict.get('email')) is not None:
1431214276
args['email'] = email
14313-
if (enrollments := _dict.get('enrollments')) is not None:
14314-
args['enrollments'] = MfaEnrollments.from_dict(enrollments)
14277+
if (effective_mfa_type := _dict.get('effective_mfa_type')) is not None:
14278+
args['effective_mfa_type'] = effective_mfa_type
14279+
else:
14280+
raise ValueError(
14281+
'Required property \'effective_mfa_type\' not present in UserReportMfaEnrollmentStatus JSON'
14282+
)
14283+
if (id_based_mfa := _dict.get('id_based_mfa')) is not None:
14284+
args['id_based_mfa'] = IdBasedMfaEnrollment.from_dict(id_based_mfa)
14285+
else:
14286+
raise ValueError('Required property \'id_based_mfa\' not present in UserReportMfaEnrollmentStatus JSON')
14287+
if (account_based_mfa := _dict.get('account_based_mfa')) is not None:
14288+
args['account_based_mfa'] = AccountBasedMfaEnrollment.from_dict(account_based_mfa)
1431514289
else:
14316-
raise ValueError('Required property \'enrollments\' not present in UserReportMfaEnrollmentStatus JSON')
14290+
raise ValueError(
14291+
'Required property \'account_based_mfa\' not present in UserReportMfaEnrollmentStatus JSON'
14292+
)
1431714293
return cls(**args)
1431814294

1431914295
@classmethod
@@ -14332,11 +14308,18 @@ def to_dict(self) -> Dict:
1433214308
_dict['username'] = self.username
1433314309
if hasattr(self, 'email') and self.email is not None:
1433414310
_dict['email'] = self.email
14335-
if hasattr(self, 'enrollments') and self.enrollments is not None:
14336-
if isinstance(self.enrollments, dict):
14337-
_dict['enrollments'] = self.enrollments
14311+
if hasattr(self, 'effective_mfa_type') and self.effective_mfa_type is not None:
14312+
_dict['effective_mfa_type'] = self.effective_mfa_type
14313+
if hasattr(self, 'id_based_mfa') and self.id_based_mfa is not None:
14314+
if isinstance(self.id_based_mfa, dict):
14315+
_dict['id_based_mfa'] = self.id_based_mfa
1433814316
else:
14339-
_dict['enrollments'] = self.enrollments.to_dict()
14317+
_dict['id_based_mfa'] = self.id_based_mfa.to_dict()
14318+
if hasattr(self, 'account_based_mfa') and self.account_based_mfa is not None:
14319+
if isinstance(self.account_based_mfa, dict):
14320+
_dict['account_based_mfa'] = self.account_based_mfa
14321+
else:
14322+
_dict['account_based_mfa'] = self.account_based_mfa.to_dict()
1434014323
return _dict
1434114324

1434214325
def _to_dict(self):

test/integration/test_iam_identity_v1.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,7 @@ def test_create_api_key1(self):
328328
iam_id=self.iam_id,
329329
description='PythonSDK test apikey #1',
330330
account_id=self.account_id,
331+
expires_at='2049-01-02T18:30:00+0000',
331332
)
332333

333334
assert create_api_key_response.get_status_code() == 201
@@ -467,7 +468,10 @@ def test_update_api_key(self):
467468

468469
new_description = 'This is an updated description'
469470
update_api_key_response = self.iam_identity_service.update_api_key(
470-
id=apikey_id1, if_match=apikey_etag1, description=new_description
471+
id=apikey_id1,
472+
if_match=apikey_etag1,
473+
description=new_description,
474+
expires_at='2049-01-02T18:30:00+0000',
471475
)
472476

473477
assert update_api_key_response.get_status_code() == 200
@@ -552,10 +556,17 @@ def test_delete_api_key2(self):
552556

553557
@needscredentials
554558
def test_create_service_id(self):
559+
api_key_inside_create_service_id_request_model = {
560+
'name': 'API Key Name',
561+
'store_value': True,
562+
'expires_at': '2049-01-02T18:30:00+0000',
563+
}
564+
555565
create_service_id_response = self.iam_identity_service.create_service_id(
556566
account_id=self.account_id,
557567
name=self.serviceid_name,
558568
description='PythonSDK test serviceid',
569+
apikey=api_key_inside_create_service_id_request_model,
559570
)
560571

561572
assert create_service_id_response.get_status_code() == 201

0 commit comments

Comments
 (0)