pyxecm 1.4__py3-none-any.whl → 1.6__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.

pyxecm/otds.py CHANGED
@@ -27,30 +27,22 @@ groups_url : returns OTDS Groups REST URL
27
27
  system_config_url : returns OTDS System Config REST URL
28
28
  consolidation_url: returns OTDS consolidation URL
29
29
 
30
- authenticate : authenticates at OTDS server
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
31
32
 
32
- add_license_to_resource : Add (or update) a product license to OTDS
33
- get_license_for_resource : Get list of licenses for a resource
34
- delete_license_from_resource : Delete a license from a resource
35
- assign_user_to_license : Assign an OTDS user to a product license (feature) in OTDS.
36
- assign_partition_to_license: Assign an OTDS user partition to a license (feature) in OTDS.
37
- get_licensed_objects: Return the licensed objects (users, groups, partitions) an OTDS for a
38
- license + license feature associated with an OTDS resource (like "cs").
39
- is_user_licensed: Check if a user is licensed for a license and license feature associated
40
- with a particular OTDS resource.
41
- is_group_licensed: Check if a group is licensed for a license and license feature associated
42
- with a particular OTDS resource.
43
- is_partition_licensed: Check if a user partition is licensed for a license and license feature
44
- associated with a particular OTDS resource.
33
+ authenticate : authenticates at OTDS server
45
34
 
35
+ add_synchronized_partition: Add a Synchronized partition to OTDS
46
36
  add_partition : Add an OTDS partition
47
37
  get_partition : Get a partition with a specific name
38
+
48
39
  add_user : Add a user to a partion
49
40
  get_user : Get a user with a specific user ID (= login name @ partition)
50
41
  get_users: get all users (with option to filter)
51
42
  update_user : Update attributes of on OTDS user
52
43
  delete_user : Delete a user with a specific ID in a specific partition
53
44
  reset_user_password : Reset a password of a specific user ID
45
+
54
46
  add_group: Add an OTDS group
55
47
  get_group: Get a OTDS group by its name
56
48
  add_user_to_group : Add an OTDS user to a OTDS group
@@ -68,6 +60,20 @@ add_user_to_access_role : Add an OTDS user to to an OTDS Access Role
68
60
  add_group_to_access_role : Add an OTDS group to to an OTDS Access Role
69
61
  update_access_role_attributes: Update attributes of an existing access role
70
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
+
71
77
  add_system_attribute : Add an OTDS System Attribute
72
78
 
73
79
  get_trusted_sites : Get OTDS Trusted Sites
@@ -106,6 +112,9 @@ import logging
106
112
  import json
107
113
  import urllib.parse
108
114
  import base64
115
+ import time
116
+
117
+ from http import HTTPStatus
109
118
  import requests
110
119
 
111
120
  logger = logging.getLogger("pyxecm.otds")
@@ -121,6 +130,8 @@ REQUEST_FORM_HEADERS = {
121
130
  }
122
131
 
123
132
  REQUEST_TIMEOUT = 60
133
+ REQUEST_RETRY_DELAY = 20
134
+ REQUEST_MAX_RETRIES = 2
124
135
 
125
136
 
126
137
  class OTDS:
@@ -138,6 +149,7 @@ class OTDS:
138
149
  username: str | None = None,
139
150
  password: str | None = None,
140
151
  otds_ticket: str | None = None,
152
+ bindPassword:str | None = None,
141
153
  ):
142
154
  """Initialize the OTDS object
143
155
 
@@ -177,6 +189,11 @@ class OTDS:
177
189
  otds_config["password"] = password
178
190
  else:
179
191
  otds_config["password"] = ""
192
+
193
+ if bindPassword:
194
+ otds_config["bindPassword"] = bindPassword
195
+ else:
196
+ otds_config["bindPassword"] = ""
180
197
 
181
198
  if otds_ticket:
182
199
  self._cookie = {"OTDSTicket": otds_ticket}
@@ -191,6 +208,7 @@ class OTDS:
191
208
  otds_config["restUrl"] = otdsRestUrl
192
209
 
193
210
  otds_config["partitionUrl"] = otdsRestUrl + "/partitions"
211
+ otds_config["identityproviderprofiles"] = otdsRestUrl + "/identityproviderprofiles"
194
212
  otds_config["accessRoleUrl"] = otdsRestUrl + "/accessroles"
195
213
  otds_config["credentialUrl"] = otdsRestUrl + "/authentication/credentials"
196
214
  otds_config["oauthClientUrl"] = otdsRestUrl + "/oauthclients"
@@ -213,6 +231,8 @@ class OTDS:
213
231
  """
214
232
  return self._config
215
233
 
234
+ # end method definition
235
+
216
236
  def cookie(self) -> dict:
217
237
  """Returns the login cookie of OTDS.
218
238
  This is set by the authenticate() method
@@ -222,6 +242,8 @@ class OTDS:
222
242
  """
223
243
  return self._cookie
224
244
 
245
+ # end method definition
246
+
225
247
  def credentials(self) -> dict:
226
248
  """Returns the credentials (username + password)
227
249
 
@@ -233,6 +255,8 @@ class OTDS:
233
255
  "password": self.config()["password"],
234
256
  }
235
257
 
258
+ # end method definition
259
+
236
260
  def base_url(self) -> str:
237
261
  """Returns the base URL of OTDS
238
262
 
@@ -241,6 +265,8 @@ class OTDS:
241
265
  """
242
266
  return self.config()["baseUrl"]
243
267
 
268
+ # end method definition
269
+
244
270
  def rest_url(self) -> str:
245
271
  """Returns the REST URL of OTDS
246
272
 
@@ -249,6 +275,8 @@ class OTDS:
249
275
  """
250
276
  return self.config()["restUrl"]
251
277
 
278
+ # end method definition
279
+
252
280
  def credential_url(self) -> str:
253
281
  """Returns the Credentials URL of OTDS
254
282
 
@@ -257,6 +285,8 @@ class OTDS:
257
285
  """
258
286
  return self.config()["credentialUrl"]
259
287
 
288
+ # end method definition
289
+
260
290
  def auth_handler_url(self) -> str:
261
291
  """Returns the Auth Handler URL of OTDS
262
292
 
@@ -265,6 +295,16 @@ class OTDS:
265
295
  """
266
296
  return self.config()["authHandlerUrl"]
267
297
 
298
+ # end method definition
299
+ def synchronized_partition_url(self) -> str:
300
+ """Returns the Partition URL of OTDS
301
+
302
+ Returns:
303
+ str: synchronized partition url
304
+ """
305
+ return self.config()["identityproviderprofiles"]
306
+ # end of method definition
307
+
268
308
  def partition_url(self) -> str:
269
309
  """Returns the Partition URL of OTDS
270
310
 
@@ -273,6 +313,8 @@ class OTDS:
273
313
  """
274
314
  return self.config()["partitionUrl"]
275
315
 
316
+ # end method definition
317
+
276
318
  def access_role_url(self) -> str:
277
319
  """Returns the Access Role URL of OTDS
278
320
 
@@ -281,6 +323,8 @@ class OTDS:
281
323
  """
282
324
  return self.config()["accessRoleUrl"]
283
325
 
326
+ # end method definition
327
+
284
328
  def oauth_client_url(self) -> str:
285
329
  """Returns the OAuth Client URL of OTDS
286
330
 
@@ -289,6 +333,8 @@ class OTDS:
289
333
  """
290
334
  return self.config()["oauthClientUrl"]
291
335
 
336
+ # end method definition
337
+
292
338
  def resource_url(self) -> str:
293
339
  """Returns the Resource URL of OTDS
294
340
 
@@ -297,6 +343,8 @@ class OTDS:
297
343
  """
298
344
  return self.config()["resourceUrl"]
299
345
 
346
+ # end method definition
347
+
300
348
  def license_url(self) -> str:
301
349
  """Returns the License URL of OTDS
302
350
 
@@ -305,6 +353,8 @@ class OTDS:
305
353
  """
306
354
  return self.config()["licenseUrl"]
307
355
 
356
+ # end method definition
357
+
308
358
  def token_url(self) -> str:
309
359
  """Returns the Token URL of OTDS
310
360
 
@@ -313,6 +363,8 @@ class OTDS:
313
363
  """
314
364
  return self.config()["tokenUrl"]
315
365
 
366
+ # end method definition
367
+
316
368
  def users_url(self) -> str:
317
369
  """Returns the Users URL of OTDS
318
370
 
@@ -321,6 +373,8 @@ class OTDS:
321
373
  """
322
374
  return self.config()["usersUrl"]
323
375
 
376
+ # end method definition
377
+
324
378
  def groups_url(self) -> str:
325
379
  """Returns the Groups URL of OTDS
326
380
 
@@ -329,6 +383,8 @@ class OTDS:
329
383
  """
330
384
  return self.config()["groupsUrl"]
331
385
 
386
+ # end method definition
387
+
332
388
  def system_config_url(self) -> str:
333
389
  """Returns the System Config URL of OTDS
334
390
 
@@ -337,6 +393,8 @@ class OTDS:
337
393
  """
338
394
  return self.config()["systemConfigUrl"]
339
395
 
396
+ # end method definition
397
+
340
398
  def consolidation_url(self) -> str:
341
399
  """Returns the Consolidation URL of OTDS
342
400
 
@@ -345,6 +403,166 @@ class OTDS:
345
403
  """
346
404
  return self.config()["consolidationUrl"]
347
405
 
406
+ # end method definition
407
+
408
+ def do_request(
409
+ self,
410
+ url: str,
411
+ method: str = "GET",
412
+ headers: dict | None = None,
413
+ data: dict | None = None,
414
+ json_data: dict | None = None,
415
+ files: dict | None = None,
416
+ timeout: int | None = REQUEST_TIMEOUT,
417
+ show_error: bool = True,
418
+ show_warning: bool = False,
419
+ warning_message: str = "",
420
+ failure_message: str = "",
421
+ success_message: str = "",
422
+ max_retries: int = REQUEST_MAX_RETRIES,
423
+ retry_forever: bool = False,
424
+ parse_request_response: bool = True,
425
+ ) -> dict | None:
426
+ """Call an OTDS REST API in a safe way
427
+
428
+ 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.
444
+
445
+ Returns:
446
+ dict | None: Response of OTDS REST API or None in case of an error.
447
+ """
448
+
449
+ if headers is None:
450
+ headers = REQUEST_HEADERS
451
+
452
+ # In case of an expired session we reauthenticate and
453
+ # try 1 more time. Session expiration should not happen
454
+ # twice in a row:
455
+ retries = 0
456
+
457
+ while True:
458
+ try:
459
+ response = requests.request(
460
+ method=method,
461
+ url=url,
462
+ data=data,
463
+ json=json_data,
464
+ files=files,
465
+ headers=headers,
466
+ cookies=self.cookie(),
467
+ timeout=timeout,
468
+ )
469
+
470
+ if response.ok:
471
+ if success_message:
472
+ logger.info(success_message)
473
+ if parse_request_response:
474
+ return self.parse_request_response(response)
475
+ else:
476
+ return response
477
+ # 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...")
480
+ self.authenticate(revalidate=True)
481
+ retries += 1
482
+ else:
483
+ # Handle plain HTML responses to not pollute the logs
484
+ 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
489
+
490
+ if show_error:
491
+ logger.error(
492
+ "%s; status -> %s/%s; error -> %s",
493
+ failure_message,
494
+ response.status_code,
495
+ HTTPStatus(response.status_code).phrase,
496
+ response_text,
497
+ )
498
+ elif show_warning:
499
+ logger.warning(
500
+ "%s; status -> %s/%s; warning -> %s",
501
+ warning_message if warning_message else failure_message,
502
+ response.status_code,
503
+ HTTPStatus(response.status_code).phrase,
504
+ response_text,
505
+ )
506
+ if content_type == "text/html":
507
+ logger.debug(
508
+ "%s; status -> %s/%s; warning -> %s",
509
+ failure_message,
510
+ response.status_code,
511
+ HTTPStatus(response.status_code).phrase,
512
+ response.text,
513
+ )
514
+ return None
515
+ except requests.exceptions.Timeout:
516
+ if retries <= max_retries:
517
+ logger.warning(
518
+ "Request timed out. Retrying in %s seconds...",
519
+ str(REQUEST_RETRY_DELAY),
520
+ )
521
+ retries += 1
522
+ time.sleep(REQUEST_RETRY_DELAY) # Add a delay before retrying
523
+ else:
524
+ logger.error(
525
+ "%s; timeout error",
526
+ failure_message,
527
+ )
528
+ if retry_forever:
529
+ # If it fails after REQUEST_MAX_RETRIES retries we let it wait forever
530
+ logger.warning("Turn timeouts off and wait forever...")
531
+ timeout = None
532
+ else:
533
+ return None
534
+ except requests.exceptions.ConnectionError:
535
+ if retries <= max_retries:
536
+ logger.warning(
537
+ "Connection error. Retrying in %s seconds...",
538
+ str(REQUEST_RETRY_DELAY),
539
+ )
540
+ retries += 1
541
+ time.sleep(REQUEST_RETRY_DELAY) # Add a delay before retrying
542
+ else:
543
+ logger.error(
544
+ "%s; connection error",
545
+ failure_message,
546
+ )
547
+ if retry_forever:
548
+ # If it fails after REQUEST_MAX_RETRIES retries we let it wait forever
549
+ logger.warning("Turn timeouts off and wait forever...")
550
+ timeout = None
551
+ time.sleep(REQUEST_RETRY_DELAY) # Add a delay before retrying
552
+ else:
553
+ return None
554
+ # end try
555
+ logger.debug(
556
+ "Retrying REST API %s call -> %s... (retry = %s, cookie -> %s)",
557
+ method,
558
+ url,
559
+ str(retries),
560
+ str(self.cookie()),
561
+ )
562
+ # end while True
563
+
564
+ # end method definition
565
+
348
566
  def parse_request_response(
349
567
  self,
350
568
  response_object: object,
@@ -365,6 +583,10 @@ class OTDS:
365
583
  if not response_object:
366
584
  return None
367
585
 
586
+ if not response_object.text:
587
+ logger.warning("Response text is empty. Cannot decode response.")
588
+ return None
589
+
368
590
  try:
369
591
  dict_object = json.loads(response_object.text)
370
592
  except json.JSONDecodeError as e:
@@ -396,7 +618,7 @@ class OTDS:
396
618
 
397
619
  # Already authenticated and session still valid?
398
620
  if self._cookie and not revalidate:
399
- logger.info(
621
+ logger.debug(
400
622
  "Session still valid - return existing cookie -> %s",
401
623
  str(self._cookie),
402
624
  )
@@ -404,7 +626,7 @@ class OTDS:
404
626
 
405
627
  otds_ticket = "NotSet"
406
628
 
407
- logger.info("Requesting OTDS ticket from -> %s", self.credential_url())
629
+ logger.debug("Requesting OTDS ticket from -> %s", self.credential_url())
408
630
 
409
631
  response = None
410
632
  try:
@@ -429,7 +651,7 @@ class OTDS:
429
651
  return None
430
652
  else:
431
653
  otds_ticket = authenticate_dict["ticket"]
432
- logger.info("Ticket -> %s", otds_ticket)
654
+ logger.debug("Ticket -> %s", otds_ticket)
433
655
  else:
434
656
  logger.error("Failed to request an OTDS ticket; error -> %s", response.text)
435
657
  return None
@@ -442,1813 +664,1563 @@ class OTDS:
442
664
 
443
665
  # end method definition
444
666
 
445
- def add_license_to_resource(
446
- self,
447
- path_to_license_file: str,
448
- product_name: str,
449
- product_description: str,
450
- resource_id: str,
451
- update: bool = True,
452
- ) -> dict | None:
453
- """Add a product license to an OTDS resource.
667
+ def add_partition(self, name: str, description: str) -> dict | None:
668
+ """Add a new user partition to OTDS
454
669
 
455
670
  Args:
456
- path_to_license_file (str): fully qualified filename of the license file
457
- product_name (str): product name
458
- product_description (str): product description
459
- resource_id (str): OTDS resource ID (this is ID not the resource name!)
460
- update (bool, optional): whether or not an existing license should be updated (default = True)
671
+ name (str): name of the new partition
672
+ description (str): description of the new partition
461
673
  Returns:
462
- dict: Request response (dictionary) or None if the REST call fails
674
+ dict: Request response or None if the creation fails.
463
675
  """
464
676
 
465
- logger.info("Reading license file -> %s...", path_to_license_file)
466
- try:
467
- with open(path_to_license_file, "rt", encoding="UTF-8") as license_file:
468
- license_content = license_file.read()
469
- except IOError as exception:
470
- logger.error(
471
- "Error opening license file -> %s; error -> %s",
472
- path_to_license_file,
473
- exception.strerror,
474
- )
475
- return None
476
-
477
- licensePostBodyJson = {
478
- "description": product_description,
479
- "name": product_name,
480
- "values": [
481
- {"name": "oTLicenseFile", "values": license_content},
482
- {"name": "oTLicenseResource", "values": resource_id},
483
- {"name": "oTLicenseFingerprintGenerator", "values": [None]},
484
- ],
485
- }
677
+ partition_post_body_json = {"name": name, "description": description}
486
678
 
487
- request_url = self.license_url()
488
- # Check if we want to update an existing license:
489
- if update:
490
- existing_license = self.get_license_for_resource(resource_id)
491
- if existing_license:
492
- request_url += "/" + existing_license[0]["id"]
493
- else:
494
- logger.info(
495
- "No existing license for resource -> %s found - adding a new license...",
496
- resource_id,
497
- )
498
- # change strategy to create a new license:
499
- update = False
679
+ request_url = self.partition_url()
500
680
 
