pyxecm 1.6__py3-none-any.whl → 2.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of pyxecm might be problematic. Click here for more details.

Files changed (78) hide show
  1. pyxecm/__init__.py +7 -4
  2. pyxecm/avts.py +727 -254
  3. pyxecm/coreshare.py +686 -467
  4. pyxecm/customizer/__init__.py +16 -4
  5. pyxecm/customizer/__main__.py +58 -0
  6. pyxecm/customizer/api/__init__.py +5 -0
  7. pyxecm/customizer/api/__main__.py +6 -0
  8. pyxecm/customizer/api/app.py +163 -0
  9. pyxecm/customizer/api/auth/__init__.py +1 -0
  10. pyxecm/customizer/api/auth/functions.py +92 -0
  11. pyxecm/customizer/api/auth/models.py +13 -0
  12. pyxecm/customizer/api/auth/router.py +78 -0
  13. pyxecm/customizer/api/common/__init__.py +1 -0
  14. pyxecm/customizer/api/common/functions.py +47 -0
  15. pyxecm/customizer/api/common/metrics.py +92 -0
  16. pyxecm/customizer/api/common/models.py +21 -0
  17. pyxecm/customizer/api/common/payload_list.py +870 -0
  18. pyxecm/customizer/api/common/router.py +72 -0
  19. pyxecm/customizer/api/settings.py +128 -0
  20. pyxecm/customizer/api/terminal/__init__.py +1 -0
  21. pyxecm/customizer/api/terminal/router.py +87 -0
  22. pyxecm/customizer/api/v1_csai/__init__.py +1 -0
  23. pyxecm/customizer/api/v1_csai/router.py +87 -0
  24. pyxecm/customizer/api/v1_maintenance/__init__.py +1 -0
  25. pyxecm/customizer/api/v1_maintenance/functions.py +100 -0
  26. pyxecm/customizer/api/v1_maintenance/models.py +12 -0
  27. pyxecm/customizer/api/v1_maintenance/router.py +76 -0
  28. pyxecm/customizer/api/v1_otcs/__init__.py +1 -0
  29. pyxecm/customizer/api/v1_otcs/functions.py +61 -0
  30. pyxecm/customizer/api/v1_otcs/router.py +179 -0
  31. pyxecm/customizer/api/v1_payload/__init__.py +1 -0
  32. pyxecm/customizer/api/v1_payload/functions.py +179 -0
  33. pyxecm/customizer/api/v1_payload/models.py +51 -0
  34. pyxecm/customizer/api/v1_payload/router.py +499 -0
  35. pyxecm/customizer/browser_automation.py +721 -286
  36. pyxecm/customizer/customizer.py +1076 -1425
  37. pyxecm/customizer/exceptions.py +35 -0
  38. pyxecm/customizer/guidewire.py +1186 -0
  39. pyxecm/customizer/k8s.py +901 -379
  40. pyxecm/customizer/log.py +107 -0
  41. pyxecm/customizer/m365.py +2967 -920
  42. pyxecm/customizer/nhc.py +1169 -0
  43. pyxecm/customizer/openapi.py +258 -0
  44. pyxecm/customizer/payload.py +18228 -7820
  45. pyxecm/customizer/pht.py +717 -286
  46. pyxecm/customizer/salesforce.py +516 -342
  47. pyxecm/customizer/sap.py +58 -41
  48. pyxecm/customizer/servicenow.py +611 -372
  49. pyxecm/customizer/settings.py +445 -0
  50. pyxecm/customizer/successfactors.py +408 -346
  51. pyxecm/customizer/translate.py +83 -48
  52. pyxecm/helper/__init__.py +5 -2
  53. pyxecm/helper/assoc.py +83 -43
  54. pyxecm/helper/data.py +2406 -870
  55. pyxecm/helper/logadapter.py +27 -0
  56. pyxecm/helper/web.py +229 -101
  57. pyxecm/helper/xml.py +596 -171
  58. pyxecm/maintenance_page/__init__.py +5 -0
  59. pyxecm/maintenance_page/__main__.py +6 -0
  60. pyxecm/maintenance_page/app.py +51 -0
  61. pyxecm/maintenance_page/settings.py +28 -0
  62. pyxecm/maintenance_page/static/favicon.avif +0 -0
  63. pyxecm/maintenance_page/templates/maintenance.html +165 -0
  64. pyxecm/otac.py +235 -141
  65. pyxecm/otawp.py +2668 -1220
  66. pyxecm/otca.py +569 -0
  67. pyxecm/otcs.py +7956 -3237
  68. pyxecm/otds.py +2178 -925
  69. pyxecm/otiv.py +36 -21
  70. pyxecm/otmm.py +1272 -325
  71. pyxecm/otpd.py +231 -127
  72. pyxecm-2.0.1.dist-info/METADATA +122 -0
  73. pyxecm-2.0.1.dist-info/RECORD +76 -0
  74. {pyxecm-1.6.dist-info → pyxecm-2.0.1.dist-info}/WHEEL +1 -1
  75. pyxecm-1.6.dist-info/METADATA +0 -53
  76. pyxecm-1.6.dist-info/RECORD +0 -32
  77. {pyxecm-1.6.dist-info → pyxecm-2.0.1.dist-info/licenses}/LICENSE +0 -0
  78. {pyxecm-1.6.dist-info → pyxecm-2.0.1.dist-info}/top_level.txt +0 -0
pyxecm/otds.py CHANGED
@@ -1,130 +1,57 @@
1
- """
2
- OTDS Module to implement functions to read / write OTDS objects
3
- such as Ressources, Users, Groups, Licenses, Trusted Sites, OAuth Clients, ...
4
-
5
- Important: userIDs consists of login name + "@" + partition name
6
-
7
- Class: OTDS
8
- Methods:
9
-
10
- __init__ : class initializer
11
- config : returns config data set
12
- cookie : returns cookie information
13
- credentials: returns set of username and password
14
-
15
- base_url : returns OTDS base URL
16
- rest_url : returns OTDS REST base URL
17
- credential_url : returns the OTDS Credentials REST URL
18
- authHandler_url : returns the OTDS Authentication Handler REST URL
19
- partition_url : returns OTDS Partition REST URL
20
- access_role_url : returns OTDS Access Role REST URL
21
- oauth_client_url : returns OTDS OAuth Client REST URL
22
- resource_url : returns OTDS Resource REST URL
23
- license_url : returns OTDS License REST URL
24
- token_url : returns OTDS Token REST URL
25
- users_url : returns OTDS Users REST URL
26
- groups_url : returns OTDS Groups REST URL
27
- system_config_url : returns OTDS System Config REST URL
28
- consolidation_url: returns OTDS consolidation URL
29
-
30
- do_request: call an OTDS REST API in a safe way.
31
- parse_request_response: Converts the request response to a Python dict in a safe way
32
-
33
- authenticate : authenticates at OTDS server
34
-
35
- add_synchronized_partition: Add a Synchronized partition to OTDS
36
- add_partition : Add an OTDS partition
37
- get_partition : Get a partition with a specific name
38
-
39
- add_user : Add a user to a partion
40
- get_user : Get a user with a specific user ID (= login name @ partition)
41
- get_users: get all users (with option to filter)
42
- update_user : Update attributes of on OTDS user
43
- delete_user : Delete a user with a specific ID in a specific partition
44
- reset_user_password : Reset a password of a specific user ID
45
-
46
- add_group: Add an OTDS group
47
- get_group: Get a OTDS group by its name
48
- add_user_to_group : Add an OTDS user to a OTDS group
49
- add_group_to_parent_group : Add on OTDS group to a parent group
50
-
51
- add_resource : Add a new resource to OTDS
52
- get_resource : Get an OTDS resource with a specific name
53
- update_resource: Update an existing OTDS resource
54
- activate_resource : Activate an OTDS resource
55
-
56
- get_access_roles : Get all OTDS Access Roles
57
- get_access_role: Get an OTDS Access Role with a specific name
58
- add_partition_to_access_role : Add an OTDS Partition to to an OTDS Access Role
59
- add_user_to_access_role : Add an OTDS user to to an OTDS Access Role
60
- add_group_to_access_role : Add an OTDS group to to an OTDS Access Role
61
- update_access_role_attributes: Update attributes of an existing access role
62
-
63
- add_license_to_resource : Add (or update) a product license to OTDS
64
- get_license_for_resource : Get list of licenses for a resource
65
- delete_license_from_resource : Delete a license from a resource
66
- assign_user_to_license : Assign an OTDS user to a product license (feature) in OTDS.
67
- assign_partition_to_license: Assign an OTDS user partition to a license (feature) in OTDS.
68
- get_licensed_objects: Return the licensed objects (users, groups, partitions) an OTDS for a
69
- license + license feature associated with an OTDS resource (like "cs").
70
- is_user_licensed: Check if a user is licensed for a license and license feature associated
71
- with a particular OTDS resource.
72
- is_group_licensed: Check if a group is licensed for a license and license feature associated
73
- with a particular OTDS resource.
74
- is_partition_licensed: Check if a user partition is licensed for a license and license feature
75
- associated with a particular OTDS resource.
76
-
77
- add_system_attribute : Add an OTDS System Attribute
78
-
79
- get_trusted_sites : Get OTDS Trusted Sites
80
- add_trusted_site : Add a new trusted site to OTDS
81
-
82
- enable_audit: enable OTDS audit
83
-
84
- add_oauth_client : Add a new OAuth client to OTDS
85
- get_oauth_client : Get an OAuth client with a specific client ID
86
- update_oauth_client : Update an OAuth client
87
- add_oauth_clients_to_access_role : Add an OTDS OAuth Client to an OTDS Access Role
88
- get_access_token : Get an OTDS Access Token
89
-
90
- get_auth_handler: Gen an auth handler with a given name
91
- add_auth_handler_saml: Add an authentication handler for SAML (e.g. for SuccessFactors)
92
- add_auth_handler_sap: Add an authentication handler for SAP
93
- add_auth_handler_oauth: Add an authentication handler for OAuth (used for Salesforce)
94
-
95
- consolidate: Consolidate an OTDS resource
96
- impersonate_resource: Configure impersonation for an OTDS resource
97
- impersonate_oauth_client: Configure impersonation for an OTDS OAuth Client
98
-
99
- get_password_policy: get the global password policy
100
- update_password_policy: updates the global password policy
1
+ """OTDS Module to implement functions to read / write OTDS objects.
2
+
3
+ This includes Ressources, Users, Groups, Licenses, Trusted Sites, OAuth Clients, ...
4
+
5
+ The documentation for the used REST APIs can be found here:
6
+ - [https://developer.opentext.com](https://developer.opentext.com/ce/products/opentext-directory-services)
101
7
 
8
+
9
+ !!! tip
10
+ Important: userIDs consists of login name + "@" + partition name
102
11
  """
103
12
 
104
13
  __author__ = "Dr. Marc Diefenbruch"
105
- __copyright__ = "Copyright 2024, OpenText"
106
- __credits__ = ["Kai-Philip Gatzweiler", "Jim Bennett"]
14
+ __copyright__ = "Copyright (C) 2024-2025, OpenText"
15
+ __credits__ = ["Kai-Philip Gatzweiler"]
107
16
  __maintainer__ = "Dr. Marc Diefenbruch"
108
17
  __email__ = "mdiefenb@opentext.com"
109
18
 
110
- import os
111
- import logging
112
- import json
113
- import urllib.parse
114
19
  import base64
20
+ import json
21
+ import logging
22
+ import os
23
+ import platform
24
+ import sys
25
+ import tempfile
115
26
  import time
116
-
27
+ import urllib.parse
117
28
  from http import HTTPStatus
29
+ from importlib.metadata import version
30
+
118
31
  import requests
119
32
 
120
- logger = logging.getLogger("pyxecm.otds")
33
+ APP_NAME = "pyxecm"
34
+ APP_VERSION = version("pyxecm")
35
+ MODULE_NAME = APP_NAME + ".otds"
36
+
37
+ PYTHON_VERSION = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
38
+ OS_INFO = f"{platform.system()} {platform.release()}"
39
+ ARCH_INFO = platform.machine()
40
+ REQUESTS_VERSION = requests.__version__
41
+
42
+ USER_AGENT = (
43
+ f"{APP_NAME}/{APP_VERSION} ({MODULE_NAME}/{APP_VERSION}; "
44
+ f"Python/{PYTHON_VERSION}; {OS_INFO}; {ARCH_INFO}; Requests/{REQUESTS_VERSION})"
45
+ )
121
46
 
122
47
  REQUEST_HEADERS = {
48
+ "User-Agent": USER_AGENT,
123
49
  "accept": "application/json;charset=utf-8",
124
50
  "Content-Type": "application/json",
125
51
  }
126
52
 
127
53
  REQUEST_FORM_HEADERS = {
54
+ "User-Agent": USER_AGENT,
128
55
  "accept": "application/json;charset=utf-8",
129
56
  "Content-Type": "application/x-www-form-urlencoded",
130
57
  }
@@ -133,9 +60,13 @@ REQUEST_TIMEOUT = 60
133
60
  REQUEST_RETRY_DELAY = 20
134
61
  REQUEST_MAX_RETRIES = 2
135
62
 
63
+ default_logger = logging.getLogger(MODULE_NAME)
64
+
136
65
 
137
66
  class OTDS:
138
- """Used to automate stettings in OpenText Directory Services (OTDS)."""
67
+ """Class OTDS is used to automate stettings in OpenText Directory Services (OTDS)."""
68
+
69
+ logger: logging.Logger = default_logger
139
70
 
140
71
  _config = None
141
72
  _cookie = None
@@ -149,19 +80,38 @@ class OTDS:
149
80
  username: str | None = None,
150
81
  password: str | None = None,
151
82
  otds_ticket: str | None = None,
152
- bindPassword:str | None = None,
153
- ):
154
- """Initialize the OTDS object
83
+ bind_password: str | None = None,
84
+ admin_partition: str = "otds.admin",
85
+ logger: logging.Logger = default_logger,
86
+ ) -> None:
87
+ """Initialize the OTDS object.
155
88
 
156
89
  Args:
157
- protocol (str): either http or https
158
- hostname (str): hostname of otds
159
- port (int): port number - typically 80 or 443
160
- username (str, optional): otds user name. Optional if otds_ticket is provided.
161
- password (str, optional): otds password. Optional if otds_ticket is provided.
162
- otds_ticket (str, optional): Authentication ticket of OTDS
90
+ protocol (str):
91
+ This is either http or https.
92
+ hostname (str):
93
+ The hostname of OTDS.
94
+ port (int):
95
+ The port number - typically 80 or 443.
96
+ username (str, optional):
97
+ The OTDS user name. Optional if otds_ticket is provided.
98
+ password (str, optional):
99
+ The OTDS password. Optional if otds_ticket is provided.
100
+ otds_ticket (str | None, optional):
101
+ Authentication ticket of OTDS.
102
+ bind_password (str | None, optional): TODO
103
+ admin_partition (str, optional):
104
+ Name of the admin partition. Default is "otds.admin".
105
+ logger (logging.Logger, optional):
106
+ The logging object to use for all log messages. Defaults to default_logger.
107
+
163
108
  """
164
109
 
110
+ if logger != default_logger:
111
+ self.logger = logger.getChild("otds")
112
+ for logfilter in logger.filters:
113
+ self.logger.addFilter(logfilter)
114
+
165
115
  # Initialize otdsConfig as an empty dictionary
166
116
  otds_config = {}
167
117
 
@@ -189,67 +139,101 @@ class OTDS:
189
139
  otds_config["password"] = password
190
140
  else:
191
141
  otds_config["password"] = ""
192
-
193
- if bindPassword:
194
- otds_config["bindPassword"] = bindPassword
142
+
143
+ if bind_password:
144
+ otds_config["bindPassword"] = bind_password
195
145
  else:
196
146
  otds_config["bindPassword"] = ""
197
147
 
148
+ otds_config["adminPartition"] = admin_partition
149
+
198
150
  if otds_ticket:
199
151
  self._cookie = {"OTDSTicket": otds_ticket}
200
152
 
201
- otdsBaseUrl = protocol + "://" + otds_config["hostname"]
153
+ otds_base_url = protocol + "://" + otds_config["hostname"]
202
154
  if str(port) not in ["80", "443"]:
203
- otdsBaseUrl += ":{}".format(port)
204
- otdsBaseUrl += "/otdsws"
205
- otds_config["baseUrl"] = otdsBaseUrl
206
-
207
- otdsRestUrl = otdsBaseUrl + "/rest"
208
- otds_config["restUrl"] = otdsRestUrl
209
-
210
- otds_config["partitionUrl"] = otdsRestUrl + "/partitions"
211
- otds_config["identityproviderprofiles"] = otdsRestUrl + "/identityproviderprofiles"
212
- otds_config["accessRoleUrl"] = otdsRestUrl + "/accessroles"
213
- otds_config["credentialUrl"] = otdsRestUrl + "/authentication/credentials"
214
- otds_config["oauthClientUrl"] = otdsRestUrl + "/oauthclients"
215
- otds_config["tokenUrl"] = otdsBaseUrl + "/oauth2/token"
216
- otds_config["resourceUrl"] = otdsRestUrl + "/resources"
217
- otds_config["licenseUrl"] = otdsRestUrl + "/licensemanagement/licenses"
218
- otds_config["usersUrl"] = otdsRestUrl + "/users"
219
- otds_config["groupsUrl"] = otdsRestUrl + "/groups"
220
- otds_config["systemConfigUrl"] = otdsRestUrl + "/systemconfig"
221
- otds_config["authHandlerUrl"] = otdsRestUrl + "/authhandlers"
222
- otds_config["consolidationUrl"] = otdsRestUrl + "/consolidation"
155
+ otds_base_url += ":{}".format(port)
156
+ otds_base_url += "/otdsws"
157
+ otds_config["baseUrl"] = otds_base_url
158
+
159
+ otds_rest_url = otds_base_url + "/rest"
160
+ otds_config["restUrl"] = otds_rest_url
161
+
162
+ otds_config["partitionUrl"] = otds_rest_url + "/partitions"
163
+ otds_config["identityproviderprofiles"] = otds_rest_url + "/identityproviderprofiles"
164
+ otds_config["accessRoleUrl"] = otds_rest_url + "/accessroles"
165
+ otds_config["credentialUrl"] = otds_rest_url + "/authentication/credentials"
166
+ otds_config["ticketforuserUrl"] = otds_rest_url + "/authentication/ticketforuser"
167
+ otds_config["oauthClientUrl"] = otds_rest_url + "/oauthclients"
168
+ otds_config["tokenUrl"] = otds_base_url + "/oauth2/token"
169
+ otds_config["resourceUrl"] = otds_rest_url + "/resources"
170
+ otds_config["licenseUrl"] = otds_rest_url + "/licensemanagement/licenses"
171
+ otds_config["usersUrl"] = otds_rest_url + "/users"
172
+ otds_config["currentUserUrl"] = otds_rest_url + "/currentuser"
173
+ otds_config["groupsUrl"] = otds_rest_url + "/groups"
174
+ otds_config["systemConfigUrl"] = otds_rest_url + "/systemconfig"
175
+ otds_config["authHandlerUrl"] = otds_rest_url + "/authhandlers"
176
+ otds_config["consolidationUrl"] = otds_rest_url + "/consolidation"
177
+ otds_config["rolesUrl"] = otds_rest_url + "/roles"
223
178
 
224
179
  self._config = otds_config
225
180
 
226
181
  def config(self) -> dict:
227
- """Returns the configuration dictionary
182
+ """Return the configuration dictionary.
228
183
 
229
184
  Returns:
230
- dict: Configuration dictionary
185
+ dict:
186
+ The configuration dictionary.
187
+
231
188
  """
232
189
  return self._config
233
190
 
234
191
  # end method definition
235
192
 
236
193
  def cookie(self) -> dict:
237
- """Returns the login cookie of OTDS.
238
- This is set by the authenticate() method
194
+ """Return the login cookie of OTDS.
195
+
196
+ This is set by the authenticate() method
197
+
198
+ Returns:
199
+ dict:
200
+ The OTDS cookie.
201
+
202
+ """
203
+ return self._cookie
204
+
205
+ # end method definition
206
+
207
+ def set_cookie(self, ticket: str) -> dict:
208
+ """Return the login cookie of OTDS.
209
+
210
+ This is set by the authenticate() method
211
+
212
+ Args:
213
+ ticket (str):
214
+ The new ticket value for the cookie.
239
215
 
240
216
  Returns:
241
- dict: OTDS cookie
217
+ dict:
218
+ The updated OTDS cookie.
219
+
242
220
  """
221
+
222
+ self._cookie["OTDSTicket"] = ticket
223
+
243
224
  return self._cookie
244
225
 
245
226
  # end method definition
246
227
 
247
228
  def credentials(self) -> dict:
248
- """Returns the credentials (username + password)
229
+ """Return the credentials (username + password).
249
230
 
250
231
  Returns:
251
- dict: dictionary with username and password
232
+ dict:
233
+ The dictionary with username and password.
234
+
252
235
  """
236
+
253
237
  return {
254
238
  "userName": self.config()["username"],
255
239
  "password": self.config()["password"],
@@ -258,153 +242,225 @@ class OTDS:
258
242
  # end method definition
259
243
 
260
244
  def base_url(self) -> str:
261
- """Returns the base URL of OTDS
245
+ """Return the base URL of OTDS.
262
246
 
263
247
  Returns:
264
- str: base URL
248
+ str:
249
+ The base URL.
250
+
265
251
  """
252
+
266
253
  return self.config()["baseUrl"]
267
254
 
268
255
  # end method definition
269
256
 
270
257
  def rest_url(self) -> str:
271
- """Returns the REST URL of OTDS
258
+ """Return the REST URL of OTDS.
272
259
 
273
260
  Returns:
274
- str: REST URL
261
+ str:
262
+ The REST URL.
263
+
275
264
  """
265
+
276
266
  return self.config()["restUrl"]
277
267
 
278
268
  # end method definition
279
269
 
280
270
  def credential_url(self) -> str:
281
- """Returns the Credentials URL of OTDS
271
+ """Return the Credentials URL of OTDS.
282
272
 
283
273
  Returns:
284
- str: Credentials URL
274
+ str:
275
+ The credentials URL.
276
+
285
277
  """
278
+
286
279
  return self.config()["credentialUrl"]
287
280
 
288
281
  # end method definition
289
282
 
290
283
  def auth_handler_url(self) -> str:
291
- """Returns the Auth Handler URL of OTDS
284
+ """Return the Auth Handler URL of OTDS.
292
285
 
293
286
  Returns:
294
- str: Auth Handler URL
287
+ str:
288
+ The auth handler URL.
289
+
295
290
  """
291
+
296
292
  return self.config()["authHandlerUrl"]
297
293
 
298
294
  # end method definition
299
295
  def synchronized_partition_url(self) -> str:
300
- """Returns the Partition URL of OTDS
296
+ """Return the Partition URL of OTDS.
301
297
 
302
298
  Returns:
303
- str: synchronized partition url
299
+ str:
300
+ The synchronized partition URL.
301
+
304
302
  """
303
+
305
304
  return self.config()["identityproviderprofiles"]
306
- # end of method definition
305
+
306
+ # end of method definition
307
307
 
308
308
  def partition_url(self) -> str:
309
- """Returns the Partition URL of OTDS
309
+ """Return the partition URL of OTDS.
310
310
 
311
311
  Returns:
312
- str: Partition URL
312
+ str:
313
+ The partition URL.
314
+
313
315
  """
316
+
314
317
  return self.config()["partitionUrl"]
315
318
 
316
319
  # end method definition
317
320
 
318
321
  def access_role_url(self) -> str:
319
- """Returns the Access Role URL of OTDS
322
+ """Return the access role URL of OTDS.
320
323
 
321
324
  Returns:
322
- str: Access Role URL
325
+ str:
326
+ The access role URL.
327
+
323
328
  """
329
+
324
330
  return self.config()["accessRoleUrl"]
325
331
 
326
332
  # end method definition
327
333
 
328
334
  def oauth_client_url(self) -> str:
329
- """Returns the OAuth Client URL of OTDS
335
+ """Return the OAuth client URL of OTDS.
330
336
 
