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