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