501
- logger.info(
502
- "Adding product license -> %s for product -> %s to resource -> %s; calling -> %s",
503
- path_to_license_file,
504
- product_description,
505
- resource_id,
681
+ logger.debug(
682
+ "Adding user partition -> '%s' (%s); calling -> %s",
683
+ name,
684
+ description,
506
685
  request_url,
507
686
  )
508
687
 
509
- retries = 0
510
- while True:
511
- if update:
512
- # Do a REST PUT call for update an existing license:
513
- response = requests.put(
514
- url=request_url,
515
- json=licensePostBodyJson,
516
- headers=REQUEST_HEADERS,
517
- cookies=self.cookie(),
518
- timeout=None,
519
- )
520
- else:
521
- # Do a REST POST call for creation of a new license:
522
- response = requests.post(
523
- url=request_url,
524
- json=licensePostBodyJson,
525
- headers=REQUEST_HEADERS,
526
- cookies=self.cookie(),
527
- timeout=None,
528
- )
529
- if response.ok:
530
- return self.parse_request_response(response)
531
- # Check if Session has expired - then re-authenticate and try once more
532
- elif response.status_code == 401 and retries == 0:
533
- logger.warning("Session has expired - try to re-authenticate...")
534
- self.authenticate(revalidate=True)
535
- retries += 1
536
- else:
537
- logger.error(
538
- "Failed to add product license -> %s for product -> %s; error -> %s (%s)",
539
- path_to_license_file,
540
- product_description,
541
- response.text,
542
- response.status_code,
543
- )
544
- return None
688
+ return self.do_request(
689
+ url=request_url,
690
+ method="POST",
691
+ json_data=partition_post_body_json,
692
+ timeout=None,
693
+ failure_message="Failed to add user partition -> '{}'".format(name),
694
+ )
545
695
 
546
696
  # end method definition
547
697
 
548
- def get_license_for_resource(self, resource_id: str):
549
- """Get a product license for a resource in OTDS.
698
+ def get_partition(self, name: str, show_error: bool = True) -> dict | None:
699
+ """Get an existing user partition from OTDS
550
700
 
551
701
  Args:
552
- resource_id (str): OTDS resource ID (this is ID not the resource name!)
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
553
705
  Returns:
554
- Licenses for a resource or None if the REST call fails
555
-
556
- licenses have this format:
557
- {
558
- '_oTLicenseType': 'NON-PRODUCTION',
559
- '_oTLicenseResource': '7382094f-a434-4714-9696-82864b6803da',
560
- '_oTLicenseResourceName': 'cs',
561
- '_oTLicenseProduct': 'EXTENDED_ECM',
562
- 'name': 'EXTENDED_ECM¹7382094f-a434-4714-9696-82864b6803da',
563
- 'location': 'cn=EXTENDED_ECM¹7382094f-a434-4714-9696-82864b6803da,ou=Licenses,dc=identity,dc=opentext,dc=net',
564
- 'id': 'cn=EXTENDED_ECM¹7382094f-a434-4714-9696-82864b6803da,ou=Licenses,dc=identity,dc=opentext,dc=net',
565
- 'description': 'CS license',
566
- 'values': [{...}, {...}, {...}, {...}, {...}, {...}, {...}, {...}, {...}, ...]
567
- }
706
+ dict: Request response or None if the REST call fails.
568
707
  """
569
708
 
570
- request_url = (
571
- self.license_url()
572
- + "/assignedlicenses?resourceID="
573
- + resource_id
574
- + "&validOnly=false"
575
- )
709
+ request_url = "{}/{}".format(self.config()["partitionUrl"], name)
576
710
 
577
- logger.info(
578
- "Get license for resource -> %s; calling -> %s", resource_id, request_url
579
- )
711
+ logger.debug("Get user partition -> '%s'; calling -> %s", name, request_url)
580
712
 
581
- retries = 0
582
- while True:
583
- response = requests.get(
584
- url=request_url,
585
- headers=REQUEST_HEADERS,
586
- cookies=self.cookie(),
587
- timeout=None,
588
- )
589
- if response.ok:
590
- response_dict = self.parse_request_response(response)
591
- if not response_dict:
592
- return None
593
- return response_dict["licenseObjects"]["_licenses"]
594
- # Check if Session has expired - then re-authenticate and try once more
595
- elif response.status_code == 401 and retries == 0:
596
- logger.warning("Session has expired - try to re-authenticate...")
597
- self.authenticate(revalidate=True)
598
- retries += 1
599
- else:
600
- logger.error(
601
- "Failed to get license for resource -> %s; error -> %s (%s)",
602
- resource_id,
603
- response.text,
604
- response.status_code,
605
- )
606
- return None
713
+ return self.do_request(
714
+ url=request_url,
715
+ method="GET",
716
+ timeout=None,
717
+ failure_message="Failed to get user partition -> '{}'".format(name),
718
+ show_error=show_error,
719
+ )
607
720
 
608
721
  # end method definition
609
722
 
610
- def delete_license_from_resource(self, resource_id: str, license_id: str) -> bool:
611
- """Delete a product license for a resource in OTDS.
723
+ def add_user(
724
+ self,
725
+ partition: str,
726
+ name: str,
727
+ description: str = "",
728
+ first_name: str = "",
729
+ last_name: str = "",
730
+ email: str = "",
731
+ ) -> dict | None:
732
+ """Add a new user to a user partition in OTDS
612
733
 
613
734
  Args:
614
- resource_id (str): OTDS resource ID (this is ID not the resource name!)
615
- license_id (str): OTDS license ID (this is the ID not the license name!)
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
616
741
  Returns:
617
- bool: True if successful or False if the REST call fails
742
+ dict: Request response or None if the creation fails.
618
743
  """
619
744
 
620
- request_url = "{}/{}".format(self.license_url(), license_id)
621
-
622
- logger.info(
623
- "Deleting product license -> %s from resource -> %s; calling -> %s",
624
- license_id,
625
- resource_id,
745
+ user_post_body_json = {
746
+ "userPartitionID": partition,
747
+ "values": [
748
+ {"name": "sn", "values": [last_name]},
749
+ {"name": "givenName", "values": [first_name]},
750
+ {"name": "mail", "values": [email]},
751
+ ],
752
+ "name": name,
753
+ "description": description,
754
+ }
755
+
756
+ request_url = self.users_url()
757
+
758
+ logger.debug(
759
+ "Adding user -> '%s' to partition -> '%s'; calling -> %s",
760
+ name,
761
+ partition,
626
762
  request_url,
627
763
  )
764
+ logger.debug("User Attributes -> %s", str(user_post_body_json))
628
765
 
629
- retries = 0
630
- while True:
631
- response = requests.delete(
632
- url=request_url,
633
- headers=REQUEST_HEADERS,
634
- cookies=self.cookie(),
635
- timeout=None,
636
- )
637
- if response.ok:
638
- return True
639
- # Check if Session has expired - then re-authenticate and try once more
640
- elif response.status_code == 401 and retries == 0:
641
- logger.warning("Session has expired - try to re-authenticate...")
642
- self.authenticate(revalidate=True)
643
- retries += 1
644
- else:
645
- logger.error(
646
- "Failed to delete license -> %s for resource -> %s; error -> %s (%s)",
647
- license_id,
648
- resource_id,
649
- response.text,
650
- response.status_code,
651
- )
652
- return False
766
+ return self.do_request(
767
+ url=request_url,
768
+ method="POST",
769
+ json_data=user_post_body_json,
770
+ timeout=None,
771
+ failure_message="Failed to add user -> '{}'".format(name),
772
+ )
653
773
 
654
774
  # end method definition
655
775
 
656
- def assign_user_to_license(
657
- self,
658
- partition: str,
659
- user_id: str,
660
- resource_id: str,
661
- license_feature: str,
662
- license_name: str,
663
- license_type: str = "Full",
664
- ) -> bool:
665
- """Assign an OTDS user to a product license (feature) in OTDS.
776
+ def get_user(self, partition: str, user_id: str) -> dict | None:
777
+ """Get a user by its partition and user ID
666
778
 
667
779
  Args:
668
- partition (str): user partition in OTDS, e.g. "Content Server Members"
780
+ partition (str): name of the partition
669
781
  user_id (str): ID of the user (= login name)
670
- resource_id (str): OTDS resource ID (this is ID not the resource name!)
671
- license_feature (str): name of the license feature
672
- license_name (str): name of the license to assign
673
- license_type (str, optional): deault is "Full", Extended ECM also has "Occasional"
674
782
  Returns:
675
- bool: True if successful or False if the REST call fails or the license is not found
783
+ dict: Request response or None if the user was not found.
676
784
  """
677
785
 
678
- licenses = self.get_license_for_resource(resource_id)
679
-
680
- for lic in licenses:
681
- if lic["_oTLicenseProduct"] == license_name:
682
- license_location = lic["id"]
683
-
684
- try:
685
- license_location
686
- except UnboundLocalError:
687
- logger.error(
688
- "Cannot find license -> %s for resource -> %s",
689
- license_name,
690
- resource_id,
691
- )
692
- return False
693
-
694
- user = self.get_user(partition, user_id)
695
- if user:
696
- user_location = user["location"]
697
- else:
698
- logger.error("Cannot find location for user -> %s", user_id)
699
- return False
700
-
701
- licensePostBodyJson = {
702
- "_oTLicenseType": license_type,
703
- "_oTLicenseProduct": "users",
704
- "name": user_location,
705
- "values": [{"name": "counter", "values": [license_feature]}],
706
- }
707
-
708
- request_url = self.license_url() + "/object/" + license_location
786
+ request_url = self.users_url() + "/" + user_id + "@" + partition
709
787
 
710
- logger.info(
711
- "Assign license feature -> %s of license -> %s associated with resource -> %s to user -> %s; calling -> %s",
712
- license_feature,
713
- license_location,
714
- resource_id,
788
+ logger.debug(
789
+ "Get user -> '%s' in partition -> '%s'; calling -> %s",
715
790
  user_id,
791
+ partition,
716
792
  request_url,
717
793
  )
718
794
 
719
- retries = 0
720
- while True:
721
- response = requests.post(
722
- url=request_url,
723
- json=licensePostBodyJson,
724
- headers=REQUEST_HEADERS,
725
- cookies=self.cookie(),
726
- timeout=None,
727
- )
728
- if response.ok:
729
- logger.info(
730
- "Added license feature -> %s for user -> %s",
731
- license_feature,
732
- user_id,
733
- )
734
- return True
735
- # Check if Session has expired - then re-authenticate and try once more
736
- elif response.status_code == 401 and retries == 0:
737
- logger.warning("Session has expired - try to re-authenticate...")
738
- self.authenticate(revalidate=True)
739
- retries += 1
740
- else:
741
- logger.error(
742
- "Failed to add license feature -> %s associated with resource -> %s to user -> %s; error -> %s (%s)",
743
- license_feature,
744
- resource_id,
745
- user_id,
746
- response.text,
747
- response.status_code,
748
- )
749
- return False
795
+ return self.do_request(
796
+ url=request_url,
797
+ method="GET",
798
+ timeout=None,
799
+ failure_message="Failed to get user -> '{}'".format(user_id),
800
+ )
750
801
 
751
802
  # end method definition
752
803
 
753
- def assign_partition_to_license(
754
- self,
755
- partition_name: str,
756
- resource_id: str,
757
- license_feature: str,
758
- license_name: str,
759
- license_type: str = "Full",
760
- ) -> bool:
761
- """Assign an OTDS partition to a product license (feature).
804
+ def get_users(self, partition: str = "", limit: int | None = None) -> dict | None:
805
+ """Get all users in a partition partition
762
806
 
763
807
  Args:
764
- partition_name (str): user partition in OTDS, e.g. "Content Server Members"
765
- resource_id (str): OTDS resource ID (this is ID not the resource name!)
766
- license_feature (str): name of the license feature, e.g. "X2" or "ADDON_ENGINEERING"
767
- license_name (str): name of the license to assign, e.g. "EXTENDED_ECM" or "INTELLGENT_VIEWIMG"
768
- license_type (str, optional): deault is "Full", Extended ECM also has "Occasional"
808
+ partition (str, optional): name of the partition
809
+ limit (int): maximum number of users to return
769
810
  Returns:
770
- bool: True if successful or False if the REST call fails or the license is not found
811
+ dict: Request response or None if the user was not found.
771
812
  """
772
813
 
773
- licenses = self.get_license_for_resource(resource_id)
774
- if not licenses:
775
- logger.error(
776
- "Resource with ID -> %s does not exist or has no licenses", resource_id
777
- )
778
- return False
779
-
780
- # licenses have this format:
781
- # {
782
- # '_oTLicenseType': 'NON-PRODUCTION',
783
- # '_oTLicenseResource': '7382094f-a434-4714-9696-82864b6803da',
784
- # '_oTLicenseResourceName': 'cs',
785
- # '_oTLicenseProduct': 'EXTENDED_ECM',
786
- # 'name': 'EXTENDED_ECM¹7382094f-a434-4714-9696-82864b6803da',
787
- # 'location': 'cn=EXTENDED_ECM¹7382094f-a434-4714-9696-82864b6803da,ou=Licenses,dc=identity,dc=opentext,dc=net',
788
- # 'id': 'cn=EXTENDED_ECM¹7382094f-a434-4714-9696-82864b6803da,ou=Licenses,dc=identity,dc=opentext,dc=net',
789
- # 'description': 'CS license',
790
- # 'values': [{...}, {...}, {...}, {...}, {...}, {...}, {...}, {...}, {...}, ...]
791
- # }
792
- for lic in licenses:
793
- if lic["_oTLicenseProduct"] == license_name:
794
- license_location = lic["id"]
814
+ # Add query parameters (these are NOT passed via JSon body!)
815
+ query = {}
816
+ if limit:
817
+ query["limit"] = limit
818
+ if partition:
819
+ query["where_partition_name"] = partition
795
820
 
796
- try:
797
- license_location
798
- except UnboundLocalError:
799
- logger.error(
800
- "Cannot find license -> %s for resource -> %s",
801
- license_name,
802
- resource_id,
803
- )
804
- return False
821
+ encoded_query = urllib.parse.urlencode(query, doseq=True)
805
822
 
806
- licensePostBodyJson = {
807
- "_oTLicenseType": license_type,
808
- "_oTLicenseProduct": "partitions",
809
- "name": partition_name,
810
- "values": [{"name": "counter", "values": [license_feature]}],
811
- }
823
+ request_url = self.users_url()
824
+ if query:
825
+ request_url += "?{}".format(encoded_query)
812
826
 
813
- request_url = self.license_url() + "/object/" + license_location
827
+ if partition:
828
+ logger.debug(
829
+ "Get all users in partition -> '%s' (limit -> %s); calling -> %s",
830
+ partition,
831
+ limit,
832
+ request_url,
833
+ )
834
+ failure_message = "Failed to get all users in partition -> '{}'".format(
835
+ partition
836
+ )
837
+ else:
838
+ logger.debug(
839
+ "Get all users (limit -> %s); calling -> %s",
840
+ limit,
841
+ request_url,
842
+ )
843
+ failure_message = "Failed to get all users"
814
844
 
815
- logger.info(
816
- "Assign license feature -> %s of license -> %s associated with resource -> %s to partition -> %s; calling -> %s",
817
- license_feature,
818
- license_location,
819
- resource_id,
820
- partition_name,
821
- request_url,
845
+ return self.do_request(
846
+ url=request_url, method="GET", timeout=None, failure_message=failure_message
822
847
  )
823
848
 
824
- retries = 0
825
- while True:
826
- response = requests.post(
827
- url=request_url,
828
- json=licensePostBodyJson,
829
- headers=REQUEST_HEADERS,
830
- cookies=self.cookie(),
831
- timeout=None,
832
- )
833
- if response.ok:
834
- logger.info(
835
- "Added license feature -> %s for partition -> %s",
836
- license_feature,
837
- partition_name,
838
- )
839
- return True
840
- # Check if Session has expired - then re-authenticate and try once more
841
- elif response.status_code == 401 and retries == 0:
842
- logger.warning("Session has expired - try to re-authenticate...")
843
- self.authenticate(revalidate=True)
844
- retries += 1
845
- else:
846
- logger.error(
847
- "Failed to add license feature -> %s associated with resource -> %s to partition -> %s; error -> %s (%s)",
848
- license_feature,
849
- resource_id,
850
- partition_name,
851
- response.text,
852
- response.status_code,
853
- )
854
- return False
855
-
856
849
  # end method definition
857
850
 
858
- def get_licensed_objects(
859
- self,
860
- resource_id: str,
861
- license_feature: str,
862
- license_name: str,
851
+ def update_user(
852
+ self, partition: str, user_id: str, attribute_name: str, attribute_value: str
863
853
  ) -> dict | None:
864
- """Return the licensed objects (users, groups, partitions) in OTDS for a license + license feature
865
- associated with an OTDS resource (like "cs").
854
+ """Update a user attribute with a new value
866
855
 
867
856
  Args:
868
- resource_id (str): OTDS resource ID (this is ID not the resource name!)
869
- license_feature (str): name of the license feature, e.g. "X2" or "ADDON_ENGINEERING"
870
- license_name (str): name of the license to assign, e.g. "EXTENDED_ECM" or "INTELLGENT_VIEWIMG"
871
- Returns:
872
- dict: data structure of licensed objects
873
-
874
- Example return value:
875
- {
876
- 'status': 0,
877
- 'displayString': 'Success',
878
- 'exceptions': None,
879
- 'retValue': 0,
880
- 'listGroupsResults': {'groups': [...], 'actualPageSize': 0, 'nextPageCookie': None, 'requestedPageSize': 250},
881
- 'listUsersResults': {'users': [...], 'actualPageSize': 53, 'nextPageCookie': None, 'requestedPageSize': 250},
882
- 'listUserPartitionResult': {'_userPartitions': [...], 'warningMessage': None, 'actualPageSize': 0, 'nextPageCookie': None, 'requestedPageSize': 250},
883
- 'version': 1
884
- }
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.
885
863
  """
886
864
 
887
- licenses = self.get_license_for_resource(resource_id)
888
- if not licenses:
889
- logger.error(
890
- "Resource with ID -> %s does not exist or has no licenses", resource_id
891
- )
892
- return False
865
+ if attribute_name in ["description"]:
866
+ user_patch_body_json = {
867
+ "userPartitionID": partition,
868
+ attribute_name: attribute_value,
869
+ }
870
+ else:
871
+ user_patch_body_json = {
872
+ "userPartitionID": partition,
873
+ "values": [{"name": attribute_name, "values": [attribute_value]}],
874
+ }
893
875
 
894
- # licenses have this format:
895
- # {
896
- # '_oTLicenseType': 'NON-PRODUCTION',
897
- # '_oTLicenseResource': '7382094f-a434-4714-9696-82864b6803da',
898
- # '_oTLicenseResourceName': 'cs',
899
- # '_oTLicenseProduct': 'EXTENDED_ECM',
900
- # 'name': 'EXTENDED_ECM¹7382094f-a434-4714-9696-82864b6803da',
901
- # 'location': 'cn=EXTENDED_ECM¹7382094f-a434-4714-9696-82864b6803da,ou=Licenses,dc=identity,dc=opentext,dc=net',
902
- # 'id': 'cn=EXTENDED_ECM¹7382094f-a434-4714-9696-82864b6803da,ou=Licenses,dc=identity,dc=opentext,dc=net',
903
- # 'description': 'CS license',
904
- # 'values': [{...}, {...}, {...}, {...}, {...}, {...}, {...}, {...}, {...}, ...]
905
- # }
906
- for lic in licenses:
907
- if lic["_oTLicenseProduct"] == license_name:
908
- license_location = lic["location"]
876
+ request_url = self.users_url() + "/" + user_id
909
877
 
910
- try:
911
- license_location
912
- except UnboundLocalError:
913
- logger.error(
914
- "Cannot find license -> %s for resource -> %s",
915
- license_name,
916
- resource_id,
917
- )
918
- return False
878
+ logger.debug(
879
+ "Update user -> '%s' attribute -> '%s' to value -> '%s'; calling -> %s",
880
+ user_id,
881
+ attribute_name,
882
+ attribute_value,
883
+ request_url,
884
+ )
919
885
 
920
- request_url = (
921
- self.license_url()
922
- + "/object/"
923
- + license_location
924
- + "?counter="
925
- + license_feature
886
+ return self.do_request(
887
+ url=request_url,
888
+ method="PATCH",
889
+ json_data=user_patch_body_json,
890
+ timeout=None,
891
+ failure_message="Failed to update user -> '{}'".format(user_id),
926
892
  )
927
893
 
928
- logger.info(
929
- "Get licensed objects for license -> %s and license feature -> %s associated with resource -> %s; calling -> %s",
930
- license_name,
931
- license_feature,
932
- resource_id,
894
+ # end method definition
895
+
896
+ def delete_user(self, partition: str, user_id: str) -> bool:
897
+ """Delete an existing user
898
+
899
+ Args:
900
+ partition (str): name of the partition
901
+ user_id (str): Id (= login name) of the user
902
+ Returns:
903
+ bool: True = success, False = error
904
+ """
905
+
906
+ request_url = self.users_url() + "/" + user_id + "@" + partition
907
+
908
+ logger.debug(
909
+ "Delete user -> '%s' in partition -> '%s'; calling -> %s",
910
+ user_id,
911
+ partition,
933
912
  request_url,
934
913
  )
935
914
 
936
- retries = 0
937
- while True:
938
- response = requests.get(
939
- url=request_url,
940
- headers=REQUEST_HEADERS,
941
- cookies=self.cookie(),
942
- timeout=None,
943
- )
944
- if response.ok:
945
- return self.parse_request_response(response)
946
- # Check if Session has expired - then re-authenticate and try once more
947
- elif response.status_code == 401 and retries == 0:
948
- logger.warning("Session has expired - try to re-authenticate...")
949
- self.authenticate(revalidate=True)
950
- retries += 1
951
- else:
952
- logger.error(
953
- "Failed to get licensed objects for license -> %s and license feature -> %s associated with resource -> %s; error -> %s (%s)",
954
- license_name,
955
- license_feature,
956
- resource_id,
957
- response.text,
958
- response.status_code,
959
- )
960
- return None
915
+ response = self.do_request(
916
+ url=request_url,
917
+ method="DELETE",
918
+ timeout=None,
919
+ failure_message="Failed to delete user -> '{}'".format(user_id),
920
+ parse_request_response=False,
921
+ )
922
+
923
+ if response and response.ok:
924
+ return True
925
+
926
+ return False
961
927
 
962
928
  # end method definition
963
929
 
964
- def is_user_licensed(
965
- self, user_name: str, resource_id: str, license_feature: str, license_name: str
966
- ) -> bool:
967
- """Check if a user is licensed for a license and license feature associated with a particular OTDS resource.
930
+ def reset_user_password(self, user_id: str, password: str) -> bool:
931
+ """Reset a password of an existing user
968
932
 
969
933
  Args:
970
- user_name (str): login name of the OTDS user
971
- resource_id (str): OTDS resource ID (this is ID not the resource name!)
972
- license_feature (str): name of the license feature, e.g. "X2" or "ADDON_ENGINEERING"
973
- license_name (str): name of the license to assign, e.g. "EXTENDED_ECM" or "INTELLGENT_VIEWIMG"
974
-
934
+ user_id (str): Id (= login name) of the user
935
+ password (str): new password of the user
975
936
  Returns:
976
- bool: True if the user is licensed and False otherwise
937
+ bool: True = success, False = error.
977
938
  """
978
939
 
979
- response = self.get_licensed_objects(
980
- resource_id=resource_id,
981
- license_feature=license_feature,
982
- license_name=license_name,
983
- )
984
-
985
- if not response or not response["listUsersResults"]:
986
- return False
940
+ user_post_body_json = {"newPassword": password}
987
941
 
988
- users = response["listUsersResults"]["users"]
942
+ request_url = "{}/{}/password".format(self.users_url(), user_id)
989
943
 
990
- if not users:
991
- return False
944
+ logger.debug(
945
+ "Resetting password for user -> '%s'; calling -> %s", user_id, request_url
946
+ )
992
947
 
993
- user = next(
994
- (item for item in users if item["name"] == user_name),
995
- None,
948
+ response = self.do_request(
949
+ url=request_url,
950
+ method="PUT",
951
+ json_data=user_post_body_json,
952
+ timeout=None,
953
+ failure_message="Failed to reset password for user -> '{}'".format(user_id),
954
+ parse_request_response=False,
996
955
  )
997
956
 
998
- if user:
957
+ if response and response.ok:
999
958
  return True
1000
959
 
1001
960
  return False
1002
961
 
1003
962
  # end method definition
1004
963
 
1005
- def is_group_licensed(
1006
- self, group_name: str, resource_id: str, license_feature: str, license_name: str
1007
- ) -> bool:
1008
- """Check if a group is licensed for a license and license feature associated with a particular OTDS resource.
964
+ def add_group(self, partition: str, name: str, description: str) -> dict | None:
965
+ """Add a new user group to a user partition in OTDS
1009
966
 
1010
967
  Args:
1011
- group_name (str): name of the OTDS user group
1012
- resource_id (str): OTDS resource ID (this is ID not the resource name!)
1013
- license_feature (str): name of the license feature, e.g. "X2" or "ADDON_ENGINEERING"
1014
- license_name (str): name of the license to assign, e.g. "EXTENDED_ECM" or "INTELLGENT_VIEWIMG"
1015
-
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
1016
971
  Returns:
1017
- bool: True if the group is licensed and False otherwise
972
+ dict: Request response (json) or None if the creation fails.
1018
973
  """
1019
974
 
1020
- response = self.get_licensed_objects(
1021
- resource_id=resource_id,
1022
- license_feature=license_feature,
1023
- license_name=license_name,
975
+ group_post_body_json = {
976
+ "userPartitionID": partition,
977
+ "name": name,
978
+ "description": description,
979
+ }
980
+
981
+ request_url = self.groups_url()
982
+
983
+ logger.debug(
984
+ "Adding group -> '%s' to partition -> '%s'; calling -> %s",
985
+ name,
986
+ partition,
987
+ request_url,
1024
988
  )
989
+ logger.debug("Group Attributes -> %s", str(group_post_body_json))
1025
990
 
1026
- if not response or not response["listGroupsResults"]:
1027
- return False
991
+ return self.do_request(
992
+ url=request_url,
993
+ method="POST",
994
+ json_data=group_post_body_json,
995
+ timeout=None,
996
+ failure_message="Failed to reset password for user -> '{}'".format(name),
997
+ )
1028
998
 
1029
- groups = response["listGroupsResults"]["groups"]
999
+ # end method definition
1030
1000
 
1031
- if not groups:
1032
- return False
1001
+ def get_group(self, group: str, show_error: bool = True) -> dict | None:
1002
+ """Get a OTDS group by its group name
1033
1003
 
1034
- group = next(
1035
- (item for item in groups if item["name"] == group_name),
1036
- None,
1037
- )
1004
+ 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:
1010
+ {
1011
+ 'numMembers': 7,
1012
+ 'userPartitionID': 'Content Server Members',
1013
+ 'name': 'Sales',
1014
+ 'location': 'oTGroup=3f921294-b92a-4c9e-bf7c-b50df16bb937,orgunit=groups,partition=Content Server Members,dc=identity,dc=opentext,dc=net',
1015
+ 'id': 'Sales@Content Server Members',
1016
+ 'values': [{...}, {...}, {...}, {...}, {...}, {...}, {...}, {...}, {...}, ...],
1017
+ 'description': None,
1018
+ 'uuid': '3f921294-b92a-4c9e-bf7c-b50df16bb937',
1019
+ 'objectClass': 'oTGroup',
1020
+ 'customAttributes': None,
1021
+ 'originUUID': None,
1022
+ 'urlId': 'Sales@Content Server Members',
1023
+ 'urlLocation': 'oTGroup=3f921294-b92a-4c9e-bf7c-b50df16bb937,orgunit=groups,partition=Content Server Members,dc=identity,dc=opentext,dc=net'
1024
+ }
1025
+ """
1038
1026
 
1039
- if group:
1040
- return True
1027
+ request_url = self.groups_url() + "/" + group
1041
1028
 
1042
- return False
1029
+ logger.debug("Get group -> '%s'; calling -> %s", group, request_url)
1030
+
1031
+ return self.do_request(
1032
+ url=request_url,
1033
+ method="GET",
1034
+ timeout=None,
1035
+ failure_message="Failed to get group -> '{}'".format(group),
1036
+ show_error=show_error,
1037
+ )
1043
1038
 
1044
1039
  # end method definition
1045
1040
 
1046
- def is_partition_licensed(
1047
- self,
1048
- partition_name: str,
1049
- resource_id: str,
1050
- license_feature: str,
1051
- license_name: str,
1052
- ) -> bool:
1053
- """Check if a partition is licensed for a license and license feature associated with a particular OTDS resource.
1041
+ def add_user_to_group(self, user: str, group: str) -> bool:
1042
+ """Add an existing user to an existing group in OTDS
1054
1043
 
1055
1044
  Args:
1056
- partition_name (str): name of the OTDS user partition, e.g. "Content Server Members"
1057
- resource_id (str): OTDS resource ID (this is ID not the resource name!)
1058
- license_feature (str): name of the license feature, e.g. "X2" or "ADDON_ENGINEERING"
1059
- license_name (str): name of the license to assign, e.g. "EXTENDED_ECM" or "INTELLGENT_VIEWIMG"
1060
-
1045
+ user (str): name of the OTDS user (needs to exist)
1046
+ group (str): name of the OTDS group (needs to exist)
1061
1047
  Returns:
1062
- bool: True if the partition is licensed and False otherwise
1048
+ bool: True, if request is successful, False otherwise.
1063
1049
  """
1064
1050
 
1065
- response = self.get_licensed_objects(
1066
- resource_id=resource_id,
1067
- license_feature=license_feature,
1068
- license_name=license_name,
1069
- )
1070
-
1071
- if not response or not response["listUserPartitionResult"]:
1072
- return False
1051
+ user_to_group_post_body_json = {"stringList": [group]}
1073
1052
 
1074
- partitions = response["listUserPartitionResult"]["_userPartitions"]
1053
+ request_url = self.users_url() + "/" + user + "/memberof"
1075
1054
 
1076
- if not partitions:
1077
- return False
1055
+ logger.debug(
1056
+ "Adding user -> '%s' to group -> '%s'; calling -> %s",
1057
+ user,
1058
+ group,
1059
+ request_url,
1060
+ )
1078
1061
 
1079
- partition = next(
1080
- (item for item in partitions if item["name"] == partition_name),
1081
- None,
1062
+ # OTDS delivers an empty response.text for the API, so we don't parse it here:
1063
+ response = self.do_request(
1064
+ url=request_url,
1065
+ method="POST",
1066
+ json_data=user_to_group_post_body_json,
1067
+ timeout=None,
1068
+ failure_message="Failed to add user -> '{}' to group -> '{}'".format(
1069
+ user, group
1070
+ ),
1071
+ parse_request_response=False,
1082
1072
  )
1083
1073
 
1084
- if partition:
1074
+ if response and response.ok:
1085
1075
  return True
1086
1076
 
1087
1077
  return False
1088
1078
 
1089
1079
  # end method definition
1090
1080
 
1091
- def add_partition(self, name: str, description: str) -> dict | None:
1092
- """Add a new user partition to OTDS
1081
+ 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
1093
1083
 
1094
1084
  Args:
1095
- name (str): name of the new partition
1096
- description (str): description of the new partition
1085
+ group (str): name of the OTDS group (needs to exist)
1086
+ parent_group (str): name of the OTDS parent group (needs to exist)
1097
1087
  Returns:
1098
- dict: Request response or None if the creation fails.
1088
+ bool: True, if request is successful, False otherwise.
1099
1089
  """
1100
1090
 
1101
- partitionPostBodyJson = {"name": name, "description": description}
1091
+ group_to_parent_group_post_body_json = {"stringList": [parent_group]}
1102
1092
 
1103
- request_url = self.partition_url()
1093
+ request_url = self.groups_url() + "/" + group + "/memberof"
1104
1094
 
1105
- logger.info(
1106
- "Adding user partition -> %s (%s); calling -> %s",
1107
- name,
1108
- description,
1095
+ logger.debug(
1096
+ "Adding group -> '%s' to parent group -> '%s'; calling -> %s",
1097
+ group,
1098
+ parent_group,
1109
1099
  request_url,
1110
1100
  )
1111
1101
 
1112
- retries = 0
1113
- while True:
1114
- response = requests.post(
1115
- url=request_url,
1116
- json=partitionPostBodyJson,
1117
- headers=REQUEST_HEADERS,
1118
- cookies=self.cookie(),
1119
- timeout=None,
1120
- )
1121
- if response.ok:
1122
- return self.parse_request_response(response)
1123
- # Check if Session has expired - then re-authenticate and try once more
1124
- elif response.status_code == 401 and retries == 0:
1125
- logger.warning("Session has expired - try to re-authenticate...")
1126
- self.authenticate(revalidate=True)
1127
- retries += 1
1128
- else:
1129
- logger.error(
1130
- "Failed to add user partition -> %s; error -> %s (%s)",
1131
- name,
1132
- response.text,
1133
- response.status_code,
1134
- )
1135
- return None
1136
-
1137
- # end method definition
1138
-
1139
- def get_partition(self, name: str, show_error: bool = True) -> dict | None:
1140
- """Get an existing user partition from OTDS
1141
-
1142
- Args:
1143
- name (str): name of the partition to retrieve
1144
- show_error (bool, optional): whether or not we want to log an error
1145
- if partion is not found
1146
- Returns:
1147
- dict: Request response or None if the REST call fails.
1148
- """
1149
-
1150
- request_url = "{}/{}".format(self.config()["partitionUrl"], name)
1102
+ # OTDS delivers an empty response.text for the API, so we don't parse it here:
1103
+ response = self.do_request(
1104
+ url=request_url,
1105
+ method="POST",
1106
+ json_data=group_to_parent_group_post_body_json,
1107
+ timeout=None,
1108
+ failure_message="Failed to add group -> '{}' to parent group -> '{}'".format(
1109
+ group, parent_group
1110
+ ),
1111
+ parse_request_response=False,
1112
+ )
1151
1113
 
1152
- logger.info("Getting user partition -> %s; calling -> %s", name, request_url)
1114
+ if response and response.ok:
1115
+ return True
1153
1116
 
1154
- retries = 0
1155
- while True:
1156
- response = requests.get(
1157
- url=request_url,
1158
- headers=REQUEST_HEADERS,
1159
- cookies=self.cookie(),
1160
- timeout=None,
1161
- )
1162
- if response.ok:
1163
- return self.parse_request_response(response)
1164
- # Check if Session has expired - then re-authenticate and try once more
1165
- elif response.status_code == 401 and retries == 0:
1166
- logger.warning("Session has expired - try to re-authenticate...")
1167
- self.authenticate(revalidate=True)
1168
- retries += 1
1169
- else:
1170
- if show_error:
1171
- logger.error(
1172
- "Failed to get partition -> %s; warning -> %s (%s)",
1173
- name,
1174
- response.text,
1175
- response.status_code,
1176
- )
1177
- return None
1117
+ return False
1178
1118
 
1179
1119
  # end method definition
1180
1120
 
1181
- def add_user(
1121
+ def add_resource(
1182
1122
  self,
1183
- partition: str,
1184
1123
  name: str,
1185
1124
  description: str = "",
1186
- first_name: str = "",
1187
- last_name: str = "",
1188
- email: str = "",
1125
+ display_name: str = "",
1126
+ allow_impersonation: bool = True,
1127
+ resource_id: str | None = None,
1128
+ secret: str | None = None, # needs to be 16 bytes!
1129
+ additional_payload: dict | None = None,
1189
1130
  ) -> dict | None:
1190
- """Add a new user to a user partition in OTDS
1131
+ """Add an OTDS resource
1191
1132
 
1192
1133
  Args:
1193
- partition (str): name of the OTDS user partition (needs to exist)
1194
- name (str): login name of the new user
1195
- description (str, optional): description of the new user
1196
- first_name (str, optional): first name of the new user
1197
- last_name (str, optional): last name of the new user
1198
- email (str, optional): email address of the new user
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
1199
1138
  Returns:
1200
- dict: Request response or None if the creation fails.
1139
+ dict: Request response (dictionary) or None if the REST call fails.
1201
1140
  """
1202
1141
 
1203
- userPostBodyJson = {
1204
- "userPartitionID": partition,
1205
- "values": [
1206
- {"name": "sn", "values": [last_name]},
1207
- {"name": "givenName", "values": [first_name]},
1208
- {"name": "mail", "values": [email]},
1209
- ],
1210
- "name": name,
1142
+ resource_post_body = {
1143
+ "resourceName": name,
1211
1144
  "description": description,
1145
+ "displayName": display_name,
1146
+ "allowImpersonation": allow_impersonation,
1212
1147
  }
1213
1148
 
1214
- request_url = self.users_url()
1149
+ if resource_id and not secret:
1150
+ logger.error(
1151
+ "A resource ID can only be specified if a secret value is also provided!"
1152
+ )
1153
+ return None
1154
+
1155
+ if resource_id:
1156
+ resource_post_body["resourceID"] = resource_id
1157
+ if secret:
1158
+ if len(secret) != 24 or not secret.endswith("=="):
1159
+ logger.warning(
1160
+ "The secret should by 24 characters long and should end with '=='"
1161
+ )
1162
+ resource_post_body["secretKey"] = secret
1163
+
1164
+ # Check if there's additional payload for the body provided to handle special cases:
1165
+ if additional_payload:
1166
+ # Merge additional payload:
1167
+ resource_post_body.update(additional_payload)
1215
1168
 
1216
- logger.info(
1217
- "Adding user -> %s to partition -> %s; calling -> %s",
1169
+ request_url = self.config()["resourceUrl"]
1170
+
1171
+ logger.debug(
1172
+ "Adding resource -> '%s' ('%s'); calling -> %s",
1218
1173
  name,
1219
- partition,
1174
+ description,
1220
1175
  request_url,
1221
1176
  )
1222
- logger.debug("User Attributes -> %s", str(userPostBodyJson))
1223
1177
 
1224
- retries = 0
1225
- while True:
1226
- response = requests.post(
1227
- url=request_url,
1228
- json=userPostBodyJson,
1229
- headers=REQUEST_HEADERS,
1230
- cookies=self.cookie(),
1231
- timeout=None,
1232
- )
1233
- if response.ok:
1234
- return self.parse_request_response(response)
1235
- # Check if Session has expired - then re-authenticate and try once more
1236
- elif response.status_code == 401 and retries == 0:
1237
- logger.warning("Session has expired - try to re-authenticate...")
1238
- self.authenticate(revalidate=True)
1239
- retries += 1
1240
- else:
1241
- logger.error(
1242
- "Failed to add user -> %s; error -> %s (%s)",
1243
- name,
1244
- response.text,
1245
- response.status_code,
1246
- )
1247
- return None
1178
+ return self.do_request(
1179
+ url=request_url,
1180
+ method="POST",
1181
+ json_data=resource_post_body,
1182
+ timeout=None,
1183
+ failure_message="Failed to add resource -> '{}'".format(name),
1184
+ )
1248
1185
 
1249
1186
  # end method definition
1250
1187
 
1251
- def get_user(self, partition: str, user_id: str) -> dict | None:
1252
- """Get a user by its partition and user ID
1188
+ def get_resource(self, name: str, show_error: bool = False) -> dict | None:
1189
+ """Get an existing OTDS resource
1253
1190
 
1254
1191
  Args:
1255
- partition (str): name of the partition
1256
- user_id (str): ID of the user (= login name)
1192
+ name (str): name of the new OTDS resource
1193
+ show_error (bool, optional): treat as error if resource is not found
1257
1194
  Returns:
1258
- dict: Request response or None if the user was not found.
1195
+ dict: Request response or None if the REST call fails.
1196
+
1197
+ Example:
1198
+ {
1199
+ 'resourceName': 'cs',
1200
+ 'id': 'cs',
1201
+ 'description': 'Content Server',
1202
+ 'displayName': 'IDEA-TE DEV - Extended ECM 24.4.0',
1203
+ 'resourceID': 'd441e5cb-68ef-4cb7-a8a0-037ba6b35522',
1204
+ 'resourceState': 1,
1205
+ 'userSynchronizationState': True,
1206
+ 'resourceDN': 'oTResource=d441e5cb-68ef-4cb7-a8a0-037ba6b35522,dc=identity,dc=opentext,dc=net',
1207
+ 'resourceType': 'cs',
1208
+ 'accessRoleList': [{...}],
1209
+ 'impersonateList': None,
1210
+ 'impersonateAnonymousList': None,
1211
+ 'pcCreatePermissionAllowed': True,
1212
+ 'pcModifyPermissionAllowed': True,
1213
+ 'pcDeletePermissionAllowed': True,
1214
+ 'logoutURL': 'https://otawp.dev.idea-te.eimdemo.com/home/system/wcp/sso/sso_logout.htm',
1215
+ 'logoutMethod': 'GET',
1216
+ 'allowImpersonation': True,
1217
+ 'connectionHealthy': True,
1218
+ 'connectorName': 'Content Server',
1219
+ 'connectorid': 'cs',
1220
+ 'userAttributeMapping': [{...}, {...}, {...}, {...}, {...}, {...}, {...}, {...}, {...}, {...}, {...}, {...}, {...}, {...}, {...}],
1221
+ 'groupAttributeMapping': [{...}, {...}],
1222
+ 'connectionParamInfo': [{...}, {...}, {...}, {...}, {...}, {...}, {...}],
1223
+ 'logonStyle': None,
1224
+ 'logonUXVersion': 0
1225
+ }
1259
1226
  """
1260
1227
 
1261
- request_url = self.users_url() + "/" + user_id + "@" + partition
1228
+ request_url = "{}/{}".format(self.config()["resourceUrl"], name)
1262
1229
 
1263
- logger.info(
1264
- "Get user -> %s in partition -> %s; calling -> %s",
1265
- user_id,
1266
- partition,
1267
- request_url,
1268
- )
1230
+ logger.debug("Get resource -> '%s'; calling -> %s", name, request_url)
1269
1231
 
1270
- retries = 0
1271
- while True:
1272
- response = requests.get(
1273
- url=request_url,
1274
- headers=REQUEST_HEADERS,
1275
- cookies=self.cookie(),
1276
- timeout=None,
1277
- )
1278
- if response.ok:
1279
- return self.parse_request_response(response)
1280
- # Check if Session has expired - then re-authenticate and try once more
1281
- elif response.status_code == 401 and retries == 0:
1282
- logger.warning("Session has expired - try to re-authenticate...")
1283
- self.authenticate(revalidate=True)
1284
- retries += 1
1285
- else:
1286
- logger.error(
1287
- "Failed to get user -> %s; error -> %s (%s)",
1288
- user_id,
1289
- response.text,
1290
- response.status_code,
1291
- )
1292
- return None
1232
+ return self.do_request(
1233
+ url=request_url,
1234
+ method="GET",
1235
+ timeout=None,
1236
+ failure_message="Failed to get resource -> '{}'".format(name),
1237
+ show_error=show_error,
1238
+ )
1293
1239
 
1294
1240
  # end method definition
1295
1241
 
1296
- def get_users(self, partition: str = "", limit: int | None = None) -> dict | None:
1297
- """Get all users in a partition partition
1242
+ def update_resource(
1243
+ self, name: str, resource: object, show_error: bool = True
1244
+ ) -> dict | None:
1245
+ """Update an existing OTDS resource
1298
1246
 
1299
1247
  Args:
1300
- partition (str, optional): name of the partition
1301
- limit (int): maximum number of users to return
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
1302
1251
  Returns:
1303
- dict: Request response or None if the user was not found.
1252
+ dict: Request response (json) or None if the REST call fails.
1304
1253
  """
1305
1254
 
1306
- # Add query parameters (these are NOT passed via JSon body!)
1307
- query = {}
1308
- if limit:
1309
- query["limit"] = limit
1310
- if partition:
1311
- query["where_partition_name"] = partition
1312
-
1313
- encodedQuery = urllib.parse.urlencode(query, doseq=True)
1314
-
1315
- request_url = self.users_url()
1316
- if query:
1317
- request_url += "?{}".format(encodedQuery)
1255
+ request_url = "{}/{}".format(self.config()["resourceUrl"], name)
1318
1256
 
1319
- if partition:
1320
- logger.info(
1321
- "Get all users in partition -> %s (limit -> %s); calling -> %s",
1322
- partition,
1323
- limit,
1324
- request_url,
1325
- )
1326
- else:
1327
- logger.info(
1328
- "Get all users (limit -> %s); calling -> %s",
1329
- limit,
1330
- request_url,
1331
- )
1257
+ logger.debug("Updating resource -> '%s'; calling -> %s", name, request_url)
1332
1258
 
1333
- retries = 0
1334
- while True:
1335
- response = requests.get(
1336
- url=request_url,
1337
- headers=REQUEST_HEADERS,
1338
- cookies=self.cookie(),
1339
- timeout=None,
1340
- )
1341
- if response.ok:
1342
- return self.parse_request_response(response)
1343
- # Check if Session has expired - then re-authenticate and try once more
1344
- elif response.status_code == 401 and retries == 0:
1345
- logger.warning("Session has expired - try to re-authenticate...")
1346
- self.authenticate(revalidate=True)
1347
- retries += 1
1348
- else:
1349
- if partition:
1350
- logger.error(
1351
- "Failed to get users in partition -> %s; error -> %s (%s)",
1352
- partition,
1353
- response.text,
1354
- response.status_code,
1355
- )
1356
- else:
1357
- logger.error(
1358
- "Failed to get users; error -> %s (%s)",
1359
- response.text,
1360
- response.status_code,
1361
- )
1362
- return None
1259
+ return self.do_request(
1260
+ url=request_url,
1261
+ method="PUT",
1262
+ json_data=resource,
1263
+ timeout=None,
1264
+ failure_message="Failed to update resource -> '{}'".format(name),
1265
+ show_error=show_error,
1266
+ )
1363
1267
 
1364
1268
  # end method definition
1365
1269
 
1366
- def update_user(
1367
- self, partition: str, user_id: str, attribute_name: str, attribute_value: str
1368
- ) -> dict | None:
1369
- """Update a user attribute with a new value
1270
+ def activate_resource(self, resource_id: str) -> dict | None:
1271
+ """Activate an OTDS resource
1370
1272
 
1371
1273
  Args:
1372
- partition (str): name of the partition
1373
- user_id (str): ID of the user (= login name)
1374
- attribute_name (str): name of the attribute
1375
- attribute_value (str): new value of the attribute
1376
- Return:
1377
- dict: Request response or None if the update fails.
1274
+ resource_id (str): ID of the OTDS resource
1275
+ Returns:
1276
+ dict: Request response (json) or None if the REST call fails.
1378
1277
  """
1379
1278
 
1380
- if attribute_name in ["description"]:
1381
- userPatchBodyJson = {
1382
- "userPartitionID": partition,
1383
- attribute_name: attribute_value,
1384
- }
1385
- else:
1386
- userPatchBodyJson = {
1387
- "userPartitionID": partition,
1388
- "values": [{"name": attribute_name, "values": [attribute_value]}],
1389
- }
1279
+ resource_post_body_json = {}
1390
1280
 
1391
- request_url = self.users_url() + "/" + user_id
1281
+ request_url = "{}/{}/activate".format(self.config()["resourceUrl"], resource_id)
1392
1282
 
1393
- logger.info(
1394
- "Update user -> %s attribute -> %s to value -> %s; calling -> %s",
1395
- user_id,
1396
- attribute_name,
1397
- attribute_value,
1398
- request_url,
1283
+ logger.debug(
1284
+ "Activating resource -> '%s'; calling -> %s", resource_id, request_url
1399
1285
  )
1400
1286
 
1401
- retries = 0
1402
- while True:
1403
- response = requests.patch(
1404
- url=request_url,
1405
- json=userPatchBodyJson,
1406
- headers=REQUEST_HEADERS,
1407
- cookies=self.cookie(),
1408
- timeout=None,
1409
- )
1410
- if response.ok:
1411
- return self.parse_request_response(response)
1412
- # Check if Session has expired - then re-authenticate and try once more
1413
- elif response.status_code == 401 and retries == 0:
1414
- logger.warning("Session has expired - try to re-authenticate...")
1415
- self.authenticate(revalidate=True)
1416
- retries += 1
1417
- else:
1418
- logger.error(
1419
- "Failed to update user -> %s; error -> %s (%s)",
1420
- user_id,
1421
- response.text,
1422
- response.status_code,
1423
- )
1424
- return None
1287
+ return self.do_request(
1288
+ url=request_url,
1289
+ method="POST",
1290
+ json_data=resource_post_body_json,
1291
+ timeout=None,
1292
+ failure_message="Failed to activate resource -> '{}'".format(resource_id),
1293
+ )
1425
1294
 
1426
1295
  # end method definition
1427
1296
 
1428
- def delete_user(self, partition: str, user_id: str) -> bool:
1429
- """Delete an existing user
1297
+ def get_access_roles(self) -> dict | None:
1298
+ """Get a list of all OTDS access roles
1430
1299
 
1431
1300
  Args:
1432
- partition (str): name of the partition
1433
- user_id (str): Id (= login name) of the user
1301
+ None
1434
1302
  Returns:
1435
- bool: True = success, False = error
1303
+ dict: Request response or None if the REST call fails.
1436
1304
  """
1437
1305
 
1438
- request_url = self.users_url() + "/" + user_id + "@" + partition
1306
+ request_url = self.config()["accessRoleUrl"]
1439
1307
 
1440
- logger.info(
1441
- "Delete user -> %s in partition -> %s; calling -> %s",
1442
- user_id,
1443
- partition,
1444
- request_url,
1445
- )
1308
+ logger.debug("Retrieving access roles; calling -> %s", request_url)
1446
1309
 
1447
- retries = 0
1448
- while True:
1449
- response = requests.delete(
1450
- url=request_url,
1451
- headers=REQUEST_HEADERS,
1452
- cookies=self.cookie(),
1453
- timeout=None,
1454
- )
1455
- if response.ok:
1456
- return True
1457
- # Check if Session has expired - then re-authenticate and try once more
1458
- elif response.status_code == 401 and retries == 0:
1459
- logger.warning("Session has expired - try to re-authenticate...")
1460
- self.authenticate(revalidate=True)
1461
- retries += 1
1462
- else:
1463
- logger.error(
1464
- "Failed to delete user -> %s; error -> %s (%s)",
1465
- user_id,
1466
- response.text,
1467
- response.status_code,
1468
- )
1469
- return False
1310
+ return self.do_request(
1311
+ url=request_url,
1312
+ method="GET",
1313
+ timeout=None,
1314
+ failure_message="Failed to get access roles",
1315
+ )
1470
1316
 
1471
1317
  # end method definition
1472
1318
 
1473
- def reset_user_password(self, user_id: str, password: str) -> bool:
1474
- """Reset a password of an existing user
1319
+ def get_access_role(self, access_role: str) -> dict | None:
1320
+ """Get an OTDS access role
1475
1321
 
1476
1322
  Args:
1477
- user_id (str): Id (= login name) of the user
1478
- password (str): new password of the user
1323
+ name (str): name of the access role
1479
1324
  Returns:
1480
- bool: True = success, False = error.
1325
+ dict: Request response (json) or None if the REST call fails.
1481
1326
  """
1482
1327
 
1483
- userPostBodyJson = {"newPassword": password}
1328
+ request_url = self.config()["accessRoleUrl"] + "/" + access_role
1484
1329
 
1485
- request_url = "{}/{}/password".format(self.users_url(), user_id)
1330
+ logger.debug("Get access role -> '%s'; calling -> %s", access_role, request_url)
1486
1331
 
1487
- logger.info(
1488
- "Resetting password for user -> %s; calling -> %s", user_id, request_url
1332
+ return self.do_request(
1333
+ url=request_url,
1334
+ method="GET",
1335
+ timeout=None,
1336
+ failure_message="Failed to get access role -> '{}'".format(access_role),
1489
1337
  )
1490
1338
 
1491
- retries = 0
1492
- while True:
1493
- response = requests.put(
1494
- url=request_url,
1495
- json=userPostBodyJson,
1496
- headers=REQUEST_HEADERS,
1497
- cookies=self.cookie(),
1498
- timeout=None,
1499
- )
1500
- if response.ok:
1501
- return True
1502
- # Check if Session has expired - then re-authenticate and try once more
1503
- elif response.status_code == 401 and retries == 0:
1504
- logger.warning("Session has expired - try to re-authenticate...")
1505
- self.authenticate(revalidate=True)
1506
- retries += 1
1507
- else:
1508
- logger.error(
1509
- "Failed to reset password for user -> %s; error -> %s (%s)",
1510
- user_id,
1511
- response.text,
1512
- response.status_code,
1513
- )
1514
- return False
1515
-
1516
1339
  # end method definition
1517
1340
 
1518
- def add_group(self, partition: str, name: str, description: str) -> dict | None:
1519
- """Add a new user group to a user partition in OTDS
1341
+ def add_partition_to_access_role(
1342
+ self, access_role: str, partition: str, location: str = ""
1343
+ ) -> bool:
1344
+ """Add an OTDS partition to an OTDS access role
1520
1345
 
1521
1346
  Args:
1522
- partition (str): name of the OTDS user partition (needs to exist)
1523
- name (str): name of the new group
1524
- description (str): description of the new group
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 ("")
1525
1351
  Returns:
1526
- dict: Request response (json) or None if the creation fails.
1352
+ bool: True if partition is in access role or has been successfully added.
1353
+ False if partition has been not been added (error)
1527
1354
  """
1528
1355
 
1529
- groupPostBodyJson = {
1530
- "userPartitionID": partition,
1531
- "name": name,
1532
- "description": description,
1356
+ access_role_post_body_json = {
1357
+ "userPartitions": [{"name": partition, "location": location}]
1533
1358
  }
1534
1359
 
1535
- request_url = self.groups_url()
1360
+ request_url = "{}/{}/members".format(
1361
+ self.config()["accessRoleUrl"], access_role
1362
+ )
1536
1363
 
1537
- logger.info(
1538
- "Adding group -> %s to partition -> %s; calling -> %s",
1539
- name,
1364
+ logger.debug(
1365
+ "Add user partition -> '%s' to access role -> '%s'; calling -> %s",
1540
1366
  partition,
1367
+ access_role,
1541
1368
  request_url,
1542
1369
  )
1543
- logger.debug("Group Attributes -> %s", str(groupPostBodyJson))
1544
1370
 
1545
- retries = 0
1546
- while True:
1547
- response = requests.post(
1548
- url=request_url,
1549
- json=groupPostBodyJson,
1550
- headers=REQUEST_HEADERS,
1551
- cookies=self.cookie(),
1552
- timeout=None,
1553
- )
1554
- if response.ok:
1555
- return self.parse_request_response(response)
1556
- # Check if Session has expired - then re-authenticate and try once more
1557
- elif response.status_code == 401 and retries == 0:
1558
- logger.warning("Session has expired - try to re-authenticate...")
1559
- self.authenticate(revalidate=True)
1560
- retries += 1
1561
- else:
1562
- logger.error(
1563
- "Failed to add group -> %s; error -> %s (%s)",
1564
- name,
1565
- response.text,
1566
- response.status_code,
1567
- )
1568
- return None
1371
+ response = self.do_request(
1372
+ url=request_url,
1373
+ method="POST",
1374
+ json_data=access_role_post_body_json,
1375
+ timeout=None,
1376
+ failure_message="Failed to add partition -> '{}' to access role -> '{}'".format(
1377
+ partition, access_role
1378
+ ),
1379
+ parse_request_response=False,
1380
+ )
1381
+
1382
+ if response and response.ok:
1383
+ return True
1384
+
1385
+ return False
1569
1386
 
1570
1387
  # end method definition
1571
1388
 
1572
- def get_group(self, group: str) -> dict | None:
1573
- """Get a OTDS group by its group name
1389
+ def add_user_to_access_role(
1390
+ self, access_role: str, user_id: str, location: str = ""
1391
+ ) -> bool:
1392
+ """Add an OTDS user to an OTDS access role
1574
1393
 
1575
1394
  Args:
1576
- group (str): ID of the group (= group name)
1577
- Return:
1578
- dict: Request response or None if the group was not found.
1579
- Example values:
1580
- {
1581
- 'numMembers': 7,
1582
- 'userPartitionID': 'Content Server Members',
1583
- 'name': 'Sales',
1584
- 'location': 'oTGroup=3f921294-b92a-4c9e-bf7c-b50df16bb937,orgunit=groups,partition=Content Server Members,dc=identity,dc=opentext,dc=net',
1585
- 'id': 'Sales@Content Server Members',
1586
- 'values': [{...}, {...}, {...}, {...}, {...}, {...}, {...}, {...}, {...}, ...],
1587
- 'description': None,
1588
- 'uuid': '3f921294-b92a-4c9e-bf7c-b50df16bb937',
1589
- 'objectClass': 'oTGroup',
1590
- 'customAttributes': None,
1591
- 'originUUID': None,
1592
- 'urlId': 'Sales@Content Server Members',
1593
- 'urlLocation': 'oTGroup=3f921294-b92a-4c9e-bf7c-b50df16bb937,orgunit=groups,partition=Content Server Members,dc=identity,dc=opentext,dc=net'
1594
- }
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 ("")
1399
+ Returns:
1400
+ bool: True if user is in access role or has been successfully added.
1401
+ False if user has not been added (error)
1595
1402
  """
1596
1403
 
1597
- request_url = self.groups_url() + "/" + group
1598
-
1599
- logger.info("Get group -> %s; calling -> %s", group, request_url)
1404
+ # get existing members to check if user is already a member:
1405
+ access_roles_get_response = self.get_access_role(access_role)
1406
+ if not access_roles_get_response:
1407
+ return False
1600
1408
 
1601
- retries = 0
1602
- while True:
1603
- response = requests.get(
1604
- url=request_url,
1605
- headers=REQUEST_HEADERS,
1606
- cookies=self.cookie(),
1607
- timeout=None,
1608
- )
1609
- if response.ok:
1610
- return self.parse_request_response(response)
1611
- # Check if Session has expired - then re-authenticate and try once more
1612
- elif response.status_code == 401 and retries == 0:
1613
- logger.warning("Session has expired - try to re-authenticate...")
1614
- self.authenticate(revalidate=True)
1615
- retries += 1
1616
- else:
1617
- logger.error(
1618
- "Failed to get group -> %s; error -> %s (%s)",
1619
- group,
1620
- response.text,
1621
- response.status_code,
1409
+ # Checking if user already added to access role
1410
+ accessRoleUsers = access_roles_get_response["accessRoleMembers"]["users"]
1411
+ for user in accessRoleUsers:
1412
+ if user["displayName"] == user_id:
1413
+ logger.debug(
1414
+ "User -> '%s' already added to access role -> '%s'",
1415
+ user_id,
1416
+ access_role,
1622
1417
  )
1623
- return None
1418
+ return True
1419
+
1420
+ logger.debug(
1421
+ "User -> '%s' is not yet in access role -> '%s' - adding...",
1422
+ user_id,
1423
+ access_role,
1424
+ )
1425
+
1426
+ # create payload for REST call:
1427
+ access_role_post_body_json = {
1428
+ "users": [{"name": user_id, "location": location}]
1429
+ }
1430
+
1431
+ request_url = "{}/{}/members".format(
1432
+ self.config()["accessRoleUrl"], access_role
1433
+ )
1434
+
1435
+ logger.debug(
1436
+ "Add user -> %s to access role -> %s; calling -> %s",
1437
+ user_id,
1438
+ access_role,
1439
+ request_url,
1440
+ )
1441
+
1442
+ response = self.do_request(
1443
+ url=request_url,
1444
+ method="POST",
1445
+ json_data=access_role_post_body_json,
1446
+ timeout=None,
1447
+ failure_message="Failed to add user -> '{}' to access role -> '{}'".format(
1448
+ user_id, access_role
1449
+ ),
1450
+ parse_request_response=False,
1451
+ )
1452
+
1453
+ if response and response.ok:
1454
+ return True
1455
+
1456
+ return False
1624
1457
 
1625
1458
  # end method definition
1626
1459
 
1627
- def add_user_to_group(self, user: str, group: str) -> bool:
1628
- """Add an existing user to an existing group in OTDS
1460
+ def add_group_to_access_role(
1461
+ self, access_role: str, group: str, location: str = ""
1462
+ ) -> bool:
1463
+ """Add an OTDS group to an OTDS access role
1629
1464
 
1630
1465
  Args:
1631
- user (str): name of the OTDS user (needs to exist)
1632
- group (str): name of the OTDS group (needs to exist)
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 ("")
1633
1470
  Returns:
1634
- bool: True, if request is successful, False otherwise.
1471
+ bool: True if group is in access role or has been successfully added.
1472
+ False if group has been not been added (error)
1635
1473
  """
1636
1474
 
1637
- userToGroupPostBodyJson = {"stringList": [group]}
1638
-
1639
- request_url = self.users_url() + "/" + user + "/memberof"
1475
+ # get existing members to check if user is already a member:
1476
+ access_roles_get_response = self.get_access_role(access_role)
1477
+ if not access_roles_get_response:
1478
+ return False
1640
1479
 
1641
- logger.info(
1642
- "Adding user -> %s to group -> %s; calling -> %s", user, group, request_url
1643
- )
1644
-
1645
- retries = 0
1646
- while True:
1647
- response = requests.post(
1648
- url=request_url,
1649
- json=userToGroupPostBodyJson,
1650
- headers=REQUEST_HEADERS,
1651
- cookies=self.cookie(),
1652
- timeout=None,
1653
- )
1654
- if response.ok:
1655
- return True
1656
- # Check if Session has expired - then re-authenticate and try once more
1657
- elif response.status_code == 401 and retries == 0:
1658
- logger.warning("Session has expired - try to re-authenticate...")
1659
- self.authenticate(revalidate=True)
1660
- retries += 1
1661
- else:
1662
- logger.error(
1663
- "Failed to add user -> %s to group -> %s; error -> %s (%s)",
1664
- user,
1480
+ # Checking if group already added to access role
1481
+ access_role_groups = access_roles_get_response["accessRoleMembers"]["groups"]
1482
+ for access_role_group in access_role_groups:
1483
+ if access_role_group["name"] == group:
1484
+ logger.debug(
1485
+ "Group -> '%s' already added to access role -> '%s'",
1665
1486
  group,
1666
- response.text,
1667
- response.status_code,
1487
+ access_role,
1668
1488
  )
1669
- return False
1489
+ return True
1490
+
1491
+ logger.debug(
1492
+ "Group -> '%s' is not yet in access role -> '%s' - adding...",
1493
+ group,
1494
+ access_role,
1495
+ )
1496
+
1497
+ # create payload for REST call:
1498
+ access_role_post_body_json = {"groups": [{"name": group, "location": location}]}
1499
+
1500
+ request_url = "{}/{}/members".format(
1501
+ self.config()["accessRoleUrl"], access_role
1502
+ )
1503
+
1504
+ logger.debug(
1505
+ "Add group -> '%s' to access role -> '%s'; calling -> %s",
1506
+ group,
1507
+ access_role,
1508
+ request_url,
1509
+ )
1510
+
1511
+ response = self.do_request(
1512
+ url=request_url,
1513
+ method="POST",
1514
+ json_data=access_role_post_body_json,
1515
+ timeout=None,
1516
+ failure_message="Failed to add group -> '{}' to access role -> '{}'".format(
1517
+ group, access_role
1518
+ ),
1519
+ parse_request_response=False,
1520
+ )
1521
+
1522
+ if response and response.ok:
1523
+ return True
1524
+
1525
+ return False
1670
1526
 
1671
1527
  # end method definition
1672
1528
 
1673
- def add_group_to_parent_group(self, group: str, parent_group: str) -> bool:
1674
- """Add an existing group to an existing parent group in OTDS
1529
+ def update_access_role_attributes(
1530
+ self, name: str, attribute_list: list
1531
+ ) -> dict | None:
1532
+ """Update some attributes of an existing OTDS Access Role
1675
1533
 
1676
1534
  Args:
1677
- group (str): name of the OTDS group (needs to exist)
1678
- parent_group (str): name of the OTDS parent group (needs to exist)
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"]}]
1679
1539
  Returns:
1680
- bool: True, if request is successful, False otherwise.
1540
+ dict: Request response (json) or None if the REST call fails.
1681
1541
  """
1682
1542
 
1683
- groupToParentGroupPostBodyJson = {"stringList": [parent_group]}
1543
+ # Return if list is empty:
1544
+ if not attribute_list:
1545
+ return None
1684
1546
 
1685
- request_url = self.groups_url() + "/" + group + "/memberof"
1547
+ # create payload for REST call:
1548
+ access_role = self.get_access_role(name)
1549
+ if not access_role:
1550
+ logger.error("Failed to get access role -> '%s'", name)
1551
+ return None
1686
1552
 
1687
- logger.info(
1688
- "Adding group -> %s to parent group -> %s; calling -> %s",
1689
- group,
1690
- parent_group,
1553
+ access_role_put_body_json = {"attributes": attribute_list}
1554
+
1555
+ request_url = "{}/{}/attributes".format(self.config()["accessRoleUrl"], name)
1556
+
1557
+ logger.debug(
1558
+ "Update access role -> '%s' with attributes -> %s; calling -> %s",
1559
+ name,
1560
+ str(access_role_put_body_json),
1691
1561
  request_url,
1692
1562
  )
1693
1563
 
1694
- retries = 0
1695
- while True:
1696
- response = requests.post(
1697
- url=request_url,
1698
- json=groupToParentGroupPostBodyJson,
1699
- headers=REQUEST_HEADERS,
1700
- cookies=self.cookie(),
1701
- timeout=None,
1702
- )
1703
-
1704
- if response.ok:
1705
- return True
1706
- # Check if Session has expired - then re-authenticate and try once more
1707
- elif response.status_code == 401 and retries == 0:
1708
- logger.warning("Session has expired - try to re-authenticate...")
1709
- self.authenticate(revalidate=True)
1710
- retries += 1
1711
- else:
1712
- logger.error(
1713
- "Failed to add group -> %s to parent group -> %s; error -> %s (%s)",
1714
- group,
1715
- parent_group,
1716
- response.text,
1717
- response.status_code,
1718
- )
1719
- return False
1564
+ return self.do_request(
1565
+ url=request_url,
1566
+ method="PUT",
1567
+ json_data=access_role_put_body_json,
1568
+ timeout=None,
1569
+ failure_message="Failed to update access role -> '{}'".format(access_role),
1570
+ )
1720
1571
 
1721
1572
  # end method definition
1722
1573
 
1723
- def add_resource(
1574
+ def add_license_to_resource(
1724
1575
  self,
1725
- name: str,
1726
- description: str,
1727
- display_name: str,
1728
- additional_payload: dict | None = None,
1576
+ path_to_license_file: str,
1577
+ product_name: str,
1578
+ product_description: str,
1579
+ resource_id: str,
1580
+ update: bool = True,
1729
1581
  ) -> dict | None:
1730
- """Add an OTDS resource
1582
+ """Add a product license to an OTDS resource.
1731
1583
 
1732
1584
  Args:
1733
- name (str): name of the new OTDS resource
1734
- description (str): description of the new OTDS resource
1735
- display_name (str): display name of the OTDS resource
1736
- additional_payload (dict, optional): additional values for the json payload
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)
1737
1590
  Returns:
1738
- dict: Request response (dictionary) or None if the REST call fails.
1591
+ dict: Request response (dictionary) or None if the REST call fails
1739
1592
  """
1740
1593
 
1741
- resourcePostBodyJson = {
1742
- "resourceName": name,
1743
- "description": description,
1744
- "displayName": display_name,
1745
- }
1594
+ logger.debug("Reading license file -> '%s'...", path_to_license_file)
1595
+ try:
1596
+ with open(path_to_license_file, "rt", encoding="UTF-8") as license_file:
1597
+ license_content = license_file.read()
1598
+ except IOError as exception:
1599
+ logger.error(
1600
+ "Error opening license file -> '%s'; error -> %s",
1601
+ path_to_license_file,
1602
+ exception.strerror,
1603
+ )
1604
+ return None
1746
1605
 
1747
- # Check if there's additional payload for the body provided to handle special cases:
1748
- if additional_payload:
1749
- # Merge additional payload:
1750
- resourcePostBodyJson.update(additional_payload)
1606
+ license_post_body_json = {
1607
+ "description": product_description,
1608
+ "name": product_name,
1609
+ "values": [
1610
+ {"name": "oTLicenseFile", "values": license_content},
1611
+ {"name": "oTLicenseResource", "values": resource_id},
1612
+ {"name": "oTLicenseFingerprintGenerator", "values": [None]},
1613
+ ],
1614
+ }
1751
1615
 
1752
- request_url = self.config()["resourceUrl"]
1616
+ request_url = self.license_url()
1617
+ # Check if we want to update an existing license:
1618
+ if update:
1619
+ existing_license = self.get_license_for_resource(resource_id)
1620
+ if existing_license:
1621
+ request_url += "/" + existing_license[0]["id"]
1622
+ else:
1623
+ logger.debug(
1624
+ "No existing license found for resource -> '%s' - adding a new license...",
1625
+ resource_id,
1626
+ )
1627
+ # change strategy to create a new license:
1628
+ update = False
1753
1629
 
1754
- logger.info(
1755
- "Adding resource -> %s (%s); calling -> %s", name, description, request_url
1630
+ logger.debug(
1631
+ "Adding product license -> '%s' for product -> '%s' to resource ->'%s'; calling -> %s",
1632
+ path_to_license_file,
1633
+ product_description,
1634
+ resource_id,
1635
+ request_url,
1756
1636
  )
1757
1637
 
1758
- retries = 0
1759
- while True:
1760
- response = requests.post(
1638
+ if update:
1639
+ # Do a REST PUT call for update an existing license:
1640
+ return self.do_request(
1761
1641
  url=request_url,
1762
- json=resourcePostBodyJson,
1763
- headers=REQUEST_HEADERS,
1764
- cookies=self.cookie(),
1642
+ method="PUT",
1643
+ json_data=license_post_body_json,
1765
1644
  timeout=None,
1645
+ failure_message="Failed to update product license -> '{}' for product -> '{}'".format(
1646
+ path_to_license_file, product_description
1647
+ ),
1648
+ )
1649
+ else:
1650
+ # Do a REST POST call for creation of a new license:
1651
+ return self.do_request(
1652
+ url=request_url,
1653
+ method="POST",
1654
+ json_data=license_post_body_json,
1655
+ timeout=None,
1656
+ failure_message="Failed to add product license -> '{}' for product -> '{}'".format(
1657
+ path_to_license_file, product_description
1658
+ ),
1766
1659
  )
1767
- if response.ok:
1768
- return self.parse_request_response(response)
1769
- # Check if Session has expired - then re-authenticate and try once more
1770
- elif response.status_code == 401 and retries == 0:
1771
- logger.warning("Session has expired - try to re-authenticate...")
1772
- self.authenticate(revalidate=True)
1773
- retries += 1
1774
- else:
1775
- logger.error(
1776
- "Failed to add resource -> %s; error -> %s (%s)",
1777
- name,
1778
- response.text,
1779
- response.status_code,
1780
- )
1781
- return None
1782
1660
 
1783
1661
  # end method definition
1784
1662
 
1785
- def get_resource(self, name: str, show_error: bool = False) -> dict | None:
1786
- """Get an existing OTDS resource
1663
+ def get_license_for_resource(self, resource_id: str):
1664
+ """Get a product license for a resource in OTDS.
1787
1665
 
1788
1666
  Args:
1789
- name (str): name of the new OTDS resource
1790
- show_error (bool, optional): treat as error if resource is not found
1667
+ resource_id (str): OTDS resource ID (this is ID not the resource name!)
1791
1668
  Returns:
1792
- dict: Request response or None if the REST call fails.
1669
+ Licenses for a resource or None if the REST call fails
1670
+
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
+ }
1793
1683
  """
1794
1684
 
1795
- request_url = "{}/{}".format(self.config()["resourceUrl"], name)
1685
+ request_url = (
1686
+ self.license_url()
1687
+ + "/assignedlicenses?resourceID="
1688
+ + resource_id
1689
+ + "&validOnly=false"
1690
+ )
1796
1691
 
1797
- logger.info("Retrieving resource -> %s; calling -> %s", name, request_url)
1692
+ logger.debug(
1693
+ "Get license for resource -> %s; calling -> %s", resource_id, request_url
1694
+ )
1798
1695
 
1799
- retries = 0
1800
- while True:
1801
- response = requests.get(
1802
- url=request_url,
1803
- headers=REQUEST_HEADERS,
1804
- cookies=self.cookie(),
1805
- timeout=None,
1806
- )
1807
- if response.ok:
1808
- return self.parse_request_response(response)
1809
- # Check if Session has expired - then re-authenticate and try once more
1810
- elif response.status_code == 401 and retries == 0:
1811
- logger.warning("Session has expired - try to re-authenticate...")
1812
- self.authenticate(revalidate=True)
1813
- retries += 1
1814
- else:
1815
- # We don't necessarily want to log an error as this function
1816
- # is also used in wait loops:
1817
- if show_error:
1818
- logger.warning(
1819
- "Failed to retrieve resource -> %s; warning -> %s",
1820
- name,
1821
- response.text,
1822
- )
1823
- else:
1824
- logger.info("Resource -> %s not found.", name)
1825
- return None
1696
+ response = self.do_request(
1697
+ url=request_url,
1698
+ method="GET",
1699
+ timeout=None,
1700
+ failure_message="Failed to get license for resource -> '{}'".format(
1701
+ resource_id
1702
+ ),
1703
+ )
1704
+
1705
+ if not response:
1706
+ return None
1707
+
1708
+ return response["licenseObjects"]["_licenses"]
1826
1709
 
1827
1710
  # end method definition
1828
1711
 
1829
- def update_resource(
1830
- self, name: str, resource: object, show_error: bool = True
1831
- ) -> dict | None:
1832
- """Update an existing OTDS resource
1712
+ def delete_license_from_resource(self, resource_id: str, license_id: str) -> bool:
1713
+ """Delete a product license for a resource in OTDS.
1833
1714
 
1834
1715
  Args:
1835
- name (str): name of the new OTDS resource
1836
- resource (object): updated resource object of get_resource called before
1837
- show_error (bool, optional): treat as error if resource is not found
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!)
1838
1718
  Returns:
1839
- dict: Request response (json) or None if the REST call fails.
1719
+ bool: True if successful or False if the REST call fails
1840
1720
  """
1841
1721
 
1842
- request_url = "{}/{}".format(self.config()["resourceUrl"], name)
1843
-
1844
- logger.info("Updating resource -> %s; calling -> %s", name, request_url)
1722
+ request_url = "{}/{}".format(self.license_url(), license_id)
1845
1723
 
1846
- retries = 0
1847
- while True:
1848
- response = requests.put(
1849
- url=request_url,
1850
- json=resource,
1851
- headers=REQUEST_HEADERS,
1852
- cookies=self.cookie(),
1853
- timeout=None,
1854
- )
1855
- if response.ok:
1856
- return self.parse_request_response(response)
1857
- # Check if Session has expired - then re-authenticate and try once more
1858
- elif response.status_code == 401 and retries == 0:
1859
- logger.warning("Session has expired - try to re-authenticate...")
1860
- self.authenticate(revalidate=True)
1861
- retries += 1
1862
- else:
1863
- # We don't necessarily want to log an error as this function
1864
- # is also used in wait loops:
1865
- if show_error:
1866
- logger.warning(
1867
- "Failed to retrieve resource -> %s; warning -> %s",
1868
- name,
1869
- response.text,
1870
- )
1871
- else:
1872
- logger.info("Resource -> %s not found.", name)
1873
- return None
1724
+ logger.debug(
1725
+ "Deleting product license -> '%s' from resource -> '%s'; calling -> %s",
1726
+ license_id,
1727
+ resource_id,
1728
+ request_url,
1729
+ )
1730
+
1731
+ response = self.do_request(
1732
+ url=request_url,
1733
+ method="DELETE",
1734
+ timeout=None,
1735
+ failure_message="Failed to delete license -> '{}' for resource -> '{}'".format(
1736
+ license_id, resource_id
1737
+ ),
1738
+ parse_request_response=False,
1739
+ )
1740
+
1741
+ if response and response.ok:
1742
+ return True
1743
+
1744
+ return False
1874
1745
 
1875
1746
  # end method definition
1876
1747
 
1877
- def activate_resource(self, resource_id: str) -> dict | None:
1878
- """Activate an OTDS resource
1748
+ def assign_user_to_license(
1749
+ self,
1750
+ partition: str,
1751
+ user_id: str,
1752
+ resource_id: str,
1753
+ license_feature: str,
1754
+ license_name: str,
1755
+ license_type: str = "Full",
1756
+ ) -> bool:
1757
+ """Assign an OTDS user to a product license (feature) in OTDS.
1879
1758
 
1880
1759
  Args:
1881
- resource_id (str): ID of the OTDS resource
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"
1882
1766
  Returns:
1883
- dict: Request response (json) or None if the REST call fails.
1767
+ bool: True if successful or False if the REST call fails or the license is not found
1884
1768
  """
1885
1769
 
1886
- resourcePostBodyJson = {}
1770
+ licenses = self.get_license_for_resource(resource_id)
1887
1771
 
1888
- request_url = "{}/{}/activate".format(self.config()["resourceUrl"], resource_id)
1772
+ for lic in licenses:
1773
+ if lic["_oTLicenseProduct"] == license_name:
1774
+ license_location = lic["id"]
1775
+
1776
+ try:
1777
+ license_location
1778
+ except UnboundLocalError:
1779
+ logger.error(
1780
+ "Cannot find license -> '%s' for resource -> %s",
1781
+ license_name,
1782
+ resource_id,
1783
+ )
1784
+ return False
1785
+
1786
+ user = self.get_user(partition, user_id)
1787
+ if user:
1788
+ user_location = user["location"]
1789
+ else:
1790
+ logger.error("Cannot find location for user -> '%s'", user_id)
1791
+ return False
1792
+
1793
+ license_post_body_json = {
1794
+ "_oTLicenseType": license_type,
1795
+ "_oTLicenseProduct": "users",
1796
+ "name": user_location,
1797
+ "values": [{"name": "counter", "values": [license_feature]}],
1798
+ }
1799
+
1800
+ request_url = self.license_url() + "/object/" + license_location
1889
1801
 
1890
- logger.info(
1891
- "Activating resource -> %s; calling -> %s", resource_id, request_url
1802
+ logger.debug(
1803
+ "Assign license feature -> '%s' of license -> '%s' associated with resource -> '%s' to user -> '%s'; calling -> %s",
1804
+ license_feature,
1805
+ license_location,
1806
+ resource_id,
1807
+ user_id,
1808
+ request_url,
1892
1809
  )
1893
1810
 
1894
- retries = 0
1895
- while True:
1896
- response = requests.post(
1897
- url=request_url,
1898
- json=resourcePostBodyJson,
1899
- headers=REQUEST_HEADERS,
1900
- cookies=self.cookie(),
1901
- timeout=None,
1811
+ response = self.do_request(
1812
+ url=request_url,
1813
+ method="POST",
1814
+ json_data=license_post_body_json,
1815
+ timeout=None,
1816
+ failure_message="Failed to add license feature -> '{}' associated with resource -> '{}' to user -> '{}'".format(
1817
+ license_feature, resource_id, user_id
1818
+ ),
1819
+ parse_request_response=False,
1820
+ )
1821
+
1822
+ if response and response.ok:
1823
+ logger.debug(
1824
+ "Added license feature -> '%s' to user -> '%s'",
1825
+ license_feature,
1826
+ user_id,
1902
1827
  )
1903
- if response.ok:
1904
- return self.parse_request_response(response)
1905
- # Check if Session has expired - then re-authenticate and try once more
1906
- elif response.status_code == 401 and retries == 0:
1907
- logger.warning("Session has expired - try to re-authenticate...")
1908
- self.authenticate(revalidate=True)
1909
- retries += 1
1910
- else:
1911
- logger.error(
1912
- "Failed to activate resource -> %s; error -> %s (%s)",
1913
- resource_id,
1914
- response.text,
1915
- response.status_code,
1916
- )
1917
- return None
1828
+ return True
1829
+
1830
+ return False
1918
1831
 
1919
1832
  # end method definition
1920
1833
 
1921
- def get_access_roles(self) -> dict | None:
1922
- """Get a list of all OTDS access roles
1834
+ def assign_partition_to_license(
1835
+ self,
1836
+ partition_name: str,
1837
+ resource_id: str,
1838
+ license_feature: str,
1839
+ license_name: str,
1840
+ license_type: str = "Full",
1841
+ ) -> bool:
1842
+ """Assign an OTDS partition to a product license (feature).
1923
1843
 
1924
1844
  Args:
1925
- None
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"
1926
1850
  Returns:
1927
- dict: Request response or None if the REST call fails.
1851
+ bool: True if successful or False if the REST call fails or the license is not found
1928
1852
  """
1929
1853
 
1930
- request_url = self.config()["accessRoleUrl"]
1854
+ licenses = self.get_license_for_resource(resource_id)
1855
+ if not licenses:
1856
+ logger.error(
1857
+ "Resource with ID -> '%s' does not exist or has no licenses",
1858
+ resource_id,
1859
+ )
1860
+ return False
1931
1861
 
1932
- logger.info("Retrieving access roles; calling -> %s", request_url)
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
+ for lic in licenses:
1875
+ if lic["_oTLicenseProduct"] == license_name:
1876
+ license_location = lic["id"]
1933
1877
 
1934
- retries = 0
1935
- while True:
1936
- response = requests.get(
1937
- url=request_url,
1938
- headers=REQUEST_HEADERS,
1939
- cookies=self.cookie(),
1940
- timeout=None,
1878
+ try:
1879
+ license_location
1880
+ except UnboundLocalError:
1881
+ logger.error(
1882
+ "Cannot find license -> %s for resource -> %s",
1883
+ license_name,
1884
+ resource_id,
1941
1885
  )
1942
- if response.ok:
1943
- return self.parse_request_response(response)
1944
- # Check if Session has expired - then re-authenticate and try once more
1945
- elif response.status_code == 401 and retries == 0:
1946
- logger.warning("Session has expired - try to re-authenticate...")
1947
- self.authenticate(revalidate=True)
1948
- retries += 1
1949
- else:
1950
- logger.error(
1951
- "Failed to retrieve access roles; error -> %s (%s)",
1952
- response.text,
1953
- response.status_code,
1954
- )
1955
- return None
1886
+ return False
1887
+
1888
+ license_post_body_json = {
1889
+ "_oTLicenseType": license_type,
1890
+ "_oTLicenseProduct": "partitions",
1891
+ "name": partition_name,
1892
+ "values": [{"name": "counter", "values": [license_feature]}],
1893
+ }
1894
+
1895
+ request_url = self.license_url() + "/object/" + license_location
1896
+
1897
+ logger.debug(
1898
+ "Assign license feature -> '%s' of license -> '%s' associated with resource -> '%s' to partition -> '%s'; calling -> %s",
1899
+ license_feature,
1900
+ license_location,
1901
+ resource_id,
1902
+ partition_name,
1903
+ request_url,
1904
+ )
1905
+
1906
+ response = self.do_request(
1907
+ url=request_url,
1908
+ method="POST",
1909
+ json_data=license_post_body_json,
1910
+ timeout=None,
1911
+ failure_message="Failed to add license feature -> '{}' associated with resource -> '{}' to partition -> '{}'".format(
1912
+ license_feature, resource_id, partition_name
1913
+ ),
1914
+ parse_request_response=False,
1915
+ )
1916
+
1917
+ if response and response.ok:
1918
+ logger.debug(
1919
+ "Added license feature -> '%s' to partition -> '%s'",
1920
+ license_feature,
1921
+ partition_name,
1922
+ )
1923
+ return True
1924
+
1925
+ return False
1956
1926
 
1957
1927
  # end method definition
1958
1928
 
1959
- def get_access_role(self, access_role: str) -> dict | None:
1960
- """Get an OTDS access role
1929
+ def get_licensed_objects(
1930
+ self,
1931
+ resource_id: str,
1932
+ license_feature: str,
1933
+ license_name: str,
1934
+ ) -> 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").
1961
1937
 
1962
1938
  Args:
1963
- name (str): name of the access role
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"
1964
1942
  Returns:
1965
- dict: Request response (json) or None if the REST call fails.
1943
+ dict: data structure of licensed objects
1944
+
1945
+ Example return value:
1946
+ {
1947
+ 'status': 0,
1948
+ 'displayString': 'Success',
1949
+ 'exceptions': None,
1950
+ '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},
1954
+ 'version': 1
1955
+ }
1966
1956
  """
1967
1957
 
1968
- request_url = self.config()["accessRoleUrl"] + "/" + access_role
1958
+ licenses = self.get_license_for_resource(resource_id)
1959
+ if not licenses:
1960
+ logger.error(
1961
+ "Resource with ID -> '%s' does not exist or has no licenses",
1962
+ resource_id,
1963
+ )
1964
+ return False
1969
1965
 
1970
- logger.info(
1971
- "Retrieving access role -> %s; calling -> %s", access_role, request_url
1972
- )
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
+ for lic in licenses:
1979
+ if lic["_oTLicenseProduct"] == license_name:
1980
+ license_location = lic["location"]
1973
1981
 
1974
- retries = 0
1975
- while True:
1976
- response = requests.get(
1977
- url=request_url,
1978
- headers=REQUEST_HEADERS,
1979
- cookies=self.cookie(),
1980
- timeout=None,
1982
+ try:
1983
+ license_location
1984
+ except UnboundLocalError:
1985
+ logger.error(
1986
+ "Cannot find license -> %s for resource -> %s",
1987
+ license_name,
1988
+ resource_id,
1981
1989
  )
1982
- if response.ok:
1983
- return self.parse_request_response(response)
1984
- # Check if Session has expired - then re-authenticate and try once more
1985
- elif response.status_code == 401 and retries == 0:
1986
- logger.warning("Session has expired - try to re-authenticate...")
1987
- self.authenticate(revalidate=True)
1988
- retries += 1
1989
- else:
1990
- logger.error(
1991
- "Failed to retrieve access role -> %s; error -> %s (%s)",
1992
- access_role,
1993
- response.text,
1994
- response.status_code,
1995
- )
1996
- return None
1990
+ return False
1991
+
1992
+ request_url = (
1993
+ self.license_url()
1994
+ + "/object/"
1995
+ + license_location
1996
+ + "?counter="
1997
+ + license_feature
1998
+ )
1999
+
2000
+ logger.debug(
2001
+ "Get licensed objects for license -> %s and license feature -> %s associated with resource -> %s; calling -> %s",
2002
+ license_name,
2003
+ license_feature,
2004
+ resource_id,
2005
+ request_url,
2006
+ )
2007
+
2008
+ return self.do_request(
2009
+ url=request_url,
2010
+ method="GET",
2011
+ timeout=None,
2012
+ failure_message="Failed to get licensed objects for license -> '{}' and license feature -> '{}' associated with resource -> '{}'".format(
2013
+ license_name, license_feature, resource_id
2014
+ ),
2015
+ )
1997
2016
 
1998
2017
  # end method definition
1999
2018
 
2000
- def add_partition_to_access_role(
2001
- self, access_role: str, partition: str, location: str = ""
2019
+ def is_user_licensed(
2020
+ self, user_name: str, resource_id: str, license_feature: str, license_name: str
2002
2021
  ) -> bool:
2003
- """Add an OTDS partition to an OTDS access role
2022
+ """Check if a user is licensed for a license and license feature associated with a particular OTDS resource.
2004
2023
 
2005
2024
  Args:
2006
- access_role (str): name of the OTDS access role
2007
- partition (str): name of the partition
2008
- location (str, optional): this is kind of a unique identifier DN (Distinguished Name)
2009
- most of the times you will want to keep it to empty string ("")
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"
2029
+
2010
2030
  Returns:
2011
- bool: True if partition is in access role or has been successfully added.
2012
- False if partition has been not been added (error)
2031
+ bool: True if the user is licensed and False otherwise
2013
2032
  """
2014
2033
 
2015
- accessRolePostBodyJson = {
2016
- "userPartitions": [{"name": partition, "location": location}]
2017
- }
2018
-
2019
- request_url = "{}/{}/members".format(
2020
- self.config()["accessRoleUrl"], access_role
2021
- )
2022
-
2023
- logger.info(
2024
- "Add user partition -> %s to access role -> %s; calling -> %s",
2025
- partition,
2026
- access_role,
2027
- request_url,
2034
+ response = self.get_licensed_objects(
2035
+ resource_id=resource_id,
2036
+ license_feature=license_feature,
2037
+ license_name=license_name,
2028
2038
  )
2029
2039
 
2030
- retries = 0
2031
- while True:
2032
- response = requests.post(
2033
- url=request_url,
2034
- json=accessRolePostBodyJson,
2035
- headers=REQUEST_HEADERS,
2036
- cookies=self.cookie(),
2037
- timeout=None,
2038
- )
2039
- if response.ok:
2040
- return True
2041
- elif response.status_code == 401 and retries == 0:
2042
- logger.warning("Session has expired - try to re-authenticate...")
2043
- self.authenticate(revalidate=True)
2044
- retries += 1
2045
- else:
2046
- logger.error(
2047
- "Failed to add partition -> %s to access role -> %s; error -> %s (%s)",
2048
- partition,
2049
- access_role,
2050
- response.text,
2051
- response.status_code,
2052
- )
2053
- return False
2040
+ if not response or not response["listUsersResults"]:
2041
+ return False
2042
+
2043
+ users = response["listUsersResults"]["users"]
2044
+
2045
+ if not users:
2046
+ return False
2047
+
2048
+ user = next(
2049
+ (item for item in users if item["name"] == user_name),
2050
+ None,
2051
+ )
2052
+
2053
+ if user:
2054
+ return True
2055
+
2056
+ return False
2054
2057
 
2055
2058
  # end method definition
2056
2059
 
2057
- def add_user_to_access_role(
2058
- self, access_role: str, user_id: str, location: str = ""
2060
+ def is_group_licensed(
2061
+ self, group_name: str, resource_id: str, license_feature: str, license_name: str
2059
2062
  ) -> bool:
2060
- """Add an OTDS user to an OTDS access role
2063
+ """Check if a group is licensed for a license and license feature associated with a particular OTDS resource.
2061
2064
 
2062
2065
  Args:
2063
- access_role (str): name of the OTDS access role
2064
- user_id (str): ID of the user (= login name)
2065
- location (str, optional): this is kind of a unique identifier DN (Distinguished Name)
2066
- most of the times you will want to keep it to empty string ("")
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"
2070
+
2067
2071
  Returns:
2068
- bool: True if user is in access role or has been successfully added.
2069
- False if user has not been added (error)
2072
+ bool: True if the group is licensed and False otherwise
2070
2073
  """
2071
2074
 
2072
- # get existing members to check if user is already a member:
2073
- accessRolesGetResponse = self.get_access_role(access_role)
2075
+ response = self.get_licensed_objects(
2076
+ resource_id=resource_id,
2077
+ license_feature=license_feature,
2078
+ license_name=license_name,
2079
+ )
2074
2080
 
2075
- if not accessRolesGetResponse:
2081
+ if not response or not response["listGroupsResults"]:
2076
2082
  return False
2077
2083
 
2078
- # Checking if user already added to access role
2079
- accessRoleUsers = accessRolesGetResponse["accessRoleMembers"]["users"]
2080
- for user in accessRoleUsers:
2081
- if user["displayName"] == user_id:
2082
- logger.info(
2083
- "User -> %s already added to access role -> %s",
2084
- user_id,
2085
- access_role,
2086
- )
2087
- return True
2088
-
2089
- logger.info(
2090
- "User -> %s is not yet in access role -> %s - adding...",
2091
- user_id,
2092
- access_role,
2093
- )
2084
+ groups = response["listGroupsResults"]["groups"]
2094
2085
 
2095
- # create payload for REST call:
2096
- accessRolePostBodyJson = {"users": [{"name": user_id, "location": location}]}
2086
+ if not groups:
2087
+ return False
2097
2088
 
2098
- request_url = "{}/{}/members".format(
2099
- self.config()["accessRoleUrl"], access_role
2089
+ group = next(
2090
+ (item for item in groups if item["name"] == group_name),
2091
+ None,
2100
2092
  )
2101
2093
 
2102
- logger.info(
2103
- "Add user -> %s to access role -> %s; calling -> %s",
2104
- user_id,
2105
- access_role,
2106
- request_url,
2107
- )
2094
+ if group:
2095
+ return True
2108
2096
 
2109
- retries = 0
2110
- while True:
2111
- response = requests.post(
2112
- url=request_url,
2113
- json=accessRolePostBodyJson,
2114
- headers=REQUEST_HEADERS,
2115
- cookies=self.cookie(),
2116
- timeout=None,
2117
- )
2118
- if response.ok:
2119
- return True
2120
- elif response.status_code == 401 and retries == 0:
2121
- logger.warning("Session has expired - try to re-authenticate...")
2122
- self.authenticate(revalidate=True)
2123
- retries += 1
2124
- else:
2125
- logger.error(
2126
- "Failed to add user -> %s to access role -> %s; error -> %s (%s)",
2127
- user_id,
2128
- access_role,
2129
- response.text,
2130
- response.status_code,
2131
- )
2132
- return False
2097
+ return False
2133
2098
 
2134
2099
  # end method definition
2135
2100
 
2136
- def add_group_to_access_role(
2137
- self, access_role: str, group: str, location: str = ""
2101
+ def is_partition_licensed(
2102
+ self,
2103
+ partition_name: str,
2104
+ resource_id: str,
2105
+ license_feature: str,
2106
+ license_name: str,
2138
2107
  ) -> bool:
2139
- """Add an OTDS group to an OTDS access role
2108
+ """Check if a partition is licensed for a license and license feature associated with a particular OTDS resource.
2140
2109
 
2141
2110
  Args:
2142
- access_role (str): name of the OTDS access role
2143
- group (str): name of the group
2144
- location (str, optional): this is kind of a unique identifier DN (Distinguished Name)
2145
- most of the times you will want to keep it to empty string ("")
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"
2115
+
2146
2116
  Returns:
2147
- bool: True if group is in access role or has been successfully added.
2148
- False if group has been not been added (error)
2117
+ bool: True if the partition is licensed and False otherwise
2149
2118
  """
2150
2119
 
2151
- # get existing members to check if user is already a member:
2152
- accessRolesGetResponse = self.get_access_role(access_role)
2153
- if not accessRolesGetResponse:
2120
+ response = self.get_licensed_objects(
2121
+ resource_id=resource_id,
2122
+ license_feature=license_feature,
2123
+ license_name=license_name,
2124
+ )
2125
+
2126
+ if not response or not response["listUserPartitionResult"]:
2154
2127
  return False
2155
2128
 
2156
- # Checking if group already added to access role
2157
- accessRoleGroups = accessRolesGetResponse["accessRoleMembers"]["groups"]
2158
- for accessRoleGroup in accessRoleGroups:
2159
- if accessRoleGroup["name"] == group:
2160
- logger.info(
2161
- "Group -> %s already added to access role -> %s", group, access_role
2162
- )
2163
- return True
2129
+ partitions = response["listUserPartitionResult"]["_userPartitions"]
2164
2130
 
2165
- logger.info(
2166
- "Group -> %s is not yet in access role -> %s - adding...",
2167
- group,
2168
- access_role,
2131
+ if not partitions:
2132
+ return False
2133
+
2134
+ partition = next(
2135
+ (item for item in partitions if item["name"] == partition_name),
2136
+ None,
2169
2137
  )
2170
2138
 
2171
- # create payload for REST call:
2172
- accessRolePostBodyJson = {"groups": [{"name": group, "location": location}]}
2139
+ if partition:
2140
+ return True
2173
2141
 
2174
- request_url = "{}/{}/members".format(
2175
- self.config()["accessRoleUrl"], access_role
2176
- )
2142
+ return False
2177
2143
 
2178
- logger.info(
2179
- "Add group -> %s to access role -> %s; calling -> %s",
2180
- group,
2181
- access_role,
2144
+ # end method definition
2145
+
2146
+ def import_synchronized_partition_members(self, name: str) -> dict:
2147
+ """Import users and groups to partition
2148
+
2149
+ Args:
2150
+ name (str): name of the partition in which users need to be imported
2151
+ Returns:
2152
+ dict: Request response or None if the creation fails.
2153
+ """
2154
+ 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",
2158
+ name,
2182
2159
  request_url,
2183
2160
  )
2184
-
2185
2161
  retries = 0
2186
2162
  while True:
2187
2163
  response = requests.post(
2188
2164
  url=request_url,
2189
- json=accessRolePostBodyJson,
2165
+ json=command,
2190
2166
  headers=REQUEST_HEADERS,
2191
2167
  cookies=self.cookie(),
2192
2168
  timeout=None,
2193
2169
  )
2194
- if response.ok:
2170
+ if response.status_code == 204:
2195
2171
  return True
2172
+ # Check if Session has expired - then re-authenticate and try once more
2196
2173
  elif response.status_code == 401 and retries == 0:
2197
- logger.warning("Session has expired - try to re-authenticate...")
2174
+ logger.debug("Session has expired - try to re-authenticate...")
2198
2175
  self.authenticate(revalidate=True)
2199
2176
  retries += 1
2200
2177
  else:
2201
2178
  logger.error(
2202
- "Failed to add group -> %s to access role -> %s; error -> %s (%s)",
2203
- group,
2204
- access_role,
2179
+ "Failed to Import users and groups to synchronized partition -> %s; error -> %s (%s)",
2180
+ name,
2205
2181
  response.text,
2206
2182
  response.status_code,
2207
2183
  )
2208
- return False
2209
-
2210
- # end method definition
2211
-
2212
- def update_access_role_attributes(
2213
- self, name: str, attribute_list: list
2214
- ) -> dict | None:
2215
- """Update some attributes of an existing OTDS Access Role
2184
+ return None
2185
+
2186
+ # 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
2216
2190
 
2217
2191
  Args:
2218
- name (str): name of the existing access role
2219
- attribute_list (list): list of attribute name and attribute value pairs
2220
- The values need to be a list as well. Example:
2221
- [{name: "pushAllGroups", values: ["True"]}]
2192
+ name (str): name of the new partition
2193
+ description (str): description of the new partition
2194
+ data (dict): data for creating synchronized partition
2222
2195
  Returns:
2223
- dict: Request response (json) or None if the REST call fails.
2196
+ dict: Request response or None if the creation fails.
2224
2197
  """
2225
-
2226
- # Return if list is empty:
2227
- if not attribute_list:
2228
- return None
2229
-
2230
- # create payload for REST call:
2231
- access_role = self.get_access_role(name)
2232
- if not access_role:
2233
- logger.error("Failed to get access role -> %s", name)
2234
- return None
2235
-
2236
- accessRolePutBodyJson = {"attributes": attribute_list}
2237
-
2238
- request_url = "{}/{}/attributes".format(self.config()["accessRoleUrl"], name)
2239
-
2240
- logger.info(
2241
- "Update access role -> %s with attributes -> %s; calling -> %s",
2198
+ synchronizedPartitionPostBodyJson = {
2199
+ "ipConnectionParameter": [
2200
+ ],
2201
+ "ipAuthentication": {
2202
+ },
2203
+ "objectClassNameMapping": [
2204
+
2205
+ ],
2206
+ "basicInfo": {
2207
+ },
2208
+ "basicAttributes": []
2209
+ }
2210
+ synchronizedPartitionPostBodyJson.update(data)
2211
+ request_url = self.synchronized_partition_url()
2212
+ logger.debug(
2213
+ "Adding synchronized partition -> %s (%s); calling -> %s",
2242
2214
  name,
2243
- accessRolePutBodyJson,
2215
+ description,
2244
2216
  request_url,
2245
2217
  )
2246
-
2218
+ synchronizedPartitionPostBodyJson["ipAuthentication"]["bindPassword"] = self.config()["bindPassword"]
2247
2219
  retries = 0
2248
2220
  while True:
2249
- response = requests.put(
2221
+ response = requests.post(
2250
2222
  url=request_url,
2251
- json=accessRolePutBodyJson,
2223
+ json=synchronizedPartitionPostBodyJson,
2252
2224
  headers=REQUEST_HEADERS,
2253
2225
  cookies=self.cookie(),
2254
2226
  timeout=None,
@@ -2257,19 +2229,19 @@ class OTDS:
2257
2229
  return self.parse_request_response(response)
2258
2230
  # Check if Session has expired - then re-authenticate and try once more
2259
2231
  elif response.status_code == 401 and retries == 0:
2260
- logger.warning("Session has expired - try to re-authenticate...")
2232
+ logger.debug("Session has expired - try to re-authenticate...")
2261
2233
  self.authenticate(revalidate=True)
2262
2234
  retries += 1
2263
2235
  else:
2264
2236
  logger.error(
2265
- "Failed to update access role -> %s; error -> %s (%s)",
2237
+ "Failed to add synchronized partition -> %s; error -> %s (%s)",
2266
2238
  name,
2267
2239
  response.text,
2268
2240
  response.status_code,
2269
2241
  )
2270
2242
  return None
2271
-
2272
- # end method definition
2243
+
2244
+ # end of method definition
2273
2245
 
2274
2246
  def add_system_attribute(
2275
2247
  self, name: str, value: str, description: str = ""
@@ -2284,7 +2256,7 @@ class OTDS:
2284
2256
  dict: Request response (dictionary) or None if the REST call fails.
2285
2257
  """
2286
2258
 
2287
- systemAttributePostBodyJson = {
2259
+ system_attribute_post_body_json = {
2288
2260
  "name": name,
2289
2261
  "value": value,
2290
2262
  "friendlyName": description,
@@ -2293,46 +2265,30 @@ class OTDS:
2293
2265
  request_url = "{}/system_attributes".format(self.config()["systemConfigUrl"])
2294
2266
 
2295
2267
  if description:
2296
- logger.info(
2297
- "Add system attribute -> %s (%s) with value -> %s; calling -> %s",
2268
+ logger.debug(
2269
+ "Add system attribute -> '%s' ('%s') with value -> %s; calling -> %s",
2298
2270
  name,
2299
2271
  description,
2300
2272
  value,
2301
2273
  request_url,
2302
2274
  )
2303
2275
  else:
2304
- logger.info(
2305
- "Add system attribute -> %s with value -> %s; calling -> %s",
2276
+ logger.debug(
2277
+ "Add system attribute -> '%s' with value -> %s; calling -> %s",
2306
2278
  name,
2307
2279
  value,
2308
2280
  request_url,
2309
2281
  )
2310
2282
 
2311
- retries = 0
2312
- while True:
2313
- response = requests.post(
2314
- url=request_url,
2315
- json=systemAttributePostBodyJson,
2316
- headers=REQUEST_HEADERS,
2317
- cookies=self.cookie(),
2318
- timeout=None,
2319
- )
2320
- if response.ok:
2321
- return self.parse_request_response(response)
2322
- # Check if Session has expired - then re-authenticate and try once more
2323
- elif response.status_code == 401 and retries == 0:
2324
- logger.warning("Session has expired - try to re-authenticate...")
2325
- self.authenticate(revalidate=True)
2326
- retries += 1
2327
- else:
2328
- logger.error(
2329
- "Failed to add system attribute -> %s with value -> %s; error -> %s (%s)",
2330
- name,
2331
- value,
2332
- response.text,
2333
- response.status_code,
2334
- )
2335
- return None
2283
+ return self.do_request(
2284
+ url=request_url,
2285
+ method="POST",
2286
+ json_data=system_attribute_post_body_json,
2287
+ timeout=None,
2288
+ failure_message="Failed to add system attribute -> '{}' with value -> '{}'".format(
2289
+ name, value
2290
+ ),
2291
+ )
2336
2292
 
2337
2293
  # end method definition
2338
2294
 
@@ -2347,30 +2303,14 @@ class OTDS:
2347
2303
 
2348
2304
  request_url = "{}/whitelist".format(self.config()["systemConfigUrl"])
2349
2305
 
2350
- logger.info("Retrieving trusted sites; calling -> %s", request_url)
2306
+ logger.debug("Get trusted sites; calling -> %s", request_url)
2351
2307
 
2352
- retries = 0
2353
- while True:
2354
- response = requests.get(
2355
- url=request_url,
2356
- headers=REQUEST_HEADERS,
2357
- cookies=self.cookie(),
2358
- timeout=None,
2359
- )
2360
- if response.ok:
2361
- return self.parse_request_response(response)
2362
- # Check if Session has expired - then re-authenticate and try once more
2363
- elif response.status_code == 401 and retries == 0:
2364
- logger.warning("Session has expired - try to re-authenticate...")
2365
- self.authenticate(revalidate=True)
2366
- retries += 1
2367
- else:
2368
- logger.error(
2369
- "Failed to retrieve trusted sites; error -> %s (%s)",
2370
- response.text,
2371
- response.status_code,
2372
- )
2373
- return None
2308
+ return self.do_request(
2309
+ url=request_url,
2310
+ method="GET",
2311
+ timeout=None,
2312
+ failure_message="Failed to get trusted sites",
2313
+ )
2374
2314
 
2375
2315
  # end method definition
2376
2316
 
@@ -2383,38 +2323,36 @@ class OTDS:
2383
2323
  dict: Request response or None if the REST call fails.
2384
2324
  """
2385
2325
 
2386
- trustedSitePostBodyJson = {"stringList": [trusted_site]}
2326
+ trusted_site_post_body_json = {"stringList": [trusted_site]}
2387
2327
 
2388
2328
  # we need to first retrieve the existing sites and then
2389
2329
  # append the new one:
2390
- existingTrustedSites = self.get_trusted_sites()
2330
+ existing_trusted_sites = self.get_trusted_sites()
2391
2331
 
2392
- if existingTrustedSites:
2393
- trustedSitePostBodyJson["stringList"].extend(
2394
- existingTrustedSites["stringList"]
2332
+ if existing_trusted_sites:
2333
+ trusted_site_post_body_json["stringList"].extend(
2334
+ existing_trusted_sites["stringList"]
2395
2335
  )
2396
2336
 
2397
2337
  request_url = "{}/whitelist".format(self.config()["systemConfigUrl"])
2398
2338
 
2399
- logger.info("Add trusted site -> %s; calling -> %s", trusted_site, request_url)
2339
+ logger.debug(
2340
+ "Add trusted site -> '%s'; calling -> %s", trusted_site, request_url
2341
+ )
2400
2342
 
2401
- response = requests.put(
2343
+ response = self.do_request(
2402
2344
  url=request_url,
2403
- json=trustedSitePostBodyJson,
2404
- headers=REQUEST_HEADERS,
2405
- cookies=self.cookie(),
2345
+ method="PUT",
2346
+ json_data=trusted_site_post_body_json,
2406
2347
  timeout=None,
2348
+ failure_message="Failed to add trusted site -> '{}'".format(trusted_site),
2349
+ parse_request_response=False, # don't parse it!
2407
2350
  )
2351
+
2408
2352
  if not response.ok:
2409
- logger.error(
2410
- "Failed to add trusted site -> %s; error -> %s (%s)",
2411
- trusted_site,
2412
- response.text,
2413
- response.status_code,
2414
- )
2415
2353
  return None
2416
2354
 
2417
- return response # don't parse it!
2355
+ return response
2418
2356
 
2419
2357
  # end method definition
2420
2358
 
@@ -2427,7 +2365,7 @@ class OTDS:
2427
2365
  Request response (json) or None if the REST call fails.
2428
2366
  """
2429
2367
 
2430
- auditPutBodyJson = {
2368
+ audit_put_body_json = {
2431
2369
  "daysToKeep": "7",
2432
2370
  "enabled": "true",
2433
2371
  "auditTo": "DATABASE",
@@ -2484,22 +2422,16 @@ class OTDS:
2484
2422
 
2485
2423
  request_url = "{}/audit".format(self.config()["systemConfigUrl"])
2486
2424
 
2487
- logger.info("Enable audit; calling -> %s", request_url)
2488
-
2489
- response = requests.put(
2490
- url=request_url,
2491
- json=auditPutBodyJson,
2492
- headers=REQUEST_HEADERS,
2493
- cookies=self.cookie(),
2494
- timeout=None,
2495
- )
2496
- if not response.ok:
2497
- logger.error(
2498
- "Failed to enable audit; error -> %s (%s)",
2499
- response.text,
2500
- response.status_code,
2501
- )
2502
- return response
2425
+ logger.debug("Enable audit; calling -> %s", request_url)
2426
+
2427
+ return self.do_request(
2428
+ url=request_url,
2429
+ method="PUT",
2430
+ json_data=audit_put_body_json,
2431
+ timeout=None,
2432
+ failure_message="Failed to enable audit",
2433
+ parse_request_response=False,
2434
+ )
2503
2435
 
2504
2436
  # end method definition
2505
2437
 
@@ -2583,7 +2515,7 @@ class OTDS:
2583
2515
  if default_scopes is None:
2584
2516
  default_scopes = []
2585
2517
 
2586
- oauthClientPostBodyJson = {
2518
+ oauth_client_post_body_json = {
2587
2519
  "id": client_id,
2588
2520
  "description": description,
2589
2521
  "redirectURLs": redirect_urls,
@@ -2601,41 +2533,24 @@ class OTDS:
2601
2533
 
2602
2534
  # Do we have a predefined client secret?
2603
2535
  if secret:
2604
- oauthClientPostBodyJson["secret"] = secret
2536
+ oauth_client_post_body_json["secret"] = secret
2605
2537
 
2606
2538
  request_url = self.oauth_client_url()
2607
2539
 
2608
- logger.info(
2609
- "Adding oauth client -> %s (%s); calling -> %s",
2540
+ logger.debug(
2541
+ "Adding oauth client -> '%s' (%s); calling -> %s",
2610
2542
  description,
2611
2543
  client_id,
2612
2544
  request_url,
2613
2545
  )
2614
2546
 
2615
- retries = 0
2616
- while True:
2617
- response = requests.post(
2618
- url=request_url,
2619
- json=oauthClientPostBodyJson,
2620
- headers=REQUEST_HEADERS,
2621
- cookies=self.cookie(),
2622
- timeout=None,
2623
- )
2624
- if response.ok:
2625
- return self.parse_request_response(response)
2626
- # Check if Session has expired - then re-authenticate and try once more
2627
- elif response.status_code == 401 and retries == 0:
2628
- logger.warning("Session has expired - try to re-authenticate...")
2629
- self.authenticate(revalidate=True)
2630
- retries += 1
2631
- else:
2632
- logger.error(
2633
- "Failed to add OAuth client -> %s; error -> %s (%s)",
2634
- client_id,
2635
- response.text,
2636
- response.status_code,
2637
- )
2638
- return None
2547
+ return self.do_request(
2548
+ url=request_url,
2549
+ method="POST",
2550
+ json_data=oauth_client_post_body_json,
2551
+ timeout=None,
2552
+ failure_message="Failed to add OAuth client -> {}".format(client_id),
2553
+ )
2639
2554
 
2640
2555
  # end method definition
2641
2556
 
@@ -2651,32 +2566,15 @@ class OTDS:
2651
2566
 
2652
2567
  request_url = "{}/{}".format(self.oauth_client_url(), client_id)
2653
2568
 
2654
- logger.info("Get oauth client -> %s; calling -> %s", client_id, request_url)
2569
+ logger.debug("Get oauth client -> '%s'; calling -> %s", client_id, request_url)
2655
2570
 
2656
- retries = 0
2657
- while True:
2658
- response = requests.get(
2659
- url=request_url,
2660
- headers=REQUEST_HEADERS,
2661
- cookies=self.cookie(),
2662
- timeout=None,
2663
- )
2664
- if response.ok:
2665
- return self.parse_request_response(response)
2666
- # Check if Session has expired - then re-authenticate and try once more
2667
- elif response.status_code == 401 and retries == 0:
2668
- logger.warning("Session has expired - try to re-authenticate...")
2669
- self.authenticate(revalidate=True)
2670
- retries += 1
2671
- else:
2672
- if show_error:
2673
- logger.error(
2674
- "Failed to get oauth client -> %s; error -> %s (%s)",
2675
- client_id,
2676
- response.text,
2677
- response.status_code,
2678
- )
2679
- return None
2571
+ return self.do_request(
2572
+ url=request_url,
2573
+ method="GET",
2574
+ timeout=None,
2575
+ failure_message="Failed to get oauth client -> '{}'".format(client_id),
2576
+ show_error=show_error,
2577
+ )
2680
2578
 
2681
2579
  # end method definition
2682
2580
 
@@ -2692,41 +2590,24 @@ class OTDS:
2692
2590
  dict: Request response (json) or None if the REST call fails.
2693
2591
  """
2694
2592
 
2695
- oauthClientPatchBodyJson = updates
2593
+ oauth_client_patch_body_json = updates
2696
2594
 
2697
2595
  request_url = "{}/{}".format(self.oauth_client_url(), client_id)
2698
2596
 
2699
- logger.info(
2700
- "Update OAuth client -> %s with -> %s; calling -> %s",
2597
+ logger.debug(
2598
+ "Update OAuth client -> '%s' with -> %s; calling -> %s",
2701
2599
  client_id,
2702
- updates,
2600
+ str(updates),
2703
2601
  request_url,
2704
2602
  )
2705
2603
 
2706
- retries = 0
2707
- while True:
2708
- response = requests.patch(
2709
- url=request_url,
2710
- json=oauthClientPatchBodyJson,
2711
- headers=REQUEST_HEADERS,
2712
- cookies=self.cookie(),
2713
- timeout=None,
2714
- )
2715
- if response.ok:
2716
- return self.parse_request_response(response)
2717
- # Check if Session has expired - then re-authenticate and try once more
2718
- elif response.status_code == 401 and retries == 0:
2719
- logger.warning("Session has expired - try to re-authenticate...")
2720
- self.authenticate(revalidate=True)
2721
- retries += 1
2722
- else:
2723
- logger.error(
2724
- "Failed to update OAuth client -> %s; error -> %s (%s)",
2725
- client_id,
2726
- response.text,
2727
- response.status_code,
2728
- )
2729
- return None
2604
+ return self.do_request(
2605
+ url=request_url,
2606
+ method="PATCH",
2607
+ json_data=oauth_client_patch_body_json,
2608
+ timeout=None,
2609
+ failure_message="Failed to update OAuth client -> '{}'".format(client_id),
2610
+ )
2730
2611
 
2731
2612
  # end method definition
2732
2613
 
@@ -2741,40 +2622,25 @@ class OTDS:
2741
2622
 
2742
2623
  request_url = self.config()["accessRoleUrl"] + "/" + access_role_name
2743
2624
 
2744
- logger.info(
2745
- "Get access role -> %s; calling -> %s", access_role_name, request_url
2625
+ logger.debug(
2626
+ "Get access role -> '%s'; calling -> %s", access_role_name, request_url
2746
2627
  )
2747
2628
 
2748
- retries = 0
2749
- while True:
2750
- response = requests.get(
2751
- url=request_url,
2752
- headers=REQUEST_HEADERS,
2753
- cookies=self.cookie(),
2754
- timeout=None,
2755
- )
2756
- if response.ok:
2757
- accessRolesJson = self.parse_request_response(response)
2758
- break
2759
- # Check if Session has expired - then re-authenticate and try once more
2760
- elif response.status_code == 401 and retries == 0:
2761
- logger.warning("Session has expired - try to re-authenticate...")
2762
- self.authenticate(revalidate=True)
2763
- retries += 1
2764
- else:
2765
- logger.error(
2766
- "Failed to retrieve role -> %s; url -> %s : error -> %s (%s)",
2767
- access_role_name,
2768
- request_url,
2769
- response.text,
2770
- response.status_code,
2771
- )
2772
- return None
2629
+ access_role = self.do_request(
2630
+ url=request_url,
2631
+ method="GET",
2632
+ timeout=None,
2633
+ failure_message="Failed to retrieve access role -> '{}'".format(
2634
+ access_role_name
2635
+ ),
2636
+ )
2637
+ if not access_role:
2638
+ return None
2773
2639
 
2774
2640
  # Checking if OAuthClients partition already added to access role
2775
- userPartitions = accessRolesJson["accessRoleMembers"]["userPartitions"]
2776
- for userPartition in userPartitions:
2777
- if userPartition["userPartition"] == "OAuthClients":
2641
+ user_partitions = access_role["accessRoleMembers"]["userPartitions"]
2642
+ for user_partition in user_partitions:
2643
+ if user_partition["userPartition"] == "OAuthClients":
2778
2644
  logger.error(
2779
2645
  "OAuthClients partition already added to role -> %s",
2780
2646
  access_role_name,
@@ -2784,58 +2650,42 @@ class OTDS:
2784
2650
  # Getting location info for the OAuthClients partition
2785
2651
  # so it can be added to access roles json
2786
2652
  request_url = self.config()["partitionsUrl"] + "/OAuthClients"
2787
- partitionsResponse = requests.get(
2653
+
2654
+ response = self.do_request(
2788
2655
  url=request_url,
2789
- headers=REQUEST_HEADERS,
2790
- cookies=self.cookie(),
2656
+ method="GET",
2791
2657
  timeout=None,
2658
+ failure_message="Failed to get partition info for OAuthClients for role -> '{}'".format(
2659
+ access_role_name
2660
+ ),
2792
2661
  )
2793
- if partitionsResponse.ok:
2794
- response_dict = self.parse_request_response(partitionsResponse)
2795
- if not response_dict:
2796
- return None
2797
- oauthClientLocation = response_dict["location"]
2798
- else:
2799
- logger.error(
2800
- "Failed to get partition info for OAuthClients; url -> %s : error -> %s (%s)",
2801
- request_url,
2802
- partitionsResponse.text,
2803
- response.status_code,
2804
- )
2662
+ if not response:
2805
2663
  return None
2806
2664
 
2665
+ oauth_client_location = response["location"]
2666
+
2807
2667
  # adding OAuthClients info to acess roles organizational units
2808
- oauthClientsOuBlock = {
2809
- "location": oauthClientLocation,
2810
- "name": oauthClientLocation,
2668
+ oauth_clients_ou_block = {
2669
+ "location": oauth_client_location,
2670
+ "name": oauth_client_location,
2811
2671
  "userPartition": None,
2812
2672
  }
2813
- accessRolesJson["accessRoleMembers"]["organizationalUnits"].append(
2814
- oauthClientsOuBlock
2673
+ access_role["accessRoleMembers"]["organizationalUnits"].append(
2674
+ oauth_clients_ou_block
2815
2675
  )
2816
2676
 
2817
- response = requests.put(
2677
+ return self.do_request(
2818
2678
  url=request_url,
2819
- json=accessRolesJson,
2820
- headers=REQUEST_HEADERS,
2821
- cookies=self.cookie(),
2679
+ method="PUT",
2822
2680
  timeout=None,
2681
+ warning_message="Failed to add OAuthClients to access role -> '{}'".format(
2682
+ access_role_name
2683
+ ),
2684
+ show_error=False,
2685
+ show_warning=True,
2686
+ parse_request_response=False,
2823
2687
  )
2824
2688
 
2825
- if response.ok:
2826
- logger.info(
2827
- "OauthClients partition successfully added to access role -> %s",
2828
- access_role_name,
2829
- )
2830
- else:
2831
- logger.warning(
2832
- "Status code of -> %s returned attempting to add OAuthClients to access role -> %s: error -> %s",
2833
- response.status_code,
2834
- access_role_name,
2835
- response.text,
2836
- )
2837
- return response
2838
-
2839
2689
  # end method definition
2840
2690
 
2841
2691
  def get_access_token(self, client_id: str, client_secret: str) -> str | None:
@@ -2925,34 +2775,17 @@ class OTDS:
2925
2775
 
2926
2776
  request_url = "{}/{}".format(self.auth_handler_url(), name)
2927
2777
 
2928
- logger.info(
2929
- "Getting authentication handler -> %s; calling -> %s", name, request_url
2778
+ logger.debug(
2779
+ "Getting authentication handler -> '%s'; calling -> %s", name, request_url
2930
2780
  )
2931
2781
 
2932
- retries = 0
2933
- while True:
2934
- response = requests.get(
2935
- url=request_url,
2936
- headers=REQUEST_HEADERS,
2937
- cookies=self.cookie(),
2938
- timeout=None,
2939
- )
2940
- if response.ok:
2941
- return self.parse_request_response(response)
2942
- # Check if Session has expired - then re-authenticate and try once more
2943
- elif response.status_code == 401 and retries == 0:
2944
- logger.warning("Session has expired - try to re-authenticate...")
2945
- self.authenticate(revalidate=True)
2946
- retries += 1
2947
- else:
2948
- if show_error:
2949
- logger.error(
2950
- "Failed to get authentication handler -> %s; warning -> %s (%s)",
2951
- name,
2952
- response.text,
2953
- response.status_code,
2954
- )
2955
- return None
2782
+ return self.do_request(
2783
+ url=request_url,
2784
+ method="GET",
2785
+ timeout=None,
2786
+ failure_message="Failed to get authentication handler -> '{}'".format(name),
2787
+ show_error=show_error,
2788
+ )
2956
2789
 
2957
2790
  # end method definition
2958
2791
 
@@ -2995,7 +2828,7 @@ class OTDS:
2995
2828
  if auth_principal_attributes is None:
2996
2829
  auth_principal_attributes = ["oTExternalID1", "oTUserID1"]
2997
2830
 
2998
- authHandlerPostBodyJson = {
2831
+ auth_handler_post_body_json = {
2999
2832
  "_name": name,
3000
2833
  "_description": description,
3001
2834
  "_class": "com.opentext.otds.as.drivers.saml.SAML2Handler",
@@ -3005,107 +2838,292 @@ class OTDS:
3005
2838
  "_scope": scope,
3006
2839
  "_properties": [
3007
2840
  {
3008
- "_key": "com.opentext.otds.as.drivers.saml.provider_name",
3009
- "_name": "Identity Provider (IdP) Name",
3010
- "_description": "The name of the identity provider. This should be a single word since it will be part of the metadata URL.",
3011
- "_value": provider_name,
2841
+ "_key": "com.opentext.otds.as.drivers.saml.provider_name",
2842
+ "_name": "Identity Provider (IdP) Name",
2843
+ "_description": "The name of the identity provider. This should be a single word since it will be part of the metadata URL.",
2844
+ "_value": provider_name,
2845
+ },
2846
+ {
2847
+ "_key": "com.opentext.otds.as.drivers.saml.provider_metadata_description",
2848
+ "_name": "IdP Metadata URL",
2849
+ "_description": "The URL for the IdP's federation metadata. The metadata will be automatically updated by OTDS daily at midnight.",
2850
+ "_value": saml_url,
2851
+ },
2852
+ {
2853
+ "_key": "com.opentext.otds.as.drivers.saml.provider_nameid_format",
2854
+ "_name": "IdP NameID Format",
2855
+ "_description": "Specifies which NameID format supported by the identity provider contains the desired user identifier. The value in this identifier must correspond to the value of the user attribute specified for the authentication principal attribute. This value is usually set to urn:oasis:names:tc:SAML:2.0:nameid-format:persistent or urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress. Please ensure this is consistent with the identity provider's configuration.",
2856
+ "_value": nameid_format,
2857
+ },
2858
+ {
2859
+ "_key": "com.opentext.otds.as.drivers.saml._impersonator_claim",
2860
+ "_name": "Claim for impersonating user",
2861
+ "_description": "A claim that contains the ID of the actor/impersonator for the user identified by NameID. It must be in the same format as NameID.",
2862
+ "_value": "loggedinuserid",
2863
+ },
2864
+ {
2865
+ "_key": "com.opentext.otds.as.drivers.saml.sp_url",
2866
+ "_name": "OTDS SP Endpoint",
2867
+ "_description": "Specifies the service provider URL that will be used to identify OTDS to the identity provider. If not specified, the URL will be taken from the request. This generally needs to be configured for environments in which OTDS is behind a reverse-proxy.",
2868
+ "_value": otds_sp_endpoint,
2869
+ },
2870
+ {
2871
+ "_key": "com.opentext.otds.as.drivers.saml.enable_sp_sso",
2872
+ "_name": "Active By Default",
2873
+ "_description": "Whether to activate this handler for any request to the OTDS login page. If true, any login request to the OTDS login page will be redirected to the IdP. If false, the user has to select the provider on the login page.",
2874
+ "_value": active_by_default,
2875
+ },
2876
+ {
2877
+ "_key": "com.opentext.otds.as.drivers.saml._signature_alg",
2878
+ "_name": "XML Signature Algorithm",
2879
+ "_description": "Only relevant when certificate and private key are configured. Default is http://www.w3.org/2000/09/xmldsig#rsa-sha1. Valid values are defined at http://www.w3.org/TR/xmldsig-core1/#sec-AlgID.",
2880
+ "_value": "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
2881
+ },
2882
+ {
2883
+ "_key": "com.opentext.otds.as.drivers.saml.use_acs_url",
2884
+ "_name": "Use AssertionConsumerServiceURL",
2885
+ "_description": "Set to true to have the SAML AuthnRequest use AssertionConsumerServiceURL instead of AssertionConsumerServiceIndex",
2886
+ "_value": "true",
2887
+ },
2888
+ {
2889
+ "_key": "com.opentext.otds.as.drivers.saml.grace_period",
2890
+ "_name": "Grace Period",
2891
+ "_description": 'Specifies the number of minutes to allow for "NotBefore" and "NotOnOrAfter" fields when validating assertions in order to account for time difference between the identity provider and this service provider.',
2892
+ "_value": "5",
2893
+ },
2894
+ {
2895
+ "_key": "com.opentext.otds.as.drivers.saml.auth_request_binding",
2896
+ "_name": "Auth Request Binding",
2897
+ "_description": "Specifies the preferred SAML binding to use for sending the AuthnRequest, provided it is supported by the identity provider.",
2898
+ "_value": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
2899
+ },
2900
+ {
2901
+ "_key": "com.opentext.otds.as.drivers.saml.auth_response_binding",
2902
+ "_name": "Auth Response Binding",
2903
+ "_description": "Specifies the SAML binding to use for the response to an AuthnRequest",
2904
+ "_value": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
2905
+ },
2906
+ {
2907
+ "_key": "com.opentext.otds.as.drivers.saml.claim1",
2908
+ "_name": "Claim 1",
2909
+ "_description": "SAML attribute/claim that should be mapped to an OTDS user attribute. This value is case sensitive. Note that mapped claims are only relevant if the corresponding account is auto-provisioned in OTDS. See the Administration Guide for details.",
2910
+ "_value": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
2911
+ },
2912
+ {
2913
+ "_key": "com.opentext.otds.as.drivers.saml.claimAttribute1",
2914
+ "_name": "OTDS Attribute 1",
2915
+ "_description": "OTDS user attribute to which the SAML attribute/claim should be mapped",
2916
+ "_value": "mail",
2917
+ },
2918
+ {
2919
+ "_key": "com.opentext.otds.as.drivers.saml.claim2",
2920
+ "_name": "Claim 2",
2921
+ "_value": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname",
2922
+ },
2923
+ {
2924
+ "_key": "com.opentext.otds.as.drivers.saml.claimAttribute2",
2925
+ "_name": "OTDS Attribute 2",
2926
+ "_value": "givenName",
2927
+ },
2928
+ {
2929
+ "_key": "com.opentext.otds.as.drivers.saml.claim3",
2930
+ "_name": "Claim 3",
2931
+ "_value": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname",
2932
+ },
2933
+ {
2934
+ "_key": "com.opentext.otds.as.drivers.saml.claimAttribute3",
2935
+ "_name": "OTDS Attribute 3",
2936
+ "_value": "sn",
2937
+ },
2938
+ {
2939
+ "_key": "com.opentext.otds.as.drivers.saml.claim4",
2940
+ "_name": "Claim 4",
2941
+ "_value": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name",
2942
+ },
2943
+ {
2944
+ "_key": "com.opentext.otds.as.drivers.saml.claimAttribute4",
2945
+ "_name": "OTDS Attribute 4",
2946
+ "_value": "displayName",
2947
+ },
2948
+ {
2949
+ "_key": "com.opentext.otds.as.drivers.saml.claim5",
2950
+ "_name": "Claim 5",
2951
+ "_value": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/streetaddress",
2952
+ },
2953
+ {
2954
+ "_key": "com.opentext.otds.as.drivers.saml.claimAttribute5",
2955
+ "_name": "OTDS Attribute 5",
2956
+ "_value": "oTStreetAddress",
2957
+ },
2958
+ {
2959
+ "_key": "com.opentext.otds.as.drivers.saml.claim6",
2960
+ "_name": "Claim 6",
2961
+ "_value": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/locality",
2962
+ },
2963
+ {
2964
+ "_key": "com.opentext.otds.as.drivers.saml.claimAttribute6",
2965
+ "_name": "OTDS Attribute 6",
2966
+ "_value": "l",
2967
+ },
2968
+ {
2969
+ "_key": "com.opentext.otds.as.drivers.saml.claim7",
2970
+ "_name": "Claim 7",
2971
+ "_value": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/stateorprovince",
2972
+ },
2973
+ {
2974
+ "_key": "com.opentext.otds.as.drivers.saml.claimAttribute7",
2975
+ "_name": "OTDS Attribute 7",
2976
+ "_value": "st",
2977
+ },
2978
+ {
2979
+ "_key": "com.opentext.otds.as.drivers.saml.claim8",
2980
+ "_name": "Claim 8",
2981
+ "_value": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/postalcode",
2982
+ },
2983
+ {
2984
+ "_key": "com.opentext.otds.as.drivers.saml.claimAttribute8",
2985
+ "_name": "OTDS Attribute 8",
2986
+ "_value": "postalCode",
2987
+ },
2988
+ {
2989
+ "_key": "com.opentext.otds.as.drivers.saml.claim9",
2990
+ "_name": "Claim 9",
2991
+ "_value": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/country",
2992
+ },
2993
+ {
2994
+ "_key": "com.opentext.otds.as.drivers.saml.claimAttribute9",
2995
+ "_name": "OTDS Attribute 9",
2996
+ "_value": "countryName",
2997
+ },
2998
+ {
2999
+ "_key": "com.opentext.otds.as.drivers.saml.claim10",
3000
+ "_name": "Claim 10",
3001
+ "_value": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/otherphone",
3002
+ },
3003
+ {
3004
+ "_key": "com.opentext.otds.as.drivers.saml.claimAttribute10",
3005
+ "_name": "OTDS Attribute 10",
3006
+ "_value": "oTTelephoneNumber",
3007
+ },
3008
+ {
3009
+ "_key": "com.opentext.otds.as.drivers.saml.claim11",
3010
+ "_name": "Claim 11",
3011
+ "_value": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/homephone",
3012
3012
  },
3013
3013
  {
3014
- "_key": "com.opentext.otds.as.drivers.saml.provider_metadata_description",
3015
- "_name": "IdP Metadata URL",
3016
- "_description": "The URL for the IdP's federation metadata. The metadata will be automatically updated by OTDS daily at midnight.",
3017
- "_value": saml_url,
3014
+ "_key": "com.opentext.otds.as.drivers.saml.claimAttribute11",
3015
+ "_name": "OTDS Attribute 11",
3016
+ "_value": "homePhone",
3018
3017
  },
3019
3018
  {
3020
- "_key": "com.opentext.otds.as.drivers.saml.provider_nameid_format",
3021
- "_name": "IdP NameID Format",
3022
- "_description": "Specifies which NameID format supported by the identity provider contains the desired user identifier. The value in this identifier must correspond to the value of the user attribute specified for the authentication principal attribute. This value is usually set to urn:oasis:names:tc:SAML:2.0:nameid-format:persistent or urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress. Please ensure this is consistent with the identity provider's configuration.",
3023
- "_value": nameid_format,
3019
+ "_key": "com.opentext.otds.as.drivers.saml.claim12",
3020
+ "_name": "Claim 12",
3021
+ "_value": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/dateofbirth",
3024
3022
  },
3025
3023
  {
3026
- "_key": "com.opentext.otds.as.drivers.saml._impersonator_claim",
3027
- "_name": "Claim for impersonating user",
3028
- "_description": "A claim that contains the ID of the actor/impersonator for the user identified by NameID. It must be in the same format as NameID.",
3029
- "_value": "loggedinuserid",
3024
+ "_key": "com.opentext.otds.as.drivers.saml.claimAttribute12",
3025
+ "_name": "OTDS Attribute 12",
3026
+ "_value": "birthDate",
3030
3027
  },
3031
3028
  {
3032
- "_key": "com.opentext.otds.as.drivers.saml.sp_url",
3033
- "_name": "OTDS SP Endpoint",
3034
- "_description": "Specifies the service provider URL that will be used to identify OTDS to the identity provider. If not specified, the URL will be taken from the request. This generally needs to be configured for environments in which OTDS is behind a reverse-proxy.",
3035
- "_value": otds_sp_endpoint,
3029
+ "_key": "com.opentext.otds.as.drivers.saml.claim13",
3030
+ "_name": "Claim 13",
3031
+ "_value": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/gender",
3036
3032
  },
3037
3033
  {
3038
- "_key": "com.opentext.otds.as.drivers.saml.enable_sp_sso",
3039
- "_name": "Active By Default",
3040
- "_description": "Whether to activate this handler for any request to the OTDS login page. If true, any login request to the OTDS login page will be redirected to the IdP. If false, the user has to select the provider on the login page.",
3041
- "_value": active_by_default,
3034
+ "_key": "com.opentext.otds.as.drivers.saml.claimAttribute13",
3035
+ "_name": "OTDS Attribute 13",
3036
+ "_value": "gender",
3042
3037
  },
3043
3038
  {
3044
- "_key": "com.opentext.otds.as.drivers.saml._signature_alg",
3045
- "_name": "XML Signature Algorithm",
3046
- "_description": "Only relevant when certificate and private key are configured. Default is http://www.w3.org/2000/09/xmldsig#rsa-sha1. Valid values are defined at http://www.w3.org/TR/xmldsig-core1/#sec-AlgID.",
3047
- "_value": "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
3039
+ "_key": "com.opentext.otds.as.drivers.saml.claim14",
3040
+ "_name": "Claim 14",
3041
+ "_value": "",
3048
3042
  },
3049
3043
  {
3050
- "_key": "com.opentext.otds.as.drivers.saml.use_acs_url",
3051
- "_name": "Use AssertionConsumerServiceURL",
3052
- "_description": "Set to true to have the SAML AuthnRequest use AssertionConsumerServiceURL instead of AssertionConsumerServiceIndex",
3053
- "_value": "true",
3044
+ "_key": "com.opentext.otds.as.drivers.saml.claimAttribute14",
3045
+ "_name": "OTDS Attribute 14",
3046
+ "_value": "",
3054
3047
  },
3055
3048
  {
3056
- "_key": "com.opentext.otds.as.drivers.saml.grace_period",
3057
- "_name": "Grace Period",
3058
- "_description": 'Specifies the number of minutes to allow for "NotBefore" and "NotOnOrAfter" fields when validating assertions in order to account for time difference between the identity provider and this service provider.',
3059
- "_value": "5",
3049
+ "_key": "com.opentext.otds.as.drivers.saml.claim15",
3050
+ "_name": "Claim 15",
3051
+ "_value": "http://schemas.xmlsoap.org/claims/Group",
3060
3052
  },
3061
3053
  {
3062
- "_key": "com.opentext.otds.as.drivers.saml.auth_request_binding",
3063
- "_name": "Auth Request Binding",
3064
- "_description": "Specifies the preferred SAML binding to use for sending the AuthnRequest, provided it is supported by the identity provider.",
3065
- "_value": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
3054
+ "_key": "com.opentext.otds.as.drivers.saml.claimAttribute15",
3055
+ "_name": "OTDS Attribute 15",
3056
+ "_value": "oTMemberOf",
3066
3057
  },
3067
3058
  {
3068
- "_key": "com.opentext.otds.as.drivers.saml.auth_response_binding",
3069
- "_name": "Auth Response Binding",
3070
- "_description": "Specifies the SAML binding to use for the response to an AuthnRequest",
3071
- "_value": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
3059
+ "_key": "com.opentext.otds.as.drivers.saml.claim16",
3060
+ "_name": "Claim 16",
3061
+ "_value": "http://schemas.xmlsoap.org/claims/Department",
3062
+ },
3063
+ {
3064
+ "_key": "com.opentext.otds.as.drivers.saml.claimAttribute16",
3065
+ "_name": "OTDS Attribute 16",
3066
+ "_value": "oTDepartment",
3067
+ },
3068
+ {
3069
+ "_key": "com.opentext.otds.as.drivers.saml.claim17",
3070
+ "_name": "Claim 17",
3071
+ "_value": "http://schemas.xmlsoap.org/claims/Title",
3072
+ },
3073
+ {
3074
+ "_key": "com.opentext.otds.as.drivers.saml.claimAttribute17",
3075
+ "_name": "OTDS Attribute 17",
3076
+ "_value": "title",
3077
+ },
3078
+ {
3079
+ "_key": "com.opentext.otds.as.drivers.saml.claim18",
3080
+ "_name": "Claim 18",
3081
+ "_value": "",
3082
+ },
3083
+ {
3084
+ "_key": "com.opentext.otds.as.drivers.saml.claimAttribute18",
3085
+ "_name": "OTDS Attribute 18",
3086
+ "_value": "",
3087
+ },
3088
+ {
3089
+ "_key": "com.opentext.otds.as.drivers.saml.claim19",
3090
+ "_name": "Claim 19",
3091
+ "_value": "http://schemas.microsoft.com/ws/2008/06/identity/claims/role",
3092
+ },
3093
+ {
3094
+ "_key": "com.opentext.otds.as.drivers.saml.claimAttribute19",
3095
+ "_name": "OTDS Attribute 19",
3096
+ "_value": "oTMemberOf",
3097
+ },
3098
+ {
3099
+ "_key": "com.opentext.otds.as.drivers.saml.claim20",
3100
+ "_name": "Claim 20",
3101
+ "_value": "",
3102
+ },
3103
+ {
3104
+ "_key": "com.opentext.otds.as.drivers.saml.claimAttribute20",
3105
+ "_name": "OTDS Attribute 20",
3106
+ "_value": "",
3072
3107
  },
3073
3108
  ],
3074
3109
  }
3075
3110
 
3076
3111
  request_url = self.auth_handler_url()
3077
3112
 
3078
- logger.info(
3079
- "Adding SAML auth handler -> %s (%s); calling -> %s",
3113
+ logger.debug(
3114
+ "Adding SAML auth handler -> '%s' ('%s'); calling -> %s",
3080
3115
  name,
3081
3116
  description,
3082
3117
  request_url,
3083
3118
  )
3084
3119
 
3085
- retries = 0
3086
- while True:
3087
- response = requests.post(
3088
- url=request_url,
3089
- json=authHandlerPostBodyJson,
3090
- headers=REQUEST_HEADERS,
3091
- cookies=self.cookie(),
3092
- timeout=None,
3093
- )
3094
- if response.ok:
3095
- return self.parse_request_response(response)
3096
- # Check if Session has expired - then re-authenticate and try once more
3097
- elif response.status_code == 401 and retries == 0:
3098
- logger.warning("Session has expired - try to re-authenticate...")
3099
- self.authenticate(revalidate=True)
3100
- retries += 1
3101
- else:
3102
- logger.error(
3103
- "Failed to add SAML auth handler -> %s; error -> %s (%s)",
3104
- name,
3105
- response.text,
3106
- response.status_code,
3107
- )
3108
- return None
3120
+ return self.do_request(
3121
+ url=request_url,
3122
+ method="POST",
3123
+ json_data=auth_handler_post_body_json,
3124
+ timeout=None,
3125
+ failure_message="Failed to add SAML auth handler -> '{}'".format(name),
3126
+ )
3109
3127
 
3110
3128
  # end method definition
3111
3129
 
@@ -3140,7 +3158,7 @@ class OTDS:
3140
3158
  auth_principal_attributes = ["oTExternalID1"]
3141
3159
 
3142
3160
  # 1. Prepare the body for the AuthHandler REST call:
3143
- authHandlerPostBodyJson = {
3161
+ auth_handler_post_body_json = {
3144
3162
  "_name": name,
3145
3163
  "_description": description,
3146
3164
  "_class": "com.opentext.otds.as.drivers.sapssoext.SAPSSOEXTAuthHandler",
@@ -3196,43 +3214,40 @@ class OTDS:
3196
3214
  # 2. Create the auth handler in OTDS
3197
3215
  request_url = self.auth_handler_url()
3198
3216
 
3199
- logger.info(
3200
- "Adding SAP auth handler -> %s (%s); calling -> %s",
3217
+ logger.debug(
3218
+ "Adding SAP auth handler -> '%s' ('%s'); calling -> %s",
3201
3219
  name,
3202
3220
  description,
3203
3221
  request_url,
3204
3222
  )
3205
3223
 
3206
- response = requests.post(
3224
+ response = self.do_request(
3207
3225
  url=request_url,
3208
- json=authHandlerPostBodyJson,
3209
- headers=REQUEST_HEADERS,
3210
- cookies=self.cookie(),
3226
+ method="POST",
3227
+ json_data=auth_handler_post_body_json,
3211
3228
  timeout=None,
3229
+ failure_message="Failed to add SAP auth handler -> '{}'".format(name),
3230
+ parse_request_response=False,
3212
3231
  )
3213
- if not response.ok:
3214
- logger.error(
3215
- "Failed to add SAP auth handler -> %s; error -> %s (%s)",
3216
- name,
3217
- response.text,
3218
- response.status_code,
3219
- )
3232
+ if not response or not response.ok:
3220
3233
  return None
3221
3234
 
3222
3235
  # 3. Upload the certificate file:
3223
3236
 
3224
3237
  # Check that the certificate (PSE) file is readable:
3225
- logger.info("Reading certificate file -> %s...", certificate_file)
3238
+ logger.debug("Reading certificate file -> '%s'...", certificate_file)
3226
3239
  try:
3227
3240
  # PSE files are binary - so we need to open with "rb":
3228
- with open(certificate_file, "rb") as certFile:
3229
- certContent = certFile.read()
3230
- if not certContent:
3231
- logger.error("No data in certificate file -> %s", certificate_file)
3241
+ with open(certificate_file, "rb") as cert_file:
3242
+ cert_content = cert_file.read()
3243
+ if not cert_content:
3244
+ logger.error(
3245
+ "No data in certificate file -> '%s'", certificate_file
3246
+ )
3232
3247
  return None
3233
3248
  except IOError as exception:
3234
3249
  logger.error(
3235
- "Unable to open certificate file -> %s; error -> %s",
3250
+ "Unable to open certificate file -> '%s'; error -> %s",
3236
3251
  certificate_file,
3237
3252
  exception.strerror,
3238
3253
  )
@@ -3243,43 +3258,45 @@ class OTDS:
3243
3258
  try:
3244
3259
  # If file is not base64 encoded the next statement will throw an exception
3245
3260
  # (this is good)
3246
- certContentDecoded = base64.b64decode(certContent, validate=True)
3247
- certContentEncoded = base64.b64encode(certContentDecoded).decode("utf-8")
3248
- if certContentEncoded == certContent.decode("utf-8"):
3249
- logger.info(
3250
- "Certificate file -> %s is base64 encoded", certificate_file
3261
+ cert_content_decoded = base64.b64decode(cert_content, validate=True)
3262
+ cert_content_encoded = base64.b64encode(cert_content_decoded).decode(
3263
+ "utf-8"
3264
+ )
3265
+ if cert_content_encoded == cert_content.decode("utf-8"):
3266
+ logger.debug(
3267
+ "Certificate file -> '%s' is base64 encoded", certificate_file
3251
3268
  )
3252
3269
  cert_file_encoded = True
3253
3270
  else:
3254
3271
  cert_file_encoded = False
3255
3272
  except TypeError:
3256
- logger.info(
3257
- "Certificate file -> %s is not base64 encoded", certificate_file
3273
+ logger.debug(
3274
+ "Certificate file -> '%s' is not base64 encoded", certificate_file
3258
3275
  )
3259
3276
  cert_file_encoded = False
3260
3277
 
3261
3278
  if cert_file_encoded:
3262
3279
  certificate_file = "/tmp/" + os.path.basename(certificate_file)
3263
- logger.info("Writing decoded certificate file -> %s...", certificate_file)
3280
+ logger.debug("Writing decoded certificate file -> %s...", certificate_file)
3264
3281
  try:
3265
3282
  # PSE files need to be binary - so we need to open with "wb":
3266
- with open(certificate_file, "wb") as certFile:
3267
- certFile.write(base64.b64decode(certContent))
3283
+ with open(certificate_file, "wb") as cert_file:
3284
+ cert_file.write(base64.b64decode(cert_content))
3268
3285
  except IOError as exception:
3269
3286
  logger.error(
3270
- "Failed writing to file -> %s; error -> %s",
3287
+ "Failed writing to file -> '%s'; error -> %s",
3271
3288
  certificate_file,
3272
3289
  exception.strerror,
3273
3290
  )
3274
3291
  return None
3275
3292
 
3276
- authHandlerPostData = {
3293
+ auth_handler_post_data = {
3277
3294
  "file1_property": "com.opentext.otds.as.drivers.sapssoext.certificate1"
3278
3295
  }
3279
3296
 
3280
3297
  # It is important to send the file pointer and not the actual file content
3281
3298
  # otherwise the file is send base64 encoded which we don't want:
3282
- authHandlerPostFiles = {
3299
+ auth_handler_post_files = {
3283
3300
  "file1": (
3284
3301
  os.path.basename(certificate_file),
3285
3302
  open(certificate_file, "rb"),
@@ -3289,8 +3306,8 @@ class OTDS:
3289
3306
 
3290
3307
  request_url = self.auth_handler_url() + "/" + name + "/files"
3291
3308
 
3292
- logger.info(
3293
- "Uploading certificate file -> %s for SAP auth handler -> %s (%s); calling -> %s",
3309
+ logger.debug(
3310
+ "Uploading certificate file -> '%s' for SAP auth handler -> '%s' ('%s'); calling -> %s",
3294
3311
  certificate_file,
3295
3312
  name,
3296
3313
  description,
@@ -3302,14 +3319,14 @@ class OTDS:
3302
3319
  # then requests will send a multipart/form-data POST automatically:
3303
3320
  response = requests.post(
3304
3321
  url=request_url,
3305
- data=authHandlerPostData,
3306
- files=authHandlerPostFiles,
3322
+ data=auth_handler_post_data,
3323
+ files=auth_handler_post_files,
3307
3324
  cookies=self.cookie(),
3308
3325
  timeout=None,
3309
3326
  )
3310
3327
  if not response.ok:
3311
3328
  logger.error(
3312
- "Failed to upload certificate file -> %s for SAP auth handler -> %s; error -> %s (%s)",
3329
+ "Failed to upload certificate file -> '%s' for SAP auth handler -> '%s'; error -> %s (%s)",
3313
3330
  certificate_file,
3314
3331
  name,
3315
3332
  response.text,
@@ -3366,7 +3383,7 @@ class OTDS:
3366
3383
  auth_principal_attributes = ["oTExtraAttr0"]
3367
3384
 
3368
3385
  # 1. Prepare the body for the AuthHandler REST call:
3369
- authHandlerPostBodyJson = {
3386
+ auth_handler_post_body_json = {
3370
3387
  "_name": name,
3371
3388
  "_description": description,
3372
3389
  "_class": "com.opentext.otds.as.drivers.http.OAuth2Handler",
@@ -3705,37 +3722,20 @@ class OTDS:
3705
3722
 
3706
3723
  request_url = self.auth_handler_url()
3707
3724
 
3708
- logger.info(
3709
- "Adding OAuth auth handler -> %s (%s); calling -> %s",
3725
+ logger.debug(
3726
+ "Adding OAuth auth handler -> '%s' ('%s'); calling -> %s",
3710
3727
  name,
3711
3728
  description,
3712
3729
  request_url,
3713
3730
  )
3714
3731
 
3715
- retries = 0
3716
- while True:
3717
- response = requests.post(
3718
- url=request_url,
3719
- json=authHandlerPostBodyJson,
3720
- headers=REQUEST_HEADERS,
3721
- cookies=self.cookie(),
3722
- timeout=None,
3723
- )
3724
- if response.ok:
3725
- return self.parse_request_response(response)
3726
- # Check if Session has expired - then re-authenticate and try once more
3727
- elif response.status_code == 401 and retries == 0:
3728
- logger.warning("Session has expired - try to re-authenticate...")
3729
- self.authenticate(revalidate=True)
3730
- retries += 1
3731
- else:
3732
- logger.error(
3733
- "Failed to add OAuth auth handler -> %s; error -> %s (%s)",
3734
- name,
3735
- response.text,
3736
- response.status_code,
3737
- )
3738
- return None
3732
+ return self.do_request(
3733
+ url=request_url,
3734
+ method="POST",
3735
+ json_data=auth_handler_post_body_json,
3736
+ timeout=None,
3737
+ failure_message="Failed to add OAuth auth handler -> '{}'".format(name),
3738
+ )
3739
3739
 
3740
3740
  # end method definition
3741
3741
 
@@ -3750,7 +3750,9 @@ class OTDS:
3750
3750
 
3751
3751
  resource = self.get_resource(resource_name)
3752
3752
  if not resource:
3753
- logger.error("Resource -> %s not found - cannot consolidate", resource_name)
3753
+ logger.error(
3754
+ "Resource -> '%s' not found - cannot consolidate", resource_name
3755
+ )
3754
3756
  return False
3755
3757
 
3756
3758
  resource_dn = resource["resourceDN"]
@@ -3758,7 +3760,7 @@ class OTDS:
3758
3760
  logger.error("Resource DN is empty - cannot consolidate")
3759
3761
  return False
3760
3762
 
3761
- consolidationPostBodyJson = {
3763
+ consolidation_post_body_json = {
3762
3764
  "cleanupUsersInResource": False,
3763
3765
  "cleanupGroupsInResource": False,
3764
3766
  "resourceList": [resource_dn],
@@ -3767,33 +3769,28 @@ class OTDS:
3767
3769
 
3768
3770
  request_url = "{}".format(self.consolidation_url())
3769
3771
 
3770
- logger.info(
3771
- "Consolidation of resource -> %s; calling -> %s", resource_dn, request_url
3772
+ logger.debug(
3773
+ "Consolidation of resource -> %s (%s); calling -> %s",
3774
+ resource_name,
3775
+ resource_dn,
3776
+ request_url,
3772
3777
  )
3773
3778
 
3774
- retries = 0
3775
- while True:
3776
- response = requests.post(
3777
- url=request_url,
3778
- json=consolidationPostBodyJson,
3779
- headers=REQUEST_HEADERS,
3780
- cookies=self.cookie(),
3781
- timeout=None,
3782
- )
3783
- if response.ok:
3784
- return True
3785
- # Check if Session has expired - then re-authenticate and try once more
3786
- elif response.status_code == 401 and retries == 0:
3787
- logger.warning("Session has expired - try to re-authenticate...")
3788
- self.authenticate(revalidate=True)
3789
- retries += 1
3790
- else:
3791
- logger.error(
3792
- "Failed to consolidate; error -> %s (%s)",
3793
- response.text,
3794
- response.status_code,
3795
- )
3796
- return False
3779
+ response = self.do_request(
3780
+ url=request_url,
3781
+ method="POST",
3782
+ json_data=consolidation_post_body_json,
3783
+ timeout=None,
3784
+ failure_message="Failed to consolidate resource -> '{}'".format(
3785
+ resource_name
3786
+ ),
3787
+ parse_request_response=False,
3788
+ )
3789
+
3790
+ if response and response.ok:
3791
+ return True
3792
+
3793
+ return False
3797
3794
 
3798
3795
  # end method definition
3799
3796
 
@@ -3818,42 +3815,34 @@ class OTDS:
3818
3815
  if impersonation_list is None:
3819
3816
  impersonation_list = []
3820
3817
 
3821
- impersonationPutBodyJson = {
3818
+ impersonation_put_body_json = {
3822
3819
  "allowImpersonation": allow_impersonation,
3823
3820
  "impersonateList": impersonation_list,
3824
3821
  }
3825
3822
 
3826
3823
  request_url = "{}/{}/impersonation".format(self.resource_url(), resource_name)
3827
3824
 
3828
- logger.info(
3829
- "Impersonation settings for resource -> %s; calling -> %s",
3825
+ logger.debug(
3826
+ "Impersonation settings for resource -> '%s'; calling -> %s",
3830
3827
  resource_name,
3831
3828
  request_url,
3832
3829
  )
3833
3830
 
3834
- retries = 0
3835
- while True:
3836
- response = requests.put(
3837
- url=request_url,
3838
- json=impersonationPutBodyJson,
3839
- headers=REQUEST_HEADERS,
3840
- cookies=self.cookie(),
3841
- timeout=None,
3842
- )
3843
- if response.ok:
3844
- return True
3845
- # Check if Session has expired - then re-authenticate and try once more
3846
- elif response.status_code == 401 and retries == 0:
3847
- logger.warning("Session has expired - try to re-authenticate...")
3848
- self.authenticate(revalidate=True)
3849
- retries += 1
3850
- else:
3851
- logger.error(
3852
- "Failed to set impersonation for resource -> %s; error -> %s",
3853
- resource_name,
3854
- response.text,
3855
- )
3856
- return False
3831
+ response = self.do_request(
3832
+ url=request_url,
3833
+ method="PUT",
3834
+ json_data=impersonation_put_body_json,
3835
+ timeout=None,
3836
+ failure_message="Failed to set impersonation for resource -> '{}'".format(
3837
+ resource_name
3838
+ ),
3839
+ parse_request_response=False,
3840
+ )
3841
+
3842
+ if response and response.ok:
3843
+ return True
3844
+
3845
+ return False
3857
3846
 
3858
3847
  # end method definition
3859
3848
 
@@ -3877,43 +3866,34 @@ class OTDS:
3877
3866
  if impersonation_list is None:
3878
3867
  impersonation_list = []
3879
3868
 
3880
- impersonationPutBodyJson = {
3869
+ impersonation_put_body_json = {
3881
3870
  "allowImpersonation": allow_impersonation,
3882
3871
  "impersonateList": impersonation_list,
3883
3872
  }
3884
3873
 
3885
3874
  request_url = "{}/{}/impersonation".format(self.oauth_client_url(), client_id)
3886
3875
 
3887
- logger.info(
3888
- "Impersonation settings for OAuth Client -> %s; calling -> %s",
3876
+ logger.debug(
3877
+ "Impersonation settings for OAuth Client -> '%s'; calling -> %s",
3889
3878
  client_id,
3890
3879
  request_url,
3891
3880
  )
3892
3881
 
3893
- retries = 0
3894
- while True:
3895
- response = requests.put(
3896
- url=request_url,
3897
- json=impersonationPutBodyJson,
3898
- headers=REQUEST_HEADERS,
3899
- cookies=self.cookie(),
3900
- timeout=None,
3901
- )
3902
- if response.ok:
3903
- return True
3904
- # Check if Session has expired - then re-authenticate and try once more
3905
- elif response.status_code == 401 and retries == 0:
3906
- logger.warning("Session has expired - try to re-authenticate...")
3907
- self.authenticate(revalidate=True)
3908
- retries += 1
3909
- else:
3910
- logger.error(
3911
- "Failed to set impersonation for OAuth Client -> %s; error -> %s (%s)",
3912
- client_id,
3913
- response.text,
3914
- response.status_code,
3915
- )
3916
- return False
3882
+ response = self.do_request(
3883
+ url=request_url,
3884
+ method="PUT",
3885
+ json_data=impersonation_put_body_json,
3886
+ timeout=None,
3887
+ failure_message="Failed to set impersonation for OAuth Client -> '{}'".format(
3888
+ client_id
3889
+ ),
3890
+ parse_request_response=False,
3891
+ )
3892
+
3893
+ if response and response.ok:
3894
+ return True
3895
+
3896
+ return False
3917
3897
 
3918
3898
  # end method definition
3919
3899
 
@@ -3948,30 +3928,14 @@ class OTDS:
3948
3928
 
3949
3929
  request_url = "{}/passwordpolicy".format(self.config()["systemConfigUrl"])
3950
3930
 
3951
- logger.info("Getting password policy; calling -> %s", request_url)
3931
+ logger.debug("Getting password policy; calling -> %s", request_url)
3952
3932
 
3953
- retries = 0
3954
- while True:
3955
- response = requests.get(
3956
- url=request_url,
3957
- headers=REQUEST_HEADERS,
3958
- cookies=self.cookie(),
3959
- timeout=None,
3960
- )
3961
- if response.ok:
3962
- return self.parse_request_response(response)
3963
- # Check if Session has expired - then re-authenticate and try once more
3964
- elif response.status_code == 401 and retries == 0:
3965
- logger.warning("Session has expired - try to re-authenticate...")
3966
- self.authenticate(revalidate=True)
3967
- retries += 1
3968
- else:
3969
- logger.error(
3970
- "Failed to get password policy; error -> %s (%s)",
3971
- response.text,
3972
- response.status_code,
3973
- )
3974
- return None
3933
+ return self.do_request(
3934
+ url=request_url,
3935
+ method="GET",
3936
+ timeout=None,
3937
+ failure_message="Failed to get password policy",
3938
+ )
3975
3939
 
3976
3940
  # end method definition
3977
3941
 
@@ -4009,35 +3973,26 @@ class OTDS:
4009
3973
 
4010
3974
  request_url = "{}/passwordpolicy".format(self.config()["systemConfigUrl"])
4011
3975
 
4012
- logger.info(
3976
+ logger.debug(
4013
3977
  "Update password policy with these new values -> %s; calling -> %s",
4014
- update_values,
3978
+ str(update_values),
4015
3979
  request_url,
4016
3980
  )
4017
3981
 
4018
- retries = 0
4019
- while True:
4020
- response = requests.put(
4021
- url=request_url,
4022
- json=update_values,
4023
- headers=REQUEST_HEADERS,
4024
- cookies=self.cookie(),
4025
- timeout=None,
4026
- )
4027
- if response.ok:
4028
- return True
4029
- # Check if Session has expired - then re-authenticate and try once more
4030
- elif response.status_code == 401 and retries == 0:
4031
- logger.warning("Session has expired - try to re-authenticate...")
4032
- self.authenticate(revalidate=True)
4033
- retries += 1
4034
- else:
4035
- logger.error(
4036
- "Failed to update password policy with values -> %s; error -> %s (%s)",
4037
- update_values,
4038
- response.text,
4039
- response.status_code,
4040
- )
4041
- return False
3982
+ response = self.do_request(
3983
+ url=request_url,
3984
+ method="PUT",
3985
+ json_data=update_values,
3986
+ timeout=None,
3987
+ failure_message="Failed to update password policy with values -> {}".format(
3988
+ update_values
3989
+ ),
3990
+ parse_request_response=False,
3991
+ )
3992
+
3993
+ if response and response.ok:
3994
+ return True
3995
+
3996
+ return False
4042
3997
 
4043
3998
  # end method definition