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/__init__.py +5 -0
- pyxecm/avts.py +1065 -0
- pyxecm/coreshare.py +2532 -0
- pyxecm/customizer/__init__.py +4 -0
- pyxecm/customizer/browser_automation.py +164 -54
- pyxecm/customizer/customizer.py +588 -231
- pyxecm/customizer/k8s.py +143 -29
- pyxecm/customizer/m365.py +1434 -1323
- pyxecm/customizer/payload.py +15073 -5933
- pyxecm/customizer/pht.py +926 -0
- pyxecm/customizer/salesforce.py +866 -351
- pyxecm/customizer/sap.py +4 -4
- pyxecm/customizer/servicenow.py +1467 -0
- pyxecm/customizer/successfactors.py +1056 -0
- pyxecm/helper/__init__.py +2 -0
- pyxecm/helper/assoc.py +44 -1
- pyxecm/helper/data.py +1731 -0
- pyxecm/helper/web.py +170 -46
- pyxecm/helper/xml.py +170 -34
- pyxecm/otac.py +309 -23
- pyxecm/otawp.py +1810 -0
- pyxecm/otcs.py +5308 -2985
- pyxecm/otds.py +1909 -1954
- pyxecm/otmm.py +928 -0
- pyxecm/otpd.py +13 -10
- {pyxecm-1.4.dist-info → pyxecm-1.6.dist-info}/METADATA +5 -1
- pyxecm-1.6.dist-info/RECORD +32 -0
- {pyxecm-1.4.dist-info → pyxecm-1.6.dist-info}/WHEEL +1 -1
- pyxecm-1.4.dist-info/RECORD +0 -24
- {pyxecm-1.4.dist-info → pyxecm-1.6.dist-info}/LICENSE +0 -0
- {pyxecm-1.4.dist-info → pyxecm-1.6.dist-info}/top_level.txt +0 -0
pyxecm/customizer/m365.py
CHANGED
|
@@ -12,8 +12,10 @@ credentials_user: In some cases MS Graph APIs cannot be called via
|
|
|
12
12
|
application permissions (client_id, client_secret)
|
|
13
13
|
but requires a token of a user authenticated
|
|
14
14
|
with username + password
|
|
15
|
+
|
|
15
16
|
request_header: Returns the request header for MS Graph API calls
|
|
16
17
|
request_header_user: Returns the request header used for user specific calls
|
|
18
|
+
do_request: Call an M365 Graph API in a safe way
|
|
17
19
|
parse_request_response: Parse the REST API responses and convert
|
|
18
20
|
them to Python dict in a safe way
|
|
19
21
|
exist_result_item: Check if an dict item is in the response
|
|
@@ -30,6 +32,7 @@ update_user: Update selected properties of an M365 user
|
|
|
30
32
|
get_user_licenses: Get the assigned license SKUs of a user
|
|
31
33
|
assign_license_to_user: Add an M365 license to a user (e.g. to use Office 365)
|
|
32
34
|
get_user_photo: Get the photo of a M365 user
|
|
35
|
+
download_user_photo: Download the M365 user photo and save it to the local file system
|
|
33
36
|
update_user_photo: Update a user with a profile photo (which must be in local file system)
|
|
34
37
|
|
|
35
38
|
get_groups: Get list all all groups in M365 tenant
|
|
@@ -63,6 +66,7 @@ upload_teams_app: Upload a new app package to the catalog of MS Teams apps
|
|
|
63
66
|
remove_teams_app: Remove MS Teams App for the app catalog
|
|
64
67
|
assign_teams_app_to_user: Assign (add) a MS Teams app to a M365 user.
|
|
65
68
|
upgrade_teams_app_of_user: Upgrade a MS teams app for a user.
|
|
69
|
+
remove_teams_app_from_user: Remove a M365 Teams app from a M365 user.
|
|
66
70
|
assign_teams_app_to_team: Assign (add) a MS Teams app to a M365 team
|
|
67
71
|
(so that it afterwards can be added as a Tab in a M365 Teams Channel)
|
|
68
72
|
upgrade_teams_app_of_team: Upgrade a MS teams app for a specific team.
|
|
@@ -74,6 +78,17 @@ add_sensitivity_label: Assign a existing sensitivity label to a user.
|
|
|
74
78
|
THIS IS CURRENTLY NOT WORKING!
|
|
75
79
|
assign_sensitivity_label_to_user: Create a new sensitivity label in M365
|
|
76
80
|
THIS IS CURRENTLY NOT WORKING!
|
|
81
|
+
|
|
82
|
+
upload_outlook_app: Upload the M365 Outlook Add-In as "Integrated" App to M365 Admin Center. (NOT WORKING)
|
|
83
|
+
get_app_registration: Find an Azure App Registration based on its name
|
|
84
|
+
add_app_registration: Add an Azure App Registration
|
|
85
|
+
update_app_registration: Update an Azure App Registration
|
|
86
|
+
|
|
87
|
+
get_mail: Get email from inbox of a given user and a given sender (from)
|
|
88
|
+
get_mail_body: Get full email body for a given email ID
|
|
89
|
+
extract_url_from_message_body: Parse the email body to extract (a potentially multi-line) URL from the body.
|
|
90
|
+
delete_mail: Delete email from inbox of a given user and a given email ID.
|
|
91
|
+
email_verification: Process email verification
|
|
77
92
|
"""
|
|
78
93
|
|
|
79
94
|
__author__ = "Dr. Marc Diefenbruch"
|
|
@@ -89,10 +104,15 @@ import re
|
|
|
89
104
|
import time
|
|
90
105
|
import urllib.parse
|
|
91
106
|
import zipfile
|
|
92
|
-
from
|
|
107
|
+
from datetime import datetime
|
|
93
108
|
|
|
109
|
+
from urllib.parse import quote
|
|
110
|
+
from http import HTTPStatus
|
|
94
111
|
import requests
|
|
95
112
|
|
|
113
|
+
from pyxecm.helper.web import HTTP
|
|
114
|
+
from pyxecm.customizer.browser_automation import BrowserAutomation
|
|
115
|
+
|
|
96
116
|
logger = logging.getLogger("pyxecm.customizer.m365")
|
|
97
117
|
|
|
98
118
|
request_login_headers = {
|
|
@@ -100,6 +120,9 @@ request_login_headers = {
|
|
|
100
120
|
"Accept": "application/json",
|
|
101
121
|
}
|
|
102
122
|
|
|
123
|
+
REQUEST_TIMEOUT = 60
|
|
124
|
+
REQUEST_RETRY_DELAY = 20
|
|
125
|
+
REQUEST_MAX_RETRIES = 3
|
|
103
126
|
|
|
104
127
|
class M365(object):
|
|
105
128
|
"""Used to automate stettings in Microsoft 365 via the Graph API."""
|
|
@@ -107,6 +130,7 @@ class M365(object):
|
|
|
107
130
|
_config: dict
|
|
108
131
|
_access_token = None
|
|
109
132
|
_user_access_token = None
|
|
133
|
+
_http_object: HTTP | None = None
|
|
110
134
|
|
|
111
135
|
def __init__(
|
|
112
136
|
self,
|
|
@@ -116,6 +140,7 @@ class M365(object):
|
|
|
116
140
|
domain: str,
|
|
117
141
|
sku_id: str,
|
|
118
142
|
teams_app_name: str,
|
|
143
|
+
teams_app_external_id: str,
|
|
119
144
|
):
|
|
120
145
|
"""Initialize the M365 object
|
|
121
146
|
|
|
@@ -126,6 +151,7 @@ class M365(object):
|
|
|
126
151
|
domain (str): M365 domain
|
|
127
152
|
sku_id (str): License SKU for M365 users
|
|
128
153
|
teams_app_name (str): name of the Extended ECM app for MS Teams
|
|
154
|
+
teams_app_external_id (str): external ID of the Extended ECM app for MS Teams
|
|
129
155
|
"""
|
|
130
156
|
|
|
131
157
|
m365_config = {}
|
|
@@ -137,6 +163,10 @@ class M365(object):
|
|
|
137
163
|
m365_config["domain"] = domain
|
|
138
164
|
m365_config["skuId"] = sku_id
|
|
139
165
|
m365_config["teamsAppName"] = teams_app_name
|
|
166
|
+
m365_config["teamsAppExternalId"] = (
|
|
167
|
+
teams_app_external_id # this is the external App ID
|
|
168
|
+
)
|
|
169
|
+
m365_config["teamsAppInternalId"] = None # will be set later...
|
|
140
170
|
m365_config[
|
|
141
171
|
"authenticationUrl"
|
|
142
172
|
] = "https://login.microsoftonline.com/{}/oauth2/v2.0/token".format(tenant_id)
|
|
@@ -162,6 +192,7 @@ class M365(object):
|
|
|
162
192
|
m365_config["applicationsUrl"] = m365_config["graphUrl"] + "applications"
|
|
163
193
|
|
|
164
194
|
self._config = m365_config
|
|
195
|
+
self._http_object = HTTP()
|
|
165
196
|
|
|
166
197
|
def config(self) -> dict:
|
|
167
198
|
"""Returns the configuration dictionary
|
|
@@ -242,6 +273,183 @@ class M365(object):
|
|
|
242
273
|
|
|
243
274
|
# end method definition
|
|
244
275
|
|
|
276
|
+
def do_request(
|
|
277
|
+
self,
|
|
278
|
+
url: str,
|
|
279
|
+
method: str = "GET",
|
|
280
|
+
headers: dict | None = None,
|
|
281
|
+
data: dict | None = None,
|
|
282
|
+
json_data: dict | None = None,
|
|
283
|
+
files: dict | None = None,
|
|
284
|
+
params: dict | None = None,
|
|
285
|
+
timeout: int | None = REQUEST_TIMEOUT,
|
|
286
|
+
show_error: bool = True,
|
|
287
|
+
show_warning: bool = False,
|
|
288
|
+
warning_message: str = "",
|
|
289
|
+
failure_message: str = "",
|
|
290
|
+
success_message: str = "",
|
|
291
|
+
max_retries: int = REQUEST_MAX_RETRIES,
|
|
292
|
+
retry_forever: bool = False,
|
|
293
|
+
parse_request_response: bool = True,
|
|
294
|
+
stream: bool = False,
|
|
295
|
+
) -> dict | None:
|
|
296
|
+
"""Call an M365 Graph API in a safe way
|
|
297
|
+
|
|
298
|
+
Args:
|
|
299
|
+
url (str): URL to send the request to.
|
|
300
|
+
method (str, optional): HTTP method (GET, POST, etc.). Defaults to "GET".
|
|
301
|
+
headers (dict | None, optional): Request Headers. Defaults to None.
|
|
302
|
+
data (dict | None, optional): Request payload. Defaults to None
|
|
303
|
+
files (dict | None, optional): Dictionary of {"name": file-tuple} for multipart encoding upload.
|
|
304
|
+
file-tuple can be a 2-tuple ("filename", fileobj) or a 3-tuple ("filename", fileobj, "content_type")
|
|
305
|
+
params (dict | None, optional): Add key-value pairs to the query string of the URL.
|
|
306
|
+
When you use the params parameter, requests automatically appends
|
|
307
|
+
the key-value pairs to the URL as part of the query string
|
|
308
|
+
timeout (int | None, optional): Timeout for the request in seconds. Defaults to REQUEST_TIMEOUT.
|
|
309
|
+
show_error (bool, optional): Whether or not an error should be logged in case of a failed REST call.
|
|
310
|
+
If False, then only a warning is logged. Defaults to True.
|
|
311
|
+
warning_message (str, optional): Specific warning message. Defaults to "". If not given the error_message will be used.
|
|
312
|
+
failure_message (str, optional): Specific error message. Defaults to "".
|
|
313
|
+
success_message (str, optional): Specific success message. Defaults to "".
|
|
314
|
+
max_retries (int, optional): How many retries on Connection errors? Default is REQUEST_MAX_RETRIES.
|
|
315
|
+
retry_forever (bool, optional): Eventually wait forever - without timeout. Defaults to False.
|
|
316
|
+
parse_request_response (bool, optional): should the response.text be interpreted as json and loaded into a dictionary. True is the default.
|
|
317
|
+
stream (bool, optional): parameter is used to control whether the response content should be immediately downloaded or streamed incrementally
|
|
318
|
+
|
|
319
|
+
Returns:
|
|
320
|
+
dict | None: Response of OTDS REST API or None in case of an error.
|
|
321
|
+
"""
|
|
322
|
+
|
|
323
|
+
if headers is None:
|
|
324
|
+
logger.error("Missing request header. Cannot send request to Core Share!")
|
|
325
|
+
return None
|
|
326
|
+
|
|
327
|
+
# In case of an expired session we reauthenticate and
|
|
328
|
+
# try 1 more time. Session expiration should not happen
|
|
329
|
+
# twice in a row:
|
|
330
|
+
retries = 0
|
|
331
|
+
|
|
332
|
+
while True:
|
|
333
|
+
try:
|
|
334
|
+
response = requests.request(
|
|
335
|
+
method=method,
|
|
336
|
+
url=url,
|
|
337
|
+
data=data,
|
|
338
|
+
json=json_data,
|
|
339
|
+
files=files,
|
|
340
|
+
params=params,
|
|
341
|
+
headers=headers,
|
|
342
|
+
timeout=timeout,
|
|
343
|
+
stream=stream,
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
if response.ok:
|
|
347
|
+
if success_message:
|
|
348
|
+
logger.info(success_message)
|
|
349
|
+
if parse_request_response:
|
|
350
|
+
return self.parse_request_response(response)
|
|
351
|
+
else:
|
|
352
|
+
return response
|
|
353
|
+
# Check if Session has expired - then re-authenticate and try once more
|
|
354
|
+
elif response.status_code == 401 and retries == 0:
|
|
355
|
+
logger.debug("Session has expired - try to re-authenticate...")
|
|
356
|
+
self.authenticate(revalidate=True)
|
|
357
|
+
headers = self.request_header()
|
|
358
|
+
retries += 1
|
|
359
|
+
elif (
|
|
360
|
+
response.status_code in [502, 503, 504]
|
|
361
|
+
and retries < REQUEST_MAX_RETRIES
|
|
362
|
+
):
|
|
363
|
+
logger.warning(
|
|
364
|
+
"M365 Graph API delivered server side error -> %s; retrying in %s seconds...",
|
|
365
|
+
response.status_code,
|
|
366
|
+
(retries + 1) * 60,
|
|
367
|
+
)
|
|
368
|
+
time.sleep((retries + 1) * 60)
|
|
369
|
+
retries += 1
|
|
370
|
+
else:
|
|
371
|
+
# Handle plain HTML responses to not pollute the logs
|
|
372
|
+
content_type = response.headers.get("content-type", None)
|
|
373
|
+
if content_type == "text/html":
|
|
374
|
+
response_text = "HTML content (only printed in debug log)"
|
|
375
|
+
else:
|
|
376
|
+
response_text = response.text
|
|
377
|
+
|
|
378
|
+
if show_error:
|
|
379
|
+
logger.error(
|
|
380
|
+
"%s; status -> %s/%s; error -> %s",
|
|
381
|
+
failure_message,
|
|
382
|
+
response.status_code,
|
|
383
|
+
HTTPStatus(response.status_code).phrase,
|
|
384
|
+
response_text,
|
|
385
|
+
)
|
|
386
|
+
elif show_warning:
|
|
387
|
+
logger.warning(
|
|
388
|
+
"%s; status -> %s/%s; warning -> %s",
|
|
389
|
+
warning_message if warning_message else failure_message,
|
|
390
|
+
response.status_code,
|
|
391
|
+
HTTPStatus(response.status_code).phrase,
|
|
392
|
+
response_text,
|
|
393
|
+
)
|
|
394
|
+
if content_type == "text/html":
|
|
395
|
+
logger.debug(
|
|
396
|
+
"%s; status -> %s/%s; warning -> %s",
|
|
397
|
+
failure_message,
|
|
398
|
+
response.status_code,
|
|
399
|
+
HTTPStatus(response.status_code).phrase,
|
|
400
|
+
response.text,
|
|
401
|
+
)
|
|
402
|
+
return None
|
|
403
|
+
except requests.exceptions.Timeout:
|
|
404
|
+
if retries <= max_retries:
|
|
405
|
+
logger.warning(
|
|
406
|
+
"Request timed out. Retrying in %s seconds...",
|
|
407
|
+
str(REQUEST_RETRY_DELAY),
|
|
408
|
+
)
|
|
409
|
+
retries += 1
|
|
410
|
+
time.sleep(REQUEST_RETRY_DELAY) # Add a delay before retrying
|
|
411
|
+
else:
|
|
412
|
+
logger.error(
|
|
413
|
+
"%s; timeout error",
|
|
414
|
+
failure_message,
|
|
415
|
+
)
|
|
416
|
+
if retry_forever:
|
|
417
|
+
# If it fails after REQUEST_MAX_RETRIES retries we let it wait forever
|
|
418
|
+
logger.warning("Turn timeouts off and wait forever...")
|
|
419
|
+
timeout = None
|
|
420
|
+
else:
|
|
421
|
+
return None
|
|
422
|
+
except requests.exceptions.ConnectionError:
|
|
423
|
+
if retries <= max_retries:
|
|
424
|
+
logger.warning(
|
|
425
|
+
"Connection error. Retrying in %s seconds...",
|
|
426
|
+
str(REQUEST_RETRY_DELAY),
|
|
427
|
+
)
|
|
428
|
+
retries += 1
|
|
429
|
+
time.sleep(REQUEST_RETRY_DELAY) # Add a delay before retrying
|
|
430
|
+
else:
|
|
431
|
+
logger.error(
|
|
432
|
+
"%s; connection error",
|
|
433
|
+
failure_message,
|
|
434
|
+
)
|
|
435
|
+
if retry_forever:
|
|
436
|
+
# If it fails after REQUEST_MAX_RETRIES retries we let it wait forever
|
|
437
|
+
logger.warning("Turn timeouts off and wait forever...")
|
|
438
|
+
timeout = None
|
|
439
|
+
time.sleep(REQUEST_RETRY_DELAY) # Add a delay before retrying
|
|
440
|
+
else:
|
|
441
|
+
return None
|
|
442
|
+
# end try
|
|
443
|
+
logger.debug(
|
|
444
|
+
"Retrying REST API %s call -> %s... (retry = %s)",
|
|
445
|
+
method,
|
|
446
|
+
url,
|
|
447
|
+
str(retries),
|
|
448
|
+
)
|
|
449
|
+
# end while True
|
|
450
|
+
|
|
451
|
+
# end method definition
|
|
452
|
+
|
|
245
453
|
def parse_request_response(
|
|
246
454
|
self,
|
|
247
455
|
response_object: requests.Response,
|
|
@@ -396,7 +604,7 @@ class M365(object):
|
|
|
396
604
|
|
|
397
605
|
# Already authenticated and session still valid?
|
|
398
606
|
if self._access_token and not revalidate:
|
|
399
|
-
logger.
|
|
607
|
+
logger.debug(
|
|
400
608
|
"Session still valid - return existing access token -> %s",
|
|
401
609
|
str(self._access_token),
|
|
402
610
|
)
|
|
@@ -405,7 +613,7 @@ class M365(object):
|
|
|
405
613
|
request_url = self.config()["authenticationUrl"]
|
|
406
614
|
request_header = request_login_headers
|
|
407
615
|
|
|
408
|
-
logger.
|
|
616
|
+
logger.debug("Requesting M365 Access Token from -> %s", request_url)
|
|
409
617
|
|
|
410
618
|
authenticate_post_body = self.credentials()
|
|
411
619
|
authenticate_response = None
|
|
@@ -415,7 +623,7 @@ class M365(object):
|
|
|
415
623
|
request_url,
|
|
416
624
|
data=authenticate_post_body,
|
|
417
625
|
headers=request_header,
|
|
418
|
-
timeout=
|
|
626
|
+
timeout=REQUEST_TIMEOUT,
|
|
419
627
|
)
|
|
420
628
|
except requests.exceptions.ConnectionError as exception:
|
|
421
629
|
logger.warning(
|
|
@@ -459,7 +667,7 @@ class M365(object):
|
|
|
459
667
|
request_url = self.config()["authenticationUrl"]
|
|
460
668
|
request_header = request_login_headers
|
|
461
669
|
|
|
462
|
-
logger.
|
|
670
|
+
logger.debug(
|
|
463
671
|
"Requesting M365 Access Token for user -> %s from -> %s",
|
|
464
672
|
username,
|
|
465
673
|
request_url,
|
|
@@ -473,7 +681,7 @@ class M365(object):
|
|
|
473
681
|
request_url,
|
|
474
682
|
data=authenticate_post_body,
|
|
475
683
|
headers=request_header,
|
|
476
|
-
timeout=
|
|
684
|
+
timeout=REQUEST_TIMEOUT,
|
|
477
685
|
)
|
|
478
686
|
except requests.exceptions.ConnectionError as exception:
|
|
479
687
|
logger.warning(
|
|
@@ -492,7 +700,7 @@ class M365(object):
|
|
|
492
700
|
logger.debug("User Access Token -> %s", access_token)
|
|
493
701
|
else:
|
|
494
702
|
logger.error(
|
|
495
|
-
"Failed to request an M365 Access Token for user -> %s; error -> %s",
|
|
703
|
+
"Failed to request an M365 Access Token for user -> '%s'; error -> %s",
|
|
496
704
|
username,
|
|
497
705
|
authenticate_response.text,
|
|
498
706
|
)
|
|
@@ -509,40 +717,21 @@ class M365(object):
|
|
|
509
717
|
"""Get list all all users in M365 tenant
|
|
510
718
|
|
|
511
719
|
Returns:
|
|
512
|
-
dict: Dictionary of all users.
|
|
720
|
+
dict: Dictionary of all M365 users.
|
|
513
721
|
"""
|
|
514
722
|
|
|
515
723
|
request_url = self.config()["usersUrl"]
|
|
516
724
|
request_header = self.request_header()
|
|
517
725
|
|
|
518
|
-
logger.
|
|
726
|
+
logger.debug("Get list of all M365 users; calling -> %s", request_url)
|
|
519
727
|
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
logger.warning("Session has expired - try to re-authenticate...")
|
|
528
|
-
self.authenticate(revalidate=True)
|
|
529
|
-
request_header = self.request_header()
|
|
530
|
-
retries += 1
|
|
531
|
-
elif response.status_code in [502, 503, 504] and retries < 3:
|
|
532
|
-
logger.warning(
|
|
533
|
-
"M365 Graph API delivered server side error -> %s; retrying in %s seconds...",
|
|
534
|
-
response.status_code,
|
|
535
|
-
(retries + 1) * 60,
|
|
536
|
-
)
|
|
537
|
-
time.sleep((retries + 1) * 60)
|
|
538
|
-
retries += 1
|
|
539
|
-
else:
|
|
540
|
-
logger.error(
|
|
541
|
-
"Failed to get list of users; status -> %s; error -> %s",
|
|
542
|
-
response.status_code,
|
|
543
|
-
response.text,
|
|
544
|
-
)
|
|
545
|
-
return None
|
|
728
|
+
return self.do_request(
|
|
729
|
+
url=request_url,
|
|
730
|
+
method="GET",
|
|
731
|
+
headers=request_header,
|
|
732
|
+
timeout=REQUEST_TIMEOUT,
|
|
733
|
+
failure_message="Failed to get list of M365 users!",
|
|
734
|
+
)
|
|
546
735
|
|
|
547
736
|
# end method definition
|
|
548
737
|
|
|
@@ -573,41 +762,44 @@ class M365(object):
|
|
|
573
762
|
}
|
|
574
763
|
"""
|
|
575
764
|
|
|
765
|
+
# Some sanity checks:
|
|
766
|
+
if not "@" in user_email or not "." in user_email:
|
|
767
|
+
logger.error("User email -> %s is not a valid email address", user_email)
|
|
768
|
+
return None
|
|
769
|
+
|
|
770
|
+
# if there's an alias in the E-Mail Adress we remove it as
|
|
771
|
+
# MS Graph seems to not support an alias to lookup a user object.
|
|
772
|
+
if "+" in user_email:
|
|
773
|
+
logger.info(
|
|
774
|
+
"Removing Alias from email address -> %s to determine M365 principal name...",
|
|
775
|
+
user_email,
|
|
776
|
+
)
|
|
777
|
+
# Find the index of the '+' character
|
|
778
|
+
alias_index = user_email.find("+")
|
|
779
|
+
|
|
780
|
+
# Find the index of the '@' character
|
|
781
|
+
domain_index = user_email.find("@")
|
|
782
|
+
|
|
783
|
+
# Construct the email address without the alias
|
|
784
|
+
user_email = user_email[:alias_index] + user_email[domain_index:]
|
|
785
|
+
logger.info(
|
|
786
|
+
"M365 user principal name -> %s",
|
|
787
|
+
user_email,
|
|
788
|
+
)
|
|
789
|
+
|
|
576
790
|
request_url = self.config()["usersUrl"] + "/" + user_email
|
|
577
791
|
request_header = self.request_header()
|
|
578
792
|
|
|
579
|
-
logger.
|
|
793
|
+
logger.debug("Get M365 user -> %s; calling -> %s", user_email, request_url)
|
|
580
794
|
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
self.authenticate(revalidate=True)
|
|
590
|
-
request_header = self.request_header()
|
|
591
|
-
retries += 1
|
|
592
|
-
elif response.status_code in [502, 503, 504] and retries < 3:
|
|
593
|
-
logger.warning(
|
|
594
|
-
"M365 Graph API delivered server side error -> %s; retrying in %s seconds...",
|
|
595
|
-
response.status_code,
|
|
596
|
-
(retries + 1) * 60,
|
|
597
|
-
)
|
|
598
|
-
time.sleep((retries + 1) * 60)
|
|
599
|
-
retries += 1
|
|
600
|
-
else:
|
|
601
|
-
if show_error:
|
|
602
|
-
logger.error(
|
|
603
|
-
"Failed to get M365 user -> %s; status -> %s; error -> %s",
|
|
604
|
-
user_email,
|
|
605
|
-
response.status_code,
|
|
606
|
-
response.text,
|
|
607
|
-
)
|
|
608
|
-
else:
|
|
609
|
-
logger.info("M365 User -> %s not found.", user_email)
|
|
610
|
-
return None
|
|
795
|
+
return self.do_request(
|
|
796
|
+
url=request_url,
|
|
797
|
+
method="GET",
|
|
798
|
+
headers=request_header,
|
|
799
|
+
timeout=REQUEST_TIMEOUT,
|
|
800
|
+
failure_message="Failed to get M365 user -> '{}'".format(user_email),
|
|
801
|
+
show_error=show_error,
|
|
802
|
+
)
|
|
611
803
|
|
|
612
804
|
# end method definition
|
|
613
805
|
|
|
@@ -657,40 +849,16 @@ class M365(object):
|
|
|
657
849
|
request_url = self.config()["usersUrl"]
|
|
658
850
|
request_header = self.request_header()
|
|
659
851
|
|
|
660
|
-
logger.
|
|
852
|
+
logger.debug("Adding M365 user -> %s; calling -> %s", email, request_url)
|
|
661
853
|
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
if response.ok:
|
|
671
|
-
return self.parse_request_response(response)
|
|
672
|
-
# Check if Session has expired - then re-authenticate and try once more
|
|
673
|
-
elif response.status_code == 401 and retries == 0:
|
|
674
|
-
logger.warning("Session has expired - try to re-authenticate...")
|
|
675
|
-
self.authenticate(revalidate=True)
|
|
676
|
-
request_header = self.request_header()
|
|
677
|
-
retries += 1
|
|
678
|
-
elif response.status_code in [502, 503, 504] and retries < 3:
|
|
679
|
-
logger.warning(
|
|
680
|
-
"M365 Graph API delivered server side error -> %s; retrying in %s seconds...",
|
|
681
|
-
response.status_code,
|
|
682
|
-
(retries + 1) * 60,
|
|
683
|
-
)
|
|
684
|
-
time.sleep((retries + 1) * 60)
|
|
685
|
-
retries += 1
|
|
686
|
-
else:
|
|
687
|
-
logger.error(
|
|
688
|
-
"Failed to add M365 user -> %s; status -> %s; error -> %s",
|
|
689
|
-
email,
|
|
690
|
-
response.status_code,
|
|
691
|
-
response.text,
|
|
692
|
-
)
|
|
693
|
-
return None
|
|
854
|
+
return self.do_request(
|
|
855
|
+
url=request_url,
|
|
856
|
+
method="POST",
|
|
857
|
+
headers=request_header,
|
|
858
|
+
data=json.dumps(user_post_body),
|
|
859
|
+
timeout=REQUEST_TIMEOUT,
|
|
860
|
+
failure_message="Failed to add M365 user -> '{}'".format(email),
|
|
861
|
+
)
|
|
694
862
|
|
|
695
863
|
# end method definition
|
|
696
864
|
|
|
@@ -698,6 +866,9 @@ class M365(object):
|
|
|
698
866
|
"""Update selected properties of an M365 user. Documentation
|
|
699
867
|
on user properties is here: https://learn.microsoft.com/en-us/graph/api/user-update
|
|
700
868
|
|
|
869
|
+
Args:
|
|
870
|
+
user_id (str): ID of the user (can also be email). This is also the unique identifier
|
|
871
|
+
updated_settings (dict): new data to update the user with
|
|
701
872
|
Returns:
|
|
702
873
|
dict | None: Response of the M365 Graph API or None if the call fails.
|
|
703
874
|
"""
|
|
@@ -705,46 +876,23 @@ class M365(object):
|
|
|
705
876
|
request_url = self.config()["usersUrl"] + "/" + user_id
|
|
706
877
|
request_header = self.request_header()
|
|
707
878
|
|
|
708
|
-
logger.
|
|
709
|
-
"Updating M365 user -> %s with -> %s; calling -> %s",
|
|
879
|
+
logger.debug(
|
|
880
|
+
"Updating M365 user with ID -> %s with -> %s; calling -> %s",
|
|
710
881
|
user_id,
|
|
711
882
|
str(updated_settings),
|
|
712
883
|
request_url,
|
|
713
884
|
)
|
|
714
885
|
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
# Check if Session has expired - then re-authenticate and try once more
|
|
726
|
-
elif response.status_code == 401 and retries == 0:
|
|
727
|
-
logger.warning("Session has expired - try to re-authenticate...")
|
|
728
|
-
self.authenticate(revalidate=True)
|
|
729
|
-
request_header = self.request_header()
|
|
730
|
-
retries += 1
|
|
731
|
-
elif response.status_code in [502, 503, 504] and retries < 3:
|
|
732
|
-
logger.warning(
|
|
733
|
-
"M365 Graph API delivered server side error -> %s; retrying in %s seconds...",
|
|
734
|
-
response.status_code,
|
|
735
|
-
(retries + 1) * 60,
|
|
736
|
-
)
|
|
737
|
-
time.sleep((retries + 1) * 60)
|
|
738
|
-
retries += 1
|
|
739
|
-
else:
|
|
740
|
-
logger.error(
|
|
741
|
-
"Failed to update M365 user -> %s with -> %s; status -> %s; error -> %s",
|
|
742
|
-
user_id,
|
|
743
|
-
str(updated_settings),
|
|
744
|
-
response.status_code,
|
|
745
|
-
response.text,
|
|
746
|
-
)
|
|
747
|
-
return None
|
|
886
|
+
return self.do_request(
|
|
887
|
+
url=request_url,
|
|
888
|
+
method="PATCH",
|
|
889
|
+
headers=request_header,
|
|
890
|
+
json_data=updated_settings,
|
|
891
|
+
timeout=REQUEST_TIMEOUT,
|
|
892
|
+
failure_message="Failed to update M365 user -> '{}' with -> {}".format(
|
|
893
|
+
user_id, updated_settings
|
|
894
|
+
),
|
|
895
|
+
)
|
|
748
896
|
|
|
749
897
|
# end method definition
|
|
750
898
|
|
|
@@ -773,33 +921,13 @@ class M365(object):
|
|
|
773
921
|
request_url = self.config()["usersUrl"] + "/" + user_id + "/licenseDetails"
|
|
774
922
|
request_header = self.request_header()
|
|
775
923
|
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
logger.warning("Session has expired - try to re-authenticate...")
|
|
784
|
-
self.authenticate(revalidate=True)
|
|
785
|
-
request_header = self.request_header()
|
|
786
|
-
retries += 1
|
|
787
|
-
elif response.status_code in [502, 503, 504] and retries < 3:
|
|
788
|
-
logger.warning(
|
|
789
|
-
"M365 Graph API delivered server side error -> %s; retrying in %s seconds...",
|
|
790
|
-
response.status_code,
|
|
791
|
-
(retries + 1) * 60,
|
|
792
|
-
)
|
|
793
|
-
time.sleep((retries + 1) * 60)
|
|
794
|
-
retries += 1
|
|
795
|
-
else:
|
|
796
|
-
logger.error(
|
|
797
|
-
"Failed to get M365 licenses of user -> %s; status -> %s; error -> %s",
|
|
798
|
-
user_id,
|
|
799
|
-
response.status_code,
|
|
800
|
-
response.text,
|
|
801
|
-
)
|
|
802
|
-
return None
|
|
924
|
+
return self.do_request(
|
|
925
|
+
url=request_url,
|
|
926
|
+
method="GET",
|
|
927
|
+
headers=request_header,
|
|
928
|
+
timeout=REQUEST_TIMEOUT,
|
|
929
|
+
failure_message="Failed to get M365 licenses of user -> {}".format(user_id),
|
|
930
|
+
)
|
|
803
931
|
|
|
804
932
|
# end method definition
|
|
805
933
|
|
|
@@ -830,43 +958,23 @@ class M365(object):
|
|
|
830
958
|
"removeLicenses": [],
|
|
831
959
|
}
|
|
832
960
|
|
|
833
|
-
logger.
|
|
961
|
+
logger.debug(
|
|
834
962
|
"Assign M365 license -> %s to M365 user -> %s; calling -> %s",
|
|
835
963
|
sku_id,
|
|
836
964
|
user_id,
|
|
837
965
|
request_url,
|
|
838
966
|
)
|
|
839
967
|
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
self.authenticate(revalidate=True)
|
|
851
|
-
request_header = self.request_header()
|
|
852
|
-
retries += 1
|
|
853
|
-
elif response.status_code in [502, 503, 504] and retries < 3:
|
|
854
|
-
logger.warning(
|
|
855
|
-
"M365 Graph API delivered server side error -> %s; retrying in %s seconds...",
|
|
856
|
-
response.status_code,
|
|
857
|
-
(retries + 1) * 60,
|
|
858
|
-
)
|
|
859
|
-
time.sleep((retries + 1) * 60)
|
|
860
|
-
retries += 1
|
|
861
|
-
else:
|
|
862
|
-
logger.error(
|
|
863
|
-
"Failed to add M365 license -> %s to M365 user -> %s; status -> %s; error -> %s",
|
|
864
|
-
sku_id,
|
|
865
|
-
user_id,
|
|
866
|
-
response.status_code,
|
|
867
|
-
response.text,
|
|
868
|
-
)
|
|
869
|
-
return None
|
|
968
|
+
return self.do_request(
|
|
969
|
+
url=request_url,
|
|
970
|
+
method="POST",
|
|
971
|
+
headers=request_header,
|
|
972
|
+
json_data=license_post_body,
|
|
973
|
+
timeout=REQUEST_TIMEOUT,
|
|
974
|
+
failure_message="Failed to add M365 license -> {} to M365 user -> {}".format(
|
|
975
|
+
sku_id, user_id
|
|
976
|
+
),
|
|
977
|
+
)
|
|
870
978
|
|
|
871
979
|
# end method definition
|
|
872
980
|
|
|
@@ -885,38 +993,90 @@ class M365(object):
|
|
|
885
993
|
# Set image as content type:
|
|
886
994
|
request_header = self.request_header("image/*")
|
|
887
995
|
|
|
888
|
-
logger.
|
|
996
|
+
logger.debug("Get photo of user -> %s; calling -> %s", user_id, request_url)
|
|
889
997
|
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
998
|
+
response = self.do_request(
|
|
999
|
+
url=request_url,
|
|
1000
|
+
method="GET",
|
|
1001
|
+
headers=request_header,
|
|
1002
|
+
timeout=REQUEST_TIMEOUT,
|
|
1003
|
+
failure_message="Failed to get photo of M365 user -> {}".format(user_id),
|
|
1004
|
+
warning_message="M365 User -> {} does not yet have a photo.".format(
|
|
1005
|
+
user_id
|
|
1006
|
+
),
|
|
1007
|
+
show_error=show_error,
|
|
1008
|
+
parse_request_response=False, # the response is NOT JSON!
|
|
1009
|
+
)
|
|
1010
|
+
|
|
1011
|
+
if response and response.ok and response.content:
|
|
1012
|
+
return response.content # this is the actual image - not json!
|
|
1013
|
+
|
|
1014
|
+
return None
|
|
1015
|
+
|
|
1016
|
+
# end method definition
|
|
1017
|
+
|
|
1018
|
+
def download_user_photo(self, user_id: str, photo_path: str) -> str | None:
|
|
1019
|
+
"""Download the M365 user photo and save it to the local file system
|
|
1020
|
+
|
|
1021
|
+
Args:
|
|
1022
|
+
user_id (str): M365 GUID of the user (can also be the M365 email of the user)
|
|
1023
|
+
photo_path (str): Directory where the photo should be saved
|
|
1024
|
+
Returns:
|
|
1025
|
+
str: name of the photo file in the file system (with full path) or None if
|
|
1026
|
+
the call of the REST API fails.
|
|
1027
|
+
"""
|
|
1028
|
+
|
|
1029
|
+
request_url = self.config()["usersUrl"] + "/" + user_id + "/photo/$value"
|
|
1030
|
+
request_header = self.request_header("application/json")
|
|
1031
|
+
|
|
1032
|
+
logger.debug(
|
|
1033
|
+
"Downloading photo for M365 user with ID -> %s; calling -> %s",
|
|
1034
|
+
user_id,
|
|
1035
|
+
request_url,
|
|
1036
|
+
)
|
|
1037
|
+
|
|
1038
|
+
response = self.do_request(
|
|
1039
|
+
url=request_url,
|
|
1040
|
+
method="GET",
|
|
1041
|
+
headers=request_header,
|
|
1042
|
+
timeout=REQUEST_TIMEOUT,
|
|
1043
|
+
failure_message="Failed to download photo for user with ID -> {}".format(
|
|
1044
|
+
user_id
|
|
1045
|
+
),
|
|
1046
|
+
stream=True,
|
|
1047
|
+
parse_request_response=False,
|
|
1048
|
+
)
|
|
1049
|
+
|
|
1050
|
+
if response and response.ok:
|
|
1051
|
+
content_type = response.headers.get("Content-Type", "image/png")
|
|
1052
|
+
if content_type == "image/jpeg":
|
|
1053
|
+
file_extension = "jpg"
|
|
1054
|
+
elif content_type == "image/png":
|
|
1055
|
+
file_extension = "png"
|
|
909
1056
|
else:
|
|
910
|
-
if
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
1057
|
+
file_extension = "img" # Default extension if type is unknown
|
|
1058
|
+
file_path = os.path.join(
|
|
1059
|
+
photo_path, "{}.{}".format(user_id, file_extension)
|
|
1060
|
+
)
|
|
1061
|
+
|
|
1062
|
+
try:
|
|
1063
|
+
with open(file_path, "wb") as file:
|
|
1064
|
+
for chunk in response.iter_content(chunk_size=8192):
|
|
1065
|
+
file.write(chunk)
|
|
1066
|
+
logger.info(
|
|
1067
|
+
"Photo for M365 user with ID -> %s saved to -> '%s'",
|
|
1068
|
+
user_id,
|
|
1069
|
+
file_path,
|
|
1070
|
+
)
|
|
1071
|
+
return file_path
|
|
1072
|
+
except OSError as exception:
|
|
1073
|
+
logger.error(
|
|
1074
|
+
"Error saving photo for user with ID -> %s; error -> %s",
|
|
1075
|
+
user_id,
|
|
1076
|
+
exception,
|
|
1077
|
+
)
|
|
1078
|
+
|
|
1079
|
+
return None
|
|
920
1080
|
|
|
921
1081
|
# end method definition
|
|
922
1082
|
|
|
@@ -952,43 +1112,23 @@ class M365(object):
|
|
|
952
1112
|
|
|
953
1113
|
data = photo_data
|
|
954
1114
|
|
|
955
|
-
logger.
|
|
956
|
-
"Update M365 user -> %s with photo -> %s; calling -> %s",
|
|
1115
|
+
logger.debug(
|
|
1116
|
+
"Update M365 user with ID -> %s with photo -> %s; calling -> %s",
|
|
957
1117
|
user_id,
|
|
958
1118
|
photo_path,
|
|
959
1119
|
request_url,
|
|
960
1120
|
)
|
|
961
1121
|
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
self.authenticate(revalidate=True)
|
|
973
|
-
request_header = self.request_header()
|
|
974
|
-
retries += 1
|
|
975
|
-
elif response.status_code in [502, 503, 504] and retries < 3:
|
|
976
|
-
logger.warning(
|
|
977
|
-
"M365 Graph API delivered server side error -> %s; retrying in %s seconds...",
|
|
978
|
-
response.status_code,
|
|
979
|
-
(retries + 1) * 60,
|
|
980
|
-
)
|
|
981
|
-
time.sleep((retries + 1) * 60)
|
|
982
|
-
retries += 1
|
|
983
|
-
else:
|
|
984
|
-
logger.error(
|
|
985
|
-
"Failed to update user -> %s with photo -> %s; status -> %s; error -> %s",
|
|
986
|
-
user_id,
|
|
987
|
-
photo_path,
|
|
988
|
-
response.status_code,
|
|
989
|
-
response.text,
|
|
990
|
-
)
|
|
991
|
-
return None
|
|
1122
|
+
return self.do_request(
|
|
1123
|
+
url=request_url,
|
|
1124
|
+
method="PUT",
|
|
1125
|
+
headers=request_header,
|
|
1126
|
+
data=data,
|
|
1127
|
+
timeout=REQUEST_TIMEOUT,
|
|
1128
|
+
failure_message="Failed to update M365 user with ID -> {} with photo -> '{}'".format(
|
|
1129
|
+
user_id, photo_path
|
|
1130
|
+
),
|
|
1131
|
+
)
|
|
992
1132
|
|
|
993
1133
|
# end method definition
|
|
994
1134
|
|
|
@@ -1004,39 +1144,16 @@ class M365(object):
|
|
|
1004
1144
|
request_url = self.config()["groupsUrl"]
|
|
1005
1145
|
request_header = self.request_header()
|
|
1006
1146
|
|
|
1007
|
-
logger.
|
|
1147
|
+
logger.debug("Get list of all M365 groups; calling -> %s", request_url)
|
|
1008
1148
|
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
if response.ok:
|
|
1018
|
-
return self.parse_request_response(response)
|
|
1019
|
-
# Check if Session has expired - then re-authenticate and try once more
|
|
1020
|
-
elif response.status_code == 401 and retries == 0:
|
|
1021
|
-
logger.warning("Session has expired - try to re-authenticate...")
|
|
1022
|
-
self.authenticate(revalidate=True)
|
|
1023
|
-
request_header = self.request_header()
|
|
1024
|
-
retries += 1
|
|
1025
|
-
elif response.status_code in [502, 503, 504] and retries < 3:
|
|
1026
|
-
logger.warning(
|
|
1027
|
-
"M365 Graph API delivered server side error -> %s; retrying in %s seconds...",
|
|
1028
|
-
response.status_code,
|
|
1029
|
-
(retries + 1) * 60,
|
|
1030
|
-
)
|
|
1031
|
-
time.sleep((retries + 1) * 60)
|
|
1032
|
-
retries += 1
|
|
1033
|
-
else:
|
|
1034
|
-
logger.error(
|
|
1035
|
-
"Failed to get list of M365 groups; status -> %s; error -> %s",
|
|
1036
|
-
response.status_code,
|
|
1037
|
-
response.text,
|
|
1038
|
-
)
|
|
1039
|
-
return None
|
|
1149
|
+
return self.do_request(
|
|
1150
|
+
url=request_url,
|
|
1151
|
+
method="GET",
|
|
1152
|
+
headers=request_header,
|
|
1153
|
+
params={"$top": str(max_number)},
|
|
1154
|
+
timeout=REQUEST_TIMEOUT,
|
|
1155
|
+
failure_message="Failed to get list of M365 groups",
|
|
1156
|
+
)
|
|
1040
1157
|
|
|
1041
1158
|
# end method definition
|
|
1042
1159
|
|
|
@@ -1101,38 +1218,16 @@ class M365(object):
|
|
|
1101
1218
|
request_url = self.config()["groupsUrl"] + "?" + encoded_query
|
|
1102
1219
|
request_header = self.request_header()
|
|
1103
1220
|
|
|
1104
|
-
logger.
|
|
1221
|
+
logger.debug("Get M365 group -> '%s'; calling -> %s", group_name, request_url)
|
|
1105
1222
|
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
self.authenticate(revalidate=True)
|
|
1115
|
-
request_header = self.request_header()
|
|
1116
|
-
retries += 1
|
|
1117
|
-
elif response.status_code in [502, 503, 504] and retries < 3:
|
|
1118
|
-
logger.warning(
|
|
1119
|
-
"M365 Graph API delivered server side error -> %s; retrying in %s seconds...",
|
|
1120
|
-
response.status_code,
|
|
1121
|
-
(retries + 1) * 60,
|
|
1122
|
-
)
|
|
1123
|
-
time.sleep((retries + 1) * 60)
|
|
1124
|
-
retries += 1
|
|
1125
|
-
else:
|
|
1126
|
-
if show_error:
|
|
1127
|
-
logger.error(
|
|
1128
|
-
"Failed to get M365 group -> %s; status -> %s; error -> %s",
|
|
1129
|
-
group_name,
|
|
1130
|
-
response.status_code,
|
|
1131
|
-
response.text,
|
|
1132
|
-
)
|
|
1133
|
-
else:
|
|
1134
|
-
logger.info("M365 Group -> %s not found.", group_name)
|
|
1135
|
-
return None
|
|
1223
|
+
return self.do_request(
|
|
1224
|
+
url=request_url,
|
|
1225
|
+
method="GET",
|
|
1226
|
+
headers=request_header,
|
|
1227
|
+
timeout=REQUEST_TIMEOUT,
|
|
1228
|
+
failure_message="Failed to get M365 group -> '{}'".format(group_name),
|
|
1229
|
+
show_error=show_error,
|
|
1230
|
+
)
|
|
1136
1231
|
|
|
1137
1232
|
# end method definition
|
|
1138
1233
|
|
|
@@ -1197,41 +1292,17 @@ class M365(object):
|
|
|
1197
1292
|
request_url = self.config()["groupsUrl"]
|
|
1198
1293
|
request_header = self.request_header()
|
|
1199
1294
|
|
|
1200
|
-
logger.
|
|
1201
|
-
logger.debug("M365 group attributes -> %s", group_post_body)
|
|
1295
|
+
logger.debug("Adding M365 group -> '%s'; calling -> %s", name, request_url)
|
|
1296
|
+
logger.debug("M365 group attributes -> %s", str(group_post_body))
|
|
1202
1297
|
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
if response.ok:
|
|
1212
|
-
return self.parse_request_response(response)
|
|
1213
|
-
# Check if Session has expired - then re-authenticate and try once more
|
|
1214
|
-
elif response.status_code == 401 and retries == 0:
|
|
1215
|
-
logger.warning("Session has expired - try to re-authenticate...")
|
|
1216
|
-
self.authenticate(revalidate=True)
|
|
1217
|
-
request_header = self.request_header()
|
|
1218
|
-
retries += 1
|
|
1219
|
-
elif response.status_code in [502, 503, 504] and retries < 3:
|
|
1220
|
-
logger.warning(
|
|
1221
|
-
"M365 Graph API delivered server side error -> %s; retrying in %s seconds...",
|
|
1222
|
-
response.status_code,
|
|
1223
|
-
(retries + 1) * 60,
|
|
1224
|
-
)
|
|
1225
|
-
time.sleep((retries + 1) * 60)
|
|
1226
|
-
retries += 1
|
|
1227
|
-
else:
|
|
1228
|
-
logger.error(
|
|
1229
|
-
"Failed to add M365 group -> %s; status -> %s; error -> %s",
|
|
1230
|
-
name,
|
|
1231
|
-
response.status_code,
|
|
1232
|
-
response.text,
|
|
1233
|
-
)
|
|
1234
|
-
return None
|
|
1298
|
+
return self.do_request(
|
|
1299
|
+
url=request_url,
|
|
1300
|
+
method="POST",
|
|
1301
|
+
headers=request_header,
|
|
1302
|
+
data=json.dumps(group_post_body),
|
|
1303
|
+
timeout=REQUEST_TIMEOUT,
|
|
1304
|
+
failure_message="Failed to add M365 group -> '{}'".format(name),
|
|
1305
|
+
)
|
|
1235
1306
|
|
|
1236
1307
|
# end method definition
|
|
1237
1308
|
|
|
@@ -1261,41 +1332,22 @@ class M365(object):
|
|
|
1261
1332
|
)
|
|
1262
1333
|
request_header = self.request_header()
|
|
1263
1334
|
|
|
1264
|
-
logger.
|
|
1335
|
+
logger.debug(
|
|
1265
1336
|
"Get members of M365 group -> %s (%s); calling -> %s",
|
|
1266
1337
|
group_name,
|
|
1267
1338
|
group_id,
|
|
1268
1339
|
request_url,
|
|
1269
1340
|
)
|
|
1270
1341
|
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
request_header = self.request_header()
|
|
1281
|
-
retries += 1
|
|
1282
|
-
elif response.status_code in [502, 503, 504] and retries < 3:
|
|
1283
|
-
logger.warning(
|
|
1284
|
-
"M365 Graph API delivered server side error -> %s; retrying in %s seconds...",
|
|
1285
|
-
response.status_code,
|
|
1286
|
-
(retries + 1) * 60,
|
|
1287
|
-
)
|
|
1288
|
-
time.sleep((retries + 1) * 60)
|
|
1289
|
-
retries += 1
|
|
1290
|
-
else:
|
|
1291
|
-
logger.error(
|
|
1292
|
-
"Failed to get members of M365 group -> %s (%s); status -> %s; error -> %s",
|
|
1293
|
-
group_name,
|
|
1294
|
-
group_id,
|
|
1295
|
-
response.status_code,
|
|
1296
|
-
response.text,
|
|
1297
|
-
)
|
|
1298
|
-
return None
|
|
1342
|
+
return self.do_request(
|
|
1343
|
+
url=request_url,
|
|
1344
|
+
method="GET",
|
|
1345
|
+
headers=request_header,
|
|
1346
|
+
timeout=REQUEST_TIMEOUT,
|
|
1347
|
+
failure_message="Failed to get members of M365 group -> '{}' ({})".format(
|
|
1348
|
+
group_name, group_id
|
|
1349
|
+
),
|
|
1350
|
+
)
|
|
1299
1351
|
|
|
1300
1352
|
# end method definition
|
|
1301
1353
|
|
|
@@ -1316,47 +1368,23 @@ class M365(object):
|
|
|
1316
1368
|
"@odata.id": self.config()["directoryObjects"] + "/" + member_id
|
|
1317
1369
|
}
|
|
1318
1370
|
|
|
1319
|
-
logger.
|
|
1371
|
+
logger.debug(
|
|
1320
1372
|
"Adding member -> %s to group -> %s; calling -> %s",
|
|
1321
1373
|
member_id,
|
|
1322
1374
|
group_id,
|
|
1323
1375
|
request_url,
|
|
1324
1376
|
)
|
|
1325
1377
|
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
# Check if Session has expired - then re-authenticate and try once more
|
|
1338
|
-
if response.status_code == 401 and retries == 0:
|
|
1339
|
-
logger.warning("Session has expired - try to re-authenticate...")
|
|
1340
|
-
self.authenticate(revalidate=True)
|
|
1341
|
-
request_header = self.request_header()
|
|
1342
|
-
retries += 1
|
|
1343
|
-
elif response.status_code in [502, 503, 504] and retries < 3:
|
|
1344
|
-
logger.warning(
|
|
1345
|
-
"M365 Graph API delivered server side error -> %s; retrying in %s seconds...",
|
|
1346
|
-
response.status_code,
|
|
1347
|
-
(retries + 1) * 60,
|
|
1348
|
-
)
|
|
1349
|
-
time.sleep((retries + 1) * 60)
|
|
1350
|
-
retries += 1
|
|
1351
|
-
else:
|
|
1352
|
-
logger.error(
|
|
1353
|
-
"Failed to add member -> %s to M365 group -> %s; status -> %s; error -> %s",
|
|
1354
|
-
member_id,
|
|
1355
|
-
group_id,
|
|
1356
|
-
response.status_code,
|
|
1357
|
-
response.text,
|
|
1358
|
-
)
|
|
1359
|
-
return None
|
|
1378
|
+
return self.do_request(
|
|
1379
|
+
url=request_url,
|
|
1380
|
+
method="POST",
|
|
1381
|
+
headers=request_header,
|
|
1382
|
+
data=json.dumps(group_member_post_body),
|
|
1383
|
+
timeout=REQUEST_TIMEOUT,
|
|
1384
|
+
failure_message="Failed to add member -> {} to M365 group -> {}".format(
|
|
1385
|
+
member_id, group_id
|
|
1386
|
+
),
|
|
1387
|
+
)
|
|
1360
1388
|
|
|
1361
1389
|
# end method definition
|
|
1362
1390
|
|
|
@@ -1379,47 +1407,28 @@ class M365(object):
|
|
|
1379
1407
|
)
|
|
1380
1408
|
request_header = self.request_header()
|
|
1381
1409
|
|
|
1382
|
-
logger.
|
|
1410
|
+
logger.debug(
|
|
1383
1411
|
"Check if user -> %s is in group -> %s; calling -> %s",
|
|
1384
1412
|
member_id,
|
|
1385
1413
|
group_id,
|
|
1386
1414
|
request_url,
|
|
1387
1415
|
)
|
|
1388
1416
|
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
logger.warning(
|
|
1405
|
-
"M365 Graph API delivered server side error -> %s; retrying in %s seconds...",
|
|
1406
|
-
response.status_code,
|
|
1407
|
-
(retries + 1) * 60,
|
|
1408
|
-
)
|
|
1409
|
-
time.sleep((retries + 1) * 60)
|
|
1410
|
-
retries += 1
|
|
1411
|
-
else:
|
|
1412
|
-
# MS Graph API returns an error if the member is not in the
|
|
1413
|
-
# group. This is typically not what we want. We just return False.
|
|
1414
|
-
if show_error:
|
|
1415
|
-
logger.error(
|
|
1416
|
-
"Failed to check if user -> %s is in group -> %s; status -> %s; error -> %s",
|
|
1417
|
-
member_id,
|
|
1418
|
-
group_id,
|
|
1419
|
-
response.status_code,
|
|
1420
|
-
response.text,
|
|
1421
|
-
)
|
|
1422
|
-
return False
|
|
1417
|
+
response = self.do_request(
|
|
1418
|
+
url=request_url,
|
|
1419
|
+
method="GET",
|
|
1420
|
+
headers=request_header,
|
|
1421
|
+
timeout=REQUEST_TIMEOUT,
|
|
1422
|
+
failure_message="Failed to check if user -> {} is in group -> {}".format(
|
|
1423
|
+
member_id, group_id
|
|
1424
|
+
),
|
|
1425
|
+
show_error=show_error,
|
|
1426
|
+
)
|
|
1427
|
+
|
|
1428
|
+
if not response or not "value" in response or len(response["value"]) == 0:
|
|
1429
|
+
return False
|
|
1430
|
+
|
|
1431
|
+
return True
|
|
1423
1432
|
|
|
1424
1433
|
# end method definition
|
|
1425
1434
|
|
|
@@ -1449,41 +1458,22 @@ class M365(object):
|
|
|
1449
1458
|
)
|
|
1450
1459
|
request_header = self.request_header()
|
|
1451
1460
|
|
|
1452
|
-
logger.
|
|
1461
|
+
logger.debug(
|
|
1453
1462
|
"Get owners of M365 group -> %s (%s); calling -> %s",
|
|
1454
1463
|
group_name,
|
|
1455
1464
|
group_id,
|
|
1456
1465
|
request_url,
|
|
1457
1466
|
)
|
|
1458
1467
|
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
request_header = self.request_header()
|
|
1469
|
-
retries += 1
|
|
1470
|
-
elif response.status_code in [502, 503, 504] and retries < 3:
|
|
1471
|
-
logger.warning(
|
|
1472
|
-
"M365 Graph API delivered server side error -> %s; retrying in %s seconds...",
|
|
1473
|
-
response.status_code,
|
|
1474
|
-
(retries + 1) * 60,
|
|
1475
|
-
)
|
|
1476
|
-
time.sleep((retries + 1) * 60)
|
|
1477
|
-
retries += 1
|
|
1478
|
-
else:
|
|
1479
|
-
logger.error(
|
|
1480
|
-
"Failed to get owners of M365 group -> %s (%s); status -> %s; error -> %s",
|
|
1481
|
-
group_name,
|
|
1482
|
-
group_id,
|
|
1483
|
-
response.status_code,
|
|
1484
|
-
response.text,
|
|
1485
|
-
)
|
|
1486
|
-
return None
|
|
1468
|
+
return self.do_request(
|
|
1469
|
+
url=request_url,
|
|
1470
|
+
method="GET",
|
|
1471
|
+
headers=request_header,
|
|
1472
|
+
timeout=REQUEST_TIMEOUT,
|
|
1473
|
+
failure_message="Failed to get owners of M365 group -> '{}' ({})".format(
|
|
1474
|
+
group_name, group_id
|
|
1475
|
+
),
|
|
1476
|
+
)
|
|
1487
1477
|
|
|
1488
1478
|
# end method definition
|
|
1489
1479
|
|
|
@@ -1504,46 +1494,23 @@ class M365(object):
|
|
|
1504
1494
|
"@odata.id": self.config()["directoryObjects"] + "/" + owner_id
|
|
1505
1495
|
}
|
|
1506
1496
|
|
|
1507
|
-
logger.
|
|
1497
|
+
logger.debug(
|
|
1508
1498
|
"Adding owner -> %s to M365 group -> %s; calling -> %s",
|
|
1509
1499
|
owner_id,
|
|
1510
1500
|
group_id,
|
|
1511
1501
|
request_url,
|
|
1512
1502
|
)
|
|
1513
1503
|
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
# Check if Session has expired - then re-authenticate and try once more
|
|
1525
|
-
elif response.status_code == 401 and retries == 0:
|
|
1526
|
-
logger.warning("Session has expired - try to re-authenticate...")
|
|
1527
|
-
self.authenticate(revalidate=True)
|
|
1528
|
-
request_header = self.request_header()
|
|
1529
|
-
retries += 1
|
|
1530
|
-
elif response.status_code in [502, 503, 504] and retries < 3:
|
|
1531
|
-
logger.warning(
|
|
1532
|
-
"M365 Graph API delivered server side error -> %s; retrying in %s seconds...",
|
|
1533
|
-
response.status_code,
|
|
1534
|
-
(retries + 1) * 60,
|
|
1535
|
-
)
|
|
1536
|
-
time.sleep((retries + 1) * 60)
|
|
1537
|
-
retries += 1
|
|
1538
|
-
else:
|
|
1539
|
-
logger.error(
|
|
1540
|
-
"Failed to add owner -> %s to M365 group -> %s; status -> %s; error -> %s",
|
|
1541
|
-
owner_id,
|
|
1542
|
-
group_id,
|
|
1543
|
-
response.status_code,
|
|
1544
|
-
response.text,
|
|
1545
|
-
)
|
|
1546
|
-
return None
|
|
1504
|
+
return self.do_request(
|
|
1505
|
+
url=request_url,
|
|
1506
|
+
method="POST",
|
|
1507
|
+
headers=request_header,
|
|
1508
|
+
data=json.dumps(group_member_post_body),
|
|
1509
|
+
timeout=REQUEST_TIMEOUT,
|
|
1510
|
+
failure_message="Failed to add owner -> {} to M365 group -> {}".format(
|
|
1511
|
+
owner_id, group_id
|
|
1512
|
+
),
|
|
1513
|
+
)
|
|
1547
1514
|
|
|
1548
1515
|
# end method definition
|
|
1549
1516
|
|
|
@@ -1558,7 +1525,9 @@ class M365(object):
|
|
|
1558
1525
|
request_url = (
|
|
1559
1526
|
self.config()["directoryUrl"] + "/deletedItems/microsoft.graph.group"
|
|
1560
1527
|
)
|
|
1561
|
-
response = requests.get(
|
|
1528
|
+
response = requests.get(
|
|
1529
|
+
request_url, headers=request_header, timeout=REQUEST_TIMEOUT
|
|
1530
|
+
)
|
|
1562
1531
|
deleted_groups = self.parse_request_response(response)
|
|
1563
1532
|
|
|
1564
1533
|
for group in deleted_groups["value"]:
|
|
@@ -1568,7 +1537,9 @@ class M365(object):
|
|
|
1568
1537
|
request_url = (
|
|
1569
1538
|
self.config()["directoryUrl"] + "/deletedItems/microsoft.graph.user"
|
|
1570
1539
|
)
|
|
1571
|
-
response = requests.get(
|
|
1540
|
+
response = requests.get(
|
|
1541
|
+
request_url, headers=request_header, timeout=REQUEST_TIMEOUT
|
|
1542
|
+
)
|
|
1572
1543
|
deleted_users = self.parse_request_response(response)
|
|
1573
1544
|
|
|
1574
1545
|
for user in deleted_users["value"]:
|
|
@@ -1591,35 +1562,15 @@ class M365(object):
|
|
|
1591
1562
|
request_url = self.config()["directoryUrl"] + "/deletedItems/" + item_id
|
|
1592
1563
|
request_header = self.request_header()
|
|
1593
1564
|
|
|
1594
|
-
logger.
|
|
1565
|
+
logger.debug("Purging deleted item -> %s; calling -> %s", item_id, request_url)
|
|
1595
1566
|
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
logger.warning("Session has expired - try to re-authenticate...")
|
|
1604
|
-
self.authenticate(revalidate=True)
|
|
1605
|
-
request_header = self.request_header()
|
|
1606
|
-
retries += 1
|
|
1607
|
-
elif response.status_code in [502, 503, 504] and retries < 3:
|
|
1608
|
-
logger.warning(
|
|
1609
|
-
"M365 Graph API delivered server side error -> %s; retrying in %s seconds...",
|
|
1610
|
-
response.status_code,
|
|
1611
|
-
(retries + 1) * 60,
|
|
1612
|
-
)
|
|
1613
|
-
time.sleep((retries + 1) * 60)
|
|
1614
|
-
retries += 1
|
|
1615
|
-
else:
|
|
1616
|
-
logger.error(
|
|
1617
|
-
"Failed to purge deleted item -> %s; status -> %s; error -> %s",
|
|
1618
|
-
item_id,
|
|
1619
|
-
response.status_code,
|
|
1620
|
-
response.text,
|
|
1621
|
-
)
|
|
1622
|
-
return None
|
|
1567
|
+
return self.do_request(
|
|
1568
|
+
url=request_url,
|
|
1569
|
+
method="DELETE",
|
|
1570
|
+
headers=request_header,
|
|
1571
|
+
timeout=REQUEST_TIMEOUT,
|
|
1572
|
+
failure_message="Failed to purge deleted item -> {}".format(item_id),
|
|
1573
|
+
)
|
|
1623
1574
|
|
|
1624
1575
|
# end method definition
|
|
1625
1576
|
|
|
@@ -1644,43 +1595,33 @@ class M365(object):
|
|
|
1644
1595
|
request_url = self.config()["groupsUrl"] + "/" + group_id + "/team"
|
|
1645
1596
|
request_header = self.request_header()
|
|
1646
1597
|
|
|
1647
|
-
logger.
|
|
1598
|
+
logger.debug(
|
|
1648
1599
|
"Check if M365 Group -> %s has a M365 Team connected; calling -> %s",
|
|
1649
1600
|
group_name,
|
|
1650
1601
|
request_url,
|
|
1651
1602
|
)
|
|
1652
1603
|
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1604
|
+
response = self.do_request(
|
|
1605
|
+
url=request_url,
|
|
1606
|
+
method="GET",
|
|
1607
|
+
headers=request_header,
|
|
1608
|
+
timeout=REQUEST_TIMEOUT,
|
|
1609
|
+
failure_message="Failed to check if M365 Group -> '{}' has a M365 Team connected".format(
|
|
1610
|
+
group_name
|
|
1611
|
+
),
|
|
1612
|
+
parse_request_response=False,
|
|
1613
|
+
)
|
|
1656
1614
|
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
retries += 1
|
|
1668
|
-
elif response.status_code in [502, 503, 504] and retries < 3:
|
|
1669
|
-
logger.warning(
|
|
1670
|
-
"M365 Graph API delivered server side error -> %s; retrying in %s seconds...",
|
|
1671
|
-
response.status_code,
|
|
1672
|
-
(retries + 1) * 60,
|
|
1673
|
-
)
|
|
1674
|
-
time.sleep((retries + 1) * 60)
|
|
1675
|
-
retries += 1
|
|
1676
|
-
else:
|
|
1677
|
-
logger.error(
|
|
1678
|
-
"Failed to check if M365 Group -> %s has a M365 Team connected; status -> %s; error -> %s",
|
|
1679
|
-
group_name,
|
|
1680
|
-
response.status_code,
|
|
1681
|
-
response.text,
|
|
1682
|
-
)
|
|
1683
|
-
return False
|
|
1615
|
+
if response and response.status_code == 200: # Group has a Team assigned!
|
|
1616
|
+
logger.debug("Group -> %s has a M365 Team connected.", group_name)
|
|
1617
|
+
return True
|
|
1618
|
+
elif (
|
|
1619
|
+
not response or response.status_code == 404
|
|
1620
|
+
): # Group does not have a Team assigned!
|
|
1621
|
+
logger.debug("Group -> %s has no M365 Team connected.", group_name)
|
|
1622
|
+
return False
|
|
1623
|
+
|
|
1624
|
+
return False
|
|
1684
1625
|
|
|
1685
1626
|
# end method definition
|
|
1686
1627
|
|
|
@@ -1734,39 +1675,19 @@ class M365(object):
|
|
|
1734
1675
|
|
|
1735
1676
|
request_header = self.request_header()
|
|
1736
1677
|
|
|
1737
|
-
logger.
|
|
1738
|
-
"Lookup Microsoft 365 Teams with name -> %s; calling -> %s",
|
|
1678
|
+
logger.debug(
|
|
1679
|
+
"Lookup Microsoft 365 Teams with name -> '%s'; calling -> %s",
|
|
1739
1680
|
name,
|
|
1740
1681
|
request_url,
|
|
1741
1682
|
)
|
|
1742
1683
|
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
logger.warning("Session has expired - try to re-authenticate...")
|
|
1751
|
-
self.authenticate(revalidate=True)
|
|
1752
|
-
request_header = self.request_header()
|
|
1753
|
-
retries += 1
|
|
1754
|
-
elif response.status_code in [502, 503, 504] and retries < 3:
|
|
1755
|
-
logger.warning(
|
|
1756
|
-
"M365 Graph API delivered server side error -> %s; retrying in %s seconds...",
|
|
1757
|
-
response.status_code,
|
|
1758
|
-
(retries + 1) * 60,
|
|
1759
|
-
)
|
|
1760
|
-
time.sleep((retries + 1) * 60)
|
|
1761
|
-
retries += 1
|
|
1762
|
-
else:
|
|
1763
|
-
logger.error(
|
|
1764
|
-
"Failed to get M365 Team -> %s; status -> %s; error -> %s",
|
|
1765
|
-
name,
|
|
1766
|
-
response.status_code,
|
|
1767
|
-
response.text,
|
|
1768
|
-
)
|
|
1769
|
-
return None
|
|
1684
|
+
return self.do_request(
|
|
1685
|
+
url=request_url,
|
|
1686
|
+
method="GET",
|
|
1687
|
+
headers=request_header,
|
|
1688
|
+
timeout=REQUEST_TIMEOUT,
|
|
1689
|
+
failure_message="Failed to get M365 Team -> '{}'".format(name),
|
|
1690
|
+
)
|
|
1770
1691
|
|
|
1771
1692
|
# end method definition
|
|
1772
1693
|
|
|
@@ -1785,7 +1706,7 @@ class M365(object):
|
|
|
1785
1706
|
group_id = self.get_result_value(response, "id", 0)
|
|
1786
1707
|
if not group_id:
|
|
1787
1708
|
logger.error(
|
|
1788
|
-
"M365 Group -> %s not found. It is required for creating a corresponding M365 Team.",
|
|
1709
|
+
"M365 Group -> '%s' not found. It is required for creating a corresponding M365 Team.",
|
|
1789
1710
|
name,
|
|
1790
1711
|
)
|
|
1791
1712
|
return None
|
|
@@ -1793,7 +1714,7 @@ class M365(object):
|
|
|
1793
1714
|
response = self.get_group_owners(name)
|
|
1794
1715
|
if response is None or not "value" in response or not response["value"]:
|
|
1795
1716
|
logger.warning(
|
|
1796
|
-
"M365 Group -> %s has no owners. This is required for creating a corresponding M365 Team.",
|
|
1717
|
+
"M365 Group -> '%s' has no owners. This is required for creating a corresponding M365 Team.",
|
|
1797
1718
|
name,
|
|
1798
1719
|
)
|
|
1799
1720
|
return None
|
|
@@ -1808,41 +1729,17 @@ class M365(object):
|
|
|
1808
1729
|
request_url = self.config()["teamsUrl"]
|
|
1809
1730
|
request_header = self.request_header()
|
|
1810
1731
|
|
|
1811
|
-
logger.
|
|
1812
|
-
logger.debug("M365 Team attributes -> %s", team_post_body)
|
|
1732
|
+
logger.debug("Adding M365 Team -> '%s'; calling -> %s", name, request_url)
|
|
1733
|
+
logger.debug("M365 Team attributes -> %s", str(team_post_body))
|
|
1813
1734
|
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
if response.ok:
|
|
1823
|
-
return self.parse_request_response(response)
|
|
1824
|
-
# Check if Session has expired - then re-authenticate and try once more
|
|
1825
|
-
elif response.status_code == 401 and retries == 0:
|
|
1826
|
-
logger.warning("Session has expired - try to re-authenticate...")
|
|
1827
|
-
self.authenticate(revalidate=True)
|
|
1828
|
-
request_header = self.request_header()
|
|
1829
|
-
retries += 1
|
|
1830
|
-
elif response.status_code in [502, 503, 504] and retries < 3:
|
|
1831
|
-
logger.warning(
|
|
1832
|
-
"M365 Graph API delivered server side error -> %s; retrying in %s seconds...",
|
|
1833
|
-
response.status_code,
|
|
1834
|
-
(retries + 1) * 60,
|
|
1835
|
-
)
|
|
1836
|
-
time.sleep((retries + 1) * 60)
|
|
1837
|
-
retries += 1
|
|
1838
|
-
else:
|
|
1839
|
-
logger.error(
|
|
1840
|
-
"Failed to add M365 Team -> %s; status -> %s; error -> %s",
|
|
1841
|
-
name,
|
|
1842
|
-
response.status_code,
|
|
1843
|
-
response.text,
|
|
1844
|
-
)
|
|
1845
|
-
return None
|
|
1735
|
+
return self.do_request(
|
|
1736
|
+
url=request_url,
|
|
1737
|
+
method="POST",
|
|
1738
|
+
data=json.dumps(team_post_body),
|
|
1739
|
+
headers=request_header,
|
|
1740
|
+
timeout=REQUEST_TIMEOUT,
|
|
1741
|
+
failure_message="Failed to add M365 Team -> '{}'".format(name),
|
|
1742
|
+
)
|
|
1846
1743
|
|
|
1847
1744
|
# end method definition
|
|
1848
1745
|
|
|
@@ -1859,34 +1756,19 @@ class M365(object):
|
|
|
1859
1756
|
|
|
1860
1757
|
request_header = self.request_header()
|
|
1861
1758
|
|
|
1862
|
-
logger.
|
|
1759
|
+
logger.debug(
|
|
1863
1760
|
"Delete Microsoft 365 Teams with ID -> %s; calling -> %s",
|
|
1864
1761
|
team_id,
|
|
1865
1762
|
request_url,
|
|
1866
1763
|
)
|
|
1867
1764
|
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
logger.warning("Session has expired - try to re-authenticate...")
|
|
1876
|
-
self.authenticate(revalidate=True)
|
|
1877
|
-
request_header = self.request_header()
|
|
1878
|
-
retries += 1
|
|
1879
|
-
elif response.status_code in [502, 503, 504] and retries < 3:
|
|
1880
|
-
logger.warning(
|
|
1881
|
-
"M365 Graph API delivered server side error -> %s; retrying in %s seconds...",
|
|
1882
|
-
response.status_code,
|
|
1883
|
-
(retries + 1) * 60,
|
|
1884
|
-
)
|
|
1885
|
-
time.sleep((retries + 1) * 60)
|
|
1886
|
-
retries += 1
|
|
1887
|
-
else:
|
|
1888
|
-
logger.error("Failed to delete M365 Team with ID -> %s", team_id)
|
|
1889
|
-
return None
|
|
1765
|
+
return self.do_request(
|
|
1766
|
+
url=request_url,
|
|
1767
|
+
method="DELETE",
|
|
1768
|
+
headers=request_header,
|
|
1769
|
+
timeout=REQUEST_TIMEOUT,
|
|
1770
|
+
failure_message="Failed to delete M365 Team with ID -> {}".format(team_id),
|
|
1771
|
+
)
|
|
1890
1772
|
|
|
1891
1773
|
# end method definition
|
|
1892
1774
|
|
|
@@ -1911,40 +1793,19 @@ class M365(object):
|
|
|
1911
1793
|
|
|
1912
1794
|
request_header = self.request_header()
|
|
1913
1795
|
|
|
1914
|
-
logger.
|
|
1915
|
-
"Delete all Microsoft 365 Teams with name -> %s; calling -> %s",
|
|
1796
|
+
logger.debug(
|
|
1797
|
+
"Delete all Microsoft 365 Teams with name -> '%s'; calling -> %s",
|
|
1916
1798
|
name,
|
|
1917
1799
|
request_url,
|
|
1918
1800
|
)
|
|
1919
1801
|
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
elif response.status_code == 401 and retries == 0:
|
|
1928
|
-
logger.warning("Session has expired - try to re-authenticate...")
|
|
1929
|
-
self.authenticate(revalidate=True)
|
|
1930
|
-
request_header = self.request_header()
|
|
1931
|
-
retries += 1
|
|
1932
|
-
elif response.status_code in [502, 503, 504] and retries < 3:
|
|
1933
|
-
logger.warning(
|
|
1934
|
-
"M365 Graph API delivered server side error -> %s; retrying in %s seconds...",
|
|
1935
|
-
response.status_code,
|
|
1936
|
-
(retries + 1) * 60,
|
|
1937
|
-
)
|
|
1938
|
-
time.sleep((retries + 1) * 60)
|
|
1939
|
-
retries += 1
|
|
1940
|
-
else:
|
|
1941
|
-
logger.error(
|
|
1942
|
-
"Failed to get list of M365 Teams to delete; status -> %s; error -> %s",
|
|
1943
|
-
response.status_code,
|
|
1944
|
-
response.text,
|
|
1945
|
-
)
|
|
1946
|
-
existing_teams = None
|
|
1947
|
-
break
|
|
1802
|
+
existing_teams = self.do_request(
|
|
1803
|
+
url=request_url,
|
|
1804
|
+
method="GET",
|
|
1805
|
+
headers=request_header,
|
|
1806
|
+
timeout=REQUEST_TIMEOUT,
|
|
1807
|
+
failure_message="Failed to get list of M365 Teams to delete",
|
|
1808
|
+
)
|
|
1948
1809
|
|
|
1949
1810
|
if existing_teams:
|
|
1950
1811
|
data = existing_teams.get("value")
|
|
@@ -1956,22 +1817,22 @@ class M365(object):
|
|
|
1956
1817
|
|
|
1957
1818
|
if not response:
|
|
1958
1819
|
logger.error(
|
|
1959
|
-
"Failed to delete M365 Team -> %s (%s)", name, team_id
|
|
1820
|
+
"Failed to delete M365 Team -> '%s' (%s)", name, team_id
|
|
1960
1821
|
)
|
|
1961
1822
|
continue
|
|
1962
1823
|
counter += 1
|
|
1963
1824
|
|
|
1964
1825
|
logger.info(
|
|
1965
|
-
"%s M365 Teams with name -> %s have been deleted.",
|
|
1826
|
+
"%s M365 Teams with name -> '%s' have been deleted.",
|
|
1966
1827
|
str(counter),
|
|
1967
1828
|
name,
|
|
1968
1829
|
)
|
|
1969
1830
|
return True
|
|
1970
1831
|
else:
|
|
1971
|
-
logger.info("No M365 Teams with name -> %s found.", name)
|
|
1832
|
+
logger.info("No M365 Teams with name -> '%s' found.", name)
|
|
1972
1833
|
return False
|
|
1973
1834
|
else:
|
|
1974
|
-
logger.error("Failed to retrieve M365 Teams with name -> %s", name)
|
|
1835
|
+
logger.error("Failed to retrieve M365 Teams with name -> '%s'", name)
|
|
1975
1836
|
return False
|
|
1976
1837
|
|
|
1977
1838
|
# end method definition
|
|
@@ -1995,19 +1856,20 @@ class M365(object):
|
|
|
1995
1856
|
if not "value" in response or not response["value"]:
|
|
1996
1857
|
return False
|
|
1997
1858
|
groups = response["value"]
|
|
1859
|
+
|
|
1998
1860
|
logger.info(
|
|
1999
1861
|
"Found -> %s existing M365 groups. Checking which ones should be deleted...",
|
|
2000
1862
|
len(groups),
|
|
2001
1863
|
)
|
|
2002
1864
|
|
|
2003
|
-
# Process all groups and check if
|
|
1865
|
+
# Process all groups and check if they should be
|
|
2004
1866
|
# deleted:
|
|
2005
1867
|
for group in groups:
|
|
2006
1868
|
group_name = group["displayName"]
|
|
2007
1869
|
# Check if group is in exception list:
|
|
2008
1870
|
if group_name in exception_list:
|
|
2009
1871
|
logger.info(
|
|
2010
|
-
"M365 Group name -> %s is on the exception list. Skipping...",
|
|
1872
|
+
"M365 Group name -> '%s' is on the exception list. Skipping...",
|
|
2011
1873
|
group_name,
|
|
2012
1874
|
)
|
|
2013
1875
|
continue
|
|
@@ -2016,7 +1878,7 @@ class M365(object):
|
|
|
2016
1878
|
result = re.search(pattern, group_name)
|
|
2017
1879
|
if result:
|
|
2018
1880
|
logger.info(
|
|
2019
|
-
"M365 Group name -> %s is matching pattern -> %s. Delete it now...",
|
|
1881
|
+
"M365 Group name -> '%s' is matching pattern -> %s. Delete it now...",
|
|
2020
1882
|
group_name,
|
|
2021
1883
|
pattern,
|
|
2022
1884
|
)
|
|
@@ -2024,7 +1886,7 @@ class M365(object):
|
|
|
2024
1886
|
break
|
|
2025
1887
|
else:
|
|
2026
1888
|
logger.info(
|
|
2027
|
-
"M365 Group name -> %s is not matching any delete pattern. Skipping...",
|
|
1889
|
+
"M365 Group name -> '%s' is not matching any delete pattern. Skipping...",
|
|
2028
1890
|
group_name,
|
|
2029
1891
|
)
|
|
2030
1892
|
return True
|
|
@@ -2068,39 +1930,21 @@ class M365(object):
|
|
|
2068
1930
|
|
|
2069
1931
|
request_header = self.request_header()
|
|
2070
1932
|
|
|
2071
|
-
logger.
|
|
2072
|
-
"Retrieve channels of Microsoft 365 Team -> %s; calling -> %s",
|
|
1933
|
+
logger.debug(
|
|
1934
|
+
"Retrieve channels of Microsoft 365 Team -> '%s'; calling -> %s",
|
|
2073
1935
|
name,
|
|
2074
1936
|
request_url,
|
|
2075
1937
|
)
|
|
2076
1938
|
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
request_header = self.request_header()
|
|
2087
|
-
retries += 1
|
|
2088
|
-
elif response.status_code in [502, 503, 504] and retries < 3:
|
|
2089
|
-
logger.warning(
|
|
2090
|
-
"M365 Graph API delivered server side error -> %s; retrying in %s seconds...",
|
|
2091
|
-
response.status_code,
|
|
2092
|
-
(retries + 1) * 60,
|
|
2093
|
-
)
|
|
2094
|
-
time.sleep((retries + 1) * 60)
|
|
2095
|
-
retries += 1
|
|
2096
|
-
else:
|
|
2097
|
-
logger.error(
|
|
2098
|
-
"Failed to get Channels for M365 Team -> %s; status -> %s; error -> %s",
|
|
2099
|
-
name,
|
|
2100
|
-
response.status_code,
|
|
2101
|
-
response.text,
|
|
2102
|
-
)
|
|
2103
|
-
return None
|
|
1939
|
+
return self.do_request(
|
|
1940
|
+
url=request_url,
|
|
1941
|
+
method="GET",
|
|
1942
|
+
headers=request_header,
|
|
1943
|
+
timeout=REQUEST_TIMEOUT,
|
|
1944
|
+
failure_message="Failed to get Channels for M365 Team -> '{}' ({})".format(
|
|
1945
|
+
name, team_id
|
|
1946
|
+
),
|
|
1947
|
+
)
|
|
2104
1948
|
|
|
2105
1949
|
# end method definition
|
|
2106
1950
|
|
|
@@ -2151,7 +1995,7 @@ class M365(object):
|
|
|
2151
1995
|
None,
|
|
2152
1996
|
)
|
|
2153
1997
|
if not channel:
|
|
2154
|
-
logger.
|
|
1998
|
+
logger.error(
|
|
2155
1999
|
"Cannot find Channel -> %s on M365 Team -> %s", channel_name, team_name
|
|
2156
2000
|
)
|
|
2157
2001
|
return None
|
|
@@ -2168,43 +2012,22 @@ class M365(object):
|
|
|
2168
2012
|
|
|
2169
2013
|
request_header = self.request_header()
|
|
2170
2014
|
|
|
2171
|
-
logger.
|
|
2015
|
+
logger.debug(
|
|
2172
2016
|
"Retrieve Tabs of Microsoft 365 Teams -> %s and Channel -> %s; calling -> %s",
|
|
2173
2017
|
team_name,
|
|
2174
2018
|
channel_name,
|
|
2175
2019
|
request_url,
|
|
2176
2020
|
)
|
|
2177
2021
|
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
request_header = self.request_header()
|
|
2188
|
-
retries += 1
|
|
2189
|
-
elif response.status_code in [502, 503, 504] and retries < 3:
|
|
2190
|
-
logger.warning(
|
|
2191
|
-
"M365 Graph API delivered server side error -> %s; retrying in %s seconds...",
|
|
2192
|
-
response.status_code,
|
|
2193
|
-
(retries + 1) * 60,
|
|
2194
|
-
)
|
|
2195
|
-
time.sleep((retries + 1) * 60)
|
|
2196
|
-
retries += 1
|
|
2197
|
-
else:
|
|
2198
|
-
logger.error(
|
|
2199
|
-
"Failed to get Tabs for M365 Team -> %s (%s) and Channel -> %s (%s); status -> %s; error -> %s",
|
|
2200
|
-
team_name,
|
|
2201
|
-
team_id,
|
|
2202
|
-
channel_name,
|
|
2203
|
-
channel_id,
|
|
2204
|
-
response.status_code,
|
|
2205
|
-
response.text,
|
|
2206
|
-
)
|
|
2207
|
-
return None
|
|
2022
|
+
return self.do_request(
|
|
2023
|
+
url=request_url,
|
|
2024
|
+
method="GET",
|
|
2025
|
+
headers=request_header,
|
|
2026
|
+
timeout=REQUEST_TIMEOUT,
|
|
2027
|
+
failure_message="Failed to get Tabs for M365 Team -> '{}' ({}) and Channel -> '{}' ({})".format(
|
|
2028
|
+
team_name, team_id, channel_name, channel_id
|
|
2029
|
+
),
|
|
2030
|
+
)
|
|
2208
2031
|
|
|
2209
2032
|
# end method definition
|
|
2210
2033
|
|
|
@@ -2257,92 +2080,82 @@ class M365(object):
|
|
|
2257
2080
|
request_url = self.config()["teamsAppsUrl"] + "?" + encoded_query
|
|
2258
2081
|
|
|
2259
2082
|
if filter_expression:
|
|
2260
|
-
logger.
|
|
2083
|
+
logger.debug(
|
|
2261
2084
|
"Get list of MS Teams Apps using filter -> %s; calling -> %s",
|
|
2262
2085
|
filter_expression,
|
|
2263
2086
|
request_url,
|
|
2264
2087
|
)
|
|
2088
|
+
failure_message = (
|
|
2089
|
+
"Failed to get list of M365 Teams apps using filter -> {}".format(
|
|
2090
|
+
filter_expression
|
|
2091
|
+
)
|
|
2092
|
+
)
|
|
2265
2093
|
else:
|
|
2266
|
-
logger.
|
|
2094
|
+
logger.debug("Get list of all MS Teams Apps; calling -> %s", request_url)
|
|
2095
|
+
failure_message = "Failed to get list of M365 Teams apps"
|
|
2267
2096
|
|
|
2268
2097
|
request_header = self.request_header()
|
|
2269
2098
|
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
logger.warning("Session has expired - try to re-authenticate...")
|
|
2278
|
-
self.authenticate(revalidate=True)
|
|
2279
|
-
request_header = self.request_header()
|
|
2280
|
-
retries += 1
|
|
2281
|
-
elif response.status_code in [502, 503, 504] and retries < 3:
|
|
2282
|
-
logger.warning(
|
|
2283
|
-
"M365 Graph API delivered server side error -> %s; retrying in %s seconds...",
|
|
2284
|
-
response.status_code,
|
|
2285
|
-
(retries + 1) * 60,
|
|
2286
|
-
)
|
|
2287
|
-
time.sleep((retries + 1) * 60)
|
|
2288
|
-
retries += 1
|
|
2289
|
-
else:
|
|
2290
|
-
logger.error(
|
|
2291
|
-
"Failed to get list of M365 Teams apps; status -> %s; error -> %s",
|
|
2292
|
-
response.status_code,
|
|
2293
|
-
response.text,
|
|
2294
|
-
)
|
|
2295
|
-
return None
|
|
2099
|
+
return self.do_request(
|
|
2100
|
+
url=request_url,
|
|
2101
|
+
method="GET",
|
|
2102
|
+
headers=request_header,
|
|
2103
|
+
timeout=REQUEST_TIMEOUT,
|
|
2104
|
+
failure_message=failure_message,
|
|
2105
|
+
)
|
|
2296
2106
|
|
|
2297
2107
|
# end method definition
|
|
2298
2108
|
|
|
2299
2109
|
def get_teams_app(self, app_id: str) -> dict | None:
|
|
2300
|
-
"""Get a specific MS Teams app in catalog based on the known app ID
|
|
2110
|
+
"""Get a specific MS Teams app in catalog based on the known (internal) app ID
|
|
2301
2111
|
|
|
2302
2112
|
Args:
|
|
2303
|
-
app_id (str): ID of the app
|
|
2113
|
+
app_id (str): ID of the app (this is NOT the external ID but the internal ID)
|
|
2304
2114
|
Returns:
|
|
2305
2115
|
dict: response of the MS Graph API call or None if the call fails.
|
|
2116
|
+
|
|
2117
|
+
Examle response:
|
|
2118
|
+
{
|
|
2119
|
+
'@odata.context': 'https://graph.microsoft.com/v1.0/$metadata#appCatalogs/teamsApps(appDefinitions())/$entity',
|
|
2120
|
+
'id': 'ccabe3fb-316f-40e0-a486-1659682cb8cd',
|
|
2121
|
+
'externalId': 'dd4af790-d8ff-47a0-87ad-486318272c7a',
|
|
2122
|
+
'displayName': 'Extended ECM',
|
|
2123
|
+
'distributionMethod': 'organization',
|
|
2124
|
+
'appDefinitions@odata.context': "https://graph.microsoft.com/v1.0/$metadata#appCatalogs/teamsApps('ccabe3fb-316f-40e0-a486-1659682cb8cd')/appDefinitions",
|
|
2125
|
+
'appDefinitions': [
|
|
2126
|
+
{
|
|
2127
|
+
'id': 'Y2NhYmUzZmItMzE2Zi00MGUwLWE0ODYtMTY1OTY4MmNiOGNkIyMyNC4yLjAjI1B1Ymxpc2hlZA==',
|
|
2128
|
+
'teamsAppId': 'ccabe3fb-316f-40e0-a486-1659682cb8cd',
|
|
2129
|
+
'displayName': 'Extended ECM',
|
|
2130
|
+
'version': '24.2.0',
|
|
2131
|
+
'publishingState': 'published',
|
|
2132
|
+
'shortDescription': 'Add a tab for an Extended ECM business workspace.',
|
|
2133
|
+
'description': 'View and interact with OpenText Extended ECM business workspaces',
|
|
2134
|
+
'lastModifiedDateTime': None,
|
|
2135
|
+
'createdBy': None,
|
|
2136
|
+
'authorization': {...}
|
|
2137
|
+
}
|
|
2138
|
+
]
|
|
2139
|
+
}
|
|
2306
2140
|
"""
|
|
2307
2141
|
|
|
2308
2142
|
query = {"$expand": "AppDefinitions"}
|
|
2309
2143
|
encoded_query = urllib.parse.urlencode(query, doseq=True)
|
|
2310
2144
|
request_url = self.config()["teamsAppsUrl"] + "/" + app_id + "?" + encoded_query
|
|
2311
2145
|
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
logger.info(
|
|
2315
|
-
"Get MS Teams App with ID -> %s; calling -> %s", app_id, request_url
|
|
2146
|
+
logger.debug(
|
|
2147
|
+
"Get M365 Teams App with ID -> %s; calling -> %s", app_id, request_url
|
|
2316
2148
|
)
|
|
2317
2149
|
|
|
2318
2150
|
request_header = self.request_header()
|
|
2319
2151
|
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
logger.warning("Session has expired - try to re-authenticate...")
|
|
2328
|
-
self.authenticate(revalidate=True)
|
|
2329
|
-
request_header = self.request_header()
|
|
2330
|
-
retries += 1
|
|
2331
|
-
elif response.status_code in [502, 503, 504] and retries < 3:
|
|
2332
|
-
logger.warning(
|
|
2333
|
-
"M365 Graph API delivered server side error -> %s; retrying in %s seconds...",
|
|
2334
|
-
response.status_code,
|
|
2335
|
-
(retries + 1) * 60,
|
|
2336
|
-
)
|
|
2337
|
-
time.sleep((retries + 1) * 60)
|
|
2338
|
-
retries += 1
|
|
2339
|
-
else:
|
|
2340
|
-
logger.error(
|
|
2341
|
-
"Failed to get list of M365 Teams apps; status -> %s; error -> %s",
|
|
2342
|
-
response.status_code,
|
|
2343
|
-
response.text,
|
|
2344
|
-
)
|
|
2345
|
-
return None
|
|
2152
|
+
return self.do_request(
|
|
2153
|
+
url=request_url,
|
|
2154
|
+
method="GET",
|
|
2155
|
+
headers=request_header,
|
|
2156
|
+
timeout=REQUEST_TIMEOUT,
|
|
2157
|
+
failure_message="Failed to get M365 Teams app with ID -> {}".format(app_id),
|
|
2158
|
+
)
|
|
2346
2159
|
|
|
2347
2160
|
# end method definition
|
|
2348
2161
|
|
|
@@ -2370,7 +2183,8 @@ class M365(object):
|
|
|
2370
2183
|
+ "/teamwork/installedApps?"
|
|
2371
2184
|
+ encoded_query
|
|
2372
2185
|
)
|
|
2373
|
-
|
|
2186
|
+
|
|
2187
|
+
logger.debug(
|
|
2374
2188
|
"Get list of M365 Teams Apps for user -> %s using query -> %s; calling -> %s",
|
|
2375
2189
|
user_id,
|
|
2376
2190
|
query,
|
|
@@ -2379,33 +2193,15 @@ class M365(object):
|
|
|
2379
2193
|
|
|
2380
2194
|
request_header = self.request_header()
|
|
2381
2195
|
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
request_header = self.request_header()
|
|
2392
|
-
retries += 1
|
|
2393
|
-
elif response.status_code in [502, 503, 504] and retries < 3:
|
|
2394
|
-
logger.warning(
|
|
2395
|
-
"M365 Graph API delivered server side error -> %s; retrying in %s seconds...",
|
|
2396
|
-
response.status_code,
|
|
2397
|
-
(retries + 1) * 60,
|
|
2398
|
-
)
|
|
2399
|
-
time.sleep((retries + 1) * 60)
|
|
2400
|
-
retries += 1
|
|
2401
|
-
else:
|
|
2402
|
-
logger.error(
|
|
2403
|
-
"Failed to get list of M365 Teams Apps for user -> %s; status -> %s; error -> %s",
|
|
2404
|
-
user_id,
|
|
2405
|
-
response.status_code,
|
|
2406
|
-
response.text,
|
|
2407
|
-
)
|
|
2408
|
-
return None
|
|
2196
|
+
return self.do_request(
|
|
2197
|
+
url=request_url,
|
|
2198
|
+
method="GET",
|
|
2199
|
+
headers=request_header,
|
|
2200
|
+
timeout=REQUEST_TIMEOUT,
|
|
2201
|
+
failure_message="Failed to get M365 Teams apps for user -> {}".format(
|
|
2202
|
+
user_id
|
|
2203
|
+
),
|
|
2204
|
+
)
|
|
2409
2205
|
|
|
2410
2206
|
# end method definition
|
|
2411
2207
|
|
|
@@ -2433,7 +2229,8 @@ class M365(object):
|
|
|
2433
2229
|
+ "/installedApps?"
|
|
2434
2230
|
+ encoded_query
|
|
2435
2231
|
)
|
|
2436
|
-
|
|
2232
|
+
|
|
2233
|
+
logger.debug(
|
|
2437
2234
|
"Get list of M365 Teams Apps for M365 Team -> %s using query -> %s; calling -> %s",
|
|
2438
2235
|
team_id,
|
|
2439
2236
|
query,
|
|
@@ -2442,33 +2239,15 @@ class M365(object):
|
|
|
2442
2239
|
|
|
2443
2240
|
request_header = self.request_header()
|
|
2444
2241
|
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
request_header = self.request_header()
|
|
2455
|
-
retries += 1
|
|
2456
|
-
elif response.status_code in [502, 503, 504] and retries < 3:
|
|
2457
|
-
logger.warning(
|
|
2458
|
-
"M365 Graph API delivered server side error -> %s; retrying in %s seconds...",
|
|
2459
|
-
response.status_code,
|
|
2460
|
-
(retries + 1) * 60,
|
|
2461
|
-
)
|
|
2462
|
-
time.sleep((retries + 1) * 60)
|
|
2463
|
-
retries += 1
|
|
2464
|
-
else:
|
|
2465
|
-
logger.error(
|
|
2466
|
-
"Failed to get list of M365 Teams Apps for M365 Team -> %s; status -> %s; error -> %s",
|
|
2467
|
-
team_id,
|
|
2468
|
-
response.status_code,
|
|
2469
|
-
response.text,
|
|
2470
|
-
)
|
|
2471
|
-
return None
|
|
2242
|
+
return self.do_request(
|
|
2243
|
+
url=request_url,
|
|
2244
|
+
method="GET",
|
|
2245
|
+
headers=request_header,
|
|
2246
|
+
timeout=REQUEST_TIMEOUT,
|
|
2247
|
+
failure_message="Failed to get list of M365 Teams apps for M365 Team -> {}".format(
|
|
2248
|
+
team_id
|
|
2249
|
+
),
|
|
2250
|
+
)
|
|
2472
2251
|
|
|
2473
2252
|
# end method definition
|
|
2474
2253
|
|
|
@@ -2501,6 +2280,7 @@ class M365(object):
|
|
|
2501
2280
|
but requires a token of a user authenticated with username + password.
|
|
2502
2281
|
See https://learn.microsoft.com/en-us/graph/api/teamsapp-publish
|
|
2503
2282
|
(permissions table on that page)
|
|
2283
|
+
For updates see: https://learn.microsoft.com/en-us/graph/api/teamsapp-update?view=graph-rest-1.0&tabs=http
|
|
2504
2284
|
|
|
2505
2285
|
Args:
|
|
2506
2286
|
app_path (str): file path (with directory) to the app package to upload
|
|
@@ -2511,6 +2291,34 @@ class M365(object):
|
|
|
2511
2291
|
after installation (which is tenant specific)
|
|
2512
2292
|
Returns:
|
|
2513
2293
|
dict: Response of the MS GRAPH API REST call or None if the request fails
|
|
2294
|
+
|
|
2295
|
+
The responses are different depending if it is an install or upgrade!!
|
|
2296
|
+
|
|
2297
|
+
Example return for upgrades ("teamsAppId" is the "internal" ID of the app):
|
|
2298
|
+
{
|
|
2299
|
+
'@odata.context': "https://graph.microsoft.com/v1.0/$metadata#appCatalogs/teamsApps('3f749cca-8cb0-4925-9fa0-ba7aca2014af')/appDefinitions/$entity",
|
|
2300
|
+
'id': 'M2Y3NDljY2EtOGNiMC00OTI1LTlmYTAtYmE3YWNhMjAxNGFmIyMyNC4yLjAjI1B1Ymxpc2hlZA==',
|
|
2301
|
+
'teamsAppId': '3f749cca-8cb0-4925-9fa0-ba7aca2014af',
|
|
2302
|
+
'displayName': 'IDEA-TE - Extended ECM 24.2.0',
|
|
2303
|
+
'version': '24.2.0',
|
|
2304
|
+
'publishingState': 'published',
|
|
2305
|
+
'shortDescription': 'Add a tab for an Extended ECM business workspace.',
|
|
2306
|
+
'description': 'View and interact with OpenText Extended ECM business workspaces',
|
|
2307
|
+
'lastModifiedDateTime': None,
|
|
2308
|
+
'createdBy': None,
|
|
2309
|
+
'authorization': {
|
|
2310
|
+
'requiredPermissionSet': {...}
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2313
|
+
|
|
2314
|
+
Example return for new installations ("id" is the "internal" ID of the app):
|
|
2315
|
+
{
|
|
2316
|
+
'@odata.context': 'https://graph.microsoft.com/v1.0/$metadata#appCatalogs/teamsApps/$entity',
|
|
2317
|
+
'id': '6c672afd-37fc-46c6-8365-d499aba3808b',
|
|
2318
|
+
'externalId': 'dd4af790-d8ff-47a0-87ad-486318272c7a',
|
|
2319
|
+
'displayName': 'OpenText Extended ECM',
|
|
2320
|
+
'distributionMethod': 'organization'
|
|
2321
|
+
}
|
|
2514
2322
|
"""
|
|
2515
2323
|
|
|
2516
2324
|
if update_existing_app and not app_catalog_id:
|
|
@@ -2520,12 +2328,12 @@ class M365(object):
|
|
|
2520
2328
|
return None
|
|
2521
2329
|
|
|
2522
2330
|
if not os.path.exists(app_path):
|
|
2523
|
-
logger.error("M365 Teams app file ->
|
|
2331
|
+
logger.error("M365 Teams app file -> %s does not exist!", app_path)
|
|
2524
2332
|
return None
|
|
2525
2333
|
|
|
2526
2334
|
# Ensure that the app file is a zip file
|
|
2527
2335
|
if not app_path.endswith(".zip"):
|
|
2528
|
-
logger.error("M365 Teams app file ->
|
|
2336
|
+
logger.error("M365 Teams app file -> %s must be a zip file!", app_path)
|
|
2529
2337
|
return None
|
|
2530
2338
|
|
|
2531
2339
|
request_url = self.config()["teamsAppsUrl"]
|
|
@@ -2536,7 +2344,7 @@ class M365(object):
|
|
|
2536
2344
|
|
|
2537
2345
|
# Here we need the credentials of an authenticated user!
|
|
2538
2346
|
# (not the application credentials (client_id, client_secret))
|
|
2539
|
-
request_header = self.request_header_user("application/zip")
|
|
2347
|
+
request_header = self.request_header_user(content_type="application/zip")
|
|
2540
2348
|
|
|
2541
2349
|
with open(app_path, "rb") as f:
|
|
2542
2350
|
app_data = f.read()
|
|
@@ -2545,55 +2353,27 @@ class M365(object):
|
|
|
2545
2353
|
# Ensure that the app file contains a manifest.json file
|
|
2546
2354
|
if "manifest.json" not in z.namelist():
|
|
2547
2355
|
logger.error(
|
|
2548
|
-
"M365 Teams app file ->
|
|
2356
|
+
"M365 Teams app file -> '%s' does not contain a manifest.json file!",
|
|
2357
|
+
app_path,
|
|
2549
2358
|
)
|
|
2550
2359
|
return None
|
|
2551
2360
|
|
|
2552
|
-
logger.
|
|
2553
|
-
"Upload M365 Teams app -> %s to the MS Teams catalog; calling -> %s",
|
|
2361
|
+
logger.debug(
|
|
2362
|
+
"Upload M365 Teams app -> '%s' to the MS Teams catalog; calling -> %s",
|
|
2554
2363
|
app_path,
|
|
2555
2364
|
request_url,
|
|
2556
2365
|
)
|
|
2557
2366
|
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
logger.warning("Session has expired - try to re-authenticate...")
|
|
2569
|
-
self.authenticate(revalidate=True)
|
|
2570
|
-
request_header = self.request_header()
|
|
2571
|
-
retries += 1
|
|
2572
|
-
elif response.status_code in [502, 503, 504] and retries < 3:
|
|
2573
|
-
logger.warning(
|
|
2574
|
-
"M365 Graph API delivered server side error -> %s; retrying in %s seconds...",
|
|
2575
|
-
response.status_code,
|
|
2576
|
-
(retries + 1) * 60,
|
|
2577
|
-
)
|
|
2578
|
-
time.sleep((retries + 1) * 60)
|
|
2579
|
-
retries += 1
|
|
2580
|
-
else:
|
|
2581
|
-
if update_existing_app:
|
|
2582
|
-
logger.warning(
|
|
2583
|
-
"Failed to update existing M365 Teams app -> %s (may be because it is not a new version); status -> %s; error -> %s",
|
|
2584
|
-
app_path,
|
|
2585
|
-
response.status_code,
|
|
2586
|
-
response.text,
|
|
2587
|
-
)
|
|
2588
|
-
|
|
2589
|
-
else:
|
|
2590
|
-
logger.error(
|
|
2591
|
-
"Failed to upload new M365 Teams app -> %s; status -> %s; error -> %s",
|
|
2592
|
-
app_path,
|
|
2593
|
-
response.status_code,
|
|
2594
|
-
response.text,
|
|
2595
|
-
)
|
|
2596
|
-
return None
|
|
2367
|
+
return self.do_request(
|
|
2368
|
+
url=request_url,
|
|
2369
|
+
method="POST",
|
|
2370
|
+
headers=request_header,
|
|
2371
|
+
data=app_data,
|
|
2372
|
+
timeout=REQUEST_TIMEOUT,
|
|
2373
|
+
failure_message="Failed to update existing M365 Teams app -> '{}' (may be because it is not a new version)".format(
|
|
2374
|
+
app_path
|
|
2375
|
+
),
|
|
2376
|
+
)
|
|
2597
2377
|
|
|
2598
2378
|
# end method definition
|
|
2599
2379
|
|
|
@@ -2610,11 +2390,13 @@ class M365(object):
|
|
|
2610
2390
|
request_header = self.request_header_user()
|
|
2611
2391
|
|
|
2612
2392
|
# Make the DELETE request to remove the app from the app catalog
|
|
2613
|
-
response = requests.delete(
|
|
2393
|
+
response = requests.delete(
|
|
2394
|
+
request_url, headers=request_header, timeout=REQUEST_TIMEOUT
|
|
2395
|
+
)
|
|
2614
2396
|
|
|
2615
2397
|
# Check the status code of the response
|
|
2616
2398
|
if response.status_code == 204:
|
|
2617
|
-
logger.
|
|
2399
|
+
logger.debug(
|
|
2618
2400
|
"The M365 Teams app with ID -> %s has been successfully removed from the app catalog.",
|
|
2619
2401
|
app_id,
|
|
2620
2402
|
)
|
|
@@ -2627,93 +2409,111 @@ class M365(object):
|
|
|
2627
2409
|
|
|
2628
2410
|
# end method definition
|
|
2629
2411
|
|
|
2630
|
-
def assign_teams_app_to_user(
|
|
2412
|
+
def assign_teams_app_to_user(
|
|
2413
|
+
self,
|
|
2414
|
+
user_id: str,
|
|
2415
|
+
app_name: str = "",
|
|
2416
|
+
app_internal_id: str = "",
|
|
2417
|
+
show_error: bool = False,
|
|
2418
|
+
) -> dict | None:
|
|
2631
2419
|
"""Assigns (adds) a M365 Teams app to a M365 user.
|
|
2632
2420
|
|
|
2421
|
+
See: https://learn.microsoft.com/en-us/graph/api/userteamwork-post-installedapps?view=graph-rest-1.0&tabs=http
|
|
2422
|
+
|
|
2633
2423
|
Args:
|
|
2634
2424
|
user_id (str): M365 GUID of the user (can also be the M365 email of the user)
|
|
2635
|
-
app_name (str): exact name of the app
|
|
2425
|
+
app_name (str, optional): exact name of the app. Not needed if app_internal_id is provided
|
|
2426
|
+
app_internal_id (str, optional): internal ID of the app. If not provided it will be derived from app_name
|
|
2427
|
+
show_error (bool): whether or not an error should be displayed if the
|
|
2428
|
+
user is not found.
|
|
2636
2429
|
Returns:
|
|
2637
2430
|
dict: response of the MS Graph API call or None if the call fails.
|
|
2638
2431
|
"""
|
|
2639
2432
|
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2433
|
+
if not app_internal_id and not app_name:
|
|
2434
|
+
logger.error(
|
|
2435
|
+
"Either the internal App ID or the App name need to be provided!"
|
|
2436
|
+
)
|
|
2644
2437
|
return None
|
|
2645
2438
|
|
|
2439
|
+
if not app_internal_id:
|
|
2440
|
+
response = self.get_teams_apps(
|
|
2441
|
+
filter_expression="contains(displayName, '{}')".format(app_name)
|
|
2442
|
+
)
|
|
2443
|
+
app_internal_id = self.get_result_value(
|
|
2444
|
+
response=response, key="id", index=0
|
|
2445
|
+
)
|
|
2446
|
+
if not app_internal_id:
|
|
2447
|
+
logger.error(
|
|
2448
|
+
"M365 Teams App -> '%s' not found! Cannot assign App to user -> %s.",
|
|
2449
|
+
app_name,
|
|
2450
|
+
user_id,
|
|
2451
|
+
)
|
|
2452
|
+
return None
|
|
2453
|
+
|
|
2646
2454
|
request_url = (
|
|
2647
2455
|
self.config()["usersUrl"] + "/" + user_id + "/teamwork/installedApps"
|
|
2648
2456
|
)
|
|
2649
2457
|
request_header = self.request_header()
|
|
2650
2458
|
|
|
2651
2459
|
post_body = {
|
|
2652
|
-
"teamsApp@odata.bind": self.config()["teamsAppsUrl"] + "/" +
|
|
2460
|
+
"teamsApp@odata.bind": self.config()["teamsAppsUrl"] + "/" + app_internal_id
|
|
2653
2461
|
}
|
|
2654
2462
|
|
|
2655
|
-
logger.
|
|
2656
|
-
"Assign M365 Teams app -> %s (%s) to M365 user -> %s; calling -> %s",
|
|
2463
|
+
logger.debug(
|
|
2464
|
+
"Assign M365 Teams app -> '%s' (%s) to M365 user -> %s; calling -> %s",
|
|
2657
2465
|
app_name,
|
|
2658
|
-
|
|
2466
|
+
app_internal_id,
|
|
2659
2467
|
user_id,
|
|
2660
2468
|
request_url,
|
|
2661
2469
|
)
|
|
2662
2470
|
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
logger.warning(
|
|
2678
|
-
"M365 Graph API delivered server side error -> %s; retrying in %s seconds...",
|
|
2679
|
-
response.status_code,
|
|
2680
|
-
(retries + 1) * 60,
|
|
2681
|
-
)
|
|
2682
|
-
time.sleep((retries + 1) * 60)
|
|
2683
|
-
retries += 1
|
|
2684
|
-
else:
|
|
2685
|
-
logger.error(
|
|
2686
|
-
"Failed to assign M365 Teams app -> %s (%s) to M365 user -> %s; status -> %s; error -> %s",
|
|
2687
|
-
app_name,
|
|
2688
|
-
app_id,
|
|
2689
|
-
user_id,
|
|
2690
|
-
response.status_code,
|
|
2691
|
-
response.text,
|
|
2692
|
-
)
|
|
2693
|
-
return None
|
|
2471
|
+
return self.do_request(
|
|
2472
|
+
url=request_url,
|
|
2473
|
+
method="POST",
|
|
2474
|
+
headers=request_header,
|
|
2475
|
+
json_data=post_body,
|
|
2476
|
+
timeout=REQUEST_TIMEOUT,
|
|
2477
|
+
failure_message="Failed to assign M365 Teams app -> '{}' ({}) to M365 user -> {}".format(
|
|
2478
|
+
app_name, app_internal_id, user_id
|
|
2479
|
+
),
|
|
2480
|
+
warning_message="Failed to assign M365 Teams app -> '{}' ({}) to M365 user -> {} (could be because the app is assigned organization-wide)".format(
|
|
2481
|
+
app_name, app_internal_id, user_id
|
|
2482
|
+
),
|
|
2483
|
+
show_error=show_error,
|
|
2484
|
+
)
|
|
2694
2485
|
|
|
2695
2486
|
# end method definition
|
|
2696
2487
|
|
|
2697
|
-
def upgrade_teams_app_of_user(
|
|
2488
|
+
def upgrade_teams_app_of_user(
|
|
2489
|
+
self, user_id: str, app_name: str, app_installation_id: str | None = None
|
|
2490
|
+
) -> dict | None:
|
|
2698
2491
|
"""Upgrade a MS teams app for a user. The call will fail if the user does not
|
|
2699
2492
|
already have the app assigned. So this needs to be checked before
|
|
2700
2493
|
calling this method.
|
|
2494
|
+
See: https://learn.microsoft.com/en-us/graph/api/userteamwork-teamsappinstallation-upgrade?view=graph-rest-1.0&tabs=http
|
|
2701
2495
|
|
|
2702
2496
|
Args:
|
|
2703
2497
|
user_id (str): M365 GUID of the user (can also be the M365 email of the user)
|
|
2704
2498
|
app_name (str): exact name of the app
|
|
2499
|
+
app_installation_id (str): ID of the app installation for the user. This is neither the internal nor
|
|
2500
|
+
external app ID. It is specific for each user and app.
|
|
2705
2501
|
Returns:
|
|
2706
2502
|
dict: response of the MS Graph API call or None if the call fails.
|
|
2707
2503
|
"""
|
|
2708
2504
|
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2505
|
+
if not app_installation_id:
|
|
2506
|
+
response = self.get_teams_apps_of_user(
|
|
2507
|
+
user_id=user_id,
|
|
2508
|
+
filter_expression="contains(teamsAppDefinition/displayName, '{}')".format(
|
|
2509
|
+
app_name
|
|
2510
|
+
),
|
|
2511
|
+
)
|
|
2512
|
+
# Retrieve the installation specific App ID - this is different from thew App catalalog ID!!
|
|
2513
|
+
app_installation_id = self.get_result_value(response, "id", 0)
|
|
2714
2514
|
if not app_installation_id:
|
|
2715
2515
|
logger.error(
|
|
2716
|
-
"M365 Teams app -> %s not found for user with ID -> %s. Cannot upgrade app for this user!",
|
|
2516
|
+
"M365 Teams app -> '%s' not found for user with ID -> %s. Cannot upgrade app for this user!",
|
|
2717
2517
|
app_name,
|
|
2718
2518
|
user_id,
|
|
2719
2519
|
)
|
|
@@ -2729,43 +2529,83 @@ class M365(object):
|
|
|
2729
2529
|
)
|
|
2730
2530
|
request_header = self.request_header()
|
|
2731
2531
|
|
|
2732
|
-
logger.
|
|
2733
|
-
"Upgrade M365 Teams app -> %s (%s) of M365 user with ID -> %s; calling -> %s",
|
|
2532
|
+
logger.debug(
|
|
2533
|
+
"Upgrade M365 Teams app -> '%s' (%s) of M365 user with ID -> %s; calling -> %s",
|
|
2734
2534
|
app_name,
|
|
2735
2535
|
app_installation_id,
|
|
2736
2536
|
user_id,
|
|
2737
2537
|
request_url,
|
|
2738
2538
|
)
|
|
2739
2539
|
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2540
|
+
return self.do_request(
|
|
2541
|
+
url=request_url,
|
|
2542
|
+
method="POST",
|
|
2543
|
+
headers=request_header,
|
|
2544
|
+
timeout=REQUEST_TIMEOUT,
|
|
2545
|
+
failure_message="Failed to upgrade M365 Teams app -> '{}' ({}) of M365 user -> {}".format(
|
|
2546
|
+
app_name, app_installation_id, user_id
|
|
2547
|
+
),
|
|
2548
|
+
)
|
|
2549
|
+
|
|
2550
|
+
# end method definition
|
|
2551
|
+
|
|
2552
|
+
def remove_teams_app_from_user(
|
|
2553
|
+
self, user_id: str, app_name: str, app_installation_id: str | None = None
|
|
2554
|
+
) -> dict | None:
|
|
2555
|
+
"""Remove a M365 Teams app from a M365 user.
|
|
2556
|
+
|
|
2557
|
+
See: https://learn.microsoft.com/en-us/graph/api/userteamwork-delete-installedapps?view=graph-rest-1.0&tabs=http
|
|
2558
|
+
|
|
2559
|
+
Args:
|
|
2560
|
+
user_id (str): M365 GUID of the user (can also be the M365 email of the user)
|
|
2561
|
+
app_name (str): exact name of the app
|
|
2562
|
+
Returns:
|
|
2563
|
+
dict: response of the MS Graph API call or None if the call fails.
|
|
2564
|
+
"""
|
|
2565
|
+
|
|
2566
|
+
if not app_installation_id:
|
|
2567
|
+
response = self.get_teams_apps_of_user(
|
|
2568
|
+
user_id=user_id,
|
|
2569
|
+
filter_expression="contains(teamsAppDefinition/displayName, '{}')".format(
|
|
2570
|
+
app_name
|
|
2571
|
+
),
|
|
2572
|
+
)
|
|
2573
|
+
# Retrieve the installation specific App ID - this is different from thew App catalalog ID!!
|
|
2574
|
+
app_installation_id = self.get_result_value(response, "id", 0)
|
|
2575
|
+
if not app_installation_id:
|
|
2576
|
+
logger.error(
|
|
2577
|
+
"M365 Teams app -> '%s' not found for user with ID -> %s. Cannot remove app from this user!",
|
|
2578
|
+
app_name,
|
|
2579
|
+
user_id,
|
|
2580
|
+
)
|
|
2581
|
+
return None
|
|
2582
|
+
|
|
2583
|
+
request_url = (
|
|
2584
|
+
self.config()["usersUrl"]
|
|
2585
|
+
+ "/"
|
|
2586
|
+
+ user_id
|
|
2587
|
+
+ "/teamwork/installedApps/"
|
|
2588
|
+
+ app_installation_id
|
|
2589
|
+
)
|
|
2590
|
+
request_header = self.request_header()
|
|
2591
|
+
|
|
2592
|
+
logger.debug(
|
|
2593
|
+
"Remove M365 Teams app -> '%s' (%s) from M365 user with ID -> %s; calling -> %s",
|
|
2594
|
+
app_name,
|
|
2595
|
+
app_installation_id,
|
|
2596
|
+
user_id,
|
|
2597
|
+
request_url,
|
|
2598
|
+
)
|
|
2599
|
+
|
|
2600
|
+
return self.do_request(
|
|
2601
|
+
url=request_url,
|
|
2602
|
+
method="DELETE",
|
|
2603
|
+
headers=request_header,
|
|
2604
|
+
timeout=REQUEST_TIMEOUT,
|
|
2605
|
+
failure_message="Failed to remove M365 Teams app -> '{}' ({}) from M365 user -> {}".format(
|
|
2606
|
+
app_name, app_installation_id, user_id
|
|
2607
|
+
),
|
|
2608
|
+
)
|
|
2769
2609
|
|
|
2770
2610
|
# end method definition
|
|
2771
2611
|
|
|
@@ -2788,43 +2628,24 @@ class M365(object):
|
|
|
2788
2628
|
"teamsApp@odata.bind": self.config()["teamsAppsUrl"] + "/" + app_id
|
|
2789
2629
|
}
|
|
2790
2630
|
|
|
2791
|
-
logger.
|
|
2792
|
-
"Assign M365 Teams app -> %s to M365 Team -> %s; calling -> %s",
|
|
2631
|
+
logger.debug(
|
|
2632
|
+
"Assign M365 Teams app -> '%s' (%s) to M365 Team -> %s; calling -> %s",
|
|
2633
|
+
self.config()["teamsAppName"],
|
|
2793
2634
|
app_id,
|
|
2794
2635
|
team_id,
|
|
2795
2636
|
request_url,
|
|
2796
2637
|
)
|
|
2797
2638
|
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
self.authenticate(revalidate=True)
|
|
2809
|
-
request_header = self.request_header()
|
|
2810
|
-
retries += 1
|
|
2811
|
-
elif response.status_code in [502, 503, 504] and retries < 3:
|
|
2812
|
-
logger.warning(
|
|
2813
|
-
"M365 Graph API delivered server side error -> %s; retrying in %s seconds...",
|
|
2814
|
-
response.status_code,
|
|
2815
|
-
(retries + 1) * 60,
|
|
2816
|
-
)
|
|
2817
|
-
time.sleep((retries + 1) * 60)
|
|
2818
|
-
retries += 1
|
|
2819
|
-
else:
|
|
2820
|
-
logger.error(
|
|
2821
|
-
"Failed to assign M365 Teams app -> %s to M365 Team -> %s; status -> %s; error -> %s",
|
|
2822
|
-
app_id,
|
|
2823
|
-
team_id,
|
|
2824
|
-
response.status_code,
|
|
2825
|
-
response.text,
|
|
2826
|
-
)
|
|
2827
|
-
return None
|
|
2639
|
+
return self.do_request(
|
|
2640
|
+
url=request_url,
|
|
2641
|
+
method="POST",
|
|
2642
|
+
headers=request_header,
|
|
2643
|
+
json_data=post_body,
|
|
2644
|
+
timeout=REQUEST_TIMEOUT,
|
|
2645
|
+
failure_message="Failed to assign M365 Teams app -> '{}' ({}) to M365 Team -> {}".format(
|
|
2646
|
+
self.config()["teamsAppName"], app_id, team_id
|
|
2647
|
+
),
|
|
2648
|
+
)
|
|
2828
2649
|
|
|
2829
2650
|
# end method definition
|
|
2830
2651
|
|
|
@@ -2848,7 +2669,7 @@ class M365(object):
|
|
|
2848
2669
|
app_installation_id = self.get_result_value(response, "id", 0)
|
|
2849
2670
|
if not app_installation_id:
|
|
2850
2671
|
logger.error(
|
|
2851
|
-
"M365 Teams app -> %s not found for M365 Team with ID -> %s. Cannot upgrade app for this team!",
|
|
2672
|
+
"M365 Teams app -> '%s' not found for M365 Team with ID -> %s. Cannot upgrade app for this team!",
|
|
2852
2673
|
app_name,
|
|
2853
2674
|
team_id,
|
|
2854
2675
|
)
|
|
@@ -2864,43 +2685,23 @@ class M365(object):
|
|
|
2864
2685
|
)
|
|
2865
2686
|
request_header = self.request_header()
|
|
2866
2687
|
|
|
2867
|
-
logger.
|
|
2868
|
-
"Upgrade app -> %s (%s) of M365 team with ID -> %s; calling -> %s",
|
|
2688
|
+
logger.debug(
|
|
2689
|
+
"Upgrade app -> '%s' (%s) of M365 team with ID -> %s; calling -> %s",
|
|
2869
2690
|
app_name,
|
|
2870
2691
|
app_installation_id,
|
|
2871
2692
|
team_id,
|
|
2872
2693
|
request_url,
|
|
2873
2694
|
)
|
|
2874
2695
|
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
request_header = self.request_header()
|
|
2885
|
-
retries += 1
|
|
2886
|
-
elif response.status_code in [502, 503, 504] and retries < 3:
|
|
2887
|
-
logger.warning(
|
|
2888
|
-
"M365 Graph API delivered server side error -> %s; retrying in %s seconds...",
|
|
2889
|
-
response.status_code,
|
|
2890
|
-
(retries + 1) * 60,
|
|
2891
|
-
)
|
|
2892
|
-
time.sleep((retries + 1) * 60)
|
|
2893
|
-
retries += 1
|
|
2894
|
-
else:
|
|
2895
|
-
logger.error(
|
|
2896
|
-
"Failed to upgrade app -> %s (%s) of M365 team with ID -> %s; status -> %s; error -> %s",
|
|
2897
|
-
app_name,
|
|
2898
|
-
app_installation_id,
|
|
2899
|
-
team_id,
|
|
2900
|
-
response.status_code,
|
|
2901
|
-
response.text,
|
|
2902
|
-
)
|
|
2903
|
-
return None
|
|
2696
|
+
return self.do_request(
|
|
2697
|
+
url=request_url,
|
|
2698
|
+
method="POST",
|
|
2699
|
+
headers=request_header,
|
|
2700
|
+
timeout=REQUEST_TIMEOUT,
|
|
2701
|
+
failure_message="Failed to upgrade M365 Teams app -> '{}' ({}) of M365 team with ID -> {}".format(
|
|
2702
|
+
app_name, app_installation_id, team_id
|
|
2703
|
+
),
|
|
2704
|
+
)
|
|
2904
2705
|
|
|
2905
2706
|
# end method definition
|
|
2906
2707
|
|
|
@@ -2945,8 +2746,10 @@ class M365(object):
|
|
|
2945
2746
|
None,
|
|
2946
2747
|
)
|
|
2947
2748
|
if not channel:
|
|
2948
|
-
logger.
|
|
2949
|
-
"Cannot find Channel -> %s on M365 Team -> %s",
|
|
2749
|
+
logger.error(
|
|
2750
|
+
"Cannot find Channel -> '%s' on M365 Team -> '%s'",
|
|
2751
|
+
channel_name,
|
|
2752
|
+
team_name,
|
|
2950
2753
|
)
|
|
2951
2754
|
return None
|
|
2952
2755
|
channel_id = channel["id"]
|
|
@@ -2974,8 +2777,8 @@ class M365(object):
|
|
|
2974
2777
|
},
|
|
2975
2778
|
}
|
|
2976
2779
|
|
|
2977
|
-
logger.
|
|
2978
|
-
"Add Tab -> %s with App ID -> %s to Channel -> %s of Microsoft 365 Team -> %s; calling -> %s",
|
|
2780
|
+
logger.debug(
|
|
2781
|
+
"Add Tab -> '%s' with App ID -> %s to Channel -> '%s' of Microsoft 365 Team -> '%s'; calling -> %s",
|
|
2979
2782
|
tab_name,
|
|
2980
2783
|
app_id,
|
|
2981
2784
|
channel_name,
|
|
@@ -2983,39 +2786,16 @@ class M365(object):
|
|
|
2983
2786
|
request_url,
|
|
2984
2787
|
)
|
|
2985
2788
|
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
self.authenticate(revalidate=True)
|
|
2997
|
-
request_header = self.request_header()
|
|
2998
|
-
retries += 1
|
|
2999
|
-
elif response.status_code in [502, 503, 504] and retries < 3:
|
|
3000
|
-
logger.warning(
|
|
3001
|
-
"M365 Graph API delivered server side error -> %s; retrying in %s seconds...",
|
|
3002
|
-
response.status_code,
|
|
3003
|
-
(retries + 1) * 60,
|
|
3004
|
-
)
|
|
3005
|
-
time.sleep((retries + 1) * 60)
|
|
3006
|
-
retries += 1
|
|
3007
|
-
else:
|
|
3008
|
-
logger.error(
|
|
3009
|
-
"Failed to add Tab for M365 Team -> %s (%s) and Channel -> %s (%s); status -> %s; error -> %s; tab config -> %s",
|
|
3010
|
-
team_name,
|
|
3011
|
-
team_id,
|
|
3012
|
-
channel_name,
|
|
3013
|
-
channel_id,
|
|
3014
|
-
response.status_code,
|
|
3015
|
-
response.text,
|
|
3016
|
-
str(tab_config),
|
|
3017
|
-
)
|
|
3018
|
-
return None
|
|
2789
|
+
return self.do_request(
|
|
2790
|
+
url=request_url,
|
|
2791
|
+
method="POST",
|
|
2792
|
+
headers=request_header,
|
|
2793
|
+
json_data=tab_config,
|
|
2794
|
+
timeout=REQUEST_TIMEOUT,
|
|
2795
|
+
failure_message="Failed to add Tab for M365 Team -> '{}' ({}) and Channel -> '{}' ({})".format(
|
|
2796
|
+
team_name, team_id, channel_name, channel_id
|
|
2797
|
+
),
|
|
2798
|
+
)
|
|
3019
2799
|
|
|
3020
2800
|
# end method definition
|
|
3021
2801
|
|
|
@@ -3058,8 +2838,10 @@ class M365(object):
|
|
|
3058
2838
|
None,
|
|
3059
2839
|
)
|
|
3060
2840
|
if not channel:
|
|
3061
|
-
logger.
|
|
3062
|
-
"Cannot find Channel -> %s for M365 Team -> %s",
|
|
2841
|
+
logger.error(
|
|
2842
|
+
"Cannot find Channel -> '%s' for M365 Team -> '%s'",
|
|
2843
|
+
channel_name,
|
|
2844
|
+
team_name,
|
|
3063
2845
|
)
|
|
3064
2846
|
return None
|
|
3065
2847
|
channel_id = channel["id"]
|
|
@@ -3075,8 +2857,8 @@ class M365(object):
|
|
|
3075
2857
|
None,
|
|
3076
2858
|
)
|
|
3077
2859
|
if not tab:
|
|
3078
|
-
logger.
|
|
3079
|
-
"Cannot find Tab -> %s on M365 Team -> %s (%s) and Channel -> %s (%s)",
|
|
2860
|
+
logger.error(
|
|
2861
|
+
"Cannot find Tab -> '%s' on M365 Team -> '%s' (%s) and Channel -> '%s' (%s)",
|
|
3080
2862
|
tab_name,
|
|
3081
2863
|
team_name,
|
|
3082
2864
|
team_id,
|
|
@@ -3108,8 +2890,8 @@ class M365(object):
|
|
|
3108
2890
|
},
|
|
3109
2891
|
}
|
|
3110
2892
|
|
|
3111
|
-
logger.
|
|
3112
|
-
"Update Tab -> %s (%s) of Channel -> %s (%s) for Microsoft 365 Teams -> %s (%s) with configuration -> %s; calling -> %s",
|
|
2893
|
+
logger.debug(
|
|
2894
|
+
"Update Tab -> '%s' (%s) of Channel -> '%s' (%s) for Microsoft 365 Teams -> '%s' (%s) with configuration -> %s; calling -> %s",
|
|
3113
2895
|
tab_name,
|
|
3114
2896
|
tab_id,
|
|
3115
2897
|
channel_name,
|
|
@@ -3120,40 +2902,16 @@ class M365(object):
|
|
|
3120
2902
|
request_url,
|
|
3121
2903
|
)
|
|
3122
2904
|
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
self.authenticate(revalidate=True)
|
|
3134
|
-
request_header = self.request_header()
|
|
3135
|
-
retries += 1
|
|
3136
|
-
elif response.status_code in [502, 503, 504] and retries < 3:
|
|
3137
|
-
logger.warning(
|
|
3138
|
-
"M365 Graph API delivered server side error -> %s; retrying in %s seconds...",
|
|
3139
|
-
response.status_code,
|
|
3140
|
-
(retries + 1) * 60,
|
|
3141
|
-
)
|
|
3142
|
-
time.sleep((retries + 1) * 60)
|
|
3143
|
-
retries += 1
|
|
3144
|
-
else:
|
|
3145
|
-
logger.error(
|
|
3146
|
-
"Failed to update Tab -> %s (%s) for M365 Team -> %s (%s) and Channel -> %s (%s); status -> %s; error -> %s",
|
|
3147
|
-
tab_name,
|
|
3148
|
-
tab_id,
|
|
3149
|
-
team_name,
|
|
3150
|
-
team_id,
|
|
3151
|
-
channel_name,
|
|
3152
|
-
channel_id,
|
|
3153
|
-
response.status_code,
|
|
3154
|
-
response.text,
|
|
3155
|
-
)
|
|
3156
|
-
return None
|
|
2905
|
+
return self.do_request(
|
|
2906
|
+
url=request_url,
|
|
2907
|
+
method="PATCH",
|
|
2908
|
+
headers=request_header,
|
|
2909
|
+
json_data=tab_config,
|
|
2910
|
+
timeout=REQUEST_TIMEOUT,
|
|
2911
|
+
failure_message="Failed to update Tab -> '{}' ({}) for M365 Team -> '{}' ({}) and Channel -> '{}' ({})".format(
|
|
2912
|
+
tab_name, tab_id, team_name, team_id, channel_name, channel_id
|
|
2913
|
+
),
|
|
2914
|
+
)
|
|
3157
2915
|
|
|
3158
2916
|
# end method definition
|
|
3159
2917
|
|
|
@@ -3189,8 +2947,10 @@ class M365(object):
|
|
|
3189
2947
|
None,
|
|
3190
2948
|
)
|
|
3191
2949
|
if not channel:
|
|
3192
|
-
logger.
|
|
3193
|
-
"Cannot find Channel -> %s for M365 Team -> %s",
|
|
2950
|
+
logger.error(
|
|
2951
|
+
"Cannot find Channel -> '%s' for M365 Team -> '%s'",
|
|
2952
|
+
channel_name,
|
|
2953
|
+
team_name,
|
|
3194
2954
|
)
|
|
3195
2955
|
return False
|
|
3196
2956
|
channel_id = channel["id"]
|
|
@@ -3206,8 +2966,8 @@ class M365(object):
|
|
|
3206
2966
|
item for item in response["value"] if item["displayName"] == tab_name
|
|
3207
2967
|
]
|
|
3208
2968
|
if not tab_list:
|
|
3209
|
-
logger.
|
|
3210
|
-
"Cannot find
|
|
2969
|
+
logger.error(
|
|
2970
|
+
"Cannot find Tab -> '%s' on M365 Team -> '%s' (%s) and Channel -> '%s' (%s)",
|
|
3211
2971
|
tab_name,
|
|
3212
2972
|
team_name,
|
|
3213
2973
|
team_id,
|
|
@@ -3231,8 +2991,8 @@ class M365(object):
|
|
|
3231
2991
|
|
|
3232
2992
|
request_header = self.request_header()
|
|
3233
2993
|
|
|
3234
|
-
logger.
|
|
3235
|
-
"Delete Tab -> %s (%s) from Channel -> %s (%s) of Microsoft 365 Teams -> %s (%s); calling -> %s",
|
|
2994
|
+
logger.debug(
|
|
2995
|
+
"Delete Tab -> '%s' (%s) from Channel -> '%s' (%s) of Microsoft 365 Teams -> '%s' (%s); calling -> %s",
|
|
3236
2996
|
tab_name,
|
|
3237
2997
|
tab_id,
|
|
3238
2998
|
channel_name,
|
|
@@ -3242,49 +3002,23 @@ class M365(object):
|
|
|
3242
3002
|
request_url,
|
|
3243
3003
|
)
|
|
3244
3004
|
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
elif response.status_code == 401 and retries == 0:
|
|
3263
|
-
logger.warning("Session has expired - try to re-authenticate...")
|
|
3264
|
-
self.authenticate(revalidate=True)
|
|
3265
|
-
request_header = self.request_header()
|
|
3266
|
-
retries += 1
|
|
3267
|
-
elif response.status_code in [502, 503, 504] and retries < 3:
|
|
3268
|
-
logger.warning(
|
|
3269
|
-
"M365 Graph API delivered server side error -> %s; retrying in %s seconds...",
|
|
3270
|
-
response.status_code,
|
|
3271
|
-
(retries + 1) * 60,
|
|
3272
|
-
)
|
|
3273
|
-
time.sleep((retries + 1) * 60)
|
|
3274
|
-
retries += 1
|
|
3275
|
-
else:
|
|
3276
|
-
logger.error(
|
|
3277
|
-
"Failed to delete Tab -> %s (%s) for M365 Team -> %s (%s) and Channel -> %s (%s); status -> %s; error -> %s",
|
|
3278
|
-
tab_name,
|
|
3279
|
-
tab_id,
|
|
3280
|
-
team_name,
|
|
3281
|
-
team_id,
|
|
3282
|
-
channel_name,
|
|
3283
|
-
channel_id,
|
|
3284
|
-
response.status_code,
|
|
3285
|
-
response.text,
|
|
3286
|
-
)
|
|
3287
|
-
return False
|
|
3005
|
+
response = self.do_request(
|
|
3006
|
+
url=request_url,
|
|
3007
|
+
method="DELETE",
|
|
3008
|
+
headers=request_header,
|
|
3009
|
+
timeout=REQUEST_TIMEOUT,
|
|
3010
|
+
failure_message="Failed to delete Tab -> '{}' ({}) for M365 Team -> '{}' ({}) and Channel -> '{}' ({})".format(
|
|
3011
|
+
tab_name, tab_id, team_name, team_id, channel_name, channel_id
|
|
3012
|
+
),
|
|
3013
|
+
parse_request_response=False,
|
|
3014
|
+
)
|
|
3015
|
+
|
|
3016
|
+
if response and response.ok:
|
|
3017
|
+
break
|
|
3018
|
+
else:
|
|
3019
|
+
return False
|
|
3020
|
+
# end for tab in tab_list
|
|
3021
|
+
|
|
3288
3022
|
return True
|
|
3289
3023
|
|
|
3290
3024
|
# end method definition
|
|
@@ -3334,22 +3068,25 @@ class M365(object):
|
|
|
3334
3068
|
request_url = self.config()["securityUrl"] + "/sensitivityLabels"
|
|
3335
3069
|
request_header = self.request_header()
|
|
3336
3070
|
|
|
3337
|
-
logger.
|
|
3338
|
-
"Create M365 sensitivity label -> %s; calling -> %s", name, request_url
|
|
3071
|
+
logger.debug(
|
|
3072
|
+
"Create M365 sensitivity label -> '%s'; calling -> %s", name, request_url
|
|
3339
3073
|
)
|
|
3340
3074
|
|
|
3341
3075
|
# Send the POST request to create the label
|
|
3342
3076
|
response = requests.post(
|
|
3343
|
-
request_url,
|
|
3077
|
+
request_url,
|
|
3078
|
+
headers=request_header,
|
|
3079
|
+
data=json.dumps(payload),
|
|
3080
|
+
timeout=REQUEST_TIMEOUT,
|
|
3344
3081
|
)
|
|
3345
3082
|
|
|
3346
3083
|
# Check the response status code
|
|
3347
3084
|
if response.status_code == 201:
|
|
3348
|
-
logger.
|
|
3085
|
+
logger.debug("Label -> '%s' has been created successfully!", name)
|
|
3349
3086
|
return response
|
|
3350
3087
|
else:
|
|
3351
3088
|
logger.error(
|
|
3352
|
-
"Failed to create the M365 label -> %s! Response status code -> %s",
|
|
3089
|
+
"Failed to create the M365 label -> '%s'! Response status code -> %s",
|
|
3353
3090
|
name,
|
|
3354
3091
|
response.status_code,
|
|
3355
3092
|
)
|
|
@@ -3377,43 +3114,23 @@ class M365(object):
|
|
|
3377
3114
|
)
|
|
3378
3115
|
request_header = self.request_header()
|
|
3379
3116
|
|
|
3380
|
-
logger.
|
|
3381
|
-
"Assign label -> %s to user -> %s; calling -> %s",
|
|
3117
|
+
logger.debug(
|
|
3118
|
+
"Assign label -> '%s' to user -> '%s'; calling -> %s",
|
|
3382
3119
|
label_name,
|
|
3383
3120
|
user_email,
|
|
3384
3121
|
request_url,
|
|
3385
3122
|
)
|
|
3386
3123
|
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
self.authenticate(revalidate=True)
|
|
3398
|
-
request_header = self.request_header()
|
|
3399
|
-
retries += 1
|
|
3400
|
-
elif response.status_code in [502, 503, 504] and retries < 3:
|
|
3401
|
-
logger.warning(
|
|
3402
|
-
"M365 Graph API delivered server side error -> %s; retrying in %s seconds...",
|
|
3403
|
-
response.status_code,
|
|
3404
|
-
(retries + 1) * 60,
|
|
3405
|
-
)
|
|
3406
|
-
time.sleep((retries + 1) * 60)
|
|
3407
|
-
retries += 1
|
|
3408
|
-
else:
|
|
3409
|
-
logger.error(
|
|
3410
|
-
"Failed to assign label -> %s to M365 user -> %s; status -> %s; error -> %s",
|
|
3411
|
-
label_name,
|
|
3412
|
-
user_email,
|
|
3413
|
-
response.status_code,
|
|
3414
|
-
response.text,
|
|
3415
|
-
)
|
|
3416
|
-
return None
|
|
3124
|
+
return self.do_request(
|
|
3125
|
+
url=request_url,
|
|
3126
|
+
method="POST",
|
|
3127
|
+
headers=request_header,
|
|
3128
|
+
json_data=body,
|
|
3129
|
+
timeout=REQUEST_TIMEOUT,
|
|
3130
|
+
failure_message="Failed to assign label -> '{}' to M365 user -> '{}'".format(
|
|
3131
|
+
label_name, user_email
|
|
3132
|
+
),
|
|
3133
|
+
)
|
|
3417
3134
|
|
|
3418
3135
|
# end method definition
|
|
3419
3136
|
|
|
@@ -3438,7 +3155,7 @@ class M365(object):
|
|
|
3438
3155
|
|
|
3439
3156
|
# request_header = self.request_header()
|
|
3440
3157
|
|
|
3441
|
-
logger.
|
|
3158
|
+
logger.debug("Install Outlook Add-in from -> '%s' (NOT IMPLEMENTED)", app_path)
|
|
3442
3159
|
|
|
3443
3160
|
response = None
|
|
3444
3161
|
|
|
@@ -3464,39 +3181,21 @@ class M365(object):
|
|
|
3464
3181
|
] + "?$filter=displayName eq '{}'".format(app_registration_name)
|
|
3465
3182
|
request_header = self.request_header()
|
|
3466
3183
|
|
|
3467
|
-
logger.
|
|
3468
|
-
"Get Azure App Registration -> %s; calling -> %s",
|
|
3184
|
+
logger.debug(
|
|
3185
|
+
"Get Azure App Registration -> '%s'; calling -> %s",
|
|
3469
3186
|
app_registration_name,
|
|
3470
3187
|
request_url,
|
|
3471
3188
|
)
|
|
3472
3189
|
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
request_header = self.request_header()
|
|
3483
|
-
retries += 1
|
|
3484
|
-
elif response.status_code in [502, 503, 504] and retries < 3:
|
|
3485
|
-
logger.warning(
|
|
3486
|
-
"M365 Graph API delivered server side error -> %s; retrying in %s seconds...",
|
|
3487
|
-
response.status_code,
|
|
3488
|
-
(retries + 1) * 60,
|
|
3489
|
-
)
|
|
3490
|
-
time.sleep((retries + 1) * 60)
|
|
3491
|
-
retries += 1
|
|
3492
|
-
else:
|
|
3493
|
-
logger.error(
|
|
3494
|
-
"Cannot find Azure App Registration -> %s; status -> %s; error -> %s",
|
|
3495
|
-
app_registration_name,
|
|
3496
|
-
response.status_code,
|
|
3497
|
-
response.text,
|
|
3498
|
-
)
|
|
3499
|
-
return None
|
|
3190
|
+
return self.do_request(
|
|
3191
|
+
url=request_url,
|
|
3192
|
+
method="GET",
|
|
3193
|
+
headers=request_header,
|
|
3194
|
+
timeout=REQUEST_TIMEOUT,
|
|
3195
|
+
failure_message="Cannot find Azure App Registration -> '{}'".format(
|
|
3196
|
+
app_registration_name
|
|
3197
|
+
),
|
|
3198
|
+
)
|
|
3500
3199
|
|
|
3501
3200
|
# end method definition
|
|
3502
3201
|
|
|
@@ -3567,38 +3266,16 @@ class M365(object):
|
|
|
3567
3266
|
request_url = self.config()["applicationsUrl"]
|
|
3568
3267
|
request_header = self.request_header()
|
|
3569
3268
|
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
# Check if Session has expired - then re-authenticate and try once more
|
|
3581
|
-
elif response.status_code == 401 and retries == 0:
|
|
3582
|
-
logger.warning("Session has expired - try to re-authenticate...")
|
|
3583
|
-
self.authenticate(revalidate=True)
|
|
3584
|
-
request_header = self.request_header()
|
|
3585
|
-
retries += 1
|
|
3586
|
-
elif response.status_code in [502, 503, 504] and retries < 3:
|
|
3587
|
-
logger.warning(
|
|
3588
|
-
"M365 Graph API delivered server side error -> %s; retrying in %s seconds...",
|
|
3589
|
-
response.status_code,
|
|
3590
|
-
(retries + 1) * 60,
|
|
3591
|
-
)
|
|
3592
|
-
time.sleep((retries + 1) * 60)
|
|
3593
|
-
retries += 1
|
|
3594
|
-
else:
|
|
3595
|
-
logger.error(
|
|
3596
|
-
"Cannot add App Registration -> %s; status -> %s; error -> %s",
|
|
3597
|
-
app_registration_name,
|
|
3598
|
-
response.status_code,
|
|
3599
|
-
response.text,
|
|
3600
|
-
)
|
|
3601
|
-
return None
|
|
3269
|
+
return self.do_request(
|
|
3270
|
+
url=request_url,
|
|
3271
|
+
method="POST",
|
|
3272
|
+
headers=request_header,
|
|
3273
|
+
json_data=app_registration_data,
|
|
3274
|
+
timeout=REQUEST_TIMEOUT,
|
|
3275
|
+
failure_message="Cannot add App Registration -> '{}'".format(
|
|
3276
|
+
app_registration_name
|
|
3277
|
+
),
|
|
3278
|
+
)
|
|
3602
3279
|
|
|
3603
3280
|
# end method definition
|
|
3604
3281
|
|
|
@@ -3632,45 +3309,479 @@ class M365(object):
|
|
|
3632
3309
|
request_url = self.config()["applicationsUrl"] + "/" + app_registration_id
|
|
3633
3310
|
request_header = self.request_header()
|
|
3634
3311
|
|
|
3635
|
-
logger.
|
|
3636
|
-
"Update App Registration -> %s (%s); calling -> %s",
|
|
3312
|
+
logger.debug(
|
|
3313
|
+
"Update App Registration -> '%s' (%s); calling -> %s",
|
|
3637
3314
|
app_registration_name,
|
|
3638
3315
|
app_registration_id,
|
|
3639
3316
|
request_url,
|
|
3640
3317
|
)
|
|
3641
3318
|
|
|
3319
|
+
return self.do_request(
|
|
3320
|
+
url=request_url,
|
|
3321
|
+
method="PATCH",
|
|
3322
|
+
headers=request_header,
|
|
3323
|
+
json_data=app_registration_data,
|
|
3324
|
+
timeout=REQUEST_TIMEOUT,
|
|
3325
|
+
failure_message="Cannot update App Registration -> '{}' ({})".format(
|
|
3326
|
+
app_registration_name, app_registration_id
|
|
3327
|
+
),
|
|
3328
|
+
)
|
|
3329
|
+
|
|
3330
|
+
# end method definition
|
|
3331
|
+
|
|
3332
|
+
def get_mail(
|
|
3333
|
+
self,
|
|
3334
|
+
user_id: str,
|
|
3335
|
+
sender: str,
|
|
3336
|
+
subject: str,
|
|
3337
|
+
num_emails: int | None = None,
|
|
3338
|
+
show_error: bool = False,
|
|
3339
|
+
) -> dict | None:
|
|
3340
|
+
"""Get email from inbox of a given user and a given sender (from)
|
|
3341
|
+
This requires Mail.Read Application permissions for the Azure App being used.
|
|
3342
|
+
|
|
3343
|
+
Args:
|
|
3344
|
+
user_id (str): M365 ID of the user
|
|
3345
|
+
sender (str): sender email address to filter for
|
|
3346
|
+
num_emails (int, optional): number of matching emails to retrieve
|
|
3347
|
+
show_error (bool): whether or not an error should be displayed if the
|
|
3348
|
+
user is not found.
|
|
3349
|
+
Returns:
|
|
3350
|
+
dict: Email or None of the request fails.
|
|
3351
|
+
"""
|
|
3352
|
+
|
|
3353
|
+
# Attention: you can easily run in limitation of the MS Graph API. If selection + filtering
|
|
3354
|
+
# is too complex you can get this error: "The restriction or sort order is too complex for this operation."
|
|
3355
|
+
# that's why we first just do the ordering and then do the filtering on sender and subject
|
|
3356
|
+
# separately
|
|
3357
|
+
request_url = (
|
|
3358
|
+
self.config()["usersUrl"]
|
|
3359
|
+
+ "/"
|
|
3360
|
+
+ user_id
|
|
3361
|
+
# + "/messages?$filter=from/emailAddress/address eq '{}' and contains(subject, '{}')&$orderby=receivedDateTime desc".format(
|
|
3362
|
+
+ "/messages?$orderby=receivedDateTime desc"
|
|
3363
|
+
)
|
|
3364
|
+
if num_emails:
|
|
3365
|
+
request_url += "&$top={}".format(num_emails)
|
|
3366
|
+
|
|
3367
|
+
request_header = self.request_header()
|
|
3368
|
+
|
|
3369
|
+
logger.debug(
|
|
3370
|
+
"Get mails for user -> %s from -> '%s' with subject -> '%s'; calling -> %s",
|
|
3371
|
+
user_id,
|
|
3372
|
+
sender,
|
|
3373
|
+
subject,
|
|
3374
|
+
request_url,
|
|
3375
|
+
)
|
|
3376
|
+
|
|
3377
|
+
response = self.do_request(
|
|
3378
|
+
url=request_url,
|
|
3379
|
+
method="GET",
|
|
3380
|
+
headers=request_header,
|
|
3381
|
+
timeout=REQUEST_TIMEOUT,
|
|
3382
|
+
failure_message="Cannot retrieve emails for user -> {}".format(user_id),
|
|
3383
|
+
show_error=show_error,
|
|
3384
|
+
)
|
|
3385
|
+
|
|
3386
|
+
if response and "value" in response:
|
|
3387
|
+
messages = response["value"]
|
|
3388
|
+
|
|
3389
|
+
# Filter the messages by sender and subject in code
|
|
3390
|
+
filtered_messages = [
|
|
3391
|
+
msg
|
|
3392
|
+
for msg in messages
|
|
3393
|
+
if msg.get("from", {}).get("emailAddress", {}).get("address") == sender
|
|
3394
|
+
and subject in msg.get("subject", "")
|
|
3395
|
+
]
|
|
3396
|
+
response["value"] = filtered_messages
|
|
3397
|
+
return response
|
|
3398
|
+
|
|
3399
|
+
return None
|
|
3400
|
+
|
|
3401
|
+
# end method definition
|
|
3402
|
+
|
|
3403
|
+
def get_mail_body(self, user_id: str, email_id: str) -> str | None:
|
|
3404
|
+
"""Get full email body for a given email ID
|
|
3405
|
+
This requires Mail.Read Application permissions for the Azure App being used.
|
|
3406
|
+
|
|
3407
|
+
Args:
|
|
3408
|
+
user_id (str): M365 ID of the user
|
|
3409
|
+
email_id (str): M365 ID of the email
|
|
3410
|
+
Returns:
|
|
3411
|
+
str | None: Email body or None of the request fails.
|
|
3412
|
+
"""
|
|
3413
|
+
|
|
3414
|
+
request_url = (
|
|
3415
|
+
self.config()["usersUrl"]
|
|
3416
|
+
+ "/"
|
|
3417
|
+
+ user_id
|
|
3418
|
+
+ "/messages/"
|
|
3419
|
+
+ email_id
|
|
3420
|
+
+ "/$value"
|
|
3421
|
+
)
|
|
3422
|
+
|
|
3423
|
+
request_header = self.request_header()
|
|
3424
|
+
|
|
3425
|
+
response = self.do_request(
|
|
3426
|
+
url=request_url,
|
|
3427
|
+
method="GET",
|
|
3428
|
+
headers=request_header,
|
|
3429
|
+
timeout=REQUEST_TIMEOUT,
|
|
3430
|
+
failure_message="Cannot get email body for user -> {} and email with ID -> {}".format(
|
|
3431
|
+
user_id,
|
|
3432
|
+
email_id,
|
|
3433
|
+
),
|
|
3434
|
+
parse_request_response=False,
|
|
3435
|
+
)
|
|
3436
|
+
|
|
3437
|
+
if response and response.ok and response.content:
|
|
3438
|
+
return response.content.decode("utf-8")
|
|
3439
|
+
|
|
3440
|
+
return None
|
|
3441
|
+
|
|
3442
|
+
# end method definition
|
|
3443
|
+
|
|
3444
|
+
def extract_url_from_message_body(
|
|
3445
|
+
self,
|
|
3446
|
+
message_body: str,
|
|
3447
|
+
search_pattern: str,
|
|
3448
|
+
multi_line: bool = False,
|
|
3449
|
+
multi_line_end_marker: str = "%3D",
|
|
3450
|
+
line_end_marker: str = "=",
|
|
3451
|
+
replacements: list | None = None,
|
|
3452
|
+
) -> str | None:
|
|
3453
|
+
"""Parse the email body to extract (a potentially multi-line) URL from the body.
|
|
3454
|
+
|
|
3455
|
+
Args:
|
|
3456
|
+
message_body (str): Text of the Email body
|
|
3457
|
+
search_pattern (str): Pattern thatneeds to be in first line of the URL. This
|
|
3458
|
+
makes sure it is the right URL we are looking for.
|
|
3459
|
+
multi_line (bool, optional): Is the URL spread over multiple lines?. Defaults to False.
|
|
3460
|
+
multi_line_end_marker (str, optional): If it is a multi-line URL, what marks the end
|
|
3461
|
+
of the URL in the last line? Defaults to "%3D".
|
|
3462
|
+
line_end_marker (str, optional): What makrs the end of lines 1-(n-1)? Defaults to "=".
|
|
3463
|
+
Returns:
|
|
3464
|
+
str: URL text thathas been extracted.
|
|
3465
|
+
"""
|
|
3466
|
+
|
|
3467
|
+
if not message_body:
|
|
3468
|
+
return None
|
|
3469
|
+
|
|
3470
|
+
# Split all the lines after a CRLF:
|
|
3471
|
+
lines = [line.strip() for line in message_body.split("\r\n")]
|
|
3472
|
+
|
|
3473
|
+
# Filter out the complete URL from the extracted URLs
|
|
3474
|
+
found = False
|
|
3475
|
+
|
|
3476
|
+
url = ""
|
|
3477
|
+
|
|
3478
|
+
for line in lines:
|
|
3479
|
+
if found:
|
|
3480
|
+
# Remove line end marker - many times a "="
|
|
3481
|
+
if line.endswith(line_end_marker):
|
|
3482
|
+
line = line[:-1]
|
|
3483
|
+
for replacement in replacements:
|
|
3484
|
+
line = line.replace(replacement["from"], replacement["to"])
|
|
3485
|
+
# We consider an empty line after we found the URL to indicate the end of the URL:
|
|
3486
|
+
if line == "":
|
|
3487
|
+
break
|
|
3488
|
+
url += line
|
|
3489
|
+
if multi_line and line.endswith(multi_line_end_marker):
|
|
3490
|
+
break
|
|
3491
|
+
if not search_pattern in line:
|
|
3492
|
+
continue
|
|
3493
|
+
# Fine https:// in the current line:
|
|
3494
|
+
index = line.find("https://")
|
|
3495
|
+
if index == -1:
|
|
3496
|
+
continue
|
|
3497
|
+
# If there's any text in front of https in that line cut it:
|
|
3498
|
+
line = line[index:]
|
|
3499
|
+
# Remove line end marker - many times a "="
|
|
3500
|
+
if line.endswith(line_end_marker):
|
|
3501
|
+
line = line[:-1]
|
|
3502
|
+
for replacement in replacements:
|
|
3503
|
+
line = line.replace(replacement["from"], replacement["to"])
|
|
3504
|
+
found = True
|
|
3505
|
+
url += line
|
|
3506
|
+
if not multi_line:
|
|
3507
|
+
break
|
|
3508
|
+
|
|
3509
|
+
return url
|
|
3510
|
+
|
|
3511
|
+
# end method definition
|
|
3512
|
+
|
|
3513
|
+
def delete_mail(self, user_id: str, email_id: str) -> dict | None:
|
|
3514
|
+
"""Delete email from inbox of a given user and a given email ID.
|
|
3515
|
+
This requires Mail.ReadWrite Application permissions for the Azure App being used.
|
|
3516
|
+
|
|
3517
|
+
Args:
|
|
3518
|
+
user_id (str): M365 ID of the user
|
|
3519
|
+
email_id (str): M365 ID of the email
|
|
3520
|
+
Returns:
|
|
3521
|
+
dict: Email or None of the request fails.
|
|
3522
|
+
"""
|
|
3523
|
+
|
|
3524
|
+
request_url = (
|
|
3525
|
+
self.config()["usersUrl"] + "/" + user_id + "/messages/" + email_id
|
|
3526
|
+
)
|
|
3527
|
+
|
|
3528
|
+
request_header = self.request_header()
|
|
3529
|
+
|
|
3530
|
+
return self.do_request(
|
|
3531
|
+
url=request_url,
|
|
3532
|
+
method="DELETE",
|
|
3533
|
+
headers=request_header,
|
|
3534
|
+
timeout=REQUEST_TIMEOUT,
|
|
3535
|
+
failure_message="Cannot delete email with ID -> {} from inbox of user -> {}".format(
|
|
3536
|
+
email_id, user_id
|
|
3537
|
+
),
|
|
3538
|
+
)
|
|
3539
|
+
|
|
3540
|
+
# end method definition
|
|
3541
|
+
|
|
3542
|
+
def email_verification(
|
|
3543
|
+
self,
|
|
3544
|
+
user_email: str,
|
|
3545
|
+
sender: str,
|
|
3546
|
+
subject: str,
|
|
3547
|
+
url_search_pattern: str,
|
|
3548
|
+
line_end_marker: str = "=",
|
|
3549
|
+
multi_line: bool = True,
|
|
3550
|
+
multi_line_end_marker: str = "%3D",
|
|
3551
|
+
replacements: list | None = None,
|
|
3552
|
+
max_retries: int = 6,
|
|
3553
|
+
use_browser_automation: bool = False,
|
|
3554
|
+
password: str = "",
|
|
3555
|
+
password_field_id: str = "",
|
|
3556
|
+
password_confirmation_field_id: str = "",
|
|
3557
|
+
password_submit_xpath: str = "",
|
|
3558
|
+
terms_of_service_xpath: str = "",
|
|
3559
|
+
) -> bool:
|
|
3560
|
+
"""Process email verification
|
|
3561
|
+
|
|
3562
|
+
Args:
|
|
3563
|
+
user_email (str): Email address of user recieving the verification mail.
|
|
3564
|
+
sender (str): Email sender (address)
|
|
3565
|
+
subject (str): Email subject to look for (can be substring)
|
|
3566
|
+
url_search_pattern (str): String the URL needs to contain to identify it.
|
|
3567
|
+
multi_line_end_marker (str): If the URL spans multiple lines this is the "end" marker for the last line.
|
|
3568
|
+
replacements (list): if the URL needs some treatment these replacements can be applied.
|
|
3569
|
+
Result:
|
|
3570
|
+
bool: True = Success, False = Failure
|
|
3571
|
+
"""
|
|
3572
|
+
|
|
3573
|
+
# Determine the M365 user for the current user by
|
|
3574
|
+
# the email address:
|
|
3575
|
+
m365_user = self.get_user(user_email=user_email)
|
|
3576
|
+
m365_user_id = self.get_result_value(m365_user, "id")
|
|
3577
|
+
if not m365_user_id:
|
|
3578
|
+
logger.warning("Cannot find M365 user -> %s", user_email)
|
|
3579
|
+
return False
|
|
3580
|
+
|
|
3581
|
+
if replacements is None:
|
|
3582
|
+
replacements = [{"from": "=3D", "to": "="}]
|
|
3583
|
+
|
|
3642
3584
|
retries = 0
|
|
3643
|
-
while
|
|
3644
|
-
response =
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3585
|
+
while retries < max_retries:
|
|
3586
|
+
response = self.get_mail(
|
|
3587
|
+
user_id=m365_user_id,
|
|
3588
|
+
sender=sender,
|
|
3589
|
+
subject=subject,
|
|
3590
|
+
show_error=False,
|
|
3649
3591
|
)
|
|
3650
|
-
if response
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3592
|
+
if response and response["value"]:
|
|
3593
|
+
emails = response["value"]
|
|
3594
|
+
# potentially there may be multiple matching emails,
|
|
3595
|
+
# we want the most recent one (from today):
|
|
3596
|
+
latest_email = max(emails, key=lambda x: x["receivedDateTime"])
|
|
3597
|
+
# Extract just the date:
|
|
3598
|
+
latest_email_date = latest_email["receivedDateTime"].split("T")[0]
|
|
3599
|
+
# Get the current date (today):
|
|
3600
|
+
today_date = datetime.today().strftime("%Y-%m-%d")
|
|
3601
|
+
# We do a sanity check here: the verification mail should be from today,
|
|
3602
|
+
# otherwise we assume it is an old mail and we need to wait for the
|
|
3603
|
+
# new verification mail to yet arrive:
|
|
3604
|
+
if latest_email_date != today_date:
|
|
3605
|
+
logger.info(
|
|
3606
|
+
"Verification email not yet received (latest mail from -> %s). Waiting %s seconds...",
|
|
3607
|
+
latest_email_date,
|
|
3608
|
+
10 * (retries + 1),
|
|
3609
|
+
)
|
|
3610
|
+
time.sleep(10 * (retries + 1))
|
|
3611
|
+
retries += 1
|
|
3612
|
+
continue
|
|
3613
|
+
email_id = latest_email["id"]
|
|
3614
|
+
# The full email body needs to be loaded with a separate REST call:
|
|
3615
|
+
body_text = self.get_mail_body(user_id=m365_user_id, email_id=email_id)
|
|
3616
|
+
# Extract the verification URL.
|
|
3617
|
+
if body_text:
|
|
3618
|
+
url = self.extract_url_from_message_body(
|
|
3619
|
+
message_body=body_text,
|
|
3620
|
+
search_pattern=url_search_pattern,
|
|
3621
|
+
line_end_marker=line_end_marker,
|
|
3622
|
+
multi_line=multi_line,
|
|
3623
|
+
multi_line_end_marker=multi_line_end_marker,
|
|
3624
|
+
replacements=replacements,
|
|
3625
|
+
)
|
|
3626
|
+
else:
|
|
3627
|
+
url = ""
|
|
3628
|
+
if not url:
|
|
3629
|
+
logger.warning("Cannot find verification link in the email body!")
|
|
3630
|
+
return False
|
|
3631
|
+
# Simulate a "click" on this URL:
|
|
3632
|
+
if use_browser_automation:
|
|
3633
|
+
# Core Share needs a full browser:
|
|
3634
|
+
browser_automation_object = BrowserAutomation(
|
|
3635
|
+
take_screenshots=True,
|
|
3636
|
+
automation_name="email-verification",
|
|
3637
|
+
)
|
|
3638
|
+
logger.info(
|
|
3639
|
+
"Open URL -> %s to verify account or email change (using browser automation)",
|
|
3640
|
+
url,
|
|
3641
|
+
)
|
|
3642
|
+
success = browser_automation_object.get_page(url)
|
|
3643
|
+
if success:
|
|
3644
|
+
user_interaction_required = False
|
|
3645
|
+
logger.info(
|
|
3646
|
+
"Successfully opened URL. Browser title is -> '%s'.",
|
|
3647
|
+
browser_automation_object.get_title(),
|
|
3648
|
+
)
|
|
3649
|
+
if password_field_id:
|
|
3650
|
+
password_field = browser_automation_object.find_elem(
|
|
3651
|
+
find_elem=password_field_id, show_error=False
|
|
3652
|
+
)
|
|
3653
|
+
if password_field:
|
|
3654
|
+
# The subsequent processing is only required if
|
|
3655
|
+
# the returned page requests a password change:
|
|
3656
|
+
user_interaction_required = True
|
|
3657
|
+
logger.info(
|
|
3658
|
+
"Found password field on returned page - it seems email verification requests password entry!"
|
|
3659
|
+
)
|
|
3660
|
+
result = browser_automation_object.find_elem_and_set(
|
|
3661
|
+
find_elem=password_field_id,
|
|
3662
|
+
elem_value=password,
|
|
3663
|
+
is_sensitive=True,
|
|
3664
|
+
)
|
|
3665
|
+
if not result:
|
|
3666
|
+
logger.error(
|
|
3667
|
+
"Failed to enter password in field -> '%s'",
|
|
3668
|
+
password_field_id,
|
|
3669
|
+
)
|
|
3670
|
+
success = False
|
|
3671
|
+
else:
|
|
3672
|
+
logger.info(
|
|
3673
|
+
"No user interaction required (no password change or terms of service acceptance)."
|
|
3674
|
+
)
|
|
3675
|
+
if user_interaction_required and password_confirmation_field_id:
|
|
3676
|
+
password_confirm_field = (
|
|
3677
|
+
browser_automation_object.find_elem(
|
|
3678
|
+
find_elem=password_confirmation_field_id,
|
|
3679
|
+
show_error=False,
|
|
3680
|
+
)
|
|
3681
|
+
)
|
|
3682
|
+
if password_confirm_field:
|
|
3683
|
+
logger.info(
|
|
3684
|
+
"Found password confirmation field on returned page - it seems email verification requests consecutive password entry!"
|
|
3685
|
+
)
|
|
3686
|
+
result = browser_automation_object.find_elem_and_set(
|
|
3687
|
+
find_elem=password_confirmation_field_id,
|
|
3688
|
+
elem_value=password,
|
|
3689
|
+
is_sensitive=True,
|
|
3690
|
+
)
|
|
3691
|
+
if not result:
|
|
3692
|
+
logger.error(
|
|
3693
|
+
"Failed to enter password in field -> '%s'",
|
|
3694
|
+
password_confirmation_field_id,
|
|
3695
|
+
)
|
|
3696
|
+
success = False
|
|
3697
|
+
if user_interaction_required and password_submit_xpath:
|
|
3698
|
+
password_submit_button = (
|
|
3699
|
+
browser_automation_object.find_elem(
|
|
3700
|
+
find_elem=password_submit_xpath,
|
|
3701
|
+
find_method="xpath",
|
|
3702
|
+
show_error=False,
|
|
3703
|
+
)
|
|
3704
|
+
)
|
|
3705
|
+
if password_submit_button:
|
|
3706
|
+
logger.info(
|
|
3707
|
+
"Submit password change dialog with button -> '%s' (found with XPath -> %s)",
|
|
3708
|
+
password_submit_button.text,
|
|
3709
|
+
password_submit_xpath,
|
|
3710
|
+
)
|
|
3711
|
+
result = browser_automation_object.find_elem_and_click(
|
|
3712
|
+
find_elem=password_submit_xpath, find_method="xpath"
|
|
3713
|
+
)
|
|
3714
|
+
if not result:
|
|
3715
|
+
logger.error(
|
|
3716
|
+
"Failed to press submit button -> %s",
|
|
3717
|
+
password_submit_xpath,
|
|
3718
|
+
)
|
|
3719
|
+
success = False
|
|
3720
|
+
# The Terms of service dialog has some weird animation
|
|
3721
|
+
# which require a short wait time. It seems it is required!
|
|
3722
|
+
time.sleep(1)
|
|
3723
|
+
terms_accept_button = browser_automation_object.find_elem(
|
|
3724
|
+
find_elem=terms_of_service_xpath,
|
|
3725
|
+
find_method="xpath",
|
|
3726
|
+
show_error=False,
|
|
3727
|
+
)
|
|
3728
|
+
if terms_accept_button:
|
|
3729
|
+
logger.info(
|
|
3730
|
+
"Accept terms of service with button -> '%s' (found with XPath -> %s)",
|
|
3731
|
+
terms_accept_button.text,
|
|
3732
|
+
terms_of_service_xpath,
|
|
3733
|
+
)
|
|
3734
|
+
result = browser_automation_object.find_elem_and_click(
|
|
3735
|
+
find_elem=terms_of_service_xpath,
|
|
3736
|
+
find_method="xpath",
|
|
3737
|
+
)
|
|
3738
|
+
if not result:
|
|
3739
|
+
logger.error(
|
|
3740
|
+
"Failed to accept terms of service with button -> '%s'",
|
|
3741
|
+
terms_accept_button.text,
|
|
3742
|
+
)
|
|
3743
|
+
success = False
|
|
3744
|
+
else:
|
|
3745
|
+
logger.info("No Terms of Service acceptance required.")
|
|
3746
|
+
# end if use_browser_automation
|
|
3747
|
+
else:
|
|
3748
|
+
# Salesforce (other than Core Share) is OK with the simple HTTP GET request:
|
|
3749
|
+
logger.info("Open URL -> %s to verify account or email change", url)
|
|
3750
|
+
response = self._http_object.http_request(url=url, method="GET")
|
|
3751
|
+
success = response and response.ok
|
|
3752
|
+
|
|
3753
|
+
if success:
|
|
3754
|
+
logger.info("Remove email from inbox of user -> %s...", user_email)
|
|
3755
|
+
response = self.delete_mail(user_id=m365_user_id, email_id=email_id)
|
|
3756
|
+
if not response:
|
|
3757
|
+
logger.warning(
|
|
3758
|
+
"Couldn't remove the mail from the inbox of user -> %s",
|
|
3759
|
+
user_email,
|
|
3760
|
+
)
|
|
3761
|
+
# We have success now and can break from the while loop
|
|
3762
|
+
return True
|
|
3763
|
+
else:
|
|
3764
|
+
logger.error(
|
|
3765
|
+
"Failed to process e-mail verification for user -> %s",
|
|
3766
|
+
user_email,
|
|
3767
|
+
)
|
|
3768
|
+
return False
|
|
3769
|
+
# end if response and response["value"]
|
|
3666
3770
|
else:
|
|
3667
|
-
logger.
|
|
3668
|
-
"
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
response.text,
|
|
3771
|
+
logger.info(
|
|
3772
|
+
"Verification email not yet received (no mails with sender -> %s and subject -> '%s' found). Waiting %s seconds...",
|
|
3773
|
+
sender,
|
|
3774
|
+
subject,
|
|
3775
|
+
10 * (retries + 1),
|
|
3673
3776
|
)
|
|
3674
|
-
|
|
3777
|
+
time.sleep(10 * (retries + 1))
|
|
3778
|
+
retries += 1
|
|
3779
|
+
# end while
|
|
3780
|
+
|
|
3781
|
+
logger.warning(
|
|
3782
|
+
"Verification mail for user -> %s has not arrived in time.", user_email
|
|
3783
|
+
)
|
|
3784
|
+
|
|
3785
|
+
return False
|
|
3675
3786
|
|
|
3676
3787
|
# end method definition
|