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/coreshare.py ADDED
@@ -0,0 +1,2532 @@
1
+ """
2
+ CoreShare Module to interact with the Core Share API
3
+ See: https://confluence.opentext.com/pages/viewpage.action?spaceKey=OTC&title=APIs+Consumption+based+on+roles
4
+ See also: https://swagger.otxlab.net/ui/?branch=master&yaml=application-specific/core/core-api.yaml
5
+
6
+ Authentication - get Client Secrets:
7
+ 1. Login to Core Share as a Tenant Admin User .
8
+ 2. Navigate to Security P age.
9
+ 3. On OAuth Confidential Clients section provide Description and Redirect URLs. It will populate a
10
+ dialog with Client Secret.
11
+ 4. Copy Client Secret as it will not be available anywhere once the dialog is closed.
12
+
13
+ Class: CoreShare
14
+ Methods:
15
+
16
+ __init__ : class initializer
17
+ config : Returns config data set
18
+ credentials: Get credentials (username + password)
19
+ set_credentials: Set the credentials for Core Share based on username and password.
20
+
21
+ request_header_admin: Returns the request header used for Application calls
22
+ that require administrator credentials
23
+ request_header_user: Returns the request header used for Application calls
24
+ that require user (non-admin) credentials.
25
+ do_request: call an Core Share REST API in a safe way.
26
+ parse_request_response: Parse the REST API responses and convert
27
+ them to Python dict in a safe way
28
+ lookup_result_value: Lookup a property value based on a provided key / value pair in the response
29
+ properties of a Core Share REST API call
30
+ exist_result_item: Check if an dict item is in the response
31
+ of the Core Share API call
32
+ get_result_value: Check if a defined value (based on a key) is in the Core Share API response
33
+
34
+ authenticate_admin : Authenticates as Admin at Core Share API
35
+ authenticate_user : Authenticates as Service user at Core Share API
36
+
37
+ get_groups: Get Core Share groups.
38
+ add_group: Add a new Core Share group.
39
+ get_group_members: Get Core Share group members.
40
+ add_group_member: Add a Core Share user to a Cire Share group.
41
+ remove_group_member: Remove a Core Share user from a Core Share group.
42
+ get_group_by_id: Get a Core Share group by its ID.
43
+ get_group_by_name: Get Core Share group by its name.
44
+ search_groups: Search Core Share group(s) by name.
45
+
46
+ get_users: Get Core Share users.
47
+ get_user_by_id: Get a Core Share user by its ID.
48
+ get_user_by_name: Get Core Share user by its first and last name.
49
+ search_users: Search Core Share user(s) by name / property.
50
+ add_user: Add a new Core Share user. This requires a Tenent Admin authorization.
51
+ resend_user_invite: Resend the invite for a Core Share user.
52
+ update_user: Update a Core Share user.
53
+ add_user_access_role: Add an access role to a Core Share user.
54
+ remove_user_access_role: Remove an access role from a Core Share user.
55
+ update_user_access_roles: Define the access roles of a Core Share user.
56
+ update_user_password: Update the password of a Core Share user.
57
+ update_user_photo: Update the Core Share user photo.
58
+
59
+ get_folders: Get Core Share folders under a given parent ID.
60
+ unshare_folder: Unshare Core Share folder with a given resource ID.
61
+ delete_folder: Delete Core Share folder with a given resource ID.
62
+ delete_document: Delete Core Share document with a given resource ID.
63
+ leave_share: Remove a Core Share user from a share (i.e. the user leaves the share)
64
+ stop_share: Stop of share of a user.
65
+ cleanup_user_files: Cleanup all files of a user. This handles different types of resources.
66
+ get_group_shares: Get (incoming) shares of a Core Share group.
67
+ revoke_group_share: Revoke sharing of a folder with a group.
68
+ cleanup_group_shares: Cleanup all incoming shares of a group.
69
+ """
70
+
71
+ __author__ = "Dr. Marc Diefenbruch"
72
+ __copyright__ = "Copyright 2024, OpenText"
73
+ __credits__ = ["Kai-Philip Gatzweiler"]
74
+ __maintainer__ = "Dr. Marc Diefenbruch"
75
+ __email__ = "mdiefenb@opentext.com"
76
+
77
+ import os
78
+ import json
79
+ import logging
80
+ import time
81
+
82
+ import urllib.parse
83
+ from http import HTTPStatus
84
+ import requests
85
+
86
+ logger = logging.getLogger("pyxecm.customizer.coreshare")
87
+
88
+ REQUEST_LOGIN_HEADERS = {
89
+ "Content-Type": "application/x-www-form-urlencoded",
90
+ "Accept": "application/json",
91
+ }
92
+
93
+ REQUEST_TIMEOUT = 60
94
+ REQUEST_RETRY_DELAY = 20
95
+ REQUEST_MAX_RETRIES = 2
96
+
97
+
98
+ class CoreShare(object):
99
+ """Used to retrieve and automate stettings in Core Share."""
100
+
101
+ _config: dict
102
+ _access_token_user = None
103
+ _access_token_admin = None
104
+
105
+ def __init__(
106
+ self,
107
+ base_url: str,
108
+ sso_url: str,
109
+ client_id: str,
110
+ client_secret: str,
111
+ username: str,
112
+ password: str,
113
+ ):
114
+ """Initialize the CoreShare object
115
+
116
+ Args:
117
+ base_url (str): base URL of the Core Share tenant
118
+ sso_url (str): Single Sign On URL of the Core Share tenant
119
+ client_id (str): Core Share Client ID
120
+ client_secret (str): Core Share Client Secret
121
+ username (str): admin user name in Core Share
122
+ password (str): admin password in Core Share
123
+ """
124
+
125
+ core_share_config = {}
126
+
127
+ # Store the credentials and parameters in a config dictionary:
128
+ core_share_config["clientId"] = client_id
129
+ core_share_config["clientSecret"] = client_secret
130
+ core_share_config["username"] = username
131
+ core_share_config["password"] = password
132
+
133
+ # Set the Core Share URLs and REST API endpoints:
134
+ core_share_config["baseUrl"] = base_url
135
+ core_share_config["ssoUrl"] = sso_url
136
+ core_share_config["restUrlv1"] = core_share_config["baseUrl"] + "/api/v1"
137
+ core_share_config["restUrlv3"] = core_share_config["baseUrl"] + "/api/v3"
138
+ core_share_config["groupsUrl"] = core_share_config["restUrlv1"] + "/groups"
139
+ core_share_config["usersUrlv1"] = core_share_config["restUrlv1"] + "/users"
140
+ core_share_config["usersUrlv3"] = core_share_config["restUrlv3"] + "/users"
141
+ core_share_config["invitesUrl"] = core_share_config["restUrlv1"] + "/invites"
142
+ core_share_config["foldersUrlv1"] = core_share_config["restUrlv1"] + "/folders"
143
+ core_share_config["foldersUrlv3"] = core_share_config["restUrlv3"] + "/folders"
144
+ core_share_config["documentsUrlv1"] = (
145
+ core_share_config["restUrlv1"] + "/documents"
146
+ )
147
+ core_share_config["documentsUrlv3"] = (
148
+ core_share_config["restUrlv3"] + "/documents"
149
+ )
150
+ core_share_config["searchUrl"] = core_share_config["baseUrl"] + "/search/v1"
151
+ core_share_config["searchUserUrl"] = core_share_config["searchUrl"] + "/user"
152
+ core_share_config["searchGroupUrl"] = (
153
+ core_share_config["searchUrl"] + "/user/group-all"
154
+ )
155
+
156
+ core_share_config["sessionsUrl"] = core_share_config["restUrlv1"] + "/sessions"
157
+ core_share_config["tokenUrl"] = (
158
+ core_share_config["ssoUrl"] + "/otdsws/oauth2/token"
159
+ )
160
+ core_share_config["sessionsUrl"] = core_share_config["restUrlv1"] + "/sessions"
161
+
162
+ # Tenant Admin User Authentication information (Session URL):
163
+ core_share_config["authorizationUrlAdmin"] = (
164
+ core_share_config["sessionsUrl"]
165
+ + "?client={'type':'web'}"
166
+ + "&email="
167
+ + urllib.parse.quote(username)
168
+ + "&password="
169
+ + urllib.parse.quote(password)
170
+ )
171
+
172
+ # Tenant Service User Authentication information:
173
+ core_share_config["authorizationUrlCredentials"] = (
174
+ core_share_config["tokenUrl"]
175
+ + "?client_id="
176
+ + client_id
177
+ + "&client_secret="
178
+ + client_secret
179
+ + "&grant_type=client_credentials"
180
+ )
181
+ core_share_config["authorizationUrlPassword"] = (
182
+ core_share_config["tokenUrl"]
183
+ + "?client_id="
184
+ + client_id
185
+ + "&client_secret="
186
+ + client_secret
187
+ + "&grant_type=password"
188
+ + "&username="
189
+ + urllib.parse.quote(username)
190
+ + "&password="
191
+ + urllib.parse.quote(password)
192
+ )
193
+
194
+ self._config = core_share_config
195
+
196
+ # end method definition
197
+
198
+ def config(self) -> dict:
199
+ """Returns the configuration dictionary
200
+
201
+ Returns:
202
+ dict: Configuration dictionary
203
+ """
204
+ return self._config
205
+
206
+ # end method definition
207
+
208
+ def credentials(self) -> dict:
209
+ """Get credentials (username + password)
210
+
211
+ Returns:
212
+ dict: dictionary with username and password
213
+ """
214
+ return {
215
+ "username": self.config()["username"],
216
+ "password": self.config()["password"],
217
+ }
218
+
219
+ # end method definition
220
+
221
+ def set_credentials(self, username: str = "admin", password: str = ""):
222
+ """Set the credentials for Core Share based on username and password.
223
+
224
+ Args:
225
+ username (str, optional): Username. Defaults to "admin".
226
+ password (str, optional): Password of the user. Defaults to "".
227
+ """
228
+
229
+ logger.info("Change Core Share credentials to user -> %s...", username)
230
+
231
+ self.config()["username"] = username
232
+ self.config()["password"] = password
233
+
234
+ # As the Authorization URLs include username password
235
+ # we have to update them as well:
236
+ self.config()["authorizationUrlAdmin"] = (
237
+ self.config()["sessionsUrl"]
238
+ + "?client={'type':'web'}"
239
+ + "&email="
240
+ + urllib.parse.quote(username)
241
+ + "&password="
242
+ + urllib.parse.quote(password)
243
+ )
244
+
245
+ self.config()["authorizationUrlPassword"] = (
246
+ self.config()["tokenUrl"]
247
+ + "?client_id="
248
+ + self.config()["clientId"]
249
+ + "&client_secret="
250
+ + self.config()["clientSecret"]
251
+ + "&grant_type=password"
252
+ + "&username="
253
+ + urllib.parse.quote(username)
254
+ + "&password="
255
+ + urllib.parse.quote(password)
256
+ )
257
+
258
+ # end method definition
259
+
260
+ def request_header_admin(self, content_type: str = "application/json") -> dict:
261
+ """Returns the request header used for Application calls
262
+ that require administrator credentials.
263
+ Consists of Bearer access token and Content Type
264
+
265
+ Args:
266
+ content_type (str, optional): content type for the request
267
+ Return:
268
+ dict: request header values
269
+ """
270
+
271
+ request_header = {
272
+ "Authorization": "Bearer {}".format(self._access_token_admin),
273
+ }
274
+ if content_type:
275
+ request_header["Content-Type"] = content_type
276
+
277
+ return request_header
278
+
279
+ # end method definition
280
+
281
+ def request_header_user(self, content_type: str = "application/json") -> dict:
282
+ """Returns the request header used for Application calls
283
+ that require user (non-admin) credentials.
284
+ Consists of Bearer access token and Content Type
285
+
286
+ Args:
287
+ content_type (str, optional): content type for the request
288
+ Return:
289
+ dict: request header values
290
+ """
291
+
292
+ request_header = {
293
+ "Authorization": "Bearer {}".format(self._access_token_user),
294
+ }
295
+ if content_type:
296
+ request_header["Content-Type"] = content_type
297
+
298
+ return request_header
299
+
300
+ # end method definition
301
+
302
+ def do_request(
303
+ self,
304
+ url: str,
305
+ method: str = "GET",
306
+ headers: dict | None = None,
307
+ data: dict | None = None,
308
+ json_data: dict | None = None,
309
+ files: dict | None = None,
310
+ timeout: int | None = REQUEST_TIMEOUT,
311
+ show_error: bool = True,
312
+ show_warning: bool = False,
313
+ warning_message: str = "",
314
+ failure_message: str = "",
315
+ success_message: str = "",
316
+ max_retries: int = REQUEST_MAX_RETRIES,
317
+ retry_forever: bool = False,
318
+ parse_request_response: bool = True,
319
+ user_credentials: bool = False,
320
+ verify: bool = True,
321
+ ) -> dict | None:
322
+ """Call an OTDS REST API in a safe way
323
+
324
+ Args:
325
+ url (str): URL to send the request to.
326
+ method (str, optional): HTTP method (GET, POST, etc.). Defaults to "GET".
327
+ headers (dict | None, optional): Request Headers. Defaults to None.
328
+ data (dict | None, optional): Request payload. Defaults to None
329
+ files (dict | None, optional): Dictionary of {"name": file-tuple} for multipart encoding upload.
330
+ file-tuple can be a 2-tuple ("filename", fileobj) or a 3-tuple ("filename", fileobj, "content_type")
331
+ timeout (int | None, optional): Timeout for the request in seconds. Defaults to REQUEST_TIMEOUT.
332
+ show_error (bool, optional): Whether or not an error should be logged in case of a failed REST call.
333
+ If False, then only a warning is logged. Defaults to True.
334
+ warning_message (str, optional): Specific warning message. Defaults to "". If not given the error_message will be used.
335
+ failure_message (str, optional): Specific error message. Defaults to "".
336
+ success_message (str, optional): Specific success message. Defaults to "".
337
+ max_retries (int, optional): How many retries on Connection errors? Default is REQUEST_MAX_RETRIES.
338
+ retry_forever (bool, optional): Eventually wait forever - without timeout. Defaults to False.
339
+ parse_request_response (bool, optional): should the response.text be interpreted as json and loaded into a dictionary. True is the default.
340
+ user_credentials (bool, optional): defines if admin or user credentials are used for the REST API call. Default = False = admin credentials
341
+ verify (bool, optional): specify whether or not SSL certificates should be verified when making an HTTPS request. Default = True
342
+
343
+ Returns:
344
+ dict | None: Response of OTDS REST API or None in case of an error.
345
+ """
346
+
347
+ if headers is None:
348
+ logger.error("Missing request header. Cannot send request to Core Share!")
349
+ return None
350
+
351
+ # In case of an expired session we reauthenticate and
352
+ # try 1 more time. Session expiration should not happen
353
+ # twice in a row:
354
+ retries = 0
355
+
356
+ while True:
357
+ try:
358
+ response = requests.request(
359
+ method=method,
360
+ url=url,
361
+ data=data,
362
+ json=json_data,
363
+ files=files,
364
+ headers=headers,
365
+ timeout=timeout,
366
+ verify=verify,
367
+ )
368
+
369
+ if response.ok:
370
+ if success_message:
371
+ logger.info(success_message)
372
+ if parse_request_response:
373
+ return self.parse_request_response(response)
374
+ else:
375
+ return response
376
+ # Check if Session has expired - then re-authenticate and try once more
377
+ elif response.status_code == 401 and retries == 0:
378
+ if user_credentials:
379
+ logger.debug(
380
+ "User session has expired - try to re-authenticate..."
381
+ )
382
+ self.authenticate_user(revalidate=True)
383
+ # Make sure to not change the content type:
384
+ headers = self.request_header_user(
385
+ content_type=headers.get("Content-Type", None)
386
+ )
387
+ else:
388
+ logger.warning(
389
+ "Admin session has expired - try to re-authenticate..."
390
+ )
391
+ self.authenticate_admin(revalidate=True)
392
+ # Make sure to not change the content type:
393
+ headers = self.request_header_admin(
394
+ content_type=headers.get("Content-Type", None)
395
+ )
396
+ retries += 1
397
+ else:
398
+ # Handle plain HTML responses to not pollute the logs
399
+ content_type = response.headers.get("content-type", None)
400
+ if content_type == "text/html":
401
+ response_text = "HTML content (only printed in debug log)"
402
+ elif "image" in content_type:
403
+ response_text = "Image content (not printed)"
404
+ else:
405
+ response_text = response.text
406
+
407
+ if show_error:
408
+ logger.error(
409
+ "%s; status -> %s/%s; error -> %s",
410
+ failure_message,
411
+ response.status_code,
412
+ HTTPStatus(response.status_code).phrase,
413
+ response_text,
414
+ )
415
+ elif show_warning:
416
+ logger.warning(
417
+ "%s; status -> %s/%s; warning -> %s",
418
+ warning_message if warning_message else failure_message,
419
+ response.status_code,
420
+ HTTPStatus(response.status_code).phrase,
421
+ response_text,
422
+ )
423
+ if content_type == "text/html":
424
+ logger.debug(
425
+ "%s; status -> %s/%s; warning -> %s",
426
+ failure_message,
427
+ response.status_code,
428
+ HTTPStatus(response.status_code).phrase,
429
+ response.text,
430
+ )
431
+ return None
432
+ except requests.exceptions.Timeout:
433
+ if retries <= max_retries:
434
+ logger.warning(
435
+ "Request timed out. Retrying in %s seconds...",
436
+ str(REQUEST_RETRY_DELAY),
437
+ )
438
+ retries += 1
439
+ time.sleep(REQUEST_RETRY_DELAY) # Add a delay before retrying
440
+ else:
441
+ logger.error(
442
+ "%s; timeout error",
443
+ failure_message,
444
+ )
445
+ if retry_forever:
446
+ # If it fails after REQUEST_MAX_RETRIES retries we let it wait forever
447
+ logger.warning("Turn timeouts off and wait forever...")
448
+ timeout = None
449
+ else:
450
+ return None
451
+ except requests.exceptions.ConnectionError:
452
+ if retries <= max_retries:
453
+ logger.warning(
454
+ "Connection error. Retrying in %s seconds...",
455
+ str(REQUEST_RETRY_DELAY),
456
+ )
457
+ retries += 1
458
+ time.sleep(REQUEST_RETRY_DELAY) # Add a delay before retrying
459
+ else:
460
+ logger.error(
461
+ "%s; connection error",
462
+ failure_message,
463
+ )
464
+ if retry_forever:
465
+ # If it fails after REQUEST_MAX_RETRIES retries we let it wait forever
466
+ logger.warning("Turn timeouts off and wait forever...")
467
+ timeout = None
468
+ time.sleep(REQUEST_RETRY_DELAY) # Add a delay before retrying
469
+ else:
470
+ return None
471
+ # end try
472
+ logger.debug(
473
+ "Retrying REST API %s call -> %s... (retry = %s)",
474
+ method,
475
+ url,
476
+ str(retries),
477
+ )
478
+ # end while True
479
+
480
+ # end method definition
481
+
482
+ def parse_request_response(
483
+ self,
484
+ response_object: requests.Response,
485
+ additional_error_message: str = "",
486
+ show_error: bool = True,
487
+ ) -> dict | None:
488
+ """Converts the request response (JSon) to a Python dict in a safe way
489
+ that also handles exceptions. It first tries to load the response.text
490
+ via json.loads() that produces a dict output. Only if response.text is
491
+ not set or is empty it just converts the response_object to a dict using
492
+ the vars() built-in method.
493
+
494
+ Args:
495
+ response_object (object): this is reponse object delivered by the request call
496
+ additional_error_message (str, optional): use a more specific error message
497
+ in case of an error
498
+ show_error (bool): True: write an error to the log file
499
+ False: write a warning to the log file
500
+ Returns:
501
+ dict: response information or None in case of an error
502
+ """
503
+
504
+ if not response_object:
505
+ return None
506
+
507
+ try:
508
+ if response_object.text:
509
+ dict_object = json.loads(response_object.text)
510
+ else:
511
+ dict_object = vars(response_object)
512
+ except json.JSONDecodeError as exception:
513
+ if additional_error_message:
514
+ message = "Cannot decode response as JSon. {}; error -> {}".format(
515
+ additional_error_message, exception
516
+ )
517
+ else:
518
+ message = "Cannot decode response as JSon; error -> {}".format(
519
+ exception
520
+ )
521
+ if show_error:
522
+ logger.error(message)
523
+ else:
524
+ logger.warning(message)
525
+ return None
526
+ else:
527
+ return dict_object
528
+
529
+ # end method definition
530
+
531
+ def lookup_result_value(
532
+ self, response: dict, key: str, value: str, return_key: str
533
+ ) -> str | None:
534
+ """Lookup a property value based on a provided key / value pair in the
535
+ response properties of an Extended ECM REST API call.
536
+
537
+ Args:
538
+ response (dict): REST response from an OTCS REST Call
539
+ key (str): property name (key)
540
+ value (str): value to find in the item with the matching key
541
+ return_key (str): determines which value to return based on the name of the dict key
542
+ Returns:
543
+ str: value of the property with the key defined in "return_key"
544
+ or None if the lookup fails
545
+ """
546
+
547
+ if not response:
548
+ return None
549
+ if not "results" in response:
550
+ return None
551
+
552
+ results = response["results"]
553
+
554
+ if not results or not isinstance(results, list):
555
+ return None
556
+
557
+ for result in results:
558
+ if key in result and result[key] == value and return_key in result:
559
+ return result[return_key]
560
+ return None
561
+
562
+ # end method definition
563
+
564
+ def exist_result_item(
565
+ self, response: dict, key: str, value: str, results_marker: str = "results"
566
+ ) -> bool:
567
+ """Check existence of key / value pair in the response properties of a Core Share API call.
568
+
569
+ Args:
570
+ response (dict): REST response from a Core Share API call
571
+ key (str): property name (key)
572
+ value (str): value to find in the item with the matching key
573
+ Returns:
574
+ bool: True if the value was found, False otherwise
575
+ """
576
+
577
+ if not response:
578
+ return False
579
+
580
+ if results_marker in response:
581
+ results = response[results_marker]
582
+ if not results or not isinstance(results, list):
583
+ return False
584
+
585
+ for result in results:
586
+ if value == result[key]:
587
+ return True
588
+ else:
589
+ if not key in response:
590
+ return False
591
+ if value == response[key]:
592
+ return True
593
+
594
+ return False
595
+
596
+ # end method definition
597
+
598
+ def get_result_value(
599
+ self,
600
+ response: dict | list,
601
+ key: str,
602
+ index: int = 0,
603
+ ) -> str | None:
604
+ """Get value of a result property with a given key of a Core Share API call.
605
+
606
+ Args:
607
+ response (dict or list): REST response from a Core Share REST Call
608
+ key (str): property name (key)
609
+ index (int, optional): Index to use (1st element has index 0).
610
+ Defaults to 0.
611
+ Returns:
612
+ str: value for the key, None otherwise
613
+ """
614
+
615
+ if not response:
616
+ return None
617
+
618
+ # response is mostly a dictionary but in some cases also a list (e.g. add_group_member())
619
+ if isinstance(response, list):
620
+ if len(response) - 1 < index:
621
+ return None
622
+ if not key in response[index]:
623
+ return None
624
+ value = response[index][key]
625
+ return value
626
+
627
+ if isinstance(response, dict):
628
+ # Does response have a "results" substructure?
629
+ if "results" in response:
630
+ # we expect results to be a list!
631
+ values = response["results"]
632
+ if (
633
+ not values
634
+ or not isinstance(values, list)
635
+ or len(values) - 1 < index
636
+ ):
637
+ return None
638
+ if not key in values[index]:
639
+ return None
640
+ value = values[index][key]
641
+ else: # simple response as dictionary - try to find key in response directly:
642
+ if not key in response:
643
+ return None
644
+ value = response[key]
645
+
646
+ return value
647
+
648
+ return None
649
+
650
+ # end method definition
651
+
652
+ def authenticate_admin(
653
+ self,
654
+ revalidate: bool = False,
655
+ ) -> str | None:
656
+ """Authenticate at Core Share as Tenant Admin.
657
+
658
+ Args:
659
+ revalidate (bool, optional): determinse if a re-athentication is enforced
660
+ (e.g. if session has timed out with 401 error)
661
+ Returns:
662
+ str: Access token. Also stores access token in self._access_token. None in case of error
663
+ """
664
+
665
+ # Already authenticated and session still valid?
666
+ if self._access_token_admin and not revalidate:
667
+ logger.debug(
668
+ "Session still valid - return existing access token -> %s",
669
+ str(self._access_token_admin),
670
+ )
671
+ return self._access_token_admin
672
+
673
+ request_url = self.config()["authorizationUrlAdmin"]
674
+
675
+ request_header = REQUEST_LOGIN_HEADERS
676
+
677
+ logger.debug("Requesting Core Share Admin Access Token from -> %s", request_url)
678
+
679
+ response = None
680
+ self._access_token_admin = None
681
+
682
+ try:
683
+ response = requests.post(
684
+ request_url,
685
+ headers=request_header,
686
+ timeout=REQUEST_TIMEOUT,
687
+ )
688
+ except requests.exceptions.ConnectionError as exception:
689
+ logger.warning(
690
+ "Unable to connect to -> %s : %s",
691
+ request_url,
692
+ exception,
693
+ )
694
+ return None
695
+
696
+ if response.ok:
697
+ authenticate_dict = self.parse_request_response(response)
698
+ if not authenticate_dict:
699
+ return None
700
+ else:
701
+ cookies = response.cookies
702
+ if "AccessToken" in cookies:
703
+ access_token = cookies["AccessToken"]
704
+
705
+ # String manipulation to extract pure AccessToken
706
+ if access_token.startswith("s%3A"):
707
+ access_token = access_token[4:]
708
+ access_token = access_token.rsplit(".", 1)[0]
709
+
710
+ # Store authentication access_token:
711
+ self._access_token_admin = access_token
712
+ logger.debug(
713
+ "Tenant Admin Access Token -> %s", self._access_token_admin
714
+ )
715
+ else:
716
+ return None
717
+ else:
718
+ logger.error(
719
+ "Failed to request a Core Share Tenant Admin Access Token; error -> %s",
720
+ response.text,
721
+ )
722
+ return None
723
+
724
+ return self._access_token_admin
725
+
726
+ # end method definition
727
+
728
+ def authenticate_user(
729
+ self, revalidate: bool = False, grant_type: str = "password"
730
+ ) -> str | None:
731
+ """Authenticate at Core Share as Tenant Service User (TSU) with client ID and client secret.
732
+
733
+ Args:
734
+ revalidate (bool, optional): determinse if a re-athentication is enforced
735
+ (e.g. if session has timed out with 401 error)
736
+ grant_type (str, optional): Can either be "client_credentials" (default) or "password".
737
+ Returns:
738
+ str: Access token. Also stores access token in self._access_token. None in case of error
739
+ """
740
+
741
+ # Already authenticated and session still valid?
742
+ if self._access_token_user and not revalidate:
743
+ logger.debug(
744
+ "Session still valid - return existing access token -> %s",
745
+ str(self._access_token_user),
746
+ )
747
+ return self._access_token_user
748
+
749
+ if grant_type == "client_credentials":
750
+ request_url = self.config()["authorizationUrlCredentials"]
751
+ elif grant_type == "password":
752
+ request_url = self.config()["authorizationUrlPassword"]
753
+ else:
754
+ logger.error("Illegal grant type - authorization not possible!")
755
+ return None
756
+
757
+ request_header = REQUEST_LOGIN_HEADERS
758
+
759
+ logger.debug(
760
+ "Requesting Core Share Tenant Service User Access Token from -> %s",
761
+ request_url,
762
+ )
763
+
764
+ response = None
765
+ self._access_token_user = None
766
+
767
+ try:
768
+ response = requests.post(
769
+ request_url,
770
+ headers=request_header,
771
+ timeout=REQUEST_TIMEOUT,
772
+ )
773
+ except requests.exceptions.ConnectionError as exception:
774
+ logger.warning(
775
+ "Unable to connect to -> %s : %s",
776
+ request_url,
777
+ exception,
778
+ )
779
+ return None
780
+
781
+ if response.ok:
782
+ authenticate_dict = self.parse_request_response(response)
783
+ if not authenticate_dict:
784
+ return None
785
+ else:
786
+ # Store authentication access_token:
787
+ self._access_token_user = authenticate_dict["access_token"]
788
+ logger.debug(
789
+ "Tenant Service User Access Token -> %s", self._access_token_user
790
+ )
791
+ else:
792
+ logger.error(
793
+ "Failed to request a Core Share Tenant Service User Access Token; error -> %s",
794
+ response.text,
795
+ )
796
+ return None
797
+
798
+ return self._access_token_user
799
+
800
+ # end method definition
801
+
802
+ def get_groups(self, offset: int = 0, count: int = 25) -> dict | None:
803
+ """Get Core Share groups.
804
+
805
+ Args:
806
+ offset (int, optional): index of first group (for pagination). Defaults to 0.
807
+ count (int, optional): number of groups to return (page length). Defaults to 25.
808
+
809
+ Returns:
810
+ dict | None: Dictionary with the Core Share group data or None if the request fails.
811
+
812
+ Example response:
813
+ {
814
+ '_links': {
815
+ 'self': {'href': '/api/v1/groups?offset=undefined&count=25'},
816
+ 'next': {'href': '/api/v1/groups?offset=NaN&count=25'}
817
+ },
818
+ 'results': [
819
+ {
820
+ 'id': '2593534258421173790',
821
+ 'type': 'group',
822
+ 'tenantId': '2157293035593927996',
823
+ 'displayName': 'Innovate',
824
+ 'name': 'Innovate',
825
+ 'createdAt': '2024-05-01T09:29:36.370Z',
826
+ 'uri': '/api/v1/groups/2593534258421173790',
827
+ 'imageuri': '/img/app/group-default-lrg.png',
828
+ 'thumbnailUri': '/img/app/group-default-sm.png',
829
+ 'defaultImageUri': True,
830
+ 'description': 'Demo Company Top Level Group',
831
+ 'tenantName': 'terrarium'
832
+ }
833
+ ]
834
+ }
835
+ """
836
+
837
+ if not self._access_token_user:
838
+ self.authenticate_user()
839
+
840
+ request_header = self.request_header_user()
841
+ request_url = self.config()["groupsUrl"] + "?offset={}&count={}".format(
842
+ offset, count
843
+ )
844
+
845
+ logger.debug("Get Core Share groups; calling -> %s", request_url)
846
+
847
+ return self.do_request(
848
+ url=request_url,
849
+ method="GET",
850
+ headers=request_header,
851
+ timeout=REQUEST_TIMEOUT,
852
+ failure_message="Failed to get Core Share groups",
853
+ user_credentials=True,
854
+ )
855
+
856
+ # end method definition
857
+
858
+ def add_group(
859
+ self,
860
+ group_name: str,
861
+ description: str = "",
862
+ ) -> dict | None:
863
+ """Add a new Core Share group. This requires a Tenent Admin authorization.
864
+
865
+ Args:
866
+ group_name (str): Name of the new Core Share group
867
+ description (str): Description of the new Core Share group
868
+
869
+ Returns:
870
+ dict | None: Dictionary with the Core Share Group data or None if the request fails.
871
+
872
+ Example response:
873
+ {
874
+ "id": "2593534258421173790",
875
+ "state": "enabled",
876
+ "isEnabled": true,
877
+ "isDeleted": false,
878
+ "uri": "/api/v1/groups/2593534258421173790",
879
+ "description": "Demo Company Top Level Group",
880
+ "name": "Innovate",
881
+ "imageUri": "/img/icons/mimeIcons/mime_group32.svg",
882
+ "thumbnailUri": "/img/icons/mimeIcons/mime_group32.svg",
883
+ "defaultImageUri": true,
884
+ "memberCount": 0,
885
+ "createdAt": "2024-05-01T09:29:36.370Z",
886
+ "type": "group",
887
+ "isSync": false,
888
+ "tenantId": "2157293035593927996"
889
+ }
890
+ """
891
+
892
+ if not self._access_token_admin:
893
+ self.authenticate_admin()
894
+
895
+ request_header = self.request_header_admin()
896
+ request_url = self.config()["groupsUrl"]
897
+
898
+ payload = {"name": group_name, "description": description}
899
+
900
+ logger.debug(
901
+ "Adding Core Share group -> %s; calling -> %s", group_name, request_url
902
+ )
903
+
904
+ return self.do_request(
905
+ url=request_url,
906
+ method="POST",
907
+ headers=request_header,
908
+ data=json.dumps(payload),
909
+ timeout=REQUEST_TIMEOUT,
910
+ failure_message="Failed to add Core Share group -> '{}'".format(group_name),
911
+ user_credentials=False,
912
+ )
913
+
914
+ # end method definition
915
+
916
+ def get_group_members(self, group_id: str) -> dict | None:
917
+ """Get Core Share group members.
918
+
919
+ Args:
920
+ group_id (str): ID of the group to deliver the members for.
921
+
922
+ Returns:
923
+ dict | None: Dictionary with the Core Share group membership data or None if the request fails.
924
+
925
+ Example response:
926
+ {
927
+ 'groupMembers': [
928
+ {
929
+ 'id': '2422700172682204885',
930
+ 'type': 'user',
931
+ 'tenantId': '2157293035593927996',
932
+ 'firstName': 'Andy',
933
+ 'lastName': 'Wyatt',
934
+ 'displayName': 'Andy Wyatt',
935
+ 'title': 'Buyer',
936
+ 'company': 'terrarium',
937
+ 'email': 'awyatt@M365x41497014.onmicrosoft.com',
938
+ 'otSaaSUID': 'f5a6b58e-ad43-4e2d-a3e6-5c0fcd5cd4b1',
939
+ 'otSaaSPID': 'aa49f566-0874-41e9-9924-452852ebaf7a',
940
+ 'uri': '/api/v1/users/2422700172682204885',
941
+ 'imageuri': '/img/app/profile-default-lrg.png',
942
+ 'thumbnailUri': '/img/app/topbar-profile-default-sm.png',
943
+ 'defaultImageUri': True,
944
+ 'isSpecificGroupAdmin': False
945
+ }
946
+ ],
947
+ 'pending': [
948
+
949
+ ],
950
+ 'count': 0
951
+ }
952
+ """
953
+
954
+ if not self._access_token_admin:
955
+ self.authenticate_admin()
956
+
957
+ request_header = self.request_header_admin()
958
+ request_url = self.config()["groupsUrl"] + "/{}".format(group_id) + "/members"
959
+
960
+ logger.debug(
961
+ "Get members for Core Share group with ID -> %s; calling -> %s",
962
+ group_id,
963
+ request_url,
964
+ )
965
+
966
+ return self.do_request(
967
+ url=request_url,
968
+ method="GET",
969
+ headers=request_header,
970
+ timeout=REQUEST_TIMEOUT,
971
+ failure_message="Failed to get members of Core Share group -> '{}'".format(
972
+ group_id
973
+ ),
974
+ user_credentials=False,
975
+ )
976
+
977
+ # end method definition
978
+
979
+ def add_group_member(
980
+ self, group_id: str, user_id: str, is_group_admin: bool = False
981
+ ) -> list | None:
982
+ """Add a Core Share user to a Core Share group.
983
+
984
+ Args:
985
+ group_id (str): ID of the Core Share Group
986
+ user_id (str): ID of the Core Share User
987
+
988
+ Returns:
989
+ list | None: Dictionary with the Core Share group membership or None if the request fails.
990
+
991
+ Example Response ('errors' is only output if success = False):
992
+ [
993
+ {
994
+ 'member': 'alewis@qa.idea-te.eimdemo.com',
995
+ 'success': True,
996
+ 'user': {
997
+ 'id': '2595801699801110696',
998
+ 'email': 'alewis@qa.idea-te.eimdemo.com',
999
+ 'otSaaSUID': '41325224-bbcf-4238-82b4-a9283be74821',
1000
+ 'otSaaSPID': 'aa49f566-0874-41e9-9924-452852ebaf7a',
1001
+ 'uri': '/api/v1/users/2595801699801110696',
1002
+ 'tenantId': '2157293035593927996',
1003
+ 'title': 'Records Manager',
1004
+ 'company': 'Innovate',
1005
+ 'lastName': 'Lewis',
1006
+ 'firstName': 'Anne',
1007
+ 'displayName': 'Lewis Anne',
1008
+ 'type': 'user',
1009
+ 'imageUri': 'https://core.opentext.com/api/v1/users/2595801699801110696/photo?id=0fbedc509fdfa1d27bcb5b3615714988e5f8e24598f0fc74b776ff049faef1f2',
1010
+ 'thumbnailUri': 'https://core.opentext.com/api/v1/users/2595801699801110696/photo?s=small&id=0fbedc509fdfa1d27bcb5b3615714988e5f8e24598f0fc74b776ff049faef1f2',
1011
+ 'defaultImageUri': False,
1012
+ 'isConfirmed': True,
1013
+ 'isEnabled': True
1014
+ }
1015
+ 'errors': [
1016
+ {
1017
+ 'code': 'groupInvitationExists',
1018
+ 'message': 'The user has already been invited to the group'
1019
+ }
1020
+ ]
1021
+ }
1022
+ ]
1023
+ """
1024
+
1025
+ if not self._access_token_admin:
1026
+ self.authenticate_admin()
1027
+
1028
+ request_header = self.request_header_admin()
1029
+ request_url = self.config()["groupsUrl"] + "/{}".format(group_id) + "/members"
1030
+
1031
+ user = self.get_user_by_id(user_id=user_id)
1032
+ user_email = self.get_result_value(response=user, key="email")
1033
+
1034
+ payload = {"members": [user_email], "specificGroupRole": is_group_admin}
1035
+
1036
+ logger.debug(
1037
+ "Add Core Share user -> '%s' (%s) as %s to Core Share group with ID -> %s; calling -> %s",
1038
+ user_email,
1039
+ user_id,
1040
+ "group member" if not is_group_admin else "group admin",
1041
+ group_id,
1042
+ request_url,
1043
+ )
1044
+
1045
+ return self.do_request(
1046
+ url=request_url,
1047
+ method="POST",
1048
+ headers=request_header,
1049
+ json_data=payload,
1050
+ timeout=REQUEST_TIMEOUT,
1051
+ failure_message="Failed to add Core Share user -> '{}' to Core Share group with ID -> {}".format(
1052
+ user_email, group_id
1053
+ ),
1054
+ user_credentials=False,
1055
+ )
1056
+
1057
+ # end method definition
1058
+
1059
+ def remove_group_member(
1060
+ self, group_id: str, user_id: str, is_group_admin: bool = False
1061
+ ) -> list | None:
1062
+ """Remove a Core Share user from a Core Share group.
1063
+
1064
+ Args:
1065
+ group_id (str): ID of the Core Share Group
1066
+ user_id (str): ID of the Core Share User
1067
+
1068
+ Returns:
1069
+ list | None: Dictionary with the Core Share group membership or None if the request fails.
1070
+
1071
+ Example Response ('errors' is only output if success = False):
1072
+ [
1073
+ {
1074
+ 'member': 'alewis@qa.idea-te.eimdemo.com',
1075
+ 'success': True,
1076
+ 'errors': [
1077
+ {
1078
+ 'code': 'groupInvitationExists',
1079
+ 'message': 'The user has already been invited to the group'
1080
+ }
1081
+ ]
1082
+ }
1083
+ ]
1084
+ """
1085
+
1086
+ if not self._access_token_admin:
1087
+ self.authenticate_admin()
1088
+
1089
+ request_header = self.request_header_admin()
1090
+ request_url = self.config()["groupsUrl"] + "/{}".format(group_id) + "/members"
1091
+
1092
+ user = self.get_user_by_id(user_id=user_id)
1093
+ user_email = self.get_result_value(response=user, key="email")
1094
+
1095
+ payload = {"members": [user_email], "specificGroupRole": is_group_admin}
1096
+
1097
+ logger.debug(
1098
+ "Remove Core Share user -> '%s' (%s) as %s from Core Share group with ID -> %s; calling -> %s",
1099
+ user_email,
1100
+ user_id,
1101
+ "group member" if not is_group_admin else "group admin",
1102
+ group_id,
1103
+ request_url,
1104
+ )
1105
+
1106
+ return self.do_request(
1107
+ url=request_url,
1108
+ method="DELETE",
1109
+ headers=request_header,
1110
+ json_data=payload,
1111
+ timeout=REQUEST_TIMEOUT,
1112
+ failure_message="Failed to remove Core Share user -> '{}' ({}) from Core Share group with ID -> {}".format(
1113
+ user_email, user_id, group_id
1114
+ ),
1115
+ user_credentials=False,
1116
+ )
1117
+
1118
+ # end method definition
1119
+
1120
+ def get_group_by_id(self, group_id: str) -> dict | None:
1121
+ """Get a Core Share group by its ID.
1122
+
1123
+ Args:
1124
+ None
1125
+
1126
+ Returns:
1127
+ dict | None: Dictionary with the Core Share group data or None if the request fails.
1128
+
1129
+ Response example:
1130
+ """
1131
+
1132
+ if not self._access_token_admin:
1133
+ self.authenticate_admin()
1134
+
1135
+ request_header = self.request_header_admin()
1136
+ request_url = self.config()["groupsUrl"] + "/" + group_id
1137
+
1138
+ logger.debug(
1139
+ "Get Core Share group with ID -> %s; calling -> %s", group_id, request_url
1140
+ )
1141
+
1142
+ return self.do_request(
1143
+ url=request_url,
1144
+ method="GET",
1145
+ headers=request_header,
1146
+ timeout=REQUEST_TIMEOUT,
1147
+ failure_message="Failed to get Core Share group with ID -> {}".format(
1148
+ group_id
1149
+ ),
1150
+ user_credentials=False,
1151
+ )
1152
+
1153
+ # end method definition
1154
+
1155
+ def get_group_by_name(self, name: str) -> dict | None:
1156
+ """Get Core Share group by its name.
1157
+
1158
+ Args:
1159
+ name (str): Name of the group to search.
1160
+
1161
+ Returns:
1162
+ dict | None: Dictionary with the Core Share group data or None if the request fails.
1163
+
1164
+ Example result:
1165
+ {
1166
+ 'results': [
1167
+ {
1168
+ 'id': '2594934169968578199',
1169
+ 'type': 'group',
1170
+ 'tenantId': '2157293035593927996',
1171
+ 'displayName': 'Test Group',
1172
+ 'name': 'Test Group',
1173
+ 'createdAt': '2024-05-03T07:50:58.830Z',
1174
+ 'uri': '/api/v1/groups/2594934169968578199',
1175
+ 'imageuri': '/img/app/group-default-lrg.png',
1176
+ 'thumbnailUri': '/img/app/group-default-sm.png',
1177
+ 'defaultImageUri': True,
1178
+ 'description': '',
1179
+ 'tenantName': 'terrarium'
1180
+ }
1181
+ ],
1182
+ 'total': 1
1183
+ }
1184
+ """
1185
+
1186
+ groups = self.search_groups(
1187
+ query_string=name,
1188
+ )
1189
+
1190
+ return groups
1191
+
1192
+ # end method definition
1193
+
1194
+ def search_groups(self, query_string: str) -> dict | None:
1195
+ """Search Core Share group(s) by name.
1196
+
1197
+ Args:
1198
+ query_string(str): Query for the group name / property
1199
+
1200
+ Returns:
1201
+ dict | None: Dictionary with the Core Share user data or None if the request fails.
1202
+
1203
+ Example response:
1204
+ """
1205
+
1206
+ if not self._access_token_admin:
1207
+ self.authenticate_admin()
1208
+
1209
+ request_header = self.request_header_admin()
1210
+ request_url = self.config()["searchGroupUrl"] + "?q=" + query_string
1211
+
1212
+ logger.debug(
1213
+ "Search Core Share group by -> %s; calling -> %s", query_string, request_url
1214
+ )
1215
+
1216
+ return self.do_request(
1217
+ url=request_url,
1218
+ method="GET",
1219
+ headers=request_header,
1220
+ timeout=REQUEST_TIMEOUT,
1221
+ failure_message="Cannot find Core Share group with name / property -> {}".format(
1222
+ query_string
1223
+ ),
1224
+ user_credentials=False,
1225
+ )
1226
+
1227
+ # end method definition
1228
+
1229
+ def get_users(self) -> dict | None:
1230
+ """Get Core Share users.
1231
+
1232
+ Args:
1233
+ None
1234
+
1235
+ Returns:
1236
+ dict | None: Dictionary with the Core Share user data or None if the request fails.
1237
+
1238
+ Example response (it is a list!):
1239
+ [
1240
+ {
1241
+ 'id': '2400020228198108758',
1242
+ 'type': 'user',
1243
+ 'tenantId': '2157293035593927996',
1244
+ 'firstName': 'Technical Marketing',
1245
+ 'lastName': 'Service',
1246
+ 'displayName': 'Technical Marketing Service',
1247
+ 'title': 'Service User',
1248
+ 'company': 'terrarium',
1249
+ 'email': 'tm-service@opentext.com',
1250
+ 'otSaaSUID': 'fdb07113-4854-4f63-a208-55759ee925ce',
1251
+ 'otSaaSPID': 'aa49f566-0874-41e9-9924-452852ebaf7a',
1252
+ 'state': 'enabled',
1253
+ 'isEnabled': True,
1254
+ 'isConfirmed': True,
1255
+ 'quota': 2147483648,
1256
+ 'usage': 10400,
1257
+ 'uri': '/api/v1/users/2400020228198108758',
1258
+ 'imageuri': '/img/app/profile-default-lrg.png',
1259
+ 'thumbnailUri': '/img/app/topbar-profile-default-sm.png',
1260
+ 'defaultImageUri': True
1261
+ 'rootId': '2400020231108955735'
1262
+ 'userRoot' : {
1263
+ {
1264
+ 'size': 0,
1265
+ 'id': '2400020231108955735',
1266
+ 'resourceType': 1,
1267
+ 'name': 'Files',
1268
+ 'createdById': '2400020228198108758',
1269
+ 'created': '2023-08-08T09:31:46.654Z',
1270
+ 'lastModified': '2023-09-19T15:11:56.925Z',
1271
+ 'lastModifiedById': '2400020228198108758',
1272
+ 'currentVersionNumber': None,
1273
+ 'currentVersionId': None,
1274
+ 'childCount': '4',
1275
+ 'shareCount': 1,
1276
+ 'deleteCount': 0,
1277
+ 'trashState': 0,
1278
+ 'imageId': None,
1279
+ 'thumbnailId': None,
1280
+ 'tedsImageId': None,
1281
+ 'tedsThumbnailId': None,
1282
+ 'parentId': None,
1283
+ 'tagCount': 0,
1284
+ 'versionCommentCount': 0,
1285
+ 'draftCommentCount': 0,
1286
+ 'subTypeId': None,
1287
+ 'contentOriginId': None,
1288
+ 'externalData': None,
1289
+ 'tenantId': '2157293035593927996',
1290
+ 'nodeType': 1,
1291
+ 'likesCount': 0,
1292
+ 'commentCount': 0,
1293
+ 'createdAt': '2023-08-08T09:31:46.655Z',
1294
+ 'updatedAt': '2023-09-19T15:11:56.925Z'
1295
+ }
1296
+ }
1297
+ ...
1298
+ },
1299
+ ...
1300
+ ]
1301
+ """
1302
+
1303
+ if not self._access_token_admin:
1304
+ self.authenticate_admin()
1305
+
1306
+ request_header = self.request_header_admin()
1307
+ request_url = self.config()["usersUrlv1"]
1308
+
1309
+ logger.debug("Get Core Share users; calling -> %s", request_url)
1310
+
1311
+ return self.do_request(
1312
+ url=request_url,
1313
+ method="GET",
1314
+ headers=request_header,
1315
+ timeout=REQUEST_TIMEOUT,
1316
+ failure_message="Failed to get Core Share users",
1317
+ user_credentials=False,
1318
+ )
1319
+
1320
+ # end method definition
1321
+
1322
+ def get_user_by_id(self, user_id: str) -> dict | None:
1323
+ """Get a Core Share user by its ID.
1324
+
1325
+ Args:
1326
+ None
1327
+
1328
+ Returns:
1329
+ dict | None: Dictionary with the Core Share user data or None if the request fails.
1330
+
1331
+ Response example:
1332
+ {
1333
+ 'accessRoles': [],
1334
+ 'commentCount': 0,
1335
+ 'company': 'terrarium',
1336
+ 'createdAt': '2024-04-19T11:58:34.240Z',
1337
+ 'defaultImageUri': True,
1338
+ 'disabledAt': None,
1339
+ 'displayName': 'Sato Ken',
1340
+ 'email': 'ksato@idea-te.eimdemo.com',
1341
+ 'firstName': 'Ken',
1342
+ 'id': '2584911925942946703',
1343
+ 'otSaaSUID': '6cab5035-abbc-481c-b049-10b4efae7408',
1344
+ 'otSaaSPID': 'aa49f566-0874-41e9-9924-452852ebaf7a',
1345
+ 'imageUri': 'https://core.opentext.com/img/app/profile-default-lrg.png',
1346
+ 'invitedAt': '2024-04-19T11:58:36.307Z',
1347
+ 'isAdmin': False,
1348
+ 'isConfirmed': True,
1349
+ 'isEnabled': True,
1350
+ 'isSync': False,
1351
+ 'lastLoginDate': -1,
1352
+ 'lastName': 'Sato',
1353
+ 'likesCount': 0,
1354
+ 'rootId': '2584911935422073756',
1355
+ 'state': 'enabled',
1356
+ 'stateChanged': '2024-04-19T12:03:23.736Z',
1357
+ 'tenantId': '2157293035593927996',
1358
+ 'thumbnailUri': 'https://core.opentext.com/img/app/topbar-profile-default-sm.png',
1359
+ 'title': 'Real Estate Manager',
1360
+ 'type': 'user',
1361
+ 'updatedAt': '2024-04-19T12:03:23.731Z',
1362
+ 'uri': '/api/v1/users/2584911925942946703',
1363
+ 'userRoot': {
1364
+ 'size': 0,
1365
+ 'id': '2584911935422073756',
1366
+ 'resourceType': 1,
1367
+ 'name': 'Files',
1368
+ 'createdById': '2584911925942946703',
1369
+ 'created': '2024-04-19T11:58:35.370Z',
1370
+ 'lastModified': '2024-04-19T11:58:35.370Z',
1371
+ 'lastModifiedById': '2584911925942946703',
1372
+ 'currentVersionNumber': None,
1373
+ 'currentVersionId': None,
1374
+ 'childCount': '0',
1375
+ 'shareCount': 1,
1376
+ 'deleteCount': 0,
1377
+ 'trashState': 0,
1378
+ 'imageId': None,
1379
+ 'thumbnailId': None,
1380
+ 'tedsImageId': None,
1381
+ 'tedsThumbnailId': None,
1382
+ 'parentId': None,
1383
+ ...
1384
+ },
1385
+ 'hasRequestedDelete': False,
1386
+ 'defaultBaseUrl': 'https://core.opentext.com',
1387
+ 'quota': 10737418240,
1388
+ 'usage': 0
1389
+ }
1390
+ """
1391
+
1392
+ if not self._access_token_user:
1393
+ self.authenticate_user()
1394
+
1395
+ request_header = self.request_header_user()
1396
+ request_url = self.config()["usersUrlv1"] + "/" + user_id
1397
+
1398
+ logger.debug(
1399
+ "Get Core Share user with ID -> %s; calling -> %s", user_id, request_url
1400
+ )
1401
+
1402
+ return self.do_request(
1403
+ url=request_url,
1404
+ method="GET",
1405
+ headers=request_header,
1406
+ timeout=REQUEST_TIMEOUT,
1407
+ failure_message="Failed to get Core Share user with ID -> {}".format(
1408
+ user_id
1409
+ ),
1410
+ user_credentials=True,
1411
+ )
1412
+
1413
+ # end method definition
1414
+
1415
+ def get_user_by_name(
1416
+ self, first_name: str, last_name: str, user_status: str = "internal-native"
1417
+ ) -> dict | None:
1418
+ """Get Core Share user by its first and last name.
1419
+
1420
+ Args:
1421
+ first_name (str): First name of the users to search.
1422
+ last_name (str): Last name of the users to search.
1423
+ user_status (str, optional): type of users. Possible values:
1424
+ * internal-enabled
1425
+ * internal-pending
1426
+ * internal-locked
1427
+ * internal-native (non-SSO)
1428
+ * internal-sso
1429
+
1430
+ Returns:
1431
+ dict | None: Dictionary with the Core Share user data or None if the request fails.
1432
+ """
1433
+
1434
+ # Search the users with this first and last name (and hope this is unique ;-).
1435
+ users = self.search_users(
1436
+ query_string=first_name + " " + last_name,
1437
+ user_status=user_status,
1438
+ )
1439
+
1440
+ return users
1441
+
1442
+ # end method definition
1443
+
1444
+ def get_user_by_email(
1445
+ self, email: str, user_status: str = "internal-native"
1446
+ ) -> dict | None:
1447
+ """Get Core Share user by its email address.
1448
+
1449
+ Args:
1450
+ email (str): Email address of the users to search.
1451
+ user_status (str, optional): type of users. Possible values:
1452
+ * internal-enabled
1453
+ * internal-pending
1454
+ * internal-locked
1455
+ * internal-native (non-SSO)
1456
+ * internal-sso
1457
+
1458
+ Returns:
1459
+ dict | None: Dictionary with the Core Share user data or None if the request fails.
1460
+ """
1461
+
1462
+ # Search the users with this first and last name (and hope this is unique ;-).
1463
+ users = self.search_users(
1464
+ query_string=email,
1465
+ user_status=user_status,
1466
+ )
1467
+
1468
+ return users
1469
+
1470
+ # end method definition
1471
+
1472
+ def search_users(
1473
+ self,
1474
+ query_string: str,
1475
+ user_status: str = "internal-native",
1476
+ page_size: int = 100,
1477
+ ) -> dict | None:
1478
+ """Search Core Share user(s) by name / property. Needs to be a Tenant Administrator to do so.
1479
+
1480
+ Args:
1481
+ query_string (str): string to query the user(s)
1482
+ user_status (str, optional): type of users. Possible values:
1483
+ * internal-enabled
1484
+ * internal-pending
1485
+ * internal-locked
1486
+ * internal-native (non-SSO)
1487
+ * internal-sso
1488
+ page_size (int, optional): max number of results per page. We set the default to 100 (Web UI uses 25)
1489
+
1490
+ Returns:
1491
+ dict | None: Dictionary with the Core Share user data or None if the request fails.
1492
+
1493
+ Example response:
1494
+ {
1495
+ "results": [
1496
+ {
1497
+ "id": "2422698421996494632",
1498
+ "type": "user",
1499
+ "tenantId": "2157293035593927996",
1500
+ "firstName": "Andy",
1501
+ "lastName": "Wyatt",
1502
+ "displayName": "Andy Wyatt",
1503
+ "title": "Buyer",
1504
+ "company": "terrarium",
1505
+ "email": "awyatt@M365x46777101.onmicrosoft.com",
1506
+ "otSaaSUID": "0842d1e1-acfc-425b-994a-e2dcb4d333c6",
1507
+ "otSaaSPID": "aa49f566-0874-41e9-9924-452852ebaf7a",
1508
+ "state": "enabled",
1509
+ "isEnabled": true,
1510
+ "isConfirmed": true,
1511
+ "isAdmin": false,
1512
+ "accessRoles": [],
1513
+ "hasBeenDelegated": null,
1514
+ "createdAt": "2023-09-08T16:29:17.680Z",
1515
+ "lastLoginDate": "2023-10-05T16:14:16Z",
1516
+ "quota": 1073741824,
1517
+ "usage": 0,
1518
+ "rootId": "2422698425964306217",
1519
+ "uri": "/api/v1/users/2422698421996494632",
1520
+ "imageuri": "/img/app/profile-default-lrg.png",
1521
+ "thumbnailUri": "/img/app/topbar-profile-default-sm.png",
1522
+ "defaultImageUri": true
1523
+ },
1524
+ ...
1525
+ ]
1526
+ }
1527
+ """
1528
+
1529
+ if not self._access_token_admin:
1530
+ self.authenticate_admin()
1531
+
1532
+ request_header = self.request_header_admin()
1533
+ request_url = (
1534
+ self.config()["searchUserUrl"]
1535
+ + "/{}".format(user_status)
1536
+ + "?q="
1537
+ + query_string
1538
+ + "&pageSize="
1539
+ + str(page_size)
1540
+ )
1541
+
1542
+ logger.debug(
1543
+ "Search Core Share user by -> %s; calling -> %s", query_string, request_url
1544
+ )
1545
+
1546
+ return self.do_request(
1547
+ url=request_url,
1548
+ method="GET",
1549
+ headers=request_header,
1550
+ timeout=REQUEST_TIMEOUT,
1551
+ failure_message="Failed to search Core Share user with name / property -> {}".format(
1552
+ query_string
1553
+ ),
1554
+ user_credentials=False,
1555
+ )
1556
+
1557
+ # end method definition
1558
+
1559
+ def add_user(
1560
+ self,
1561
+ first_name: str,
1562
+ last_name: str,
1563
+ email: str,
1564
+ password: str | None = None,
1565
+ title: str | None = None,
1566
+ company: str | None = None,
1567
+ ) -> dict | None:
1568
+ """Add a new Core Share user. This requires a Tenent Admin authorization.
1569
+
1570
+ Args:
1571
+ first_name (str): First name of the new user
1572
+ last_name (str): Last name of the new user
1573
+ email (str): Email of the new Core Share user
1574
+ password (str | None, optional): Password of the new Core Share user
1575
+ title (str | None, optional): Title of the user
1576
+ company (str | None, optional): Name of the Company of the user
1577
+
1578
+ Returns:
1579
+ dict | None: Dictionary with the Core Share User data or None if the request fails.
1580
+
1581
+ Example response:
1582
+ {
1583
+ "accessRoles": [],
1584
+ "commentCount": 0,
1585
+ "company": "terrarium",
1586
+ "createdAt": "2024-05-01T09:43:22.962Z",
1587
+ "defaultImageUri": true,
1588
+ "disabledAt": null,
1589
+ "displayName": "Tester Theo",
1590
+ "email": "theo@tester.com",
1591
+ "firstName": "Theo",
1592
+ "id": "2593541192377435562",
1593
+ "otSaaSUID": "77043e17-105c-418f-b4ba-1bef9f15937c",
1594
+ "otSaaSPID": "aa49f566-0874-41e9-9924-452852ebaf7a",
1595
+ "imageUri": "https://core.opentext.com/img/app/profile-default-lrg.png",
1596
+ "invitedAt": "2024-05-01T09:43:23.658Z",
1597
+ "isAdmin": false,
1598
+ "isConfirmed": false,
1599
+ "isEnabled": true,
1600
+ "isSync": false,
1601
+ "lastLoginDate": -1,
1602
+ "lastName": "Tester",
1603
+ "likesCount": 0,
1604
+ "rootId": "2593541195170842028",
1605
+ "state": "pending",
1606
+ "stateChanged": "2024-05-01T09:43:22.959Z",
1607
+ "tenantId": "2157293035593927996",
1608
+ "thumbnailUri": "https://core.opentext.com/img/app/topbar-profile-default-sm.png",
1609
+ "title": "VP Product Management",
1610
+ "type": "user",
1611
+ "updatedAt": "2024-05-01T09:43:23.658Z",
1612
+ "uri": "/api/v1/users/2593541192377435562",
1613
+ "hasRequestedDelete": false,
1614
+ "defaultBaseUrl": "https://core.opentext.com",
1615
+ "quota": 10737418240,
1616
+ "usage": 0
1617
+ }
1618
+ """
1619
+
1620
+ if not self._access_token_admin:
1621
+ self.authenticate_admin()
1622
+
1623
+ # here we want the request to determine the content type automatically:
1624
+ request_header = self.request_header_admin(content_type="")
1625
+ request_url = self.config()["invitesUrl"]
1626
+
1627
+ payload = {
1628
+ "firstName": first_name,
1629
+ "lastName": last_name,
1630
+ "email": email,
1631
+ "quota": 10737418240,
1632
+ }
1633
+ if password:
1634
+ payload["password"] = password
1635
+ if title:
1636
+ payload["title"] = title
1637
+ if company:
1638
+ payload["company"] = company
1639
+
1640
+ logger.debug(
1641
+ "Adding Core Share user -> %s %s; calling -> %s",
1642
+ first_name,
1643
+ last_name,
1644
+ request_url,
1645
+ )
1646
+
1647
+ return self.do_request(
1648
+ url=request_url,
1649
+ method="POST",
1650
+ headers=request_header,
1651
+ json_data=payload,
1652
+ timeout=REQUEST_TIMEOUT,
1653
+ failure_message="Failed to add Core Share user -> '{} {}' ({})".format(
1654
+ first_name, last_name, email
1655
+ ),
1656
+ user_credentials=False,
1657
+ )
1658
+
1659
+ # end method definition
1660
+
1661
+ def resend_user_invite(self, user_id: str) -> dict:
1662
+ """Resend the invite for a Core Share user.
1663
+
1664
+ Args:
1665
+ user_id (str): The Core Share user ID.
1666
+
1667
+ Returns:
1668
+ dict: Response from the Core Share API.
1669
+ """
1670
+
1671
+ if not self._access_token_admin:
1672
+ self.authenticate_admin()
1673
+
1674
+ request_header = self.request_header_admin()
1675
+
1676
+ request_url = self.config()["usersUrlv1"] + "/{}".format(user_id)
1677
+
1678
+ logger.debug(
1679
+ "Resend invite for Core Share user with ID -> %s; calling -> %s",
1680
+ user_id,
1681
+ request_url,
1682
+ )
1683
+
1684
+ update_data = {"resend": True}
1685
+
1686
+ return self.do_request(
1687
+ url=request_url,
1688
+ method="PUT",
1689
+ headers=request_header,
1690
+ json_data=update_data,
1691
+ timeout=REQUEST_TIMEOUT,
1692
+ failure_message="Failed to resend invite for Core Share user with ID -> {}".format(
1693
+ user_id
1694
+ ),
1695
+ user_credentials=False,
1696
+ )
1697
+
1698
+ # end method definition
1699
+
1700
+ def update_user(self, user_id: str, update_data: dict) -> dict:
1701
+ """Update a Core Share user.
1702
+
1703
+ Args:
1704
+ user_id (str): ID of the Core Share user.
1705
+
1706
+ Returns:
1707
+ dict: Response or None if the request has failed.
1708
+ """
1709
+
1710
+ if not self._access_token_admin:
1711
+ self.authenticate_admin()
1712
+
1713
+ request_header = self.request_header_admin()
1714
+
1715
+ request_url = self.config()["usersUrlv1"] + "/{}".format(user_id)
1716
+
1717
+ logger.debug(
1718
+ "Update data of Core Share user with ID -> %s; calling -> %s",
1719
+ user_id,
1720
+ request_url,
1721
+ )
1722
+
1723
+ if "email" in update_data and not "password" in update_data:
1724
+ logger.warning(
1725
+ "Trying to update the email without providing the password. This is likely to fail..."
1726
+ )
1727
+
1728
+ return self.do_request(
1729
+ url=request_url,
1730
+ method="PUT",
1731
+ headers=request_header,
1732
+ json_data=update_data,
1733
+ timeout=REQUEST_TIMEOUT,
1734
+ failure_message="Failed to update Core Share user with ID -> {}".format(
1735
+ user_id
1736
+ ),
1737
+ user_credentials=False,
1738
+ )
1739
+
1740
+ # end method definition
1741
+
1742
+ def add_user_access_role(self, user_id: str, role_id: int) -> dict:
1743
+ """Add an access role to a Core Share user.
1744
+
1745
+ Args:
1746
+ user_id (str): The Core Share user ID.
1747
+ role_id (int): The role ID:
1748
+ * Content Manager = 5
1749
+ * Group Admin = 3
1750
+
1751
+ Returns:
1752
+ dict: Response from the Core Share API.
1753
+ """
1754
+
1755
+ if not self._access_token_admin:
1756
+ self.authenticate_admin()
1757
+
1758
+ request_header = self.request_header_admin()
1759
+
1760
+ request_url = (
1761
+ self.config()["usersUrlv1"]
1762
+ + "/{}".format(user_id)
1763
+ + "/roles/"
1764
+ + str(role_id)
1765
+ )
1766
+
1767
+ logger.debug(
1768
+ "Add access role -> %s to Core Share user with ID -> %s; calling -> %s",
1769
+ str(role_id),
1770
+ user_id,
1771
+ request_url,
1772
+ )
1773
+
1774
+ return self.do_request(
1775
+ url=request_url,
1776
+ method="PUT",
1777
+ headers=request_header,
1778
+ timeout=REQUEST_TIMEOUT,
1779
+ failure_message="Failed to add access role with ID -> {} to Core Share user with ID -> {}".format(
1780
+ role_id, user_id
1781
+ ),
1782
+ user_credentials=False,
1783
+ )
1784
+
1785
+ # end method definition
1786
+
1787
+ def remove_user_access_role(self, user_id: str, role_id: int) -> dict:
1788
+ """Remove an access role from a Core Share user.
1789
+
1790
+ Args:
1791
+ user_id (str): The Core Share user ID.
1792
+ role_id (int): The role ID:
1793
+ * Content Manager = 5
1794
+ * Group Admin = 3
1795
+
1796
+ Returns:
1797
+ dict: Response from the Core Share API.
1798
+ """
1799
+
1800
+ if not self._access_token_admin:
1801
+ self.authenticate_admin()
1802
+
1803
+ request_header = self.request_header_admin()
1804
+
1805
+ request_url = (
1806
+ self.config()["usersUrlv1"]
1807
+ + "/{}".format(user_id)
1808
+ + "/roles/"
1809
+ + str(role_id)
1810
+ )
1811
+
1812
+ logger.debug(
1813
+ "Remove access role with ID -> %s from Core Share user with ID -> %s; calling -> %s",
1814
+ str(role_id),
1815
+ user_id,
1816
+ request_url,
1817
+ )
1818
+
1819
+ return self.do_request(
1820
+ url=request_url,
1821
+ method="DELETE",
1822
+ headers=request_header,
1823
+ timeout=REQUEST_TIMEOUT,
1824
+ failure_message="Failed to remove access role with ID -> {} from Core Share user with ID -> {}".format(
1825
+ role_id, user_id
1826
+ ),
1827
+ user_credentials=False,
1828
+ )
1829
+
1830
+ # end method definition
1831
+
1832
+ def update_user_access_roles(
1833
+ self,
1834
+ user_id: str,
1835
+ is_admin: bool | None = None,
1836
+ is_content_manager: bool | None = None,
1837
+ is_group_admin: bool | None = None,
1838
+ ) -> dict:
1839
+ """Define the access roles of a Core Share user.
1840
+
1841
+ Args:
1842
+ user_id (str): ID of the Core Share user
1843
+ is_content_manager (bool | None, optional): Assign Content Manager Role if True.
1844
+ Removes Content Manager Role if False.
1845
+ Does nothing if None.
1846
+ Defaults to None.
1847
+ is_group_admin (bool | None, optional): Assign Group Admin Role if True.
1848
+ Removes Group Admin Role if False.
1849
+ Does nothing if None.
1850
+ Defaults to None.
1851
+ is_admin (bool | None, optional): Makes user Admin if True.
1852
+ Removes Admin rights if False.
1853
+ Does nothing if None.
1854
+ Defaults to None.
1855
+
1856
+ Returns:
1857
+ dict: Response from the Core Share API.
1858
+ """
1859
+
1860
+ CONTENT_MANAGER_ROLE_ID = 5
1861
+ GROUP_ADMIN_ROLE_ID = 3
1862
+
1863
+ response = None
1864
+
1865
+ # Admins don't have/need specific access roles. They are controled by isAdmin flag.
1866
+ if is_admin is not None:
1867
+ update_data = {}
1868
+ update_data["isAdmin"] = is_admin
1869
+ response = self.update_user(user_id=user_id, update_data=update_data)
1870
+
1871
+ # Only for non-admins the other two roles are usable:
1872
+ if is_content_manager is not None:
1873
+ if is_content_manager:
1874
+ response = self.add_user_access_role(
1875
+ user_id=user_id, role_id=CONTENT_MANAGER_ROLE_ID
1876
+ )
1877
+ else:
1878
+ response = self.remove_user_access_role(
1879
+ user_id=user_id, role_id=CONTENT_MANAGER_ROLE_ID
1880
+ )
1881
+
1882
+ if is_group_admin is not None:
1883
+ if is_group_admin:
1884
+ response = self.add_user_access_role(
1885
+ user_id=user_id, role_id=GROUP_ADMIN_ROLE_ID
1886
+ )
1887
+ else:
1888
+ response = self.remove_user_access_role(
1889
+ user_id=user_id, role_id=GROUP_ADMIN_ROLE_ID
1890
+ )
1891
+
1892
+ return response
1893
+
1894
+ # end method definition
1895
+
1896
+ def update_user_password(
1897
+ self, user_id: str, password: str, new_password: str
1898
+ ) -> dict:
1899
+ """Update the password of a Core Share user.
1900
+
1901
+ Args:
1902
+ user_id (str): The Core Share user ID.
1903
+ password (str): Old user password.
1904
+ new_password (str): New user password.
1905
+
1906
+ Returns:
1907
+ dict: Response from the Core Share API.
1908
+ """
1909
+
1910
+ if not self._access_token_admin:
1911
+ self.authenticate_admin()
1912
+
1913
+ request_header = self.request_header_admin()
1914
+
1915
+ request_url = self.config()["usersUrlv1"] + "/{}".format(user_id)
1916
+
1917
+ logger.debug(
1918
+ "Update password of Core Share user with ID -> %s; calling -> %s",
1919
+ user_id,
1920
+ request_url,
1921
+ )
1922
+
1923
+ update_data = {"password": password, "newpassword": new_password}
1924
+
1925
+ return self.do_request(
1926
+ url=request_url,
1927
+ method="PUT",
1928
+ headers=request_header,
1929
+ json_data=update_data,
1930
+ timeout=REQUEST_TIMEOUT,
1931
+ failure_message="Failed to update password of Core Share user with ID -> {}".format(
1932
+ user_id
1933
+ ),
1934
+ user_credentials=False,
1935
+ )
1936
+
1937
+ # end method definition
1938
+
1939
+ def update_user_photo(
1940
+ self, user_id: str, photo_path: str, mime_type: str = "image/jpeg"
1941
+ ) -> dict | None:
1942
+ """Update the Core Share user photo.
1943
+
1944
+ Args:
1945
+ user_id (str): Core Share ID of the user
1946
+ photo_path (str): file system path with the location of the photo
1947
+ Returns:
1948
+ dict | None: Dictionary with the Core Share User data or None if the request fails.
1949
+ """
1950
+
1951
+ if not self._access_token_user:
1952
+ self.authenticate_user()
1953
+
1954
+ # Check if the photo file exists
1955
+ if not os.path.isfile(photo_path):
1956
+ logger.error("Photo file -> %s not found!", photo_path)
1957
+ return None
1958
+
1959
+ try:
1960
+ # Read the photo file as binary data
1961
+ with open(photo_path, "rb") as image_file:
1962
+ photo_data = image_file.read()
1963
+ except OSError as exception:
1964
+ # Handle any errors that occurred while reading the photo file
1965
+ logger.error(
1966
+ "Error reading photo file -> %s; error -> %s", photo_path, exception
1967
+ )
1968
+ return None
1969
+
1970
+ request_url = self.config()["usersUrlv3"] + "/{}".format(user_id) + "/photo"
1971
+ files = {
1972
+ "file": (photo_path, photo_data, mime_type),
1973
+ }
1974
+
1975
+ logger.debug(
1976
+ "Update profile photo of Core Share user with ID -> %s; calling -> %s",
1977
+ user_id,
1978
+ request_url,
1979
+ )
1980
+
1981
+ return self.do_request(
1982
+ url=request_url,
1983
+ method="POST",
1984
+ headers=self.request_header_user(content_type=""),
1985
+ files=files,
1986
+ timeout=REQUEST_TIMEOUT,
1987
+ failure_message="Failed to update profile photo of Core Share user with ID -> {}".format(
1988
+ user_id
1989
+ ),
1990
+ user_credentials=True,
1991
+ verify=False,
1992
+ )
1993
+
1994
+ # end method definition
1995
+
1996
+ def get_folders(self, parent_id: str) -> list | None:
1997
+ """Get Core Share folders under a given parent ID. This runs under user credentials (not admin!)
1998
+
1999
+ Args:
2000
+ parent_id (str): ID of the parent folder or the rootID of a user
2001
+
2002
+ Returns:
2003
+ list | None: List with the Core Share folders data or None if the request fails.
2004
+
2005
+ Example response (it is a list!):
2006
+ [
2007
+ {
2008
+ 'id': '2599466250228733940',
2009
+ 'name': 'Global Trade AG (50031)',
2010
+ 'size': 0,
2011
+ 'created': '2024-05-09T13:55:24.899Z',
2012
+ 'lastModified': '2024-05-09T13:55:33.069Z',
2013
+ 'shareCount': 2,
2014
+ 'isShared': True,
2015
+ 'parentId': '2599466244163770353',
2016
+ 'uri': '/api/v1/folders/2599466250228733940',
2017
+ 'commentCount': 0,
2018
+ 'isDeleted': False,
2019
+ 'isLiked': False,
2020
+ 'likesCount': 0,
2021
+ 'locks': [],
2022
+ 'createdBy': {
2023
+ 'id': '2597156105373095597',
2024
+ 'email': '6ccf1cb3-177e-4930-8baf-2d421cf92a5f',
2025
+ 'uri': '/api/v1/users/2597156105373095597',
2026
+ 'tenantId': '2595192600759637225',
2027
+ 'tier': 'tier3',
2028
+ 'title': '',
2029
+ 'company': '',
2030
+ 'lastName': '',
2031
+ 'firstName': 'OpenText Service User',
2032
+ 'displayName': 'OpenText Service User',
2033
+ 'type': 'user',
2034
+ 'imageUri': 'https://core.opentext.com/img/app/profile-default-lrg.png',
2035
+ 'thumbnailUri': 'https://core.opentext.com/img/app/topbar-profile-default-sm.png',
2036
+ 'defaultImageUri': True,
2037
+ 'isConfirmed': True,
2038
+ 'isEnabled': True
2039
+ },
2040
+ 'lastModifiedBy': {...},
2041
+ 'owner': {...},
2042
+ 'permission': 1,
2043
+ 'hasAttachments': False,
2044
+ 'resourceType': 'folder',
2045
+ 'tagCount': 0,
2046
+ 'resourceSubType': {},
2047
+ 'contentOriginId': '0D949C67-473D-448C-8F4B-B2CCA769F586',
2048
+ 'externalData': None,
2049
+ 'childCount': 7,
2050
+ 'contentOriginator': {
2051
+ 'id': '0D949C67-473D-448C-8F4B-B2CCA769F586',
2052
+ 'name': 'IDEA-TE-QA',
2053
+ 'imageUri': '/api/v1/tenants/2595192600759637225/contentOriginator/images/0D949C67-473D-448C-8F4B-B2CCA769F586'
2054
+ }
2055
+ }
2056
+ ]
2057
+ """
2058
+
2059
+ if not self._access_token_user:
2060
+ self.authenticate_user()
2061
+
2062
+ request_header = self.request_header_user()
2063
+ request_url = (
2064
+ self.config()["foldersUrlv1"]
2065
+ + "/{}".format(parent_id)
2066
+ + "/children"
2067
+ + "?limit=25&order=lastModified:desc&filter=any"
2068
+ )
2069
+
2070
+ logger.debug(
2071
+ "Get Core Share folders under parent -> %s; calling -> %s",
2072
+ parent_id,
2073
+ request_url,
2074
+ )
2075
+
2076
+ return self.do_request(
2077
+ url=request_url,
2078
+ method="GET",
2079
+ headers=request_header,
2080
+ timeout=REQUEST_TIMEOUT,
2081
+ failure_message="Failed to get Core Share folders under parent -> {}".format(
2082
+ parent_id
2083
+ ),
2084
+ user_credentials=True,
2085
+ )
2086
+
2087
+ # end method definition
2088
+
2089
+ def unshare_folder(self, resource_id: str) -> dict | None:
2090
+ """Unshare Core Share folder with a given resource ID.
2091
+
2092
+ Args:
2093
+ resource_id (str): ID of the folder (resource) to unshare with all collaborators
2094
+
2095
+ Returns:
2096
+ dict | None: Dictionary with the Core Share folders data or None if the request fails.
2097
+
2098
+ Example response (it is a list!):
2099
+ """
2100
+
2101
+ if not self._access_token_user:
2102
+ self.authenticate_user()
2103
+
2104
+ request_header = self.request_header_user()
2105
+ request_url = (
2106
+ self.config()["foldersUrlv1"] + "/{}".format(resource_id) + "/collaborators"
2107
+ )
2108
+
2109
+ logger.debug(
2110
+ "Unshare Core Share folder -> %s; calling -> %s",
2111
+ resource_id,
2112
+ request_url,
2113
+ )
2114
+
2115
+ return self.do_request(
2116
+ url=request_url,
2117
+ method="DELETE",
2118
+ headers=request_header,
2119
+ timeout=REQUEST_TIMEOUT,
2120
+ failure_message="Failed to unshare Core Share folder with ID -> {}".format(
2121
+ resource_id
2122
+ ),
2123
+ user_credentials=True,
2124
+ )
2125
+
2126
+ # end method definition
2127
+
2128
+ def delete_folder(self, resource_id: str) -> dict | None:
2129
+ """Delete Core Share folder with a given resource ID.
2130
+
2131
+ Args:
2132
+ resource_id (str): ID of the folder (resource) to delete
2133
+
2134
+ Returns:
2135
+ dict | None: Dictionary with the Core Share request data or None if the request fails.
2136
+
2137
+ Example response (it is a list!):
2138
+ """
2139
+
2140
+ if not self._access_token_user:
2141
+ self.authenticate_user()
2142
+
2143
+ request_header = self.request_header_user()
2144
+ request_url = self.config()["foldersUrlv1"] + "/{}".format(resource_id)
2145
+
2146
+ payload = {"state": "deleted"}
2147
+
2148
+ logger.debug(
2149
+ "Delete Core Share folder -> %s; calling -> %s",
2150
+ resource_id,
2151
+ request_url,
2152
+ )
2153
+
2154
+ return self.do_request(
2155
+ url=request_url,
2156
+ method="PUT",
2157
+ headers=request_header,
2158
+ data=json.dumps(payload),
2159
+ timeout=REQUEST_TIMEOUT,
2160
+ failure_message="Failed to delete Core Share folder -> {}".format(
2161
+ resource_id
2162
+ ),
2163
+ user_credentials=True,
2164
+ )
2165
+
2166
+ # end method definition
2167
+
2168
+ def delete_document(self, resource_id: str) -> dict | None:
2169
+ """Delete Core Share document with a given resource ID.
2170
+
2171
+ Args:
2172
+ resource_id (str): ID of the document (resource) to delete
2173
+
2174
+ Returns:
2175
+ dict | None: Dictionary with the Core Share request data or None if the request fails.
2176
+
2177
+ Example response (it is a list!):
2178
+ """
2179
+
2180
+ if not self._access_token_user:
2181
+ self.authenticate_user()
2182
+
2183
+ request_header = self.request_header_user()
2184
+ request_url = self.config()["documentsUrlv1"] + "/{}".format(resource_id)
2185
+
2186
+ payload = {"state": "deleted"}
2187
+
2188
+ logger.debug(
2189
+ "Delete Core Share document -> %s; calling -> %s",
2190
+ resource_id,
2191
+ request_url,
2192
+ )
2193
+
2194
+ return self.do_request(
2195
+ url=request_url,
2196
+ method="PUT",
2197
+ headers=request_header,
2198
+ data=json.dumps(payload),
2199
+ timeout=REQUEST_TIMEOUT,
2200
+ failure_message="Failed to delete Core Share document -> {}".format(
2201
+ resource_id
2202
+ ),
2203
+ user_credentials=True,
2204
+ )
2205
+
2206
+ # end method definition
2207
+
2208
+ def leave_share(self, user_id: str, resource_id: str) -> dict | None:
2209
+ """Remove a Core Share user from a share (i.e. the user leaves the share)
2210
+
2211
+ Args:
2212
+ user_id (str): Core Share ID of the user.
2213
+ resource_id (str): Core Share ID of the shared folder.
2214
+
2215
+ Returns:
2216
+ dict | None: Reponse of the REST call or None in case of an error.
2217
+ """
2218
+
2219
+ if not self._access_token_user:
2220
+ self.authenticate_user()
2221
+
2222
+ request_header = self.request_header_user()
2223
+
2224
+ request_url = (
2225
+ self.config()["foldersUrlv1"]
2226
+ + "/{}".format(resource_id)
2227
+ + "/collaborators/"
2228
+ + str(user_id)
2229
+ )
2230
+
2231
+ payload = {"action": "LEAVE_SHARE"}
2232
+
2233
+ logger.debug(
2234
+ "User with ID -> %s leaves Core Share shared folder with ID -> %s; calling -> %s",
2235
+ user_id,
2236
+ resource_id,
2237
+ request_url,
2238
+ )
2239
+
2240
+ return self.do_request(
2241
+ url=request_url,
2242
+ method="DELETE",
2243
+ headers=request_header,
2244
+ data=json.dumps(payload),
2245
+ timeout=REQUEST_TIMEOUT,
2246
+ failure_message="User with ID -> {} failed to leave Core Share folder with ID -> {}".format(
2247
+ user_id, resource_id
2248
+ ),
2249
+ user_credentials=True,
2250
+ )
2251
+
2252
+ # end method definition
2253
+
2254
+ def stop_share(self, user_id: str, resource_id: str) -> dict | None:
2255
+ """Stop of share of a user.
2256
+
2257
+ Args:
2258
+ user_id (str): Core Share ID of the user.
2259
+ resource_id (str): Core Share ID of the shared folder.
2260
+
2261
+ Returns:
2262
+ dict | None: Response of the REST call or None in case of an error.
2263
+ """
2264
+
2265
+ if not self._access_token_user:
2266
+ self.authenticate_user()
2267
+
2268
+ request_header = self.request_header_user()
2269
+
2270
+ request_url = (
2271
+ self.config()["foldersUrlv1"] + "/{}".format(resource_id) + "/collaborators"
2272
+ )
2273
+
2274
+ logger.debug(
2275
+ "User -> %s stops sharing Core Share shared folder -> %s; calling -> %s",
2276
+ user_id,
2277
+ resource_id,
2278
+ request_url,
2279
+ )
2280
+
2281
+ return self.do_request(
2282
+ url=request_url,
2283
+ method="DELETE",
2284
+ headers=request_header,
2285
+ timeout=REQUEST_TIMEOUT,
2286
+ failure_message="User with ID -> {} failed to stop sharing Core Share folder with ID -> {}".format(
2287
+ user_id, resource_id
2288
+ ),
2289
+ user_credentials=True,
2290
+ )
2291
+
2292
+ # end method definition
2293
+
2294
+ def cleanup_user_files(
2295
+ self, user_id: str, user_login: str, user_password: str
2296
+ ) -> bool:
2297
+ """Cleanup files of a user. This handles different types of resources.
2298
+ * Local resources - not shared
2299
+ * Resources shared by the user
2300
+ * Resources shared by other users or groups
2301
+ This method inpersonate as the user. Only the user can delete its folders.
2302
+ The Core Share admin is not entitled to do this.
2303
+
2304
+ Args:
2305
+ user_id (str): Core Share ID of the user
2306
+ user_login (str): Core Share email (= login) of the user
2307
+ user_password (str): Core Share password of the user
2308
+
2309
+ Returns:
2310
+ bool: True = success, False in case of an error.
2311
+ """
2312
+
2313
+ user = self.get_user_by_id(user_id=user_id)
2314
+ user_id = self.get_result_value(user, "id")
2315
+ user_root_folder_id = self.get_result_value(user, "rootId")
2316
+
2317
+ is_confirmed = self.get_result_value(response=user, key="isConfirmed")
2318
+ if not is_confirmed:
2319
+ logger.info(
2320
+ "User -> %s is not yet confirmed - so it cannot have files to cleanup.",
2321
+ user_id,
2322
+ )
2323
+ return True
2324
+
2325
+ logger.info("Inpersonate as user -> %s to cleanup files...", user_login)
2326
+
2327
+ # Save admin credentials the class has been initialized with:
2328
+ admin_credentials = self.credentials()
2329
+
2330
+ # Change the credentials to the user owning the file - admin
2331
+ # is not allowed to see user files!
2332
+ self.set_credentials(username=user_login, password=user_password)
2333
+
2334
+ # Authenticate as given user:
2335
+ self.authenticate_user(revalidate=True)
2336
+
2337
+ success = True
2338
+
2339
+ # Get all folders of the user:
2340
+ response = self.get_folders(parent_id=user_root_folder_id)
2341
+ if not response or not response["results"]:
2342
+ logger.info("User -> %s has no items to cleanup!", user_id)
2343
+ else:
2344
+ items = response["results"]
2345
+ for item in items:
2346
+ if item["isShared"]:
2347
+ if item["owner"]["id"] == user_id:
2348
+ logger.info(
2349
+ "User -> %s stops sharing item -> %s (%s)...",
2350
+ user_id,
2351
+ item["name"],
2352
+ item["id"],
2353
+ )
2354
+ response = self.stop_share(
2355
+ user_id=user_id, resource_id=item["id"]
2356
+ )
2357
+ if not response:
2358
+ success = False
2359
+ logger.info(
2360
+ "User -> %s deletes unshared item -> %s (%s)...",
2361
+ user_id,
2362
+ item["name"],
2363
+ item["id"],
2364
+ )
2365
+ response = self.delete_folder(item["id"])
2366
+ if not response:
2367
+ success = False
2368
+ else:
2369
+ logger.info(
2370
+ "User -> %s leaves shared folder -> '%s' (%s)...",
2371
+ user_id,
2372
+ item["name"],
2373
+ item["id"],
2374
+ )
2375
+ response = self.leave_share(
2376
+ user_id=user_id, resource_id=item["id"]
2377
+ )
2378
+ if not response:
2379
+ success = False
2380
+ else:
2381
+ logger.info(
2382
+ "User -> %s deletes local item -> '%s' (%s) of type -> '%s'...",
2383
+ user_id,
2384
+ item["name"],
2385
+ item["id"],
2386
+ item["resourceType"],
2387
+ )
2388
+ if item["resourceType"] == "folder":
2389
+ response = self.delete_folder(item["id"])
2390
+ elif item["resourceType"] == "document":
2391
+ response = self.delete_document(item["id"])
2392
+ else:
2393
+ logger.error(
2394
+ "Unsupport resource type -> '%s'", item["resourceType"]
2395
+ )
2396
+ response = None
2397
+ if not response:
2398
+ success = False
2399
+
2400
+ logger.info(
2401
+ "End inpersonation and switch back to admin account -> %s...",
2402
+ admin_credentials["username"],
2403
+ )
2404
+
2405
+ # Reset credentials to admin:
2406
+ self.set_credentials(
2407
+ admin_credentials["username"], admin_credentials["password"]
2408
+ )
2409
+ # Authenticate as administrator the class has been initialized with:
2410
+ self.authenticate_user(revalidate=True)
2411
+
2412
+ return success
2413
+
2414
+ # end method definition
2415
+
2416
+ def get_group_shares(self, group_id: str) -> dict | None:
2417
+ """Get (incoming) shares of a Core Share group.
2418
+
2419
+ Args:
2420
+ group_id (str): Core Share ID of a group
2421
+
2422
+ Returns:
2423
+ dict | None: Incoming shares or None if the request fails.
2424
+ """
2425
+
2426
+ if not self._access_token_admin:
2427
+ self.authenticate_admin()
2428
+
2429
+ request_header = self.request_header_admin()
2430
+
2431
+ request_url = (
2432
+ self.config()["groupsUrl"] + "/{}".format(group_id) + "/shares/incoming"
2433
+ )
2434
+
2435
+ logger.debug(
2436
+ "Get shares of Core Share group -> %s; calling -> %s",
2437
+ group_id,
2438
+ request_url,
2439
+ )
2440
+
2441
+ return self.do_request(
2442
+ url=request_url,
2443
+ method="GET",
2444
+ headers=request_header,
2445
+ timeout=REQUEST_TIMEOUT,
2446
+ failure_message="Failed to get shares of Core Share group -> {}".format(
2447
+ group_id
2448
+ ),
2449
+ user_credentials=False,
2450
+ )
2451
+
2452
+ # end method definition
2453
+
2454
+ def revoke_group_share(self, group_id: str, resource_id: str) -> dict | None:
2455
+ """Revoke sharing of a folder with a group.
2456
+
2457
+ Args:
2458
+ group_id (str): ID of the Core Share group
2459
+ resource_id (str): ID of the Core share folder
2460
+
2461
+ Returns:
2462
+ dict | None: Response or None if the request fails.
2463
+ """
2464
+
2465
+ if not self._access_token_admin:
2466
+ self.authenticate_admin()
2467
+
2468
+ request_header = self.request_header_admin()
2469
+
2470
+ request_url = (
2471
+ self.config()["foldersUrlv1"]
2472
+ + "/{}".format(resource_id)
2473
+ + "/collaboratorsAsAdmin/"
2474
+ + str(group_id)
2475
+ )
2476
+
2477
+ logger.debug(
2478
+ "Revoke sharing of folder -> %s with group -> %s; calling -> %s",
2479
+ resource_id,
2480
+ group_id,
2481
+ request_url,
2482
+ )
2483
+
2484
+ return self.do_request(
2485
+ url=request_url,
2486
+ method="DELETE",
2487
+ headers=request_header,
2488
+ timeout=REQUEST_TIMEOUT,
2489
+ failure_message="Failed to revoke sharing Core Share folder with ID -> {} with group with ID -> {}".format(
2490
+ resource_id, group_id
2491
+ ),
2492
+ user_credentials=False,
2493
+ )
2494
+
2495
+ # end method definition
2496
+
2497
+ def cleanup_group_shares(self, group_id: str) -> bool:
2498
+ """Cleanup all incoming shares of a group.
2499
+ The Core Share admin is required to do this.
2500
+
2501
+ Args:
2502
+ group_id (str): Core Share ID of the group
2503
+
2504
+ Returns:
2505
+ bool: True = success, False in case of an error.
2506
+ """
2507
+
2508
+ response = self.get_group_shares(group_id=group_id)
2509
+
2510
+ if not response or not response["shares"]:
2511
+ logger.info("Group -> %s has no shares to revoke!", group_id)
2512
+ return True
2513
+
2514
+ success = True
2515
+
2516
+ items = response["shares"]
2517
+ for item in items:
2518
+ logger.info(
2519
+ "Revoke sharing of folder -> %s (%s) with group -> %s...",
2520
+ item["name"],
2521
+ item["id"],
2522
+ group_id,
2523
+ )
2524
+ response = self.revoke_group_share(
2525
+ group_id=group_id, resource_id=item["id"]
2526
+ )
2527
+ if not response:
2528
+ success = False
2529
+
2530
+ return success
2531
+
2532
+ # end method definition