331
337
  Returns:
332
- str: OAuth Client URL
338
+ str:
339
+ The OAuth client URL.
340
+
333
341
  """
342
+
334
343
  return self.config()["oauthClientUrl"]
335
344
 
336
345
  # end method definition
337
346
 
338
347
  def resource_url(self) -> str:
339
- """Returns the Resource URL of OTDS
348
+ """Return the resource URL of OTDS.
340
349
 
341
350
  Returns:
342
- str: Resource URL
351
+ str:
352
+ The resource URL.
353
+
343
354
  """
355
+
344
356
  return self.config()["resourceUrl"]
345
357
 
346
358
  # end method definition
347
359
 
348
360
  def license_url(self) -> str:
349
- """Returns the License URL of OTDS
361
+ """Return the License URL of OTDS.
350
362
 
351
363
  Returns:
352
- str: License URL
364
+ str:
365
+ The license URL.
366
+
353
367
  """
368
+
354
369
  return self.config()["licenseUrl"]
355
370
 
356
371
  # end method definition
357
372
 
358
373
  def token_url(self) -> str:
359
- """Returns the Token URL of OTDS
374
+ """Return the token URL of OTDS.
360
375
 
361
376
  Returns:
362
- str: Token URL
377
+ str:
378
+ The token URL.
379
+
363
380
  """
381
+
364
382
  return self.config()["tokenUrl"]
365
383
 
366
384
  # end method definition
367
385
 
368
386
  def users_url(self) -> str:
369
- """Returns the Users URL of OTDS
387
+ """Return the users URL of OTDS.
370
388
 
371
389
  Returns:
372
- str: Users URL
390
+ str:
391
+ The users URL.
392
+
373
393
  """
394
+
374
395
  return self.config()["usersUrl"]
375
396
 
376
397
  # end method definition
377
398
 
399
+ def current_user_url(self) -> str:
400
+ """Return the current user URL of OTDS.
401
+
402
+ Returns:
403
+ str:
404
+ The current user URL.
405
+
406
+ """
407
+
408
+ return self.config()["currentUserUrl"]
409
+
410
+ # end method definition
411
+
378
412
  def groups_url(self) -> str:
379
- """Returns the Groups URL of OTDS
413
+ """Return the groups URL of OTDS.
380
414
 
381
415
  Returns:
382
- str: Groups URL
416
+ str:
417
+ The groups URL.
418
+
383
419
  """
420
+
384
421
  return self.config()["groupsUrl"]
385
422
 
386
423
  # end method definition
387
424
 
388
425
  def system_config_url(self) -> str:
389
- """Returns the System Config URL of OTDS
426
+ """Return the system config URL of OTDS.
390
427
 
391
428
  Returns:
392
- str: System Config URL
429
+ str:
430
+ The system config URL.
431
+
393
432
  """
433
+
394
434
  return self.config()["systemConfigUrl"]
395
435
 
396
436
  # end method definition
397
437
 
398
438
  def consolidation_url(self) -> str:
399
- """Returns the Consolidation URL of OTDS
439
+ """Return the consolidation URL of OTDS.
400
440
 
401
441
  Returns:
402
- str: Consolidation URL
442
+ str:
443
+ The consolidation URL.
444
+
403
445
  """
446
+
404
447
  return self.config()["consolidationUrl"]
405
448
 
406
449
  # end method definition
407
450
 
451
+ def admin_partition_name(self) -> str:
452
+ """Return OTDS admin partition name.
453
+
454
+ Returns:
455
+ str:
456
+ The OTDS admin partition name.
457
+
458
+ """
459
+
460
+ return self.config()["adminPartition"]
461
+
462
+ # end method definition
463
+
408
464
  def do_request(
409
465
  self,
410
466
  url: str,
@@ -423,27 +479,49 @@ class OTDS:
423
479
  retry_forever: bool = False,
424
480
  parse_request_response: bool = True,
425
481
  ) -> dict | None:
426
- """Call an OTDS REST API in a safe way
482
+ """Call an OTDS REST API in a safe way.
427
483
 
428
484
  Args:
429
- url (str): URL to send the request to.
430
- method (str, optional): HTTP method (GET, POST, etc.). Defaults to "GET".
431
- headers (dict | None, optional): Request Headers. Defaults to None.
432
- data (dict | None, optional): Request payload. Defaults to None
433
- files (dict | None, optional): Dictionary of {"name": file-tuple} for multipart encoding upload.
434
- file-tuple can be a 2-tuple ("filename", fileobj) or a 3-tuple ("filename", fileobj, "content_type")
435
- timeout (int | None, optional): Timeout for the request in seconds. Defaults to REQUEST_TIMEOUT.
436
- show_error (bool, optional): Whether or not an error should be logged in case of a failed REST call.
437
- If False, then only a warning is logged. Defaults to True.
438
- warning_message (str, optional): Specific warning message. Defaults to "". If not given the error_message will be used.
439
- failure_message (str, optional): Specific error message. Defaults to "".
440
- success_message (str, optional): Specific success message. Defaults to "".
441
- max_retries (int, optional): How many retries on Connection errors? Default is REQUEST_MAX_RETRIES.
442
- retry_forever (bool, optional): Eventually wait forever - without timeout. Defaults to False.
443
- parse_request_response (bool, optional): should the response.text be interpreted as json and loaded into a dictionary. True is the default.
485
+ url (str):
486
+ The URL to send the request to.
487
+ method (str, optional):
488
+ The HTTP method (GET, POST, etc.). Defaults to "GET".
489
+ headers (dict | None, optional):
490
+ The request headers. Defaults to None.
491
+ data (dict | None, optional):
492
+ Request payload. Defaults to None
493
+ json_data (dict | None, optional):
494
+ Request payload for the JSON parameter. Defaults to None.
495
+ files (dict | None, optional):
496
+ Dictionary of {"name": file-tuple} for multipart encoding upload.
497
+ File-tuple can be a 2-tuple ("filename", fileobj) or a 3-tuple ("filename", fileobj, "content_type")
498
+ timeout (int | None, optional):
499
+ The timeout for the request in seconds. Defaults to REQUEST_TIMEOUT.
500
+ show_error (bool, optional):
501
+ Whether or not an error should be logged in case of a failed REST call.
502
+ If False, then only a warning is logged. Defaults to True.
503
+ show_warning (bool, optional):
504
+ Whether or not an warning should be logged in case of a
505
+ failed REST call.
506
+ If False, then only a warning is logged. Defaults to True.
507
+ warning_message (str, optional):
508
+ Specific warning message. Defaults to "". If not given the error_message will be used.
509
+ failure_message (str, optional):
510
+ Specific error message. Defaults to "".
511
+ success_message (str, optional):
512
+ Specific success message. Defaults to "".
513
+ max_retries (int, optional):
514
+ How many retries on Connection errors? Default is REQUEST_MAX_RETRIES.
515
+ retry_forever (bool, optional):
516
+ Eventually wait forever - without timeout. Defaults to False.
517
+ parse_request_response (bool, optional):
518
+ Defines if the response.text should be interpreted as json and loaded into a dictionary.
519
+ True is the default.
444
520
 
445
521
  Returns:
446
- dict | None: Response of OTDS REST API or None in case of an error.
522
+ dict | None:
523
+ Response of OTDS REST API or None in case of an error.
524
+
447
525
  """
448
526
 
449
527
  if headers is None:
@@ -469,26 +547,33 @@ class OTDS:
469
547
 
470
548
  if response.ok:
471
549
  if success_message:
472
- logger.info(success_message)
550
+ self.logger.info(success_message)
473
551
  if parse_request_response:
474
552
  return self.parse_request_response(response)
475
553
  else:
476
554
  return response
477
555
  # Check if Session has expired - then re-authenticate and try once more
478
- elif response.status_code == 401 and retries == 0:
479
- logger.debug("Session has expired - try to re-authenticate...")
556
+ elif (
557
+ (response.status_code == 401 and retries == 0)
558
+ or (
559
+ response.status_code == 400
560
+ and retries == 0
561
+ and "Expired OTDS SSO ticket"
562
+ in response.text # OTDS seems to return 400 and not 401 for token expiry (in some cases like impersonation)
563
+ )
564
+ ):
565
+ self.logger.info("Session has expired - try to re-authenticate...")
480
566
  self.authenticate(revalidate=True)
481
567
  retries += 1
482
568
  else:
483
569
  # Handle plain HTML responses to not pollute the logs
484
570
  content_type = response.headers.get("content-type", None)
485
- if content_type == "text/html":
486
- response_text = "HTML content (only printed in debug log)"
487
- else:
488
- response_text = response.text
571
+ response_text = (
572
+ "HTML content (only printed in debug log)" if content_type == "text/html" else response.text
573
+ )
489
574
 
490
575
  if show_error:
491
- logger.error(
576
+ self.logger.error(
492
577
  "%s; status -> %s/%s; error -> %s",
493
578
  failure_message,
494
579
  response.status_code,
@@ -496,7 +581,7 @@ class OTDS:
496
581
  response_text,
497
582
  )
498
583
  elif show_warning:
499
- logger.warning(
584
+ self.logger.warning(
500
585
  "%s; status -> %s/%s; warning -> %s",
501
586
  warning_message if warning_message else failure_message,
502
587
  response.status_code,
@@ -504,7 +589,7 @@ class OTDS:
504
589
  response_text,
505
590
  )
506
591
  if content_type == "text/html":
507
- logger.debug(
592
+ self.logger.debug(
508
593
  "%s; status -> %s/%s; warning -> %s",
509
594
  failure_message,
510
595
  response.status_code,
@@ -514,45 +599,45 @@ class OTDS:
514
599
  return None
515
600
  except requests.exceptions.Timeout:
516
601
  if retries <= max_retries:
517
- logger.warning(
602
+ self.logger.warning(
518
603
  "Request timed out. Retrying in %s seconds...",
519
604
  str(REQUEST_RETRY_DELAY),
520
605
  )
521
606
  retries += 1
522
607
  time.sleep(REQUEST_RETRY_DELAY) # Add a delay before retrying
523
608
  else:
524
- logger.error(
525
- "%s; timeout error",
609
+ self.logger.error(
610
+ "%s; timeout error.",
526
611
  failure_message,
527
612
  )
528
613
  if retry_forever:
529
614
  # If it fails after REQUEST_MAX_RETRIES retries we let it wait forever
530
- logger.warning("Turn timeouts off and wait forever...")
615
+ self.logger.warning("Turn timeouts off and wait forever...")
531
616
  timeout = None
532
617
  else:
533
618
  return None
534
619
  except requests.exceptions.ConnectionError:
535
620
  if retries <= max_retries:
536
- logger.warning(
621
+ self.logger.warning(
537
622
  "Connection error. Retrying in %s seconds...",
538
623
  str(REQUEST_RETRY_DELAY),
539
624
  )
540
625
  retries += 1
541
626
  time.sleep(REQUEST_RETRY_DELAY) # Add a delay before retrying
542
627
  else:
543
- logger.error(
544
- "%s; connection error",
628
+ self.logger.error(
629
+ "%s; connection error.",
545
630
  failure_message,
546
631
  )
547
632
  if retry_forever:
548
633
  # If it fails after REQUEST_MAX_RETRIES retries we let it wait forever
549
- logger.warning("Turn timeouts off and wait forever...")
634
+ self.logger.warning("Turn timeouts off and wait forever...")
550
635
  timeout = None
551
636
  time.sleep(REQUEST_RETRY_DELAY) # Add a delay before retrying
552
637
  else:
553
638
  return None
554
639
  # end try
555
- logger.debug(
640
+ self.logger.info(
556
641
  "Retrying REST API %s call -> %s... (retry = %s, cookie -> %s)",
557
642
  method,
558
643
  url,
@@ -569,22 +654,27 @@ class OTDS:
569
654
  additional_error_message: str = "",
570
655
  show_error: bool = True,
571
656
  ) -> dict | None:
572
- """Converts the request response to a Python dict in a safe way
573
- that also handles exceptions.
657
+ """Convert the request response to a dict in a safe way that also handles exceptions.
574
658
 
575
659
  Args:
576
- response_object (object): this is reponse object delivered by the request call
577
- additional_error_message (str): print a custom error message
578
- show_error (bool): if True log an error, if False log a warning
660
+ response_object (object):
661
+ This is reponse object delivered by the request call.
662
+ additional_error_message (str, optional):
663
+ Print a custom error message.
664
+ show_error (bool, optional):
665
+ If True, log an error, if False log a warning.
666
+
579
667
  Returns:
580
- dict: response dictionary or None in case of an error
668
+ dict | None:
669
+ Response dictionary or None in case of an error.
670
+
581
671
  """
582
672
 
583
673
  if not response_object:
584
674
  return None
585
675
 
586
676
  if not response_object.text:
587
- logger.warning("Response text is empty. Cannot decode response.")
677
+ self.logger.warning("Response text is empty. Cannot decode response.")
588
678
  return None
589
679
 
590
680
  try:
@@ -592,14 +682,15 @@ class OTDS:
592
682
  except json.JSONDecodeError as e:
593
683
  if additional_error_message:
594
684
  message = "Cannot decode response as JSon. {}; error -> {}".format(
595
- additional_error_message, e
685
+ additional_error_message,
686
+ e,
596
687
  )
597
688
  else:
598
689
  message = "Cannot decode response as JSon; error -> {}".format(e)
599
690
  if show_error:
600
- logger.error(message)
691
+ self.logger.error(message)
601
692
  else:
602
- logger.warning(message)
693
+ self.logger.warning(message)
603
694
  return None
604
695
  else:
605
696
  return dict_object
@@ -607,18 +698,22 @@ class OTDS:
607
698
  # end method definition
608
699
 
609
700
  def authenticate(self, revalidate: bool = False) -> dict | None:
610
- """Authenticate at Directory Services and retrieve OTCS Ticket.
701
+ """Authenticate at Directory Services and retrieve OTDS ticket.
611
702
 
612
703
  Args:
613
- revalidate (bool, optional): determine if a re-athentication is enforced
614
- (e.g. if session has timed out with 401 error)
704
+ revalidate (bool, optional):
705
+ Determine if a re-athentication is enforced.
706
+ (e.g. if session has timed out with 401 error)
707
+
615
708
  Returns:
616
- dict: Cookie information. Also stores cookie information in self._cookie
709
+ dict | None:
710
+ Cookie information. Also stores cookie information in self._cookie
711
+
617
712
  """
618
713
 
619
714
  # Already authenticated and session still valid?
620
715
  if self._cookie and not revalidate:
621
- logger.debug(
716
+ self.logger.debug(
622
717
  "Session still valid - return existing cookie -> %s",
623
718
  str(self._cookie),
624
719
  )
@@ -626,7 +721,7 @@ class OTDS:
626
721
 
627
722
  otds_ticket = "NotSet"
628
723
 
629
- logger.debug("Requesting OTDS ticket from -> %s", self.credential_url())
724
+ self.logger.debug("Requesting OTDS ticket from -> %s", self.credential_url())
630
725
 
631
726
  response = None
632
727
  try:
@@ -634,15 +729,14 @@ class OTDS:
634
729
  url=self.credential_url(),
635
730
  json=self.credentials(),
636
731
  headers=REQUEST_HEADERS,
637
- timeout=None,
732
+ timeout=REQUEST_TIMEOUT,
638
733
  )
639
734
  except requests.exceptions.RequestException as exception:
640
- logger.warning(
641
- "Unable to connect to -> %s; error -> %s",
735
+ self.logger.warning(
736
+ "Unable to connect to OTDS authentication endpoint -> %s%s. OTDS service may not be ready yet.",
642
737
  self.credential_url(),
643
- exception.strerror,
738
+ "; error -> {}".format(str(exception)) if str(exception) else "",
644
739
  )
645
- logger.warning("OTDS service may not be ready yet.")
646
740
  return None
647
741
 
648
742
  if response.ok:
@@ -651,9 +745,12 @@ class OTDS:
651
745
  return None
652
746
  else:
653
747
  otds_ticket = authenticate_dict["ticket"]
654
- logger.debug("Ticket -> %s", otds_ticket)
748
+ self.logger.debug("Ticket -> %s", otds_ticket)
655
749
  else:
656
- logger.error("Failed to request an OTDS ticket; error -> %s", response.text)
750
+ self.logger.error(
751
+ "Failed to request an OTDS ticket; error -> %s",
752
+ response.text,
753
+ )
657
754
  return None
658
755
 
659
756
  # Store authentication ticket:
@@ -664,21 +761,349 @@ class OTDS:
664
761
 
665
762
  # end method definition
666
763
 
764
+ def impersonate_user(
765
+ self,
766
+ user_id: str,
767
+ partition: str = "Content Server Members",
768
+ ticket: str = "",
769
+ ) -> dict | None:
770
+ """Impersonate as a user.
771
+
772
+ Args:
773
+ partition (str):
774
+ The partition of the user.
775
+ user_id (str):
776
+ The ID (= login) of the user.
777
+ ticket (str, optional):
778
+ Optional, if the ticket to impersonate with is already known.
779
+ Defaults to "".
780
+
781
+ Returns:
782
+ dict | None:
783
+ Information about the impersonated user.
784
+
785
+ Example:
786
+ {
787
+ 'token': None,
788
+ 'userId': 'nwheeler@Content Server Members',
789
+ 'ticket': '*OTDSSSO*Adh...*',
790
+ 'resourceID': None,
791
+ 'failureReason': None,
792
+ 'passwordExpirationTime': 0,
793
+ 'continuation': False,
794
+ 'continuationContext': None,
795
+ 'continuationData': None
796
+ }
797
+
798
+ """
799
+
800
+ if not ticket:
801
+ ticket = self._otds_ticket
802
+
803
+ request_url = self.config()["ticketforuserUrl"]
804
+
805
+ impersonate_post_body = {
806
+ "userName": user_id + "@" + partition,
807
+ "ticket": ticket,
808
+ }
809
+
810
+ self.logger.debug(
811
+ "Impersonate user -> '%s' ; calling -> %s",
812
+ user_id,
813
+ request_url,
814
+ )
815
+
816
+ return self.do_request(
817
+ url=request_url,
818
+ method="POST",
819
+ json_data=impersonate_post_body,
820
+ timeout=None,
821
+ failure_message="Failed to impersonate as user -> '{}'".format(user_id),
822
+ )
823
+
824
+ # end method definition
825
+
826
+ def add_application_role(
827
+ self,
828
+ name: str,
829
+ partition_id: str = "OAuthClients",
830
+ description: str = "",
831
+ values: list | None = None,
832
+ custom_attributes: list | None = None,
833
+ ) -> dict | None:
834
+ """Add a new application role to partition.
835
+
836
+ Args:
837
+ name (str):
838
+ The name of the new partition.
839
+ partition_id (str, optional):
840
+ ID of the partition to add the role to, defaults to "OAuthClients".
841
+ description (str):
842
+ The description of the new partition.
843
+ values (list, optional):
844
+ List of optional values to pass with the create request.
845
+ custom_attributes (list, optional):
846
+ List of optional custom attributes to pass with the create request.
847
+
848
+ Returns:
849
+ dict | None:
850
+ Request response or None if the creation fails.
851
+
852
+ """
853
+
854
+ if values is None:
855
+ values = []
856
+ role_post_body_json = {
857
+ "name": name,
858
+ "description": description,
859
+ "userPartitionID": partition_id,
860
+ "values": values if values else [],
861
+ "customAttributes": custom_attributes if custom_attributes else [],
862
+ }
863
+
864
+ request_url = self.config()["rolesUrl"]
865
+
866
+ self.logger.debug(
867
+ "Adding application role -> '%s' (%s) to partition -> '%s' ; calling -> %s",
868
+ name,
869
+ description,
870
+ partition_id,
871
+ request_url,
872
+ )
873
+
874
+ return self.do_request(
875
+ url=request_url,
876
+ method="POST",
877
+ json_data=role_post_body_json,
878
+ timeout=None,
879
+ failure_message="Failed to add application role -> '{}'".format(name),
880
+ )
881
+
882
+ # end method definition
883
+
884
+ def get_application_role(self, name: str, partition: str = "OAuthClients", show_error: bool = True) -> dict | None:
885
+ """Get an existing application role from OTDS.
886
+
887
+ Args:
888
+ name (str):
889
+ The name of the application role to retrieve.
890
+ partition (str):
891
+ Partition of the application role.
892
+ show_error (bool, optional):
893
+ Defines whether or not we want to log an error
894
+ if the partition is not found.
895
+
896
+ Returns:
897
+ dict | None:
898
+ Request response or None if the REST call fails.
899
+
900
+ """
901
+
902
+ request_url = "{}?where_filter={}".format(self.config()["rolesUrl"], name)
903
+
904
+ self.logger.debug(
905
+ "Get application Roles -> '%s'; calling -> %s",
906
+ name,
907
+ request_url,
908
+ )
909
+
910
+ response = self.do_request(
911
+ url=request_url,
912
+ method="GET",
913
+ timeout=None,
914
+ failure_message="Failed to get user partition -> '{}'".format(name),
915
+ show_error=show_error,
916
+ )
917
+
918
+ role = next(
919
+ (role for role in response.get("roles") if role["name"] == name and role["userPartitionID"] == partition),
920
+ None,
921
+ )
922
+
923
+ return role
924
+
925
+ # end method definition
926
+
927
+ def assign_user_to_application_role(
928
+ self,
929
+ user_id: str,
930
+ user_partition: str,
931
+ role_name: str,
932
+ role_partition: str = "OAuthClients",
933
+ ) -> bool:
934
+ """Assign an OTDS user to an application role in OTDS.
935
+
936
+ Args:
937
+ user_id (str):
938
+ The ID of the user (= login name) to assign to the license.
939
+ user_partition (str):
940
+ The user partition in OTDS, e.g. "Content Server Members".
941
+ role_name (str):
942
+ Name of the application role to be assigned.
943
+ role_partition (str):
944
+ The name of the partition of the Role, defaults to "OAuthClients".
945
+
946
+ Returns:
947
+ bool:
948
+ True if successful or False if the REST call fails or the license is not found.
949
+
950
+ """
951
+
952
+ user = self.get_user(user_partition, user_id)
953
+ if user:
954
+ user_location = user["location"]
955
+ else:
956
+ self.logger.error("Cannot find location for user -> '%s'", user_id)
957
+ return False
958
+
959
+ role = self.get_application_role(role_name, role_partition)
960
+ if role:
961
+ rolelocation = role.get("location")
962
+ else:
963
+ self.logger.warning("Cannot find application role -> '%s' (%s)", role_name, role_partition)
964
+ return False
965
+
966
+ role_post_body_json = {
967
+ "stringList": [
968
+ rolelocation,
969
+ ],
970
+ }
971
+
972
+ request_url = self.users_url() + "/" + user_location + "/roles"
973
+
974
+ self.logger.debug(
975
+ "Assign application role -> '%s' (%s) to user -> '%s' (%s); calling -> %s",
976
+ role_name,
977
+ role_partition,
978
+ user_id,
979
+ user_partition,
980
+ request_url,
981
+ )
982
+
983
+ response = self.do_request(
984
+ url=request_url,
985
+ method="POST",
986
+ json_data=role_post_body_json,
987
+ timeout=None,
988
+ failure_message="Failed to assign application role -> '{}' to user -> '{}'".format(
989
+ role_name,
990
+ user_id,
991
+ ),
992
+ parse_request_response=False,
993
+ )
994
+
995
+ if response and response.ok:
996
+ self.logger.debug(
997
+ "Added application role -> '%s' to user -> '%s'",
998
+ role_name,
999
+ user_id,
1000
+ )
1001
+ return True
1002
+
1003
+ return False
1004
+
1005
+ # end method definition
1006
+
1007
+ def assign_group_to_application_role(
1008
+ self,
1009
+ group_id: str,
1010
+ group_partition: str,
1011
+ role_name: str,
1012
+ role_partition: str = "OAuthClients",
1013
+ ) -> bool:
1014
+ """Assign an OTDS group to an application role in OTDS.
1015
+
1016
+ Args:
1017
+ group_id (str):
1018
+ The ID of the group to assign to the application role.
1019
+ group_partition (str):
1020
+ The group partition in OTDS, e.g. "Content Server Members".
1021
+ role_name (str):
1022
+ Name of the application role to be assigned.
1023
+ role_partition (str):
1024
+ The name of the partition of the Role, defaults to "OAuthClients".
1025
+
1026
+ Returns:
1027
+ bool:
1028
+ True if successful or False if the REST call fails or the license is not found.
1029
+
1030
+ """
1031
+
1032
+ group = self.get_group(group_id)
1033
+ if group:
1034
+ group_location = group["location"]
1035
+ else:
1036
+ self.logger.error("Cannot find location for group -> '%s'", group_id)
1037
+ return False
1038
+
1039
+ role = self.get_application_role(role_name, role_partition)
1040
+ if role:
1041
+ rolelocation = role.get("location")
1042
+ else:
1043
+ self.logger.warning("Cannot find application role -> '%s' (%s)", role_name, role_partition)
1044
+ return False
1045
+
1046
+ role_post_body_json = {
1047
+ "stringList": [
1048
+ rolelocation,
1049
+ ],
1050
+ }
1051
+
1052
+ request_url = self.groups_url() + "/" + group_location + "/roles"
1053
+
1054
+ self.logger.debug(
1055
+ "Assign application role -> '%s' (%s) to group -> '%s' (%s); calling -> %s",
1056
+ role_name,
1057
+ role_partition,
1058
+ group_id,
1059
+ group_partition,
1060
+ request_url,
1061
+ )
1062
+
1063
+ response = self.do_request(
1064
+ url=request_url,
1065
+ method="POST",
1066
+ json_data=role_post_body_json,
1067
+ timeout=None,
1068
+ failure_message="Failed to assign application role -> '{}' to group -> '{}'".format(
1069
+ role_name,
1070
+ group_id,
1071
+ ),
1072
+ parse_request_response=False,
1073
+ )
1074
+
1075
+ if response and response.ok:
1076
+ self.logger.debug(
1077
+ "Added application role -> '%s' to group -> '%s'",
1078
+ role_name,
1079
+ group_id,
1080
+ )
1081
+ return True
1082
+
1083
+ return False
1084
+
1085
+ # end method definition
1086
+
667
1087
  def add_partition(self, name: str, description: str) -> dict | None:
668
- """Add a new user partition to OTDS
1088
+ """Add a new user partition to OTDS.
669
1089
 
670
1090
  Args:
671
- name (str): name of the new partition
672
- description (str): description of the new partition
1091
+ name (str):
1092
+ The name of the new partition.
1093
+ description (str):
1094
+ The description of the new partition.
1095
+
673
1096
  Returns:
674
- dict: Request response or None if the creation fails.
1097
+ dict | None:
1098
+ Request response or None if the creation fails.
1099
+
675
1100
  """
676
1101
 
677
1102
  partition_post_body_json = {"name": name, "description": description}
678
1103
 
679
1104
  request_url = self.partition_url()
680
1105
 
681
- logger.debug(
1106
+ self.logger.debug(
682
1107
  "Adding user partition -> '%s' (%s); calling -> %s",
683
1108
  name,
684
1109
  description,
@@ -696,19 +1121,28 @@ class OTDS:
696
1121
  # end method definition
697
1122
 
698
1123
  def get_partition(self, name: str, show_error: bool = True) -> dict | None:
699
- """Get an existing user partition from OTDS
1124
+ """Get an existing user partition from OTDS.
700
1125
 
701
1126
  Args:
702
- name (str): name of the partition to retrieve
703
- show_error (bool, optional): whether or not we want to log an error
704
- if partion is not found
1127
+ name (str):
1128
+ The name of the partition to retrieve.
1129
+ show_error (bool, optional):
1130
+ Defines whether or not we want to log an error
1131
+ if the partition is not found.
1132
+
705
1133
  Returns:
706
- dict: Request response or None if the REST call fails.
1134
+ dict | None:
1135
+ Request response or None if the REST call fails.
1136
+
707
1137
  """
708
1138
 
709
1139
  request_url = "{}/{}".format(self.config()["partitionUrl"], name)
710
1140
 
711
- logger.debug("Get user partition -> '%s'; calling -> %s", name, request_url)
1141
+ self.logger.debug(
1142
+ "Get user partition -> '%s'; calling -> %s",
1143
+ name,
1144
+ request_url,
1145
+ )
712
1146
 
713
1147
  return self.do_request(
714
1148
  url=request_url,
@@ -729,17 +1163,26 @@ class OTDS:
729
1163
  last_name: str = "",
730
1164
  email: str = "",
731
1165
  ) -> dict | None:
732
- """Add a new user to a user partition in OTDS
1166
+ """Add a new user to a user partition in OTDS.
733
1167
 
734
1168
  Args:
735
- partition (str): name of the OTDS user partition (needs to exist)
736
- name (str): login name of the new user
737
- description (str, optional): description of the new user
738
- first_name (str, optional): first name of the new user
739
- last_name (str, optional): last name of the new user
740
- email (str, optional): email address of the new user
1169
+ partition (str):
1170
+ The name of the OTDS user partition (needs to exist).
1171
+ name (str):
1172
+ The login name of the new user.
1173
+ description (str, optional):
1174
+ The description of the new user. Default is empty string.
1175
+ first_name (str, optional):
1176
+ The optional first name of the new user.
1177
+ last_name (str, optional):
1178
+ The optional last name of the new user.
1179
+ email (str, optional):
1180
+ The email address of the new user.
1181
+
741
1182
  Returns:
742
- dict: Request response or None if the creation fails.
1183
+ dict | None:
1184
+ Request response or None if the creation fails.
1185
+
743
1186
  """
744
1187
 
745
1188
  user_post_body_json = {
@@ -755,13 +1198,13 @@ class OTDS:
755
1198
 
756
1199
  request_url = self.users_url()
757
1200
 
758
- logger.debug(
1201
+ self.logger.debug(
759
1202
  "Adding user -> '%s' to partition -> '%s'; calling -> %s",
760
1203
  name,
761
1204
  partition,
762
1205
  request_url,
763
1206
  )
764
- logger.debug("User Attributes -> %s", str(user_post_body_json))
1207
+ self.logger.debug("User Attributes -> %s", str(user_post_body_json))
765
1208
 
766
1209
  return self.do_request(
767
1210
  url=request_url,
@@ -774,18 +1217,23 @@ class OTDS:
774
1217
  # end method definition
775
1218
 
776
1219
  def get_user(self, partition: str, user_id: str) -> dict | None:
777
- """Get a user by its partition and user ID
1220
+ """Get an existing user by its partition and user ID.
778
1221
 
779
1222
  Args:
780
- partition (str): name of the partition
781
- user_id (str): ID of the user (= login name)
1223
+ partition (str):
1224
+ The name of the partition the user is in.
1225
+ user_id (str):
1226
+ The ID of the user (= login name).
1227
+
782
1228
  Returns:
783
- dict: Request response or None if the user was not found.
1229
+ dict | None:
1230
+ Request response or None if the user was not found.
1231
+
784
1232
  """
785
1233
 
786
1234
  request_url = self.users_url() + "/" + user_id + "@" + partition
787
1235
 
788
- logger.debug(
1236
+ self.logger.debug(
789
1237
  "Get user -> '%s' in partition -> '%s'; calling -> %s",
790
1238
  user_id,
791
1239
  partition,
@@ -801,22 +1249,141 @@ class OTDS:
801
1249
 
802
1250
  # end method definition
803
1251
 
804
- def get_users(self, partition: str = "", limit: int | None = None) -> dict | None:
805
- """Get all users in a partition partition
1252
+ def get_users(
1253
+ self,
1254
+ partition: str = "",
1255
+ where_filter: str | None = None,
1256
+ where_location: str | None = None,
1257
+ where_state: str | None = None,
1258
+ limit: int | None = None,
1259
+ page_size: int | None = None,
1260
+ attributes_as_keys: bool = True,
1261
+ next_page_cookie: str | None = None,
1262
+ ) -> dict | None:
1263
+ """Get all users in a partition. Additional filters can be applied.
806
1264
 
807
1265
  Args:
808
- partition (str, optional): name of the partition
809
- limit (int): maximum number of users to return
1266
+ partition (str, optional):
1267
+ The name of the partition.
1268
+ where_filter (str | None, optional):
1269
+ Filter returned users. This is a string filter.
1270
+ If None, no filtering applies.
1271
+ where_location (str | None, optional):
1272
+ Filter based on the DN of the Organizational Unit.
1273
+ where_state (str | None, optional):
1274
+ Filter returned users by their state. Possible values are 'enabled' and 'disabled'.
1275
+ If None, no filtering based on state applies.
1276
+ limit (int, optional):
1277
+ The maximum number of users to return. None = unlimited.
1278
+ page_size (int, optional):
1279
+ The chunk size for the number of users returned by one
1280
+ REST API call. If None, then a default of 250 is used.
1281
+ attributes_as_keys (bool, optional):
1282
+ If True, it creates a much simpler to parse result structure
1283
+ per user that includes the user attributes in a "attributes"
1284
+ dictionary where the keys are the attribute names and the
1285
+ values the attribute values.
1286
+ 'attributes': {
1287
+ 'schemaType' = ['3']
1288
+ 'cn' = ['psopentext.com']
1289
+ ...
1290
+ }
1291
+ If False a "values" list with "name" and "values" elements is created:
1292
+ 'values': [
1293
+ {
1294
+ 'name': 'schemaType',
1295
+ 'values': ['3']
1296
+ },
1297
+ {
1298
+ 'name': 'cn',
1299
+ 'values': ['xyz@opentext.com']
1300
+ },
1301
+ ...
1302
+ ]
1303
+ Default is True (= attributes as keys).
1304
+ next_page_cookie (str, optional):
1305
+ A key returned by a former call to this method in with
1306
+ a return key 'nextPageCookie' (see example below). This
1307
+ can be used to get the next page of result items.
1308
+
810
1309
  Returns:
811
- dict: Request response or None if the user was not found.
1310
+ dict | None:
1311
+ Request response or None if the user was not found.
1312
+
1313
+ Example:
1314
+ {
1315
+ 'actualPageSize': 21,
1316
+ 'users': [
1317
+ {
1318
+ 'userPartitionID': 'Content Server Members',
1319
+ 'name': 'ps@opentext.com',
1320
+ 'location': 'oTPerson=04f2d12b-b7aa-4797-b4eb-1b6e6bd5ce2e,orgunit=users,partition=Content Server Members,dc=identity,dc=opentext,dc=net',
1321
+ 'id': 'ps@opentext.com',
1322
+ 'attributes': {
1323
+ 'oTExternalID3': ['ps@opentext.com'],
1324
+ 'entryUUID': ['04f2d12b-b7aa-4797-b4eb-1b6e6bd5ce2e'],
1325
+ 'oTExternalID4': ['Content Server Membersps@opentext.com'],
1326
+ 'mail': ['ps@opentext.com'],
1327
+ 'displayName': ['Paul Smith'],
1328
+ 'oTMemberOf': ['oTGroup=6381fcfe-7b30-4bbb-b849-2cbd8f3a0a48,dc=identity,dc=opentext,dc=net'],
1329
+ 'description': ['test description'],
1330
+ 'title': ['Lead Systems Analyst'],
1331
+ 'oTExternalID1': ['ps@opentext.com'],
1332
+ 'modifyTimestamp': ['2025-01-24T10:19:22Z'],
1333
+ 'oTExternalID2': ['ps@opentext.com@Content Server Members'],
1334
+ 'createTimestamp': ['2025-01-24T10:18:53Z'],
1335
+ 'passwordChangedTime': ['2025-01-24T10:18:53Z'],
1336
+ 'UserMustChangePasswordAtNextSignIn': ['false'],
1337
+ 'sn': ['Smith'],
1338
+ 'entryDN': ['oTPerson=04f2d12b-b7aa-4797-b4eb-1b6e6bd5ce2e,orgunit=users,partition=Content Server Members,dc=identity,dc=opentext,dc=net'],
1339
+ 'oTObjectGUID': ['BPLRK7eqR5e06xtua9XOLg=='],
1340
+ 'UserCannotChangePassword': ['true'],
1341
+ 'oTLastLoginTimestamp': ['2025-01-24T10:19:21Z'],
1342
+ 'oTSource': ['cs'],
1343
+ 'PasswordNeverExpires': ['true'],
1344
+ 'givenName': ['Paul'],
1345
+ 'cn': ['ps@opentext.com'],
1346
+ 'pwdReset': ['true'],
1347
+ 'oTObjectIDInResource': ['3b461d9f-ed1d-4be3-859a-316d8eb35aa5:6650'],
1348
+ 'accountLockedOut': ['false'],
1349
+ 'schemaType': ['3'],
1350
+ 'accountDisabled': ['false']
1351
+ }
1352
+ 'values': [], # empty because this example is with attrAsKeys = True
1353
+ 'customAttributes': None,
1354
+ 'objectClass': 'oTPerson',
1355
+ 'uuid': '04f2d12b-b7aa-4797-b4eb-1b6e6bd5ce2e',
1356
+ 'description': 'test description',
1357
+ 'originUUID': None,
1358
+ 'urlId': 'xyz@opentext.com',
1359
+ 'urlLocation': 'oTPerson=04f2d12b-b7aa-4797-b4eb-1b6e6bd5ce2e,orgunit=users,partition=Content Server Members,dc=identity,dc=opentext,dc=net'
1360
+ },
1361
+ ...
1362
+ ],
1363
+ 'nextPageCookie': 'JIHw2CLHSoeTOmo7Ng/bPw==',
1364
+ 'requestedPageSize': 250
1365
+ }
1366
+
812
1367
  """
813
1368
 
814
1369
  # Add query parameters (these are NOT passed via JSon body!)
815
1370
  query = {}
816
- if limit:
817
- query["limit"] = limit
818
1371
  if partition:
819
1372
  query["where_partition_name"] = partition
1373
+ if where_filter:
1374
+ query["where_filter"] = where_filter
1375
+ if where_location:
1376
+ query["where_location"] = where_location
1377
+ if where_state:
1378
+ query["where_state"] = where_state
1379
+ if limit:
1380
+ query["limit"] = limit
1381
+ if page_size:
1382
+ query["page_size"] = page_size
1383
+ if attributes_as_keys:
1384
+ query["attrsAsKeys"] = attributes_as_keys
1385
+ if next_page_cookie:
1386
+ query["next_page_cookie"] = next_page_cookie
820
1387
 
821
1388
  encoded_query = urllib.parse.urlencode(query, doseq=True)
822
1389
 
@@ -825,17 +1392,17 @@ class OTDS:
825
1392
  request_url += "?{}".format(encoded_query)
826
1393
 
827
1394
  if partition:
828
- logger.debug(
1395
+ self.logger.debug(
829
1396
  "Get all users in partition -> '%s' (limit -> %s); calling -> %s",
830
1397
  partition,
831
1398
  limit,
832
1399
  request_url,
833
1400
  )
834
1401
  failure_message = "Failed to get all users in partition -> '{}'".format(
835
- partition
1402
+ partition,
836
1403
  )
837
1404
  else:
838
- logger.debug(
1405
+ self.logger.debug(
839
1406
  "Get all users (limit -> %s); calling -> %s",
840
1407
  limit,
841
1408
  request_url,
@@ -843,23 +1410,140 @@ class OTDS:
843
1410
  failure_message = "Failed to get all users"
844
1411
 
845
1412
  return self.do_request(
846
- url=request_url, method="GET", timeout=None, failure_message=failure_message
1413
+ url=request_url,
1414
+ method="GET",
1415
+ timeout=None,
1416
+ failure_message=failure_message,
1417
+ )
1418
+
1419
+ # end method definition
1420
+
1421
+ def get_users_iterator(
1422
+ self,
1423
+ partition: str = "",
1424
+ where_state: str | None = None,
1425
+ where_filter: str | None = None,
1426
+ where_location: str | None = None,
1427
+ page_size: int | None = None,
1428
+ ) -> iter:
1429
+ """Get an iterator object that can be used to traverse all members for a given users partition.
1430
+
1431
+ Filters such as user state, location, etc. can be applied.
1432
+
1433
+ Returning a generator avoids loading a large number of nodes into memory at once. Instead you
1434
+ can iterate over the potential large list of related workspaces.
1435
+
1436
+ Example usage:
1437
+ users = otds_object.get_users_iterator(partition="Content Server Members", page_size=10)
1438
+ for user in users:
1439
+ logger.info("Traversing user -> %s", user["name"])
1440
+
1441
+ Args:
1442
+ partition (str, optional):
1443
+ The name of the partition.
1444
+ where_filter (str | None, optional):
1445
+ Filter returned users. This is a string filter.
1446
+ If None, no filtering applies.
1447
+ where_location (str | None, optional):
1448
+ Filter based on the DN of the Organizational Unit.
1449
+ where_state (str | None, optional):
1450
+ Filter returned users by their state. Possible values are 'enabled' and 'disabled'.
1451
+ If None, no filtering based on state applies.
1452
+ page_size (int, optional):
1453
+ The chunk size for the number of users returned by one
1454
+ REST API call. If None, then a default of 250 is used.
1455
+ next_page_cookie (str, optional):
1456
+ A key returned by a former call to this method in with
1457
+ a return key 'nextPageCookie' (see example below). This
1458
+ can be used to get the next page of result items.
1459
+
1460
+ Returns:
1461
+ iter:
1462
+ A generator yielding one OTDS user per iteration.
1463
+ If the REST API fails, returns no value.
1464
+
1465
+ """
1466
+
1467
+ next_page_cookie = None
1468
+
1469
+ while True:
1470
+ response = self.get_users(
1471
+ partition=partition,
1472
+ where_filter=where_filter,
1473
+ where_location=where_location,
1474
+ where_state=where_state,
1475
+ page_size=page_size,
1476
+ next_page_cookie=next_page_cookie,
1477
+ )
1478
+ if not response or "users" not in response:
1479
+ # Don't return None! Plain return is what we need for iterators.
1480
+ # Natural Termination: If the generator does not yield, it behaves
1481
+ # like an empty iterable when used in a loop or converted to a list:
1482
+ return
1483
+
1484
+ # Yield users one at a time:
1485
+ yield from response["users"]
1486
+
1487
+ # See if we have an additional result page.
1488
+ # If not terminate the iterator and return
1489
+ # no value.
1490
+ next_page_cookie = response["nextPageCookie"]
1491
+ if not next_page_cookie:
1492
+ # Don't return None! Plain return is what we need for iterators.
1493
+ # Natural Termination: If the generator does not yield, it behaves
1494
+ # like an empty iterable when used in a loop or converted to a list:
1495
+ return
1496
+
1497
+ # end method definition
1498
+
1499
+ def get_current_user(self) -> dict | None:
1500
+ """Get the currently logged in user.
1501
+
1502
+ Returns:
1503
+ dict | None:
1504
+ Request response or None if the user was not found.
1505
+
1506
+ """
1507
+
1508
+ request_url = self.current_user_url()
1509
+
1510
+ self.logger.debug(
1511
+ "Get current user; calling -> %s",
1512
+ request_url,
1513
+ )
1514
+
1515
+ return self.do_request(
1516
+ url=request_url,
1517
+ method="GET",
1518
+ timeout=None,
1519
+ failure_message="Failed to get current user",
847
1520
  )
848
1521
 
849
1522
  # end method definition
850
1523
 
851
1524
  def update_user(
852
- self, partition: str, user_id: str, attribute_name: str, attribute_value: str
1525
+ self,
1526
+ partition: str,
1527
+ user_id: str,
1528
+ attribute_name: str,
1529
+ attribute_value: str,
853
1530
  ) -> dict | None:
854
- """Update a user attribute with a new value
1531
+ """Update a user attribute with a new value.
855
1532
 
856
1533
  Args:
857
- partition (str): name of the partition
858
- user_id (str): ID of the user (= login name)
859
- attribute_name (str): name of the attribute
860
- attribute_value (str): new value of the attribute
861
- Return:
862
- dict: Request response or None if the update fails.
1534
+ partition (str):
1535
+ The name of the partition the user is in.
1536
+ user_id (str):
1537
+ The ID of the user (= login name).
1538
+ attribute_name (str):
1539
+ The name of the attribute.
1540
+ attribute_value (str):
1541
+ The new (updated) value of the attribute.
1542
+
1543
+ Returns:
1544
+ dict | None:
1545
+ Request response or None if the update fails.
1546
+
863
1547
  """
864
1548
 
865
1549
  if attribute_name in ["description"]:
@@ -875,7 +1559,7 @@ class OTDS:
875
1559
 
876
1560
  request_url = self.users_url() + "/" + user_id
877
1561
 
878
- logger.debug(
1562
+ self.logger.debug(
879
1563
  "Update user -> '%s' attribute -> '%s' to value -> '%s'; calling -> %s",
880
1564
  user_id,
881
1565
  attribute_name,
@@ -894,18 +1578,23 @@ class OTDS:
894
1578
  # end method definition
895
1579
 
896
1580
  def delete_user(self, partition: str, user_id: str) -> bool:
897
- """Delete an existing user
1581
+ """Delete an existing user.
898
1582
 
899
1583
  Args:
900
- partition (str): name of the partition
901
- user_id (str): Id (= login name) of the user
1584
+ partition (str):
1585
+ The name of the partition the user is in.
1586
+ user_id (str):
1587
+ The ID (= login name) of the user to delete.
1588
+
902
1589
  Returns:
903
- bool: True = success, False = error
1590
+ bool:
1591
+ True = success, False = error
1592
+
904
1593
  """
905
1594
 
906
1595
  request_url = self.users_url() + "/" + user_id + "@" + partition
907
1596
 
908
- logger.debug(
1597
+ self.logger.debug(
909
1598
  "Delete user -> '%s' in partition -> '%s'; calling -> %s",
910
1599
  user_id,
911
1600
  partition,
@@ -920,29 +1609,33 @@ class OTDS:
920
1609
  parse_request_response=False,
921
1610
  )
922
1611
 
923
- if response and response.ok:
924
- return True
925
-
926
- return False
1612
+ bool(response and response.ok)
927
1613
 
928
1614
  # end method definition
929
1615
 
930
1616
  def reset_user_password(self, user_id: str, password: str) -> bool:
931
- """Reset a password of an existing user
1617
+ """Reset a password of an existing user.
932
1618
 
933
1619
  Args:
934
- user_id (str): Id (= login name) of the user
935
- password (str): new password of the user
1620
+ user_id (str):
1621
+ The Id (= login name) of the user.
1622
+ password (str):
1623
+ The new password of the user.
1624
+
936
1625
  Returns:
937
- bool: True = success, False = error.
1626
+ bool:
1627
+ True = success, False = error.
1628
+
938
1629
  """
939
1630
 
940
1631
  user_post_body_json = {"newPassword": password}
941
1632
 
942
1633
  request_url = "{}/{}/password".format(self.users_url(), user_id)
943
1634
 
944
- logger.debug(
945
- "Resetting password for user -> '%s'; calling -> %s", user_id, request_url
1635
+ self.logger.debug(
1636
+ "Resetting password for user -> '%s'; calling -> %s",
1637
+ user_id,
1638
+ request_url,
946
1639
  )
947
1640
 
948
1641
  response = self.do_request(
@@ -954,22 +1647,25 @@ class OTDS:
954
1647
  parse_request_response=False,
955
1648
  )
956
1649
 
957
- if response and response.ok:
958
- return True
959
-
960
- return False
1650
+ bool(response and response.ok)
961
1651
 
962
1652
  # end method definition
963
1653
 
964
1654
  def add_group(self, partition: str, name: str, description: str) -> dict | None:
965
- """Add a new user group to a user partition in OTDS
1655
+ """Add a new user group to a user partition in OTDS.
966
1656
 
967
1657
  Args:
968
- partition (str): name of the OTDS user partition (needs to exist)
969
- name (str): name of the new group
970
- description (str): description of the new group
1658
+ partition (str):
1659
+ The name of the OTDS user partition (needs to exist).
1660
+ name (str):
1661
+ The name of the new group.
1662
+ description (str):
1663
+ The description of the new group.
1664
+
971
1665
  Returns:
972
- dict: Request response (json) or None if the creation fails.
1666
+ dict | None:
1667
+ Request response (json) or None if the creation fails.
1668
+
973
1669
  """
974
1670
 
975
1671
  group_post_body_json = {
@@ -980,13 +1676,13 @@ class OTDS:
980
1676
 
981
1677
  request_url = self.groups_url()
982
1678
 
983
- logger.debug(
1679
+ self.logger.debug(
984
1680
  "Adding group -> '%s' to partition -> '%s'; calling -> %s",
985
1681
  name,
986
1682
  partition,
987
1683
  request_url,
988
1684
  )
989
- logger.debug("Group Attributes -> %s", str(group_post_body_json))
1685
+ self.logger.debug("Group Attributes -> %s", str(group_post_body_json))
990
1686
 
991
1687
  return self.do_request(
992
1688
  url=request_url,
@@ -999,14 +1695,19 @@ class OTDS:
999
1695
  # end method definition
1000
1696
 
1001
1697
  def get_group(self, group: str, show_error: bool = True) -> dict | None:
1002
- """Get a OTDS group by its group name
1698
+ """Get a OTDS group by its group name.
1003
1699
 
1004
1700
  Args:
1005
- group (str): ID of the group (= group name)
1006
- show_error (bool, optional): treat as error if resource is not found
1007
- Return:
1008
- dict: Request response or None if the group was not found.
1009
- Example values:
1701
+ group (str):
1702
+ The ID of the group (= group name).
1703
+ show_error (bool, optional):
1704
+ If True, log an error if resource is not found. Otherwise log a warning.
1705
+
1706
+ Returns:
1707
+ dict | None:
1708
+ Request response or None if the group was not found.
1709
+
1710
+ Example:
1010
1711
  {
1011
1712
  'numMembers': 7,
1012
1713
  'userPartitionID': 'Content Server Members',
@@ -1022,11 +1723,12 @@ class OTDS:
1022
1723
  'urlId': 'Sales@Content Server Members',
1023
1724
  'urlLocation': 'oTGroup=3f921294-b92a-4c9e-bf7c-b50df16bb937,orgunit=groups,partition=Content Server Members,dc=identity,dc=opentext,dc=net'
1024
1725
  }
1726
+
1025
1727
  """
1026
1728
 
1027
1729
  request_url = self.groups_url() + "/" + group
1028
1730
 
1029
- logger.debug("Get group -> '%s'; calling -> %s", group, request_url)
1731
+ self.logger.debug("Get group -> '%s'; calling -> %s", group, request_url)
1030
1732
 
1031
1733
  return self.do_request(
1032
1734
  url=request_url,
@@ -1038,21 +1740,275 @@ class OTDS:
1038
1740
 
1039
1741
  # end method definition
1040
1742
 
1743
+ def get_groups(
1744
+ self,
1745
+ partition: str = "",
1746
+ where_filter: str | None = None,
1747
+ where_location: str | None = None,
1748
+ limit: int | None = None,
1749
+ page_size: int | None = None,
1750
+ attributes_as_keys: bool = True,
1751
+ next_page_cookie: str | None = None,
1752
+ ) -> dict | None:
1753
+ """Get all groups in a partition. Additional filters can be applied.
1754
+
1755
+ Args:
1756
+ partition (str, optional):
1757
+ The name of the partition.
1758
+ where_filter (str | None, optional):
1759
+ Filter returned groups. This is a string filter.
1760
+ If None, no filtering applies.
1761
+ where_location (str | None, optional):
1762
+ Filter based on the DN of the Organizational Unit.
1763
+ limit (int, optional):
1764
+ The maximum number of groups to return. None = unlimited.
1765
+ page_size (int, optional):
1766
+ The chunk size for the number of groups returned by one
1767
+ REST API call. If None, then a default of 250 is used.
1768
+ attributes_as_keys (bool, optional):
1769
+ If True, it creates a much simpler to parse result structure
1770
+ per group that includes the group attributes in a "attributes"
1771
+ dictionary where the keys are the attribute names and the
1772
+ values the attribute values.
1773
+ 'attributes': {
1774
+ 'schemaType' = ['3']
1775
+ 'cn' = [...]
1776
+ ...
1777
+ }
1778
+ If False a "values" list with "name" and "values" elements is created:
1779
+ 'values': [
1780
+ {
1781
+ 'name': 'schemaType',
1782
+ 'values': ['3']
1783
+ },
1784
+ {
1785
+ 'name': 'cn',
1786
+ 'values': ['...']
1787
+ },
1788
+ ...
1789
+ ]
1790
+ Default is True (= attributes as keys).
1791
+ next_page_cookie (str, optional):
1792
+ A key returned by a former call to this method in with
1793
+ a return key 'nextPageCookie' (see example below). This
1794
+ can be used to get the next page of result items.
1795
+
1796
+ Returns:
1797
+ dict | None:
1798
+ Request response or None if the user was not found.
1799
+
1800
+ Example:
1801
+ {
1802
+ 'groups': [
1803
+ {
1804
+ 'numMembers': 0,
1805
+ 'userPartitionID': 'Content Server Members',
1806
+ 'name': 'Unified_ArchiveLink',
1807
+ 'location': 'oTGroup=050a3c27-7636-4406-a94e-dcc4947fa21f,orgunit=groups,partition=Content Server Members,dc=identity,dc=opentext,dc=net',
1808
+ 'id': 'Unified_ArchiveLink@Content Server Members',
1809
+ 'attributes': {
1810
+ 'oTExternalID3': [...],
1811
+ 'entryUUID': [...],
1812
+ 'oTExternalID4': [...],
1813
+ 'oTObjectIDInResource': [...],
1814
+ 'oTSource': [...],
1815
+ 'schemaType': [...],
1816
+ 'cn': [...],
1817
+ 'oTObjectGUID': [...],
1818
+ 'oTExternalID1': [...],
1819
+ 'entryDN': [...],
1820
+ 'oTExternalID2': [...],
1821
+ 'createTimestamp': [...]
1822
+ },
1823
+ 'values': None,
1824
+ 'customAttributes': None,
1825
+ 'objectClass': 'oTGroup',
1826
+ 'uuid': '050a3c27-7636-4406-a94e-dcc4947fa21f',
1827
+ 'description': None,
1828
+ 'originUUID': None,
1829
+ 'urlId': 'Unified_ArchiveLink@Content Server Members',
1830
+ 'urlLocation': 'oTGroup=050a3c27-7636-4406-a94e-dcc4947fa21f,orgunit=groups,partition=Content Server Members,dc=identity,dc=opentext,dc=net'
1831
+ },
1832
+ {
1833
+ 'numMembers': 0,
1834
+ 'userPartitionID': 'Content Server Members',
1835
+ 'name': 'R&D',
1836
+ 'location': 'oTGroup=24356f83-5636-47f0-9ac3-9646d3b34804,orgunit=groups,partition=Content Server Members,dc=identity,dc=opentext,dc=net',
1837
+ 'id': 'R&D@Content Server Members',
1838
+ 'attributes': {
1839
+ 'oTExternalID3': [...],
1840
+ 'entryUUID': [...],
1841
+ 'oTExternalID4': [...],
1842
+ 'oTObjectIDInResource': [...],
1843
+ 'oTSource': [...],
1844
+ 'schemaType': [...],
1845
+ 'cn': [...],
1846
+ 'oTObjectGUID': [...],
1847
+ 'oTExternalID1': [...],
1848
+ 'entryDN': [...],
1849
+ 'oTExternalID2': [...],
1850
+ 'createTimestamp': [...]
1851
+ },
1852
+ 'values': None,
1853
+ 'customAttributes': None,
1854
+ 'objectClass': 'oTGroup',
1855
+ 'uuid': '24356f83-5636-47f0-9ac3-9646d3b34804',
1856
+ 'description': None,
1857
+ 'originUUID': None,
1858
+ 'urlId': 'R&D@Content Server Members',
1859
+ 'urlLocation': 'oTGroup=24356f83-5636-47f0-9ac3-9646d3b34804,orgunit=groups,partition=Content Server Members,dc=identity,dc=opentext,dc=net'
1860
+ },
1861
+ ...
1862
+ ],
1863
+ 'actualPageSize': 5,
1864
+ 'nextPageCookie': 'JIHw2CLHSoeTOmo7Ng/bPw==',
1865
+ 'requestedPageSize': 5
1866
+ }
1867
+
1868
+ """
1869
+
1870
+ # Add query parameters (these are NOT passed via request body!)
1871
+ query = {}
1872
+ if partition:
1873
+ query["where_partition_name"] = partition
1874
+ if where_filter:
1875
+ query["where_filter"] = where_filter
1876
+ if where_location:
1877
+ query["where_location"] = where_location
1878
+ if limit:
1879
+ query["limit"] = limit
1880
+ if page_size:
1881
+ query["page_size"] = page_size
1882
+ if attributes_as_keys:
1883
+ query["attrsAsKeys"] = attributes_as_keys
1884
+ if next_page_cookie:
1885
+ query["next_page_cookie"] = next_page_cookie
1886
+
1887
+ encoded_query = urllib.parse.urlencode(query, doseq=True)
1888
+
1889
+ request_url = self.groups_url()
1890
+ if query:
1891
+ request_url += "?{}".format(encoded_query)
1892
+
1893
+ if partition:
1894
+ self.logger.debug(
1895
+ "Get all groups in partition -> '%s' (limit -> %s, page size -> %s); calling -> %s",
1896
+ partition,
1897
+ str(limit),
1898
+ str(page_size),
1899
+ request_url,
1900
+ )
1901
+ failure_message = "Failed to get all groups in partition -> '{}'".format(
1902
+ partition,
1903
+ )
1904
+ else:
1905
+ self.logger.debug(
1906
+ "Get all groups (limit -> %s); calling -> %s",
1907
+ limit,
1908
+ request_url,
1909
+ )
1910
+ failure_message = "Failed to get all groups"
1911
+
1912
+ return self.do_request(
1913
+ url=request_url,
1914
+ method="GET",
1915
+ timeout=None,
1916
+ failure_message=failure_message,
1917
+ )
1918
+
1919
+ # end method definition
1920
+
1921
+ def get_groups_iterator(
1922
+ self,
1923
+ partition: str = "",
1924
+ where_filter: str | None = None,
1925
+ where_location: str | None = None,
1926
+ page_size: int | None = None,
1927
+ ) -> iter:
1928
+ """Get an iterator object that can be used to traverse all groups for a given users partition.
1929
+
1930
+ Returning a generator avoids loading a large number of nodes into memory at once. Instead you
1931
+ can iterate over the potential large list of related workspaces.
1932
+
1933
+ Example usage:
1934
+ groups = otds_object.get_groups_iterator(partition="Content Server Members", page_size=10)
1935
+ for group in groups:
1936
+ logger.info("Traversing group -> %s", group["name"])
1937
+
1938
+ Args:
1939
+ partition (str, optional):
1940
+ The name of the partition.
1941
+ where_filter (str | None, optional):
1942
+ Filter returned groups. This is a string filter.
1943
+ If None, no filtering applies.
1944
+ where_location (str | None, optional):
1945
+ Filter based on the DN of the Organizational Unit.
1946
+ page_size (int, optional):
1947
+ The chunk size for the number of groups returned by one
1948
+ REST API call. If None, then a default of 250 is used.
1949
+ next_page_cookie (str, optional):
1950
+ A key returned by a former call to this method in with
1951
+ a return key 'nextPageCookie' (see example below). This
1952
+ can be used to get the next page of result items.
1953
+
1954
+ Returns:
1955
+ iter:
1956
+ A generator yielding one OTDS group per iteration.
1957
+ If the REST API fails, returns no value.
1958
+
1959
+ """
1960
+
1961
+ next_page_cookie = None
1962
+
1963
+ while True:
1964
+ response = self.get_groups(
1965
+ partition=partition,
1966
+ where_filter=where_filter,
1967
+ where_location=where_location,
1968
+ page_size=page_size,
1969
+ next_page_cookie=next_page_cookie,
1970
+ )
1971
+ if not response or "groups" not in response:
1972
+ # Don't return None! Plain return is what we need for iterators.
1973
+ # Natural Termination: If the generator does not yield, it behaves
1974
+ # like an empty iterable when used in a loop or converted to a list:
1975
+ return
1976
+
1977
+ # Yield users one at a time:
1978
+ yield from response["groups"]
1979
+
1980
+ # See if we have an additional result page.
1981
+ # If not terminate the iterator and return
1982
+ # no value.
1983
+ next_page_cookie = response["nextPageCookie"]
1984
+ if not next_page_cookie:
1985
+ # Don't return None! Plain return is what we need for iterators.
1986
+ # Natural Termination: If the generator does not yield, it behaves
1987
+ # like an empty iterable when used in a loop or converted to a list:
1988
+ return
1989
+
1990
+ # end method definition
1991
+
1041
1992
  def add_user_to_group(self, user: str, group: str) -> bool:
1042
- """Add an existing user to an existing group in OTDS
1993
+ """Add an existing user to an existing group in OTDS.
1043
1994
 
1044
1995
  Args:
1045
- user (str): name of the OTDS user (needs to exist)
1046
- group (str): name of the OTDS group (needs to exist)
1996
+ user (str):
1997
+ The name of the OTDS user (needs to exist).
1998
+ group (str):
1999
+ The name of the OTDS group (needs to exist).
2000
+
1047
2001
  Returns:
1048
- bool: True, if request is successful, False otherwise.
2002
+ bool:
2003
+ True, if the request is successful, False otherwise.
2004
+
1049
2005
  """
1050
2006
 
1051
2007
  user_to_group_post_body_json = {"stringList": [group]}
1052
2008
 
1053
2009
  request_url = self.users_url() + "/" + user + "/memberof"
1054
2010
 
1055
- logger.debug(
2011
+ self.logger.debug(
1056
2012
  "Adding user -> '%s' to group -> '%s'; calling -> %s",
1057
2013
  user,
1058
2014
  group,
@@ -1066,33 +2022,36 @@ class OTDS:
1066
2022
  json_data=user_to_group_post_body_json,
1067
2023
  timeout=None,
1068
2024
  failure_message="Failed to add user -> '{}' to group -> '{}'".format(
1069
- user, group
2025
+ user,
2026
+ group,
1070
2027
  ),
1071
2028
  parse_request_response=False,
1072
2029
  )
1073
2030
 
1074
- if response and response.ok:
1075
- return True
1076
-
1077
- return False
2031
+ return bool(response and response.ok)
1078
2032
 
1079
2033
  # end method definition
1080
2034
 
1081
2035
  def add_group_to_parent_group(self, group: str, parent_group: str) -> bool:
1082
- """Add an existing group to an existing parent group in OTDS
2036
+ """Add an existing group to an existing parent group in OTDS.
1083
2037
 
1084
2038
  Args:
1085
- group (str): name of the OTDS group (needs to exist)
1086
- parent_group (str): name of the OTDS parent group (needs to exist)
2039
+ group (str):
2040
+ The name of the OTDS group (needs to exist).
2041
+ parent_group (str):
2042
+ The name of the OTDS parent group (needs to exist).
2043
+
1087
2044
  Returns:
1088
- bool: True, if request is successful, False otherwise.
2045
+ bool:
2046
+ True, if the request is successful, False otherwise.
2047
+
1089
2048
  """
1090
2049
 
1091
2050
  group_to_parent_group_post_body_json = {"stringList": [parent_group]}
1092
2051
 
1093
2052
  request_url = self.groups_url() + "/" + group + "/memberof"
1094
2053
 
1095
- logger.debug(
2054
+ self.logger.debug(
1096
2055
  "Adding group -> '%s' to parent group -> '%s'; calling -> %s",
1097
2056
  group,
1098
2057
  parent_group,
@@ -1106,15 +2065,13 @@ class OTDS:
1106
2065
  json_data=group_to_parent_group_post_body_json,
1107
2066
  timeout=None,
1108
2067
  failure_message="Failed to add group -> '{}' to parent group -> '{}'".format(
1109
- group, parent_group
2068
+ group,
2069
+ parent_group,
1110
2070
  ),
1111
2071
  parse_request_response=False,
1112
2072
  )
1113
2073
 
1114
- if response and response.ok:
1115
- return True
1116
-
1117
- return False
2074
+ return bool(response and response.ok)
1118
2075
 
1119
2076
  # end method definition
1120
2077
 
@@ -1128,15 +2085,29 @@ class OTDS:
1128
2085
  secret: str | None = None, # needs to be 16 bytes!
1129
2086
  additional_payload: dict | None = None,
1130
2087
  ) -> dict | None:
1131
- """Add an OTDS resource
2088
+ """Add an OTDS resource.
1132
2089
 
1133
2090
  Args:
1134
- name (str): name of the new OTDS resource
1135
- description (str): description of the new OTDS resource
1136
- display_name (str): display name of the OTDS resource
1137
- additional_payload (dict, optional): additional values for the json payload
2091
+ name (str):
2092
+ The name of the new OTDS resource.
2093
+ description (str):
2094
+ The optional description of the new OTDS resource.
2095
+ display_name (str, optional):
2096
+ The optional display name of the OTDS resource.
2097
+ allow_impersonation (bool):
2098
+ Defines whether or not the resource allows impersonation.
2099
+ resource_id (str | None, optional):
2100
+ Allows to set a predefined resource ID. This requires the
2101
+ secret parameter in additon.
2102
+ secret (str):
2103
+ A 24 charcters secret key. Required to set a predefined resource ID.
2104
+ additional_payload (dict, optional):
2105
+ Additional values for the JSON payload.
2106
+
1138
2107
  Returns:
1139
- dict: Request response (dictionary) or None if the REST call fails.
2108
+ dict | None:
2109
+ Request response (dictionary) or None if the REST call fails.
2110
+
1140
2111
  """
1141
2112
 
1142
2113
  resource_post_body = {
@@ -1147,8 +2118,8 @@ class OTDS:
1147
2118
  }
1148
2119
 
1149
2120
  if resource_id and not secret:
1150
- logger.error(
1151
- "A resource ID can only be specified if a secret value is also provided!"
2121
+ self.logger.error(
2122
+ "A resource ID can only be specified if a secret value is also provided!",
1152
2123
  )
1153
2124
  return None
1154
2125
 
@@ -1156,8 +2127,8 @@ class OTDS:
1156
2127
  resource_post_body["resourceID"] = resource_id
1157
2128
  if secret:
1158
2129
  if len(secret) != 24 or not secret.endswith("=="):
1159
- logger.warning(
1160
- "The secret should by 24 characters long and should end with '=='"
2130
+ self.logger.warning(
2131
+ "The secret should by 24 characters long and should end with '=='",
1161
2132
  )
1162
2133
  resource_post_body["secretKey"] = secret
1163
2134
 
@@ -1168,7 +2139,7 @@ class OTDS:
1168
2139
 
1169
2140
  request_url = self.config()["resourceUrl"]
1170
2141
 
1171
- logger.debug(
2142
+ self.logger.debug(
1172
2143
  "Adding resource -> '%s' ('%s'); calling -> %s",
1173
2144
  name,
1174
2145
  description,
@@ -1186,15 +2157,19 @@ class OTDS:
1186
2157
  # end method definition
1187
2158
 
1188
2159
  def get_resource(self, name: str, show_error: bool = False) -> dict | None:
1189
- """Get an existing OTDS resource
2160
+ """Get an existing OTDS resource.
1190
2161
 
1191
2162
  Args:
1192
- name (str): name of the new OTDS resource
1193
- show_error (bool, optional): treat as error if resource is not found
2163
+ name (str):
2164
+ The name of the new OTDS resource.
2165
+ show_error (bool, optional):
2166
+ If True, log an error if resource is not found. Else log just a warning.
2167
+
1194
2168
  Returns:
1195
- dict: Request response or None if the REST call fails.
2169
+ dict | None:
2170
+ Request response or None if the REST call fails.
1196
2171
 
1197
- Example:
2172
+ Example:
1198
2173
  {
1199
2174
  'resourceName': 'cs',
1200
2175
  'id': 'cs',
@@ -1223,11 +2198,12 @@ class OTDS:
1223
2198
  'logonStyle': None,
1224
2199
  'logonUXVersion': 0
1225
2200
  }
2201
+
1226
2202
  """
1227
2203
 
1228
2204
  request_url = "{}/{}".format(self.config()["resourceUrl"], name)
1229
2205
 
1230
- logger.debug("Get resource -> '%s'; calling -> %s", name, request_url)
2206
+ self.logger.debug("Get resource -> '%s'; calling -> %s", name, request_url)
1231
2207
 
1232
2208
  return self.do_request(
1233
2209
  url=request_url,
@@ -1240,21 +2216,30 @@ class OTDS:
1240
2216
  # end method definition
1241
2217
 
1242
2218
  def update_resource(
1243
- self, name: str, resource: object, show_error: bool = True
2219
+ self,
2220
+ name: str,
2221
+ resource: object,
2222
+ show_error: bool = True,
1244
2223
  ) -> dict | None:
1245
- """Update an existing OTDS resource
2224
+ """Update an existing OTDS resource.
1246
2225
 
1247
2226
  Args:
1248
- name (str): name of the new OTDS resource
1249
- resource (object): updated resource object of get_resource called before
1250
- show_error (bool, optional): treat as error if resource is not found
2227
+ name (str):
2228
+ The name of the OTDS resource to update.
2229
+ resource (object):
2230
+ updated resource object of get_resource called before
2231
+ show_error (bool, optional):
2232
+ If True, log an error if resource is not found. Else just log a warning.
2233
+
1251
2234
  Returns:
1252
- dict: Request response (json) or None if the REST call fails.
2235
+ dict | None:
2236
+ Request response (json) or None if the REST call fails.
2237
+
1253
2238
  """
1254
2239
 
1255
2240
  request_url = "{}/{}".format(self.config()["resourceUrl"], name)
1256
2241
 
1257
- logger.debug("Updating resource -> '%s'; calling -> %s", name, request_url)
2242
+ self.logger.debug("Updating resource -> '%s'; calling -> %s", name, request_url)
1258
2243
 
1259
2244
  return self.do_request(
1260
2245
  url=request_url,
@@ -1268,20 +2253,26 @@ class OTDS:
1268
2253
  # end method definition
1269
2254
 
1270
2255
  def activate_resource(self, resource_id: str) -> dict | None:
1271
- """Activate an OTDS resource
2256
+ """Activate an OTDS resource.
1272
2257
 
1273
2258
  Args:
1274
- resource_id (str): ID of the OTDS resource
2259
+ resource_id (str):
2260
+ The ID of the OTDS resource to update.
2261
+
1275
2262
  Returns:
1276
- dict: Request response (json) or None if the REST call fails.
2263
+ dict | None:
2264
+ Request response or None if the REST call fails.
2265
+
1277
2266
  """
1278
2267
 
1279
2268
  resource_post_body_json = {}
1280
2269
 
1281
2270
  request_url = "{}/{}/activate".format(self.config()["resourceUrl"], resource_id)
1282
2271
 
1283
- logger.debug(
1284
- "Activating resource -> '%s'; calling -> %s", resource_id, request_url
2272
+ self.logger.debug(
2273
+ "Activating resource -> '%s'; calling -> %s",
2274
+ resource_id,
2275
+ request_url,
1285
2276
  )
1286
2277
 
1287
2278
  return self.do_request(
@@ -1295,17 +2286,20 @@ class OTDS:
1295
2286
  # end method definition
1296
2287
 
1297
2288
  def get_access_roles(self) -> dict | None:
1298
- """Get a list of all OTDS access roles
2289
+ """Get a list of all OTDS access roles.
1299
2290
 
1300
2291
  Args:
1301
2292
  None
2293
+
1302
2294
  Returns:
1303
- dict: Request response or None if the REST call fails.
2295
+ dict | None:
2296
+ Request response or None if the REST call fails.
2297
+
1304
2298
  """
1305
2299
 
1306
2300
  request_url = self.config()["accessRoleUrl"]
1307
2301
 
1308
- logger.debug("Retrieving access roles; calling -> %s", request_url)
2302
+ self.logger.debug("Retrieving access roles; calling -> %s", request_url)
1309
2303
 
1310
2304
  return self.do_request(
1311
2305
  url=request_url,
@@ -1317,17 +2311,25 @@ class OTDS:
1317
2311
  # end method definition
1318
2312
 
1319
2313
  def get_access_role(self, access_role: str) -> dict | None:
1320
- """Get an OTDS access role
2314
+ """Get an OTDS access role.
1321
2315
 
1322
2316
  Args:
1323
- name (str): name of the access role
2317
+ access_role (str):
2318
+ The name of the access role.
2319
+
1324
2320
  Returns:
1325
- dict: Request response (json) or None if the REST call fails.
2321
+ dict | None:
2322
+ Request response or None if the REST call fails.
2323
+
1326
2324
  """
1327
2325
 
1328
2326
  request_url = self.config()["accessRoleUrl"] + "/" + access_role
1329
2327
 
1330
- logger.debug("Get access role -> '%s'; calling -> %s", access_role, request_url)
2328
+ self.logger.debug(
2329
+ "Get access role -> '%s'; calling -> %s",
2330
+ access_role,
2331
+ request_url,
2332
+ )
1331
2333
 
1332
2334
  return self.do_request(
1333
2335
  url=request_url,
@@ -1339,29 +2341,39 @@ class OTDS:
1339
2341
  # end method definition
1340
2342
 
1341
2343
  def add_partition_to_access_role(
1342
- self, access_role: str, partition: str, location: str = ""
2344
+ self,
2345
+ access_role: str,
2346
+ partition: str,
2347
+ location: str = "",
1343
2348
  ) -> bool:
1344
- """Add an OTDS partition to an OTDS access role
2349
+ """Add an OTDS partition to an OTDS access role.
1345
2350
 
1346
2351
  Args:
1347
- access_role (str): name of the OTDS access role
1348
- partition (str): name of the partition
1349
- location (str, optional): this is kind of a unique identifier DN (Distinguished Name)
1350
- most of the times you will want to keep it to empty string ("")
2352
+ access_role (str):
2353
+ The name of the OTDS access role.
2354
+ partition (str):
2355
+ The name of the partition.
2356
+ location (str, optional):
2357
+ This is kind of a unique identifier DN (Distinguished Name)
2358
+ most of the times you will want to keep it to empty string ("")
2359
+
1351
2360
  Returns:
1352
- bool: True if partition is in access role or has been successfully added.
1353
- False if partition has been not been added (error)
2361
+ bool:
2362
+ True if partition is in access role or has been successfully added.
2363
+ False if partition has been not been added (error)
2364
+
1354
2365
  """
1355
2366
 
1356
2367
  access_role_post_body_json = {
1357
- "userPartitions": [{"name": partition, "location": location}]
2368
+ "userPartitions": [{"name": partition, "location": location}],
1358
2369
  }
1359
2370
 
1360
2371
  request_url = "{}/{}/members".format(
1361
- self.config()["accessRoleUrl"], access_role
2372
+ self.config()["accessRoleUrl"],
2373
+ access_role,
1362
2374
  )
1363
2375
 
1364
- logger.debug(
2376
+ self.logger.debug(
1365
2377
  "Add user partition -> '%s' to access role -> '%s'; calling -> %s",
1366
2378
  partition,
1367
2379
  access_role,
@@ -1374,31 +2386,38 @@ class OTDS:
1374
2386
  json_data=access_role_post_body_json,
1375
2387
  timeout=None,
1376
2388
  failure_message="Failed to add partition -> '{}' to access role -> '{}'".format(
1377
- partition, access_role
2389
+ partition,
2390
+ access_role,
1378
2391
  ),
1379
2392
  parse_request_response=False,
1380
2393
  )
1381
2394
 
1382
- if response and response.ok:
1383
- return True
1384
-
1385
- return False
2395
+ return bool(response and response.ok)
1386
2396
 
1387
2397
  # end method definition
1388
2398
 
1389
2399
  def add_user_to_access_role(
1390
- self, access_role: str, user_id: str, location: str = ""
2400
+ self,
2401
+ access_role: str,
2402
+ user_id: str,
2403
+ location: str = "",
1391
2404
  ) -> bool:
1392
- """Add an OTDS user to an OTDS access role
2405
+ """Add an OTDS user to an OTDS access role.
1393
2406
 
1394
2407
  Args:
1395
- access_role (str): name of the OTDS access role
1396
- user_id (str): ID of the user (= login name)
1397
- location (str, optional): this is kind of a unique identifier DN (Distinguished Name)
1398
- most of the times you will want to keep it to empty string ("")
2408
+ access_role (str):
2409
+ The name of the OTDS access role.
2410
+ user_id (str):
2411
+ The ID of the user (= login name) to add to the access role.
2412
+ location (str, optional):
2413
+ This is kind of a unique identifier DN (Distinguished Name)
2414
+ most of the times you will want to keep it to empty string ("").
2415
+
1399
2416
  Returns:
1400
- bool: True if user is in access role or has been successfully added.
1401
- False if user has not been added (error)
2417
+ bool:
2418
+ True if user is in access role or has been successfully added.
2419
+ False if user has not been added (error).
2420
+
1402
2421
  """
1403
2422
 
1404
2423
  # get existing members to check if user is already a member:
@@ -1407,17 +2426,17 @@ class OTDS:
1407
2426
  return False
1408
2427
 
1409
2428
  # Checking if user already added to access role
1410
- accessRoleUsers = access_roles_get_response["accessRoleMembers"]["users"]
1411
- for user in accessRoleUsers:
2429
+ access_role_users = access_roles_get_response["accessRoleMembers"]["users"]
2430
+ for user in access_role_users:
1412
2431
  if user["displayName"] == user_id:
1413
- logger.debug(
2432
+ self.logger.debug(
1414
2433
  "User -> '%s' already added to access role -> '%s'",
1415
2434
  user_id,
1416
2435
  access_role,
1417
2436
  )
1418
2437
  return True
1419
2438
 
1420
- logger.debug(
2439
+ self.logger.debug(
1421
2440
  "User -> '%s' is not yet in access role -> '%s' - adding...",
1422
2441
  user_id,
1423
2442
  access_role,
@@ -1425,14 +2444,15 @@ class OTDS:
1425
2444
 
1426
2445
  # create payload for REST call:
1427
2446
  access_role_post_body_json = {
1428
- "users": [{"name": user_id, "location": location}]
2447
+ "users": [{"name": user_id, "location": location}],
1429
2448
  }
1430
2449
 
1431
2450
  request_url = "{}/{}/members".format(
1432
- self.config()["accessRoleUrl"], access_role
2451
+ self.config()["accessRoleUrl"],
2452
+ access_role,
1433
2453
  )
1434
2454
 
1435
- logger.debug(
2455
+ self.logger.debug(
1436
2456
  "Add user -> %s to access role -> %s; calling -> %s",
1437
2457
  user_id,
1438
2458
  access_role,
@@ -1445,31 +2465,38 @@ class OTDS:
1445
2465
  json_data=access_role_post_body_json,
1446
2466
  timeout=None,
1447
2467
  failure_message="Failed to add user -> '{}' to access role -> '{}'".format(
1448
- user_id, access_role
2468
+ user_id,
2469
+ access_role,
1449
2470
  ),
1450
2471
  parse_request_response=False,
1451
2472
  )
1452
2473
 
1453
- if response and response.ok:
1454
- return True
1455
-
1456
- return False
2474
+ return bool(response and response.ok)
1457
2475
 
1458
2476
  # end method definition
1459
2477
 
1460
2478
  def add_group_to_access_role(
1461
- self, access_role: str, group: str, location: str = ""
2479
+ self,
2480
+ access_role: str,
2481
+ group: str,
2482
+ location: str = "",
1462
2483
  ) -> bool:
1463
- """Add an OTDS group to an OTDS access role
2484
+ """Add an OTDS group to an OTDS access role.
1464
2485
 
1465
2486
  Args:
1466
- access_role (str): name of the OTDS access role
1467
- group (str): name of the group
1468
- location (str, optional): this is kind of a unique identifier DN (Distinguished Name)
1469
- most of the times you will want to keep it to empty string ("")
2487
+ access_role (str):
2488
+ The name of the OTDS access role.
2489
+ group (str):
2490
+ The name of the group to add to the access role.
2491
+ location (str, optional):
2492
+ This is kind of a unique identifier DN (Distinguished Name)
2493
+ most of the times you will want to keep it to empty string ("").
2494
+
1470
2495
  Returns:
1471
- bool: True if group is in access role or has been successfully added.
1472
- False if group has been not been added (error)
2496
+ bool:
2497
+ True if group is in access role or has been successfully added.
2498
+ False if group has been not been added (error)
2499
+
1473
2500
  """
1474
2501
 
1475
2502
  # get existing members to check if user is already a member:
@@ -1481,14 +2508,14 @@ class OTDS:
1481
2508
  access_role_groups = access_roles_get_response["accessRoleMembers"]["groups"]
1482
2509
  for access_role_group in access_role_groups:
1483
2510
  if access_role_group["name"] == group:
1484
- logger.debug(
2511
+ self.logger.debug(
1485
2512
  "Group -> '%s' already added to access role -> '%s'",
1486
2513
  group,
1487
2514
  access_role,
1488
2515
  )
1489
2516
  return True
1490
2517
 
1491
- logger.debug(
2518
+ self.logger.debug(
1492
2519
  "Group -> '%s' is not yet in access role -> '%s' - adding...",
1493
2520
  group,
1494
2521
  access_role,
@@ -1498,10 +2525,11 @@ class OTDS:
1498
2525
  access_role_post_body_json = {"groups": [{"name": group, "location": location}]}
1499
2526
 
1500
2527
  request_url = "{}/{}/members".format(
1501
- self.config()["accessRoleUrl"], access_role
2528
+ self.config()["accessRoleUrl"],
2529
+ access_role,
1502
2530
  )
1503
2531
 
1504
- logger.debug(
2532
+ self.logger.debug(
1505
2533
  "Add group -> '%s' to access role -> '%s'; calling -> %s",
1506
2534
  group,
1507
2535
  access_role,
@@ -1514,30 +2542,41 @@ class OTDS:
1514
2542
  json_data=access_role_post_body_json,
1515
2543
  timeout=None,
1516
2544
  failure_message="Failed to add group -> '{}' to access role -> '{}'".format(
1517
- group, access_role
2545
+ group,
2546
+ access_role,
1518
2547
  ),
1519
2548
  parse_request_response=False,
1520
2549
  )
1521
2550
 
1522
- if response and response.ok:
1523
- return True
1524
-
1525
- return False
2551
+ return bool(response and response.ok)
1526
2552
 
1527
2553
  # end method definition
1528
2554
 
1529
2555
  def update_access_role_attributes(
1530
- self, name: str, attribute_list: list
2556
+ self,
2557
+ name: str,
2558
+ attribute_list: list,
1531
2559
  ) -> dict | None:
1532
- """Update some attributes of an existing OTDS Access Role
2560
+ """Update some attributes of an existing OTDS access role.
1533
2561
 
1534
2562
  Args:
1535
- name (str): name of the existing access role
1536
- attribute_list (list): list of attribute name and attribute value pairs
1537
- The values need to be a list as well. Example:
1538
- [{name: "pushAllGroups", values: ["True"]}]
2563
+ name (str):
2564
+ The name of the existing access role.
2565
+ attribute_list (list):
2566
+ A list of attribute name and attribute value pairs.
2567
+ The values need to be a list as well.
2568
+ Example values:
2569
+ [
2570
+ {
2571
+ name: "pushAllGroups",
2572
+ values: ["True"]
2573
+ }
2574
+ ]
2575
+
1539
2576
  Returns:
1540
- dict: Request response (json) or None if the REST call fails.
2577
+ dict | None:
2578
+ Request response or None if the REST call fails.
2579
+
1541
2580
  """
1542
2581
 
1543
2582
  # Return if list is empty:
@@ -1547,14 +2586,14 @@ class OTDS:
1547
2586
  # create payload for REST call:
1548
2587
  access_role = self.get_access_role(name)
1549
2588
  if not access_role:
1550
- logger.error("Failed to get access role -> '%s'", name)
2589
+ self.logger.error("Failed to get access role -> '%s'", name)
1551
2590
  return None
1552
2591
 
1553
2592
  access_role_put_body_json = {"attributes": attribute_list}
1554
2593
 
1555
2594
  request_url = "{}/{}/attributes".format(self.config()["accessRoleUrl"], name)
1556
2595
 
1557
- logger.debug(
2596
+ self.logger.debug(
1558
2597
  "Update access role -> '%s' with attributes -> %s; calling -> %s",
1559
2598
  name,
1560
2599
  str(access_role_put_body_json),
@@ -1582,24 +2621,31 @@ class OTDS:
1582
2621
  """Add a product license to an OTDS resource.
1583
2622
 
1584
2623
  Args:
1585
- path_to_license_file (str): fully qualified filename of the license file
1586
- product_name (str): product name
1587
- product_description (str): product description
1588
- resource_id (str): OTDS resource ID (this is ID not the resource name!)
1589
- update (bool, optional): whether or not an existing license should be updated (default = True)
2624
+ path_to_license_file (str):
2625
+ A fully qualified filename of the license file.
2626
+ product_name (str):
2627
+ The product name.
2628
+ product_description (str):
2629
+ The product description.
2630
+ resource_id (str):
2631
+ OTDS resource ID (this is ID not the resource name!).
2632
+ update (bool, optional):
2633
+ Whether or not an existing license should be updated (default = True).
2634
+
1590
2635
  Returns:
1591
- dict: Request response (dictionary) or None if the REST call fails
2636
+ dict | None:
2637
+ Request response (dictionary) or None if the REST call fails.
2638
+
1592
2639
  """
1593
2640
 
1594
- logger.debug("Reading license file -> '%s'...", path_to_license_file)
2641
+ self.logger.debug("Reading license file -> '%s'...", path_to_license_file)
1595
2642
  try:
1596
- with open(path_to_license_file, "rt", encoding="UTF-8") as license_file:
2643
+ with open(path_to_license_file, encoding="UTF-8") as license_file:
1597
2644
  license_content = license_file.read()
1598
- except IOError as exception:
1599
- logger.error(
1600
- "Error opening license file -> '%s'; error -> %s",
2645
+ except OSError:
2646
+ self.logger.error(
2647
+ "Error opening license file -> '%s'!",
1601
2648
  path_to_license_file,
1602
- exception.strerror,
1603
2649
  )
1604
2650
  return None
1605
2651
 
@@ -1620,17 +2666,19 @@ class OTDS:
1620
2666
  if existing_license:
1621
2667
  request_url += "/" + existing_license[0]["id"]
1622
2668
  else:
1623
- logger.debug(
1624
- "No existing license found for resource -> '%s' - adding a new license...",
2669
+ self.logger.info(
2670
+ "No existing license found for product -> '%s' and resource -> '%s' - adding a new license...",
2671
+ product_name,
1625
2672
  resource_id,
1626
2673
  )
1627
2674
  # change strategy to create a new license:
1628
2675
  update = False
1629
2676
 
1630
- logger.debug(
1631
- "Adding product license -> '%s' for product -> '%s' to resource ->'%s'; calling -> %s",
2677
+ self.logger.debug(
2678
+ "%s product license -> '%s' for product -> '%s' to resource ->'%s'; calling -> %s",
2679
+ "Adding" if not update else "Updating",
1632
2680
  path_to_license_file,
1633
- product_description,
2681
+ "{} ({})".format(product_name, product_description) if product_description else product_name,
1634
2682
  resource_id,
1635
2683
  request_url,
1636
2684
  )
@@ -1642,8 +2690,10 @@ class OTDS:
1642
2690
  method="PUT",
1643
2691
  json_data=license_post_body_json,
1644
2692
  timeout=None,
1645
- failure_message="Failed to update product license -> '{}' for product -> '{}'".format(
1646
- path_to_license_file, product_description
2693
+ failure_message="Failed to update product license -> '{}' for product -> '{}' to resource -> '{}'".format(
2694
+ path_to_license_file,
2695
+ "{} ({})".format(product_name, product_description) if product_description else product_name,
2696
+ resource_id,
1647
2697
  ),
1648
2698
  )
1649
2699
  else:
@@ -1653,44 +2703,47 @@ class OTDS:
1653
2703
  method="POST",
1654
2704
  json_data=license_post_body_json,
1655
2705
  timeout=None,
1656
- failure_message="Failed to add product license -> '{}' for product -> '{}'".format(
1657
- path_to_license_file, product_description
2706
+ failure_message="Failed to add product license -> '{}' for product -> '{}' to resource -> '{}'".format(
2707
+ path_to_license_file,
2708
+ "{} ({})".format(product_name, product_description) if product_description else product_name,
2709
+ resource_id,
1658
2710
  ),
1659
2711
  )
1660
2712
 
1661
2713
  # end method definition
1662
2714
 
1663
- def get_license_for_resource(self, resource_id: str):
2715
+ def get_license_for_resource(self, resource_id: str) -> dict | None:
1664
2716
  """Get a product license for a resource in OTDS.
1665
2717
 
1666
2718
  Args:
1667
- resource_id (str): OTDS resource ID (this is ID not the resource name!)
2719
+ resource_id (str):
2720
+ The OTDS resource ID (this is ID not the resource name!).
2721
+
1668
2722
  Returns:
1669
- Licenses for a resource or None if the REST call fails
2723
+ dict | None:
2724
+ Licenses for a resource or None if the REST call fails.
2725
+
2726
+ Example:
2727
+ {
2728
+ '_oTLicenseType': 'NON-PRODUCTION',
2729
+ '_oTLicenseResource': '7382094f-a434-4714-9696-82864b6803da',
2730
+ '_oTLicenseResourceName': 'cs',
2731
+ '_oTLicenseProduct': 'EXTENDED_ECM',
2732
+ 'name': 'EXTENDED_ECM¹7382094f-a434-4714-9696-82864b6803da',
2733
+ 'location': 'cn=EXTENDED_ECM¹7382094f-a434-4714-9696-82864b6803da,ou=Licenses,dc=identity,dc=opentext,dc=net',
2734
+ 'id': 'cn=EXTENDED_ECM¹7382094f-a434-4714-9696-82864b6803da,ou=Licenses,dc=identity,dc=opentext,dc=net',
2735
+ 'description': 'CS license',
2736
+ 'values': [{...}, {...}, {...}, {...}, {...}, {...}, {...}, {...}, {...}, ...]
2737
+ }
1670
2738
 
1671
- licenses have this format:
1672
- {
1673
- '_oTLicenseType': 'NON-PRODUCTION',
1674
- '_oTLicenseResource': '7382094f-a434-4714-9696-82864b6803da',
1675
- '_oTLicenseResourceName': 'cs',
1676
- '_oTLicenseProduct': 'EXTENDED_ECM',
1677
- 'name': 'EXTENDED_ECM¹7382094f-a434-4714-9696-82864b6803da',
1678
- 'location': 'cn=EXTENDED_ECM¹7382094f-a434-4714-9696-82864b6803da,ou=Licenses,dc=identity,dc=opentext,dc=net',
1679
- 'id': 'cn=EXTENDED_ECM¹7382094f-a434-4714-9696-82864b6803da,ou=Licenses,dc=identity,dc=opentext,dc=net',
1680
- 'description': 'CS license',
1681
- 'values': [{...}, {...}, {...}, {...}, {...}, {...}, {...}, {...}, {...}, ...]
1682
- }
1683
2739
  """
1684
2740
 
1685
- request_url = (
1686
- self.license_url()
1687
- + "/assignedlicenses?resourceID="
1688
- + resource_id
1689
- + "&validOnly=false"
1690
- )
2741
+ request_url = self.license_url() + "/assignedlicenses?resourceID=" + resource_id + "&validOnly=false"
1691
2742
 
1692
- logger.debug(
1693
- "Get license for resource -> %s; calling -> %s", resource_id, request_url
2743
+ self.logger.debug(
2744
+ "Get license for resource -> %s; calling -> %s",
2745
+ resource_id,
2746
+ request_url,
1694
2747
  )
1695
2748
 
1696
2749
  response = self.do_request(
@@ -1698,7 +2751,7 @@ class OTDS:
1698
2751
  method="GET",
1699
2752
  timeout=None,
1700
2753
  failure_message="Failed to get license for resource -> '{}'".format(
1701
- resource_id
2754
+ resource_id,
1702
2755
  ),
1703
2756
  )
1704
2757
 
@@ -1713,15 +2766,20 @@ class OTDS:
1713
2766
  """Delete a product license for a resource in OTDS.
1714
2767
 
1715
2768
  Args:
1716
- resource_id (str): OTDS resource ID (this is ID not the resource name!)
1717
- license_id (str): OTDS license ID (this is the ID not the license name!)
2769
+ resource_id (str):
2770
+ The OTDS resource ID (this is ID not the resource name!).
2771
+ license_id (str):
2772
+ The OTDS license ID (this is the ID not the license name!).
2773
+
1718
2774
  Returns:
1719
- bool: True if successful or False if the REST call fails
2775
+ bool:
2776
+ True if successful or False if the REST call fails
2777
+
1720
2778
  """
1721
2779
 
1722
2780
  request_url = "{}/{}".format(self.license_url(), license_id)
1723
2781
 
1724
- logger.debug(
2782
+ self.logger.debug(
1725
2783
  "Deleting product license -> '%s' from resource -> '%s'; calling -> %s",
1726
2784
  license_id,
1727
2785
  resource_id,
@@ -1733,15 +2791,13 @@ class OTDS:
1733
2791
  method="DELETE",
1734
2792
  timeout=None,
1735
2793
  failure_message="Failed to delete license -> '{}' for resource -> '{}'".format(
1736
- license_id, resource_id
2794
+ license_id,
2795
+ resource_id,
1737
2796
  ),
1738
2797
  parse_request_response=False,
1739
2798
  )
1740
2799
 
1741
- if response and response.ok:
1742
- return True
1743
-
1744
- return False
2800
+ return bool(response and response.ok)
1745
2801
 
1746
2802
  # end method definition
1747
2803
 
@@ -1756,27 +2812,53 @@ class OTDS:
1756
2812
  ) -> bool:
1757
2813
  """Assign an OTDS user to a product license (feature) in OTDS.
1758
2814
 
2815
+ licenses have this format:
2816
+ {
2817
+ '_oTLicenseType': 'NON-PRODUCTION',
2818
+ '_oTLicenseResource': '7382094f-a434-4714-9696-82864b6803da',
2819
+ '_oTLicenseResourceName': 'cs',
2820
+ '_oTLicenseProduct': 'EXTENDED_ECM',
2821
+ 'name': 'EXTENDED_ECM¹7382094f-a434-4714-9696-82864b6803da',
2822
+ 'location': 'cn=EXTENDED_ECM¹7382094f-a434-4714-9696-82864b6803da,ou=Licenses,dc=identity,dc=opentext,dc=net',
2823
+ 'id': 'cn=EXTENDED_ECM¹7382094f-a434-4714-9696-82864b6803da,ou=Licenses,dc=identity,dc=opentext,dc=net',
2824
+ 'description': 'CS license',
2825
+ 'values': [{...}, {...}, {...}, {...}, {...}, {...}, {...}, {...}, {...}, ...]
2826
+ }
2827
+
1759
2828
  Args:
1760
- partition (str): user partition in OTDS, e.g. "Content Server Members"
1761
- user_id (str): ID of the user (= login name)
1762
- resource_id (str): OTDS resource ID (this is ID not the resource name!)
1763
- license_feature (str): name of the license feature
1764
- license_name (str): name of the license to assign
1765
- license_type (str, optional): deault is "Full", Extended ECM also has "Occasional"
2829
+ partition (str):
2830
+ The user partition in OTDS, e.g. "Content Server Members".
2831
+ user_id (str):
2832
+ The ID of the user (= login name) to assign to the license.
2833
+ resource_id (str):
2834
+ The OTDS resource ID (this is ID not the resource name!).
2835
+ license_feature (str):
2836
+ The name of the license feature.
2837
+ license_name (str):
2838
+ The name of the license to assign.
2839
+ license_type (str, optional):
2840
+ The type of the license. Default is "Full", Extended ECM also has "Occasional".
2841
+
1766
2842
  Returns:
1767
- bool: True if successful or False if the REST call fails or the license is not found
2843
+ bool:
2844
+ True if successful or False if the REST call fails or the license is not found.
2845
+
1768
2846
  """
1769
2847
 
1770
2848
  licenses = self.get_license_for_resource(resource_id)
2849
+ if not licenses:
2850
+ self.logger.error(
2851
+ "Resource with ID -> '%s' does not exist or has no licenses",
2852
+ resource_id,
2853
+ )
2854
+ return False
1771
2855
 
1772
2856
  for lic in licenses:
1773
2857
  if lic["_oTLicenseProduct"] == license_name:
1774
- license_location = lic["id"]
1775
-
1776
- try:
1777
- license_location
1778
- except UnboundLocalError:
1779
- logger.error(
2858
+ license_id = lic["id"]
2859
+ break
2860
+ else:
2861
+ self.logger.error(
1780
2862
  "Cannot find license -> '%s' for resource -> %s",
1781
2863
  license_name,
1782
2864
  resource_id,
@@ -1787,7 +2869,7 @@ class OTDS:
1787
2869
  if user:
1788
2870
  user_location = user["location"]
1789
2871
  else:
1790
- logger.error("Cannot find location for user -> '%s'", user_id)
2872
+ self.logger.error("Cannot find location for user -> '%s'", user_id)
1791
2873
  return False
1792
2874
 
1793
2875
  license_post_body_json = {
@@ -1797,12 +2879,12 @@ class OTDS:
1797
2879
  "values": [{"name": "counter", "values": [license_feature]}],
1798
2880
  }
1799
2881
 
1800
- request_url = self.license_url() + "/object/" + license_location
2882
+ request_url = self.license_url() + "/object/" + license_id
1801
2883
 
1802
- logger.debug(
2884
+ self.logger.debug(
1803
2885
  "Assign license feature -> '%s' of license -> '%s' associated with resource -> '%s' to user -> '%s'; calling -> %s",
1804
2886
  license_feature,
1805
- license_location,
2887
+ license_id,
1806
2888
  resource_id,
1807
2889
  user_id,
1808
2890
  request_url,
@@ -1814,13 +2896,15 @@ class OTDS:
1814
2896
  json_data=license_post_body_json,
1815
2897
  timeout=None,
1816
2898
  failure_message="Failed to add license feature -> '{}' associated with resource -> '{}' to user -> '{}'".format(
1817
- license_feature, resource_id, user_id
2899
+ license_feature,
2900
+ resource_id,
2901
+ user_id,
1818
2902
  ),
1819
2903
  parse_request_response=False,
1820
2904
  )
1821
2905
 
1822
2906
  if response and response.ok:
1823
- logger.debug(
2907
+ self.logger.debug(
1824
2908
  "Added license feature -> '%s' to user -> '%s'",
1825
2909
  license_feature,
1826
2910
  user_id,
@@ -1841,44 +2925,51 @@ class OTDS:
1841
2925
  ) -> bool:
1842
2926
  """Assign an OTDS partition to a product license (feature).
1843
2927
 
2928
+ licenses have this format:
2929
+ {
2930
+ '_oTLicenseType': 'NON-PRODUCTION',
2931
+ '_oTLicenseResource': '7382094f-a434-4714-9696-82864b6803da',
2932
+ '_oTLicenseResourceName': 'cs',
2933
+ '_oTLicenseProduct': 'EXTENDED_ECM',
2934
+ 'name': 'EXTENDED_ECM¹7382094f-a434-4714-9696-82864b6803da',
2935
+ 'location': 'cn=EXTENDED_ECM¹7382094f-a434-4714-9696-82864b6803da,ou=Licenses,dc=identity,dc=opentext,dc=net',
2936
+ 'id': 'cn=EXTENDED_ECM¹7382094f-a434-4714-9696-82864b6803da,ou=Licenses,dc=identity,dc=opentext,dc=net',
2937
+ 'description': 'CS license',
2938
+ 'values': [{...}, {...}, {...}, {...}, {...}, {...}, {...}, {...}, {...}, ...]
2939
+ }
2940
+
1844
2941
  Args:
1845
- partition_name (str): user partition in OTDS, e.g. "Content Server Members"
1846
- resource_id (str): OTDS resource ID (this is ID not the resource name!)
1847
- license_feature (str): name of the license feature, e.g. "X2" or "ADDON_ENGINEERING"
1848
- license_name (str): name of the license to assign, e.g. "EXTENDED_ECM" or "INTELLGENT_VIEWIMG"
1849
- license_type (str, optional): deault is "Full", Extended ECM also has "Occasional"
2942
+ partition_name (str):
2943
+ The user partition in OTDS, e.g. "Content Server Members".
2944
+ resource_id (str):
2945
+ The OTDS resource ID (this is ID not the resource name!).
2946
+ license_feature (str):
2947
+ The name of the license feature, e.g. "X2" or "ADDON_ENGINEERING".
2948
+ license_name (str):
2949
+ The name of the license to assign, e.g. "EXTENDED_ECM" or "INTELLGENT_VIEWIMG".
2950
+ license_type (str, optional):
2951
+ The license type. Default is "Full", Extended ECM also has "Occasional"
2952
+
1850
2953
  Returns:
1851
- bool: True if successful or False if the REST call fails or the license is not found
2954
+ bool:
2955
+ True if successful or False if the REST call fails or the license is not found.
2956
+
1852
2957
  """
1853
2958
 
1854
2959
  licenses = self.get_license_for_resource(resource_id)
1855
2960
  if not licenses:
1856
- logger.error(
2961
+ self.logger.error(
1857
2962
  "Resource with ID -> '%s' does not exist or has no licenses",
1858
2963
  resource_id,
1859
2964
  )
1860
2965
  return False
1861
2966
 
1862
- # licenses have this format:
1863
- # {
1864
- # '_oTLicenseType': 'NON-PRODUCTION',
1865
- # '_oTLicenseResource': '7382094f-a434-4714-9696-82864b6803da',
1866
- # '_oTLicenseResourceName': 'cs',
1867
- # '_oTLicenseProduct': 'EXTENDED_ECM',
1868
- # 'name': 'EXTENDED_ECM¹7382094f-a434-4714-9696-82864b6803da',
1869
- # 'location': 'cn=EXTENDED_ECM¹7382094f-a434-4714-9696-82864b6803da,ou=Licenses,dc=identity,dc=opentext,dc=net',
1870
- # 'id': 'cn=EXTENDED_ECM¹7382094f-a434-4714-9696-82864b6803da,ou=Licenses,dc=identity,dc=opentext,dc=net',
1871
- # 'description': 'CS license',
1872
- # 'values': [{...}, {...}, {...}, {...}, {...}, {...}, {...}, {...}, {...}, ...]
1873
- # }
1874
2967
  for lic in licenses:
1875
2968
  if lic["_oTLicenseProduct"] == license_name:
1876
- license_location = lic["id"]
1877
-
1878
- try:
1879
- license_location
1880
- except UnboundLocalError:
1881
- logger.error(
2969
+ license_id = lic["id"]
2970
+ break
2971
+ else:
2972
+ self.logger.error(
1882
2973
  "Cannot find license -> %s for resource -> %s",
1883
2974
  license_name,
1884
2975
  resource_id,
@@ -1892,12 +2983,12 @@ class OTDS:
1892
2983
  "values": [{"name": "counter", "values": [license_feature]}],
1893
2984
  }
1894
2985
 
1895
- request_url = self.license_url() + "/object/" + license_location
2986
+ request_url = self.license_url() + "/object/" + license_id
1896
2987
 
1897
- logger.debug(
2988
+ self.logger.debug(
1898
2989
  "Assign license feature -> '%s' of license -> '%s' associated with resource -> '%s' to partition -> '%s'; calling -> %s",
1899
2990
  license_feature,
1900
- license_location,
2991
+ license_id,
1901
2992
  resource_id,
1902
2993
  partition_name,
1903
2994
  request_url,
@@ -1909,13 +3000,15 @@ class OTDS:
1909
3000
  json_data=license_post_body_json,
1910
3001
  timeout=None,
1911
3002
  failure_message="Failed to add license feature -> '{}' associated with resource -> '{}' to partition -> '{}'".format(
1912
- license_feature, resource_id, partition_name
3003
+ license_feature,
3004
+ resource_id,
3005
+ partition_name,
1913
3006
  ),
1914
3007
  parse_request_response=False,
1915
3008
  )
1916
3009
 
1917
3010
  if response and response.ok:
1918
- logger.debug(
3011
+ self.logger.debug(
1919
3012
  "Added license feature -> '%s' to partition -> '%s'",
1920
3013
  license_feature,
1921
3014
  partition_name,
@@ -1932,73 +3025,89 @@ class OTDS:
1932
3025
  license_feature: str,
1933
3026
  license_name: str,
1934
3027
  ) -> dict | None:
1935
- """Return the licensed objects (users, groups, partitions) in OTDS for a license + license feature
1936
- associated with an OTDS resource (like "cs").
3028
+ """Return the licensed objects for a license + license feature associated with an OTDS resource (like "cs").
3029
+
3030
+ Licensed objects can be users, groups, or partitions.
3031
+
3032
+ licenses have this format:
3033
+ {
3034
+ '_oTLicenseType': 'NON-PRODUCTION',
3035
+ '_oTLicenseResource': '7382094f-a434-4714-9696-82864b6803da',
3036
+ '_oTLicenseResourceName': 'cs',
3037
+ '_oTLicenseProduct': 'EXTENDED_ECM',
3038
+ 'name': 'EXTENDED_ECM¹7382094f-a434-4714-9696-82864b6803da',
3039
+ 'location': 'cn=EXTENDED_ECM¹7382094f-a434-4714-9696-82864b6803da,ou=Licenses,dc=identity,dc=opentext,dc=net',
3040
+ 'id': 'cn=EXTENDED_ECM¹7382094f-a434-4714-9696-82864b6803da,ou=Licenses,dc=identity,dc=opentext,dc=net',
3041
+ 'description': 'CS license',
3042
+ 'values': [{...}, {...}, {...}, {...}, {...}, {...}, {...}, {...}, {...}, ...]
3043
+ }
1937
3044
 
1938
3045
  Args:
1939
- resource_id (str): OTDS resource ID (this is ID not the resource name!)
1940
- license_feature (str): name of the license feature, e.g. "X2" or "ADDON_ENGINEERING"
1941
- license_name (str): name of the license to assign, e.g. "EXTENDED_ECM" or "INTELLGENT_VIEWIMG"
3046
+ resource_id (str):
3047
+ The OTDS resource ID (this is ID not the resource name!).
3048
+ license_feature (str):
3049
+ The name of the license feature, e.g. "X2" or "ADDON_ENGINEERING".
3050
+ license_name (str):
3051
+ The name of the license to assign, e.g. "EXTENDED_ECM" or "INTELLGENT_VIEWIMG".
3052
+
1942
3053
  Returns:
1943
- dict: data structure of licensed objects
3054
+ dict | None:
3055
+ The data structure of licensed objects or None in case of an error.
1944
3056
 
1945
- Example return value:
3057
+ Example:
1946
3058
  {
1947
3059
  'status': 0,
1948
3060
  'displayString': 'Success',
1949
3061
  'exceptions': None,
1950
3062
  'retValue': 0,
1951
- 'listGroupsResults': {'groups': [...], 'actualPageSize': 0, 'nextPageCookie': None, 'requestedPageSize': 250},
1952
- 'listUsersResults': {'users': [...], 'actualPageSize': 53, 'nextPageCookie': None, 'requestedPageSize': 250},
1953
- 'listUserPartitionResult': {'_userPartitions': [...], 'warningMessage': None, 'actualPageSize': 0, 'nextPageCookie': None, 'requestedPageSize': 250},
3063
+ 'listGroupsResults': {
3064
+ 'groups': [...],
3065
+ 'actualPageSize': 0,
3066
+ 'nextPageCookie': None,
3067
+ 'requestedPageSize': 250
3068
+ },
3069
+ 'listUsersResults': {
3070
+ 'users': [...],
3071
+ 'actualPageSize': 53,
3072
+ 'nextPageCookie': None,
3073
+ 'requestedPageSize': 250
3074
+ },
3075
+ 'listUserPartitionResult': {
3076
+ '_userPartitions': [...],
3077
+ 'warningMessage': None,
3078
+ 'actualPageSize': 0,
3079
+ 'nextPageCookie': None,
3080
+ 'requestedPageSize': 250
3081
+ },
1954
3082
  'version': 1
1955
3083
  }
3084
+
1956
3085
  """
1957
3086
 
1958
3087
  licenses = self.get_license_for_resource(resource_id)
1959
3088
  if not licenses:
1960
- logger.error(
3089
+ self.logger.error(
1961
3090
  "Resource with ID -> '%s' does not exist or has no licenses",
1962
3091
  resource_id,
1963
3092
  )
1964
3093
  return False
1965
3094
 
1966
- # licenses have this format:
1967
- # {
1968
- # '_oTLicenseType': 'NON-PRODUCTION',
1969
- # '_oTLicenseResource': '7382094f-a434-4714-9696-82864b6803da',
1970
- # '_oTLicenseResourceName': 'cs',
1971
- # '_oTLicenseProduct': 'EXTENDED_ECM',
1972
- # 'name': 'EXTENDED_ECM¹7382094f-a434-4714-9696-82864b6803da',
1973
- # 'location': 'cn=EXTENDED_ECM¹7382094f-a434-4714-9696-82864b6803da,ou=Licenses,dc=identity,dc=opentext,dc=net',
1974
- # 'id': 'cn=EXTENDED_ECM¹7382094f-a434-4714-9696-82864b6803da,ou=Licenses,dc=identity,dc=opentext,dc=net',
1975
- # 'description': 'CS license',
1976
- # 'values': [{...}, {...}, {...}, {...}, {...}, {...}, {...}, {...}, {...}, ...]
1977
- # }
1978
3095
  for lic in licenses:
1979
3096
  if lic["_oTLicenseProduct"] == license_name:
1980
3097
  license_location = lic["location"]
1981
-
1982
- try:
1983
- license_location
1984
- except UnboundLocalError:
1985
- logger.error(
3098
+ break
3099
+ else:
3100
+ self.logger.error(
1986
3101
  "Cannot find license -> %s for resource -> %s",
1987
3102
  license_name,
1988
3103
  resource_id,
1989
3104
  )
1990
3105
  return False
1991
3106
 
1992
- request_url = (
1993
- self.license_url()
1994
- + "/object/"
1995
- + license_location
1996
- + "?counter="
1997
- + license_feature
1998
- )
3107
+ request_url = self.license_url() + "/object/" + license_location + "?counter=" + license_feature
1999
3108
 
2000
- logger.debug(
2001
- "Get licensed objects for license -> %s and license feature -> %s associated with resource -> %s; calling -> %s",
3109
+ self.logger.debug(
3110
+ "Get licensed objects for license -> '%s' and license feature -> '%s' associated with resource -> '%s'; calling -> %s",
2002
3111
  license_name,
2003
3112
  license_feature,
2004
3113
  resource_id,
@@ -2010,25 +3119,37 @@ class OTDS:
2010
3119
  method="GET",
2011
3120
  timeout=None,
2012
3121
  failure_message="Failed to get licensed objects for license -> '{}' and license feature -> '{}' associated with resource -> '{}'".format(
2013
- license_name, license_feature, resource_id
3122
+ license_name,
3123
+ license_feature,
3124
+ resource_id,
2014
3125
  ),
2015
3126
  )
2016
3127
 
2017
3128
  # end method definition
2018
3129
 
2019
3130
  def is_user_licensed(
2020
- self, user_name: str, resource_id: str, license_feature: str, license_name: str
3131
+ self,
3132
+ user_name: str,
3133
+ resource_id: str,
3134
+ license_feature: str,
3135
+ license_name: str,
2021
3136
  ) -> bool:
2022
3137
  """Check if a user is licensed for a license and license feature associated with a particular OTDS resource.
2023
3138
 
2024
3139
  Args:
2025
- user_name (str): login name of the OTDS user
2026
- resource_id (str): OTDS resource ID (this is ID not the resource name!)
2027
- license_feature (str): name of the license feature, e.g. "X2" or "ADDON_ENGINEERING"
2028
- license_name (str): name of the license to assign, e.g. "EXTENDED_ECM" or "INTELLGENT_VIEWIMG"
3140
+ user_name (str):
3141
+ The login name of the OTDS user.
3142
+ resource_id (str):
3143
+ The OTDS resource ID (this is ID not the resource name!).
3144
+ license_feature (str):
3145
+ The name of the license feature, e.g. "X2" or "ADDON_ENGINEERING".
3146
+ license_name (str):
3147
+ The name of the license to assign, e.g. "EXTENDED_ECM" or "INTELLGENT_VIEWIMG".
2029
3148
 
2030
3149
  Returns:
2031
- bool: True if the user is licensed and False otherwise
3150
+ bool:
3151
+ True if the user is licensed and False otherwise.
3152
+
2032
3153
  """
2033
3154
 
2034
3155
  response = self.get_licensed_objects(
@@ -2050,26 +3171,33 @@ class OTDS:
2050
3171
  None,
2051
3172
  )
2052
3173
 
2053
- if user:
2054
- return True
2055
-
2056
- return False
3174
+ return bool(user)
2057
3175
 
2058
3176
  # end method definition
2059
3177
 
2060
3178
  def is_group_licensed(
2061
- self, group_name: str, resource_id: str, license_feature: str, license_name: str
3179
+ self,
3180
+ group_name: str,
3181
+ resource_id: str,
3182
+ license_feature: str,
3183
+ license_name: str,
2062
3184
  ) -> bool:
2063
3185
  """Check if a group is licensed for a license and license feature associated with a particular OTDS resource.
2064
3186
 
2065
3187
  Args:
2066
- group_name (str): name of the OTDS user group
2067
- resource_id (str): OTDS resource ID (this is ID not the resource name!)
2068
- license_feature (str): name of the license feature, e.g. "X2" or "ADDON_ENGINEERING"
2069
- license_name (str): name of the license to assign, e.g. "EXTENDED_ECM" or "INTELLGENT_VIEWIMG"
3188
+ group_name (str):
3189
+ The name of the OTDS user group.
3190
+ resource_id (str):
3191
+ The OTDS resource ID (this is ID not the resource name!).
3192
+ license_feature (str):
3193
+ The name of the license feature, e.g. "X2" or "ADDON_ENGINEERING".
3194
+ license_name (str):
3195
+ The name of the license to assign, e.g. "EXTENDED_ECM" or "INTELLGENT_VIEWIMG".
2070
3196
 
2071
3197
  Returns:
2072
- bool: True if the group is licensed and False otherwise
3198
+ bool:
3199
+ True if the group is licensed and False otherwise.
3200
+
2073
3201
  """
2074
3202
 
2075
3203
  response = self.get_licensed_objects(
@@ -2091,10 +3219,7 @@ class OTDS:
2091
3219
  None,
2092
3220
  )
2093
3221
 
2094
- if group:
2095
- return True
2096
-
2097
- return False
3222
+ return bool(group)
2098
3223
 
2099
3224
  # end method definition
2100
3225
 
@@ -2105,16 +3230,22 @@ class OTDS:
2105
3230
  license_feature: str,
2106
3231
  license_name: str,
2107
3232
  ) -> bool:
2108
- """Check if a partition is licensed for a license and license feature associated with a particular OTDS resource.
3233
+ """Check if a partition is licensed for a license feature associated with a particular OTDS resource.
2109
3234
 
2110
3235
  Args:
2111
- partition_name (str): name of the OTDS user partition, e.g. "Content Server Members"
2112
- resource_id (str): OTDS resource ID (this is ID not the resource name!)
2113
- license_feature (str): name of the license feature, e.g. "X2" or "ADDON_ENGINEERING"
2114
- license_name (str): name of the license to assign, e.g. "EXTENDED_ECM" or "INTELLGENT_VIEWIMG"
3236
+ partition_name (str):
3237
+ The name of the OTDS user partition, e.g. "Content Server Members".
3238
+ resource_id (str):
3239
+ The OTDS resource ID (this is ID not the resource name!).
3240
+ license_feature (str):
3241
+ The name of the license feature, e.g. "X2" or "ADDON_ENGINEERING".
3242
+ license_name (str):
3243
+ The name of the license to assign, e.g. "EXTENDED_ECM" or "INTELLGENT_VIEWIMG".
2115
3244
 
2116
3245
  Returns:
2117
- bool: True if the partition is licensed and False otherwise
3246
+ bool:
3247
+ True if the partition is licensed and False otherwise.
3248
+
2118
3249
  """
2119
3250
 
2120
3251
  response = self.get_licensed_objects(
@@ -2136,124 +3267,117 @@ class OTDS:
2136
3267
  None,
2137
3268
  )
2138
3269
 
2139
- if partition:
2140
- return True
2141
-
2142
- return False
3270
+ return bool(partition)
2143
3271
 
2144
3272
  # end method definition
2145
-
2146
- def import_synchronized_partition_members(self, name: str) -> dict:
2147
- """Import users and groups to partition
3273
+
3274
+ def import_synchronized_partition_members(self, name: str) -> bool:
3275
+ """Import users and groups to partition.
2148
3276
 
2149
3277
  Args:
2150
- name (str): name of the partition in which users need to be imported
3278
+ name (str):
3279
+ The name of the partition in which users need to be imported.
3280
+
2151
3281
  Returns:
2152
- dict: Request response or None if the creation fails.
3282
+ bool:
3283
+ True = Success, False = Error.
3284
+
2153
3285
  """
3286
+
2154
3287
  command = {"command": "import"}
2155
- request_url = self.synchronized_partition_url() + f'/{name}/command'
2156
- logger.debug(
2157
- "Importing users and groups in to partition -> %s; calling -> %s",
3288
+ request_url = self.synchronized_partition_url() + f"/{name}/command"
3289
+
3290
+ self.logger.debug(
3291
+ "Importing users and groups into partition -> '%s'; calling -> %s",
2158
3292
  name,
2159
3293
  request_url,
2160
3294
  )
2161
- retries = 0
2162
- while True:
2163
- response = requests.post(
2164
- url=request_url,
2165
- json=command,
2166
- headers=REQUEST_HEADERS,
2167
- cookies=self.cookie(),
2168
- timeout=None,
2169
- )
2170
- if response.status_code == 204:
2171
- return True
2172
- # Check if Session has expired - then re-authenticate and try once more
2173
- elif response.status_code == 401 and retries == 0:
2174
- logger.debug("Session has expired - try to re-authenticate...")
2175
- self.authenticate(revalidate=True)
2176
- retries += 1
2177
- else:
2178
- logger.error(
2179
- "Failed to Import users and groups to synchronized partition -> %s; error -> %s (%s)",
2180
- name,
2181
- response.text,
2182
- response.status_code,
2183
- )
2184
- return None
2185
-
3295
+
3296
+ response = self.do_request(
3297
+ url=request_url,
3298
+ method="POST",
3299
+ json_data=command,
3300
+ timeout=None,
3301
+ failure_message="Failed to import users and groups to synchronized partition -> '{}'".format(
3302
+ name,
3303
+ ),
3304
+ parse_request_response=False,
3305
+ )
3306
+
3307
+ return bool(response and response.ok and response.status_code == 204)
3308
+
2186
3309
  # end of method definition
2187
-
2188
- def add_synchronized_partition(self, name: str, description: str, data: str) -> dict:
2189
- """Add a new synchronized partition to OTDS
3310
+
3311
+ def add_synchronized_partition(
3312
+ self,
3313
+ name: str,
3314
+ description: str,
3315
+ data: dict,
3316
+ ) -> dict | None:
3317
+ """Add a new synchronized partition to OTDS.
2190
3318
 
2191
3319
  Args:
2192
- name (str): name of the new partition
2193
- description (str): description of the new partition
2194
- data (dict): data for creating synchronized partition
3320
+ name (str):
3321
+ The name of the new synchronized partition.
3322
+ description (str):
3323
+ The description of the new synchronized partition.
3324
+ data (dict):
3325
+ The data for creating synchronized partition
3326
+
2195
3327
  Returns:
2196
- dict: Request response or None if the creation fails.
3328
+ dict | None:
3329
+ Request response or None if the creation fails.
3330
+
2197
3331
  """
2198
- synchronizedPartitionPostBodyJson = {
2199
- "ipConnectionParameter": [
2200
- ],
2201
- "ipAuthentication": {
2202
- },
2203
- "objectClassNameMapping": [
2204
-
2205
- ],
2206
- "basicInfo": {
2207
- },
2208
- "basicAttributes": []
3332
+
3333
+ synchronized_partition_post_body_json = {
3334
+ "ipConnectionParameter": [],
3335
+ "ipAuthentication": {},
3336
+ "objectClassNameMapping": [],
3337
+ "basicInfo": {},
3338
+ "basicAttributes": [],
2209
3339
  }
2210
- synchronizedPartitionPostBodyJson.update(data)
3340
+ synchronized_partition_post_body_json.update(data)
3341
+
2211
3342
  request_url = self.synchronized_partition_url()
2212
- logger.debug(
2213
- "Adding synchronized partition -> %s (%s); calling -> %s",
3343
+ self.logger.debug(
3344
+ "Adding synchronized partition -> '%s' ('%s'); calling -> %s",
2214
3345
  name,
2215
3346
  description,
2216
3347
  request_url,
2217
3348
  )
2218
- synchronizedPartitionPostBodyJson["ipAuthentication"]["bindPassword"] = self.config()["bindPassword"]
2219
- retries = 0
2220
- while True:
2221
- response = requests.post(
2222
- url=request_url,
2223
- json=synchronizedPartitionPostBodyJson,
2224
- headers=REQUEST_HEADERS,
2225
- cookies=self.cookie(),
2226
- timeout=None,
2227
- )
2228
- if response.ok:
2229
- return self.parse_request_response(response)
2230
- # Check if Session has expired - then re-authenticate and try once more
2231
- elif response.status_code == 401 and retries == 0:
2232
- logger.debug("Session has expired - try to re-authenticate...")
2233
- self.authenticate(revalidate=True)
2234
- retries += 1
2235
- else:
2236
- logger.error(
2237
- "Failed to add synchronized partition -> %s; error -> %s (%s)",
2238
- name,
2239
- response.text,
2240
- response.status_code,
2241
- )
2242
- return None
2243
-
3349
+ synchronized_partition_post_body_json["ipAuthentication"]["bindPassword"] = self.config()["bindPassword"]
3350
+
3351
+ return self.do_request(
3352
+ url=request_url,
3353
+ method="POST",
3354
+ json_data=synchronized_partition_post_body_json,
3355
+ timeout=None,
3356
+ failure_message="Failed to add synchronized partition -> '{}'".format(name),
3357
+ )
3358
+
2244
3359
  # end of method definition
2245
3360
 
2246
3361
  def add_system_attribute(
2247
- self, name: str, value: str, description: str = ""
3362
+ self,
3363
+ name: str,
3364
+ value: str,
3365
+ description: str = "",
2248
3366
  ) -> dict | None:
2249
- """Add a new system attribute to OTDS
3367
+ """Add a new system attribute to OTDS.
2250
3368
 
2251
3369
  Args:
2252
- name (str): name of the new system attribute
2253
- value (str): value of the system attribute
2254
- description (str, optional): optional description of the system attribute
3370
+ name (str):
3371
+ The name of the new system attribute.
3372
+ value (str):
3373
+ The value of the system attribute.
3374
+ description (str, optional):
3375
+ The optional description of the system attribute.
3376
+
2255
3377
  Returns:
2256
- dict: Request response (dictionary) or None if the REST call fails.
3378
+ dict | None:
3379
+ Request response (dictionary) or None if the REST call fails.
3380
+
2257
3381
  """
2258
3382
 
2259
3383
  system_attribute_post_body_json = {
@@ -2265,7 +3389,7 @@ class OTDS:
2265
3389
  request_url = "{}/system_attributes".format(self.config()["systemConfigUrl"])
2266
3390
 
2267
3391
  if description:
2268
- logger.debug(
3392
+ self.logger.debug(
2269
3393
  "Add system attribute -> '%s' ('%s') with value -> %s; calling -> %s",
2270
3394
  name,
2271
3395
  description,
@@ -2273,7 +3397,7 @@ class OTDS:
2273
3397
  request_url,
2274
3398
  )
2275
3399
  else:
2276
- logger.debug(
3400
+ self.logger.debug(
2277
3401
  "Add system attribute -> '%s' with value -> %s; calling -> %s",
2278
3402
  name,
2279
3403
  value,
@@ -2286,24 +3410,28 @@ class OTDS:
2286
3410
  json_data=system_attribute_post_body_json,
2287
3411
  timeout=None,
2288
3412
  failure_message="Failed to add system attribute -> '{}' with value -> '{}'".format(
2289
- name, value
3413
+ name,
3414
+ value,
2290
3415
  ),
2291
3416
  )
2292
3417
 
2293
3418
  # end method definition
2294
3419
 
2295
3420
  def get_trusted_sites(self) -> dict | None:
2296
- """Get all configured OTDS trusted sites
3421
+ """Get all configured OTDS trusted sites.
2297
3422
 
2298
3423
  Args:
2299
3424
  None
3425
+
2300
3426
  Returns:
2301
- dict: Request response or None if the REST call fails.
3427
+ dict | None:
3428
+ Request response or None if the REST call fails.
3429
+
2302
3430
  """
2303
3431
 
2304
3432
  request_url = "{}/whitelist".format(self.config()["systemConfigUrl"])
2305
3433
 
2306
- logger.debug("Get trusted sites; calling -> %s", request_url)
3434
+ self.logger.debug("Get trusted sites; calling -> %s", request_url)
2307
3435
 
2308
3436
  return self.do_request(
2309
3437
  url=request_url,
@@ -2315,12 +3443,16 @@ class OTDS:
2315
3443
  # end method definition
2316
3444
 
2317
3445
  def add_trusted_site(self, trusted_site: str) -> dict | None:
2318
- """Add a new OTDS trusted site
3446
+ """Add a new OTDS trusted site.
2319
3447
 
2320
3448
  Args:
2321
- trusted_site (str): name of the new trusted site
2322
- Return:
2323
- dict: Request response or None if the REST call fails.
3449
+ trusted_site (str):
3450
+ The name of the new trusted site.
3451
+
3452
+ Returns:
3453
+ dict | None:
3454
+ Request response or None if the REST call fails.
3455
+
2324
3456
  """
2325
3457
 
2326
3458
  trusted_site_post_body_json = {"stringList": [trusted_site]}
@@ -2331,13 +3463,15 @@ class OTDS:
2331
3463
 
2332
3464
  if existing_trusted_sites:
2333
3465
  trusted_site_post_body_json["stringList"].extend(
2334
- existing_trusted_sites["stringList"]
3466
+ existing_trusted_sites["stringList"],
2335
3467
  )
2336
3468
 
2337
3469
  request_url = "{}/whitelist".format(self.config()["systemConfigUrl"])
2338
3470
 
2339
- logger.debug(
2340
- "Add trusted site -> '%s'; calling -> %s", trusted_site, request_url
3471
+ self.logger.debug(
3472
+ "Add trusted site -> '%s'; calling -> %s",
3473
+ trusted_site,
3474
+ request_url,
2341
3475
  )
2342
3476
 
2343
3477
  response = self.do_request(
@@ -2349,27 +3483,38 @@ class OTDS:
2349
3483
  parse_request_response=False, # don't parse it!
2350
3484
  )
2351
3485
 
2352
- if not response.ok:
3486
+ if not response or not response.ok:
2353
3487
  return None
2354
3488
 
2355
3489
  return response
2356
3490
 
2357
3491
  # end method definition
2358
3492
 
2359
- def enable_audit(self):
2360
- """enable OTDS Audit
3493
+ def enable_audit(
3494
+ self,
3495
+ enable: bool = True,
3496
+ days_to_keep: int = 7,
3497
+ event_types: list | None = None,
3498
+ ) -> dict | None:
3499
+ """Enable the OTDS Audit.
2361
3500
 
2362
3501
  Args:
2363
- None
2364
- Return:
2365
- Request response (json) or None if the REST call fails.
3502
+ enable (bool, optional):
3503
+ True = enable audit, False = disable audit.
3504
+ days_to_keep (int, optional):
3505
+ Days to keep the audit information. Default = 7 Days.
3506
+ event_types (list, optional):
3507
+ A list of event types to record.
3508
+ If None then a default list will be used.
3509
+
3510
+ Returns:
3511
+ dict | None:
3512
+ Request response or None if the REST call fails.
3513
+
2366
3514
  """
2367
3515
 
2368
- audit_put_body_json = {
2369
- "daysToKeep": "7",
2370
- "enabled": "true",
2371
- "auditTo": "DATABASE",
2372
- "eventIDs": [
3516
+ if event_types is None:
3517
+ event_types = [
2373
3518
  "User Create",
2374
3519
  "Group Create",
2375
3520
  "User Delete",
@@ -2417,19 +3562,30 @@ class OTDS:
2417
3562
  "Tenant Delete",
2418
3563
  "Tenant Modify",
2419
3564
  "Migration",
2420
- ],
3565
+ ]
3566
+
3567
+ audit_put_body_json = {
3568
+ "daysToKeep": str(days_to_keep),
3569
+ "enabled": enable,
3570
+ "auditTo": "DATABASE",
3571
+ "eventIDs": event_types,
2421
3572
  }
2422
3573
 
2423
3574
  request_url = "{}/audit".format(self.config()["systemConfigUrl"])
2424
3575
 
2425
- logger.debug("Enable audit; calling -> %s", request_url)
3576
+ if enable:
3577
+ self.logger.debug("Enable audit; calling -> %s", request_url)
3578
+ failure_message = "Failed to enable audit"
3579
+ else:
3580
+ self.logger.debug("Disable audit; calling -> %s", request_url)
3581
+ failure_message = "Failed to disable audit"
2426
3582
 
2427
3583
  return self.do_request(
2428
3584
  url=request_url,
2429
3585
  method="PUT",
2430
3586
  json_data=audit_put_body_json,
2431
3587
  timeout=None,
2432
- failure_message="Failed to enable audit",
3588
+ failure_message=failure_message,
2433
3589
  parse_request_response=False,
2434
3590
  )
2435
3591
 
@@ -2447,21 +3603,33 @@ class OTDS:
2447
3603
  default_scopes: list | None = None, # in OTDS UI: Default scopes
2448
3604
  secret: str = "",
2449
3605
  ) -> dict | None:
2450
- """Add a new OAuth client to OTDS
3606
+ """Add a new OAuth client to OTDS.
2451
3607
 
2452
3608
  Args:
2453
- client_id (str): name of the new OAuth client (should not have blanks)
2454
- description (str): description of the OAuth client
2455
- redirect_urls (list): list of redirect URLs (strings)
2456
- allow_impersonation (bool, optional): allow impresonation
2457
- confidential (bool, optional): is confidential
2458
- auth_scopes (list, optional): if empty then "Global"
2459
- allowed_scopes (list, optional): in OTDS UI this is called Permissible scopes
2460
- default_scopes (list, optional): in OTDS UI this is called Default scopes
2461
- secret (str, optional): predefined OAuth client secret. If empty a new secret is generated.
3609
+ client_id (str):
3610
+ The name of the new OAuth client (should not have blanks).
3611
+ description (str):
3612
+ The description of the OAuth client.
3613
+ redirect_urls (list):
3614
+ A list of redirect URLs (strings).
3615
+ allow_impersonation (bool, optional):
3616
+ Whether or not to allow impersonation.
3617
+ confidential (bool, optional):
3618
+ is confidential
3619
+ auth_scopes (list, optional):
3620
+ The authorization scope. If empty then "Global" is assumed.
3621
+ allowed_scopes (list, optional):
3622
+ In the OTDS UI this is called Permissible scopes.
3623
+ default_scopes (list, optional):
3624
+ In the OTDS UI this is called Default scopes.
3625
+ secret (str, optional):
3626
+ Predefined OAuth client secret. If empty a new secret is generated.
3627
+
2462
3628
  Returns:
2463
- dict: Request response or None if the creation fails.
2464
- Example:
3629
+ dict | None:
3630
+ Request response or None if the creation fails.
3631
+
3632
+ Example:
2465
3633
  {
2466
3634
  "description": "string",
2467
3635
  "redirectURLs": [
@@ -2503,6 +3671,7 @@ class OTDS:
2503
3671
  "urlId": "string",
2504
3672
  "urlLocation": "string"
2505
3673
  }
3674
+
2506
3675
  """
2507
3676
 
2508
3677
  # Avoid linter warning W0102:
@@ -2537,7 +3706,7 @@ class OTDS:
2537
3706
 
2538
3707
  request_url = self.oauth_client_url()
2539
3708
 
2540
- logger.debug(
3709
+ self.logger.debug(
2541
3710
  "Adding oauth client -> '%s' (%s); calling -> %s",
2542
3711
  description,
2543
3712
  client_id,
@@ -2555,18 +3724,27 @@ class OTDS:
2555
3724
  # end method definition
2556
3725
 
2557
3726
  def get_oauth_client(self, client_id: str, show_error: bool = True) -> dict | None:
2558
- """Get an existing OAuth client from OTDS
3727
+ """Get an existing OAuth client from OTDS.
2559
3728
 
2560
3729
  Args:
2561
- client_id (str): name (= ID) of the OAuth client to retrieve
2562
- show_error (bool): whether or not we want to log an error if partion is not found
3730
+ client_id (str):
3731
+ The name (= ID) of the OAuth client to retrieve
3732
+ show_error (bool, optional):
3733
+ Whether or not we want to log an error if partion is not found.
3734
+
2563
3735
  Returns:
2564
- dict: Request response (dictionary) or None if the client is not found.
3736
+ dict | None:
3737
+ Request response (dictionary) or None if the client is not found.
3738
+
2565
3739
  """
2566
3740
 
2567
3741
  request_url = "{}/{}".format(self.oauth_client_url(), client_id)
2568
3742
 
2569
- logger.debug("Get oauth client -> '%s'; calling -> %s", client_id, request_url)
3743
+ self.logger.debug(
3744
+ "Get oauth client -> '%s'; calling -> %s",
3745
+ client_id,
3746
+ request_url,
3747
+ )
2570
3748
 
2571
3749
  return self.do_request(
2572
3750
  url=request_url,
@@ -2579,22 +3757,26 @@ class OTDS:
2579
3757
  # end method definition
2580
3758
 
2581
3759
  def update_oauth_client(self, client_id: str, updates: dict) -> dict | None:
2582
- """Updates the OAuth client with new values
3760
+ """Update an OAuth client with new values.
2583
3761
 
2584
3762
  Args:
2585
- client_id (str): name (= ID) of the OAuth client
2586
- updates (dict): new values for OAuth client, e.g.
2587
- {"description": "this is the new value"}
3763
+ client_id (str):
3764
+ The name (= ID) of the OAuth client.
3765
+ updates (dict):
3766
+ New values for OAuth client, e.g.
3767
+ {"description": "this is the new value"}
2588
3768
 
2589
3769
  Returns:
2590
- dict: Request response (json) or None if the REST call fails.
3770
+ dict | None:
3771
+ Request response (json) or None if the REST call fails.
3772
+
2591
3773
  """
2592
3774
 
2593
3775
  oauth_client_patch_body_json = updates
2594
3776
 
2595
3777
  request_url = "{}/{}".format(self.oauth_client_url(), client_id)
2596
3778
 
2597
- logger.debug(
3779
+ self.logger.debug(
2598
3780
  "Update OAuth client -> '%s' with -> %s; calling -> %s",
2599
3781
  client_id,
2600
3782
  str(updates),
@@ -2611,19 +3793,25 @@ class OTDS:
2611
3793
 
2612
3794
  # end method definition
2613
3795
 
2614
- def add_oauth_clients_to_access_role(self, access_role_name: str):
2615
- """Add Oauth clients user partion to an OTDS Access Role
3796
+ def add_oauth_clients_to_access_role(self, access_role_name: str) -> dict | None:
3797
+ """Add OAuth clients (in the "OAuthClients" partition) to an OTDS access role.
2616
3798
 
2617
3799
  Args:
2618
- access_role_name (str): name of the OTDS Access Role
3800
+ access_role_name (str):
3801
+ The name of the OTDS access role.
3802
+
2619
3803
  Returns:
2620
- response of REST call or None in case of an error
3804
+ dict | None:
3805
+ Response of REST call or None in case of an error.
3806
+
2621
3807
  """
2622
3808
 
2623
3809
  request_url = self.config()["accessRoleUrl"] + "/" + access_role_name
2624
3810
 
2625
- logger.debug(
2626
- "Get access role -> '%s'; calling -> %s", access_role_name, request_url
3811
+ self.logger.debug(
3812
+ "Get access role -> '%s'; calling -> %s",
3813
+ access_role_name,
3814
+ request_url,
2627
3815
  )
2628
3816
 
2629
3817
  access_role = self.do_request(
@@ -2631,7 +3819,7 @@ class OTDS:
2631
3819
  method="GET",
2632
3820
  timeout=None,
2633
3821
  failure_message="Failed to retrieve access role -> '{}'".format(
2634
- access_role_name
3822
+ access_role_name,
2635
3823
  ),
2636
3824
  )
2637
3825
  if not access_role:
@@ -2641,7 +3829,7 @@ class OTDS:
2641
3829
  user_partitions = access_role["accessRoleMembers"]["userPartitions"]
2642
3830
  for user_partition in user_partitions:
2643
3831
  if user_partition["userPartition"] == "OAuthClients":
2644
- logger.error(
3832
+ self.logger.error(
2645
3833
  "OAuthClients partition already added to role -> %s",
2646
3834
  access_role_name,
2647
3835
  )
@@ -2656,7 +3844,7 @@ class OTDS:
2656
3844
  method="GET",
2657
3845
  timeout=None,
2658
3846
  failure_message="Failed to get partition info for OAuthClients for role -> '{}'".format(
2659
- access_role_name
3847
+ access_role_name,
2660
3848
  ),
2661
3849
  )
2662
3850
  if not response:
@@ -2671,7 +3859,7 @@ class OTDS:
2671
3859
  "userPartition": None,
2672
3860
  }
2673
3861
  access_role["accessRoleMembers"]["organizationalUnits"].append(
2674
- oauth_clients_ou_block
3862
+ oauth_clients_ou_block,
2675
3863
  )
2676
3864
 
2677
3865
  return self.do_request(
@@ -2679,7 +3867,7 @@ class OTDS:
2679
3867
  method="PUT",
2680
3868
  timeout=None,
2681
3869
  warning_message="Failed to add OAuthClients to access role -> '{}'".format(
2682
- access_role_name
3870
+ access_role_name,
2683
3871
  ),
2684
3872
  show_error=False,
2685
3873
  show_warning=True,
@@ -2689,39 +3877,42 @@ class OTDS:
2689
3877
  # end method definition
2690
3878
 
2691
3879
  def get_access_token(self, client_id: str, client_secret: str) -> str | None:
2692
- """Get the access token
3880
+ """Get the access token.
2693
3881
 
2694
3882
  Args:
2695
- client_id (str): OAuth client name (= ID)
2696
- client_secret (str): OAuth client secret. This is typically returned
2697
- by add_oauth_client() method in ["secret"] field
3883
+ client_id (str):
3884
+ The OAuth client name (= ID).
3885
+ client_secret (str):
3886
+ The OAuth client secret. This is typically returned
3887
+ by add_oauth_client() method in ["secret"] field.
2698
3888
 
2699
3889
  Returns:
2700
- str: access token, or None
3890
+ str | None:
3891
+ The access token, or None in case of an error.
3892
+
2701
3893
  """
2702
3894
 
2703
3895
  encoded_client_secret = "{}:{}".format(client_id, client_secret).encode("utf-8")
2704
- accessTokenRequestHeaders = {
2705
- "Authorization": "Basic "
2706
- + base64.b64encode(encoded_client_secret).decode("utf-8"),
3896
+
3897
+ request_header = {
3898
+ "Authorization": "Basic " + base64.b64encode(encoded_client_secret).decode("utf-8"),
2707
3899
  "Content-Type": "application/x-www-form-urlencoded",
2708
3900
  }
2709
-
2710
3901
  request_url = self.token_url()
2711
3902
 
2712
3903
  response = requests.post(
2713
3904
  url=request_url,
2714
3905
  data={"grant_type": "client_credentials"},
2715
- headers=accessTokenRequestHeaders,
2716
- timeout=None,
3906
+ headers=request_header,
3907
+ timeout=REQUEST_TIMEOUT,
2717
3908
  )
2718
3909
 
2719
3910
  access_token = None
2720
3911
  if response.ok:
2721
- accessTokenJson = self.parse_request_response(response)
3912
+ access_token = self.parse_request_response(response)
2722
3913
 
2723
- if "access_token" in accessTokenJson:
2724
- access_token = accessTokenJson["access_token"]
3914
+ if "access_token" in access_token:
3915
+ access_token = access_token["access_token"]
2725
3916
  else:
2726
3917
  return None
2727
3918
 
@@ -2733,12 +3924,17 @@ class OTDS:
2733
3924
  """Get the OTDS auth handler with a given name.
2734
3925
 
2735
3926
  Args:
2736
- name (str): Name of the authentication handler
3927
+ name (str):
3928
+ The name of the authentication handler
3929
+ show_error (bool, optional):
3930
+ Whether or not an error should be logged in case of a failed REST call.
3931
+ If False, then only a warning is logged. Defaults to True.
2737
3932
 
2738
3933
  Returns:
2739
- dict | None: auth handler dictionary, or None
3934
+ dict | None:
3935
+ The auth handler dictionary, or None in case of an error.
2740
3936
 
2741
- Example result:
3937
+ Example:
2742
3938
  {
2743
3939
  '_name': 'Salesforce',
2744
3940
  '_id': 'Salesforce',
@@ -2775,8 +3971,10 @@ class OTDS:
2775
3971
 
2776
3972
  request_url = "{}/{}".format(self.auth_handler_url(), name)
2777
3973
 
2778
- logger.debug(
2779
- "Getting authentication handler -> '%s'; calling -> %s", name, request_url
3974
+ self.logger.debug(
3975
+ "Getting authentication handler -> '%s'; calling -> %s",
3976
+ name,
3977
+ request_url,
2780
3978
  )
2781
3979
 
2782
3980
  return self.do_request(
@@ -2803,26 +4001,40 @@ class OTDS:
2803
4001
  auth_principal_attributes: list | None = None,
2804
4002
  nameid_format: str = "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified",
2805
4003
  ) -> dict | None:
2806
- """Add a new SAML authentication handler
4004
+ """Add a new SAML authentication handler.
2807
4005
 
2808
4006
  Args:
2809
- name (str): name of the new authentication handler
2810
- description (str): description of the new authentication handler
2811
- scope (str): name of the user partition (to define a scope of the auth handler)
2812
- provider_name (str): description of the new authentication handler
2813
- saml_url (str): SAML URL
2814
- otds_sp_endpoint (str): the external(!) service provider URL of OTDS
2815
- enabled (bool, optional): if the handler should be enabled or disabled. Default is True = enabled.
2816
- priority (int, optional): Priority of the Authentical Handler (compared to others). Default is 5
2817
- active_by_default (bool, optional): should OTDS redirect immediately to provider page
2818
- (not showing the OTDS login at all)
2819
- auth_principal_attributes (list, optional): List of Authentication principal attributes
2820
- nameid_format (str, optional): Specifies which NameID format supported by the identity provider
2821
- contains the desired user identifier. The value in this identifier
2822
- must correspond to the value of the user attribute specified for the
2823
- authentication principal attribute.
4007
+ name (str):
4008
+ The name of the new authentication handler.
4009
+ description (str):
4010
+ The description of the new authentication handler.
4011
+ scope (str):
4012
+ The name of the user partition (to define a scope of the auth handler)
4013
+ provider_name (str):
4014
+ The description of the new authentication handler.
4015
+ saml_url (str):
4016
+ The SAML URL.
4017
+ otds_sp_endpoint (str):
4018
+ The external(!) service provider URL of OTDS.
4019
+ enabled (bool, optional):
4020
+ Defines if the handler should be enabled or disabled. Default is True = enabled.
4021
+ priority (int, optional):
4022
+ Priority of the Authentical Handler (compared to others). Default is 5
4023
+ active_by_default (bool, optional):
4024
+ Defines whether OTDS should redirect immediately to provider page
4025
+ (not showing the OTDS login at all).
4026
+ auth_principal_attributes (list, optional):
4027
+ List of Authentication principal attributes
4028
+ nameid_format (str, optional):
4029
+ Specifies which NameID format supported by the identity provider
4030
+ contains the desired user identifier. The value in this identifier
4031
+ must correspond to the value of the user attribute specified for the
4032
+ authentication principal attribute.
4033
+
2824
4034
  Returns:
2825
- dict: Request response (dictionary) or None if the REST call fails.
4035
+ dict | None:
4036
+ Request response (dictionary) or None if the REST call fails.
4037
+
2826
4038
  """
2827
4039
 
2828
4040
  if auth_principal_attributes is None:
@@ -3110,7 +4322,7 @@ class OTDS:
3110
4322
 
3111
4323
  request_url = self.auth_handler_url()
3112
4324
 
3113
- logger.debug(
4325
+ self.logger.debug(
3114
4326
  "Adding SAML auth handler -> '%s' ('%s'); calling -> %s",
3115
4327
  name,
3116
4328
  description,
@@ -3137,23 +4349,33 @@ class OTDS:
3137
4349
  enabled: bool = True,
3138
4350
  priority: int = 10,
3139
4351
  auth_principal_attributes: list | None = None,
3140
- ):
3141
- """Add a new SAP authentication handler
4352
+ ) -> dict | None:
4353
+ """Add a new SAP authentication handler.
3142
4354
 
3143
4355
  Args:
3144
- name (str): name of the new authentication handler
3145
- description (str): description of the new authentication handler
3146
- scope (str): name of the user partition (to define a scope of the auth handler)
3147
- certificate_file (str): fully qualified file name (with path) to the certificate file
3148
- certificate_password (str): password of the certificate
3149
- enabled (bool, optional): if the handler should be enabled or disabled. Default is True = enabled.
3150
- priority (int, optional): Priority of the Authentical Handler (compared to others). Default is 5
3151
- auth_principal_attributes (list, optional): List of Authentication principal attributes
4356
+ name (str):
4357
+ The name of the new authentication handler.
4358
+ description (str):
4359
+ The description of the new authentication handler.
4360
+ scope (str):
4361
+ The name of the user partition (to define a scope of the auth handler)
4362
+ certificate_file (str):
4363
+ A fully qualified file name (with path) to the certificate file.
4364
+ certificate_password (str):
4365
+ The password of the certificate.
4366
+ enabled (bool, optional):
4367
+ Defines if the handler should be enabled or disabled. Default is True = enabled.
4368
+ priority (int, optional):
4369
+ Priority of the Authentical Handler (compared to others). Default is 10.
4370
+ auth_principal_attributes (list, optional):
4371
+ List of Authentication principal attributes.
4372
+
3152
4373
  Returns:
3153
- Request response (json) or None if the REST call fails.
4374
+ dict | None: Request response (json) or None if the REST call fails.
4375
+
3154
4376
  """
3155
4377
 
3156
- # Avoid linter warning W0102:
4378
+ # Avoid linter warning W0102 by establishing the default value inside the method:
3157
4379
  if auth_principal_attributes is None:
3158
4380
  auth_principal_attributes = ["oTExternalID1"]
3159
4381
 
@@ -3176,7 +4398,7 @@ class OTDS:
3176
4398
  "_fileName": False,
3177
4399
  "_fileExtensions": None,
3178
4400
  "_value": os.path.basename(
3179
- certificate_file
4401
+ certificate_file,
3180
4402
  ), # "TM6_Sandbox.pse" - file name only
3181
4403
  "_allowedValues": None,
3182
4404
  "_confidential": False,
@@ -3214,7 +4436,7 @@ class OTDS:
3214
4436
  # 2. Create the auth handler in OTDS
3215
4437
  request_url = self.auth_handler_url()
3216
4438
 
3217
- logger.debug(
4439
+ self.logger.debug(
3218
4440
  "Adding SAP auth handler -> '%s' ('%s'); calling -> %s",
3219
4441
  name,
3220
4442
  description,
@@ -3235,21 +4457,21 @@ class OTDS:
3235
4457
  # 3. Upload the certificate file:
3236
4458
 
3237
4459
  # Check that the certificate (PSE) file is readable:
3238
- logger.debug("Reading certificate file -> '%s'...", certificate_file)
4460
+ self.logger.debug("Reading certificate file -> '%s'...", certificate_file)
3239
4461
  try:
3240
4462
  # PSE files are binary - so we need to open with "rb":
3241
4463
  with open(certificate_file, "rb") as cert_file:
3242
4464
  cert_content = cert_file.read()
3243
4465
  if not cert_content:
3244
- logger.error(
3245
- "No data in certificate file -> '%s'", certificate_file
4466
+ self.logger.error(
4467
+ "No data in certificate file -> '%s'",
4468
+ certificate_file,
3246
4469
  )
3247
4470
  return None
3248
- except IOError as exception:
3249
- logger.error(
3250
- "Unable to open certificate file -> '%s'; error -> %s",
4471
+ except OSError:
4472
+ self.logger.error(
4473
+ "Unable to open certificate file -> '%s'!",
3251
4474
  certificate_file,
3252
- exception.strerror,
3253
4475
  )
3254
4476
  return None
3255
4477
 
@@ -3257,56 +4479,49 @@ class OTDS:
3257
4479
  # base64 encoded we will decode it and write it back into the same file
3258
4480
  try:
3259
4481
  # If file is not base64 encoded the next statement will throw an exception
3260
- # (this is good)
3261
4482
  cert_content_decoded = base64.b64decode(cert_content, validate=True)
3262
4483
  cert_content_encoded = base64.b64encode(cert_content_decoded).decode(
3263
- "utf-8"
4484
+ "utf-8",
3264
4485
  )
3265
4486
  if cert_content_encoded == cert_content.decode("utf-8"):
3266
- logger.debug(
3267
- "Certificate file -> '%s' is base64 encoded", certificate_file
4487
+ self.logger.debug(
4488
+ "Certificate file -> '%s' is base64 encoded",
4489
+ certificate_file,
3268
4490
  )
3269
4491
  cert_file_encoded = True
3270
4492
  else:
3271
4493
  cert_file_encoded = False
3272
4494
  except TypeError:
3273
- logger.debug(
3274
- "Certificate file -> '%s' is not base64 encoded", certificate_file
4495
+ self.logger.debug(
4496
+ "Certificate file -> '%s' is not base64 encoded",
4497
+ certificate_file,
3275
4498
  )
3276
4499
  cert_file_encoded = False
3277
4500
 
3278
4501
  if cert_file_encoded:
3279
- certificate_file = "/tmp/" + os.path.basename(certificate_file)
3280
- logger.debug("Writing decoded certificate file -> %s...", certificate_file)
4502
+ certificate_file = os.path.join(tempfile.gettempdir(), os.path.basename(certificate_file))
4503
+ self.logger.debug(
4504
+ "Writing decoded certificate file -> %s...",
4505
+ certificate_file,
4506
+ )
3281
4507
  try:
3282
4508
  # PSE files need to be binary - so we need to open with "wb":
3283
4509
  with open(certificate_file, "wb") as cert_file:
3284
4510
  cert_file.write(base64.b64decode(cert_content))
3285
- except IOError as exception:
3286
- logger.error(
3287
- "Failed writing to file -> '%s'; error -> %s",
4511
+ except OSError:
4512
+ self.logger.error(
4513
+ "Failed writing to file -> '%s'!",
3288
4514
  certificate_file,
3289
- exception.strerror,
3290
4515
  )
3291
4516
  return None
3292
4517
 
3293
4518
  auth_handler_post_data = {
3294
- "file1_property": "com.opentext.otds.as.drivers.sapssoext.certificate1"
3295
- }
3296
-
3297
- # It is important to send the file pointer and not the actual file content
3298
- # otherwise the file is send base64 encoded which we don't want:
3299
- auth_handler_post_files = {
3300
- "file1": (
3301
- os.path.basename(certificate_file),
3302
- open(certificate_file, "rb"),
3303
- "application/octet-stream",
3304
- )
4519
+ "file1_property": "com.opentext.otds.as.drivers.sapssoext.certificate1",
3305
4520
  }
3306
4521
 
3307
4522
  request_url = self.auth_handler_url() + "/" + name + "/files"
3308
4523
 
3309
- logger.debug(
4524
+ self.logger.debug(
3310
4525
  "Uploading certificate file -> '%s' for SAP auth handler -> '%s' ('%s'); calling -> %s",
3311
4526
  certificate_file,
3312
4527
  name,
@@ -3314,18 +4529,30 @@ class OTDS:
3314
4529
  request_url,
3315
4530
  )
3316
4531
 
3317
- # it is important to NOT pass the headers parameter here!
3318
- # Basically, if you specify a files parameter (a dictionary),
3319
- # then requests will send a multipart/form-data POST automatically:
3320
- response = requests.post(
3321
- url=request_url,
3322
- data=auth_handler_post_data,
3323
- files=auth_handler_post_files,
3324
- cookies=self.cookie(),
3325
- timeout=None,
3326
- )
4532
+ # It is important to send the file pointer and not the actual file content
4533
+ # otherwise the file is sent base64 encoded, which we don't want:
4534
+ with open(certificate_file, "rb") as file_obj:
4535
+ auth_handler_post_files = {
4536
+ "file1": (
4537
+ os.path.basename(certificate_file),
4538
+ file_obj,
4539
+ "application/octet-stream",
4540
+ ),
4541
+ }
4542
+
4543
+ # It is important to NOT pass the headers parameter here!
4544
+ # Basically, if you specify a files parameter (a dictionary),
4545
+ # then requests will send a multipart/form-data POST automatically:
4546
+ response = requests.post(
4547
+ url=request_url,
4548
+ data=auth_handler_post_data,
4549
+ files=auth_handler_post_files,
4550
+ cookies=self.cookie(),
4551
+ timeout=REQUEST_TIMEOUT,
4552
+ )
4553
+
3327
4554
  if not response.ok:
3328
- logger.error(
4555
+ self.logger.error(
3329
4556
  "Failed to upload certificate file -> '%s' for SAP auth handler -> '%s'; error -> %s (%s)",
3330
4557
  certificate_file,
3331
4558
  name,
@@ -3354,28 +4581,44 @@ class OTDS:
3354
4581
  priority: int = 10,
3355
4582
  auth_principal_attributes: list | None = None,
3356
4583
  ) -> dict | None:
3357
- """Add a new OAuth authentication handler
3358
-
3359
- Args:
3360
- name (str): name of the new authentication handler
3361
- description (str): description of the new authentication handler
3362
- scope (str): name of the user partition (to define a scope of the auth handler)
3363
- provider_name (str): the name of the authentication provider. This name is displayed on the login page.
3364
- client_id (str): the client ID
3365
- client_secret (str): the client secret
3366
- active_by_default (bool, optional): Whether to activate this handler for any request to the OTDS login page.
3367
- If True, any login request to the OTDS login page will be redirected to this OAuth provider.
3368
- If False, the user has to select the provider on the login page.
3369
- authorization_endpoint (str, optional): The URL to redirect the browser to for authentication.
3370
- It is used to retrieve the authorization code or an OIDC id_token.
3371
- token_endpoint (str, optional): The URL from which to retrieve the access token.
3372
- Not strictly required with OpenID Connect if using the implicit flow.
3373
- scope_string (str, optional): Space delimited scope values to send. Include 'openid' to use OpenID Connect.
3374
- enabled (bool, optional): if the handler should be enabled or disabled. Default is True = enabled.
3375
- priority (int, optional): Priority of the Authentical Handler (compared to others). Default is 5
3376
- auth_principal_attributes (list, optional): List of Authentication principal attributes
3377
- Returns:
3378
- dict: Request response (dictionary) or None if the REST call fails.
4584
+ """Add a new OAuth authentication handler.
4585
+
4586
+ Args:
4587
+ name (str):
4588
+ The name of the new authentication handler.
4589
+ description (str):
4590
+ The description of the new authentication handler.
4591
+ scope (str):
4592
+ The name of the user partition (to define a scope of the auth handler).
4593
+ provider_name (str):
4594
+ The name of the authentication provider. This name is displayed on the login page.
4595
+ client_id (str):
4596
+ The client ID.
4597
+ client_secret (str):
4598
+ The client secret.
4599
+ active_by_default (bool, optional):
4600
+ Defines, whether to activate this handler for any request to the OTDS login page.
4601
+ If True, any login request to the OTDS login page will be redirected to this OAuth provider.
4602
+ If False, the user has to select the provider on the login page.
4603
+ authorization_endpoint (str, optional):
4604
+ The URL to redirect the browser to for authentication.
4605
+ It is used to retrieve the authorization code or an OIDC id_token.
4606
+ token_endpoint (str, optional):
4607
+ The URL from which to retrieve the access token.
4608
+ Not strictly required with OpenID Connect if using the implicit flow.
4609
+ scope_string (str, optional):
4610
+ Space delimited scope values to send. Include 'openid' to use OpenID Connect.
4611
+ enabled (bool, optional):
4612
+ Defines if the handler should be enabled or disabled. Default is True = enabled.
4613
+ priority (int, optional):
4614
+ Priority of the Authentical Handler (compared to others). Default is 5.
4615
+ auth_principal_attributes (list, optional):
4616
+ List of Authentication principal attributes.
4617
+
4618
+ Returns:
4619
+ dict | None:
4620
+ Request response (dictionary) or None if the REST call fails.
4621
+
3379
4622
  """
3380
4623
 
3381
4624
  # Avoid linter warning W0102:
@@ -3722,7 +4965,7 @@ class OTDS:
3722
4965
 
3723
4966
  request_url = self.auth_handler_url()
3724
4967
 
3725
- logger.debug(
4968
+ self.logger.debug(
3726
4969
  "Adding OAuth auth handler -> '%s' ('%s'); calling -> %s",
3727
4970
  name,
3728
4971
  description,
@@ -3740,24 +4983,29 @@ class OTDS:
3740
4983
  # end method definition
3741
4984
 
3742
4985
  def consolidate(self, resource_name: str) -> bool:
3743
- """Consolidate an OTDS resource
4986
+ """Consolidate an OTDS resource.
3744
4987
 
3745
4988
  Args:
3746
- resource_name (str): resource to be consolidated
4989
+ resource_name (str):
4990
+ The name of the resource to be consolidated.
4991
+
3747
4992
  Returns:
3748
- bool: True if the consolidation succeeded or False if it failed.
4993
+ bool:
4994
+ True, if the consolidation succeeded or False if it failed.
4995
+
3749
4996
  """
3750
4997
 
3751
4998
  resource = self.get_resource(resource_name)
3752
4999
  if not resource:
3753
- logger.error(
3754
- "Resource -> '%s' not found - cannot consolidate", resource_name
5000
+ self.logger.error(
5001
+ "Resource -> '%s' not found - cannot consolidate",
5002
+ resource_name,
3755
5003
  )
3756
5004
  return False
3757
5005
 
3758
5006
  resource_dn = resource["resourceDN"]
3759
5007
  if not resource_dn:
3760
- logger.error("Resource DN is empty - cannot consolidate")
5008
+ self.logger.error("Resource DN is empty - cannot consolidate")
3761
5009
  return False
3762
5010
 
3763
5011
  consolidation_post_body_json = {
@@ -3769,8 +5017,8 @@ class OTDS:
3769
5017
 
3770
5018
  request_url = "{}".format(self.consolidation_url())
3771
5019
 
3772
- logger.debug(
3773
- "Consolidation of resource -> %s (%s); calling -> %s",
5020
+ self.logger.debug(
5021
+ "Consolidation of resource -> '%s' (%s); calling -> %s",
3774
5022
  resource_name,
3775
5023
  resource_dn,
3776
5024
  request_url,
@@ -3782,15 +5030,12 @@ class OTDS:
3782
5030
  json_data=consolidation_post_body_json,
3783
5031
  timeout=None,
3784
5032
  failure_message="Failed to consolidate resource -> '{}'".format(
3785
- resource_name
5033
+ resource_name,
3786
5034
  ),
3787
5035
  parse_request_response=False,
3788
5036
  )
3789
5037
 
3790
- if response and response.ok:
3791
- return True
3792
-
3793
- return False
5038
+ return bool(response and response.ok)
3794
5039
 
3795
5040
  # end method definition
3796
5041
 
@@ -3800,15 +5045,20 @@ class OTDS:
3800
5045
  allow_impersonation: bool = True,
3801
5046
  impersonation_list: list | None = None,
3802
5047
  ) -> bool:
3803
- """Configure impersonation for an OTDS resource
5048
+ """Configure impersonation for an OTDS resource.
3804
5049
 
3805
5050
  Args:
3806
- resource_name (str): resource to be configure impersonation for
3807
- allow_impersonation (bool, optional): wether to turn on or off impersonation (default = True)
3808
- impersonation_list (list, optional): list of users to restrict it to
3809
- (default = empty list = all users)
5051
+ resource_name (str):
5052
+ Name of the resource to configure impersonation for.
5053
+ allow_impersonation (bool, optional):
5054
+ Whether to turn on or off impersonation (default = True)
5055
+ impersonation_list (list, optional):
5056
+ A list of users to restrict it to (default = empty list = all users)
5057
+
3810
5058
  Returns:
3811
- bool: True if the impersonation setting succeeded or False if it failed.
5059
+ bool:
5060
+ True if the impersonation setting succeeded or False if it failed.
5061
+
3812
5062
  """
3813
5063
 
3814
5064
  # Avoid linter warning W0102:
@@ -3822,7 +5072,7 @@ class OTDS:
3822
5072
 
3823
5073
  request_url = "{}/{}/impersonation".format(self.resource_url(), resource_name)
3824
5074
 
3825
- logger.debug(
5075
+ self.logger.debug(
3826
5076
  "Impersonation settings for resource -> '%s'; calling -> %s",
3827
5077
  resource_name,
3828
5078
  request_url,
@@ -3834,15 +5084,12 @@ class OTDS:
3834
5084
  json_data=impersonation_put_body_json,
3835
5085
  timeout=None,
3836
5086
  failure_message="Failed to set impersonation for resource -> '{}'".format(
3837
- resource_name
5087
+ resource_name,
3838
5088
  ),
3839
5089
  parse_request_response=False,
3840
5090
  )
3841
5091
 
3842
- if response and response.ok:
3843
- return True
3844
-
3845
- return False
5092
+ return bool(response and response.ok)
3846
5093
 
3847
5094
  # end method definition
3848
5095
 
@@ -3852,17 +5099,23 @@ class OTDS:
3852
5099
  allow_impersonation: bool = True,
3853
5100
  impersonation_list: list | None = None,
3854
5101
  ) -> bool:
3855
- """Configure impersonation for an OTDS OAuth Client
5102
+ """Configure impersonation for an OTDS OAuth Client.
3856
5103
 
3857
5104
  Args:
3858
- client_id (str): OAuth Client to be configure impersonation for
3859
- allow_impersonation (bool, optional): wether to turn on or off impersonation (default = True)
3860
- impersonation_list (list, optional): list of users to restrict it to; (default = empty list = all users)
5105
+ client_id (str):
5106
+ The ID of the OAuth Client to configure impersonation for.
5107
+ allow_impersonation (bool | None, optional):
5108
+ Defines whether to turn on or off impersonation (default = True).
5109
+ impersonation_list (list | None, optional):
5110
+ A list of users to restrict it to; (default = empty list = all users).
5111
+
3861
5112
  Returns:
3862
- bool: True if the impersonation setting succeeded or False if it failed.
5113
+ bool:
5114
+ True if the impersonation setting succeeded or False if it failed.
5115
+
3863
5116
  """
3864
5117
 
3865
- # Avoid linter warning W0102:
5118
+ # Avoid linter warning W0102 by establishing the default inside the method:
3866
5119
  if impersonation_list is None:
3867
5120
  impersonation_list = []
3868
5121
 
@@ -3873,7 +5126,7 @@ class OTDS:
3873
5126
 
3874
5127
  request_url = "{}/{}/impersonation".format(self.oauth_client_url(), client_id)
3875
5128
 
3876
- logger.debug(
5129
+ self.logger.debug(
3877
5130
  "Impersonation settings for OAuth Client -> '%s'; calling -> %s",
3878
5131
  client_id,
3879
5132
  request_url,
@@ -3885,27 +5138,26 @@ class OTDS:
3885
5138
  json_data=impersonation_put_body_json,
3886
5139
  timeout=None,
3887
5140
  failure_message="Failed to set impersonation for OAuth Client -> '{}'".format(
3888
- client_id
5141
+ client_id,
3889
5142
  ),
3890
5143
  parse_request_response=False,
3891
5144
  )
3892
5145
 
3893
- if response and response.ok:
3894
- return True
3895
-
3896
- return False
5146
+ return bool(response and response.ok)
3897
5147
 
3898
5148
  # end method definition
3899
5149
 
3900
- def get_password_policy(self):
3901
- """Get the global password policy
5150
+ def get_password_policy(self) -> dict | None:
5151
+ """Get the global password policy.
3902
5152
 
3903
5153
  Args:
3904
5154
  None
5155
+
3905
5156
  Returns:
3906
- dict: Request response or None if the REST call fails.
5157
+ dict | None:
5158
+ Request response or None if the REST call fails.
3907
5159
 
3908
- Example response:
5160
+ Example:
3909
5161
  {
3910
5162
  'passwordHistoryMaximumCount': 3,
3911
5163
  'daysBeforeNewPasswordMayBeChanged': 1,
@@ -3924,11 +5176,12 @@ class OTDS:
3924
5176
  'blockCommonPassword': False
3925
5177
  ...
3926
5178
  }
5179
+
3927
5180
  """
3928
5181
 
3929
5182
  request_url = "{}/passwordpolicy".format(self.config()["systemConfigUrl"])
3930
5183
 
3931
- logger.debug("Getting password policy; calling -> %s", request_url)
5184
+ self.logger.debug("Getting password policy; calling -> %s", request_url)
3932
5185
 
3933
5186
  return self.do_request(
3934
5187
  url=request_url,
@@ -3940,13 +5193,14 @@ class OTDS:
3940
5193
  # end method definition
3941
5194
 
3942
5195
  def update_password_policy(self, update_values: dict) -> bool:
3943
- """Update the global password policy
5196
+ """Update the global password policy.
3944
5197
 
3945
5198
  Args:
3946
- update_values (dict): new values for selected settings.
3947
- A value of 0 means the settings is deactivated.
5199
+ update_values (dict):
5200
+ New values for selected settings.
5201
+ A value of 0 means the settings is deactivated.
3948
5202
 
3949
- Example values:
5203
+ Example:
3950
5204
  {
3951
5205
  'passwordHistoryMaximumCount': 3,
3952
5206
  'daysBeforeNewPasswordMayBeChanged': 1,
@@ -3965,15 +5219,17 @@ class OTDS:
3965
5219
  'blockCommonPassword': False
3966
5220
  ...
3967
5221
  }
5222
+
3968
5223
  Returns:
3969
- bool: True if the REST call succeeds, otherwise False. We use a boolean return
3970
- value as the response of the REST call does not have meaningful content.
5224
+ bool:
5225
+ True if the REST call succeeds, otherwise False. We use a boolean return
5226
+ value as the response of the REST call does not have meaningful content.
3971
5227
 
3972
5228
  """
3973
5229
 
3974
5230
  request_url = "{}/passwordpolicy".format(self.config()["systemConfigUrl"])
3975
5231
 
3976
- logger.debug(
5232
+ self.logger.debug(
3977
5233
  "Update password policy with these new values -> %s; calling -> %s",
3978
5234
  str(update_values),
3979
5235
  request_url,
@@ -3985,14 +5241,11 @@ class OTDS:
3985
5241
  json_data=update_values,
3986
5242
  timeout=None,
3987
5243
  failure_message="Failed to update password policy with values -> {}".format(
3988
- update_values
5244
+ update_values,
3989
5245
  ),
3990
5246
  parse_request_response=False,
3991
5247
  )
3992
5248
 
3993
- if response and response.ok:
3994
- return True
3995
-
3996
- return False
5249
+ return bool(response and response.ok)
3997
5250
 
3998
5251
  # end method definition