pyxecm 1.4__py3-none-any.whl → 1.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of pyxecm might be problematic. Click here for more details.
- pyxecm/__init__.py +3 -0
- pyxecm/coreshare.py +2636 -0
- pyxecm/customizer/__init__.py +4 -0
- pyxecm/customizer/browser_automation.py +164 -54
- pyxecm/customizer/customizer.py +451 -235
- pyxecm/customizer/k8s.py +6 -6
- pyxecm/customizer/m365.py +1136 -221
- pyxecm/customizer/payload.py +13163 -5844
- pyxecm/customizer/pht.py +503 -0
- pyxecm/customizer/salesforce.py +694 -114
- pyxecm/customizer/sap.py +4 -4
- pyxecm/customizer/servicenow.py +1221 -0
- pyxecm/customizer/successfactors.py +1056 -0
- pyxecm/helper/__init__.py +2 -0
- pyxecm/helper/assoc.py +24 -1
- pyxecm/helper/data.py +1527 -0
- pyxecm/helper/web.py +170 -46
- pyxecm/helper/xml.py +170 -34
- pyxecm/otac.py +309 -23
- pyxecm/otcs.py +2779 -698
- pyxecm/otds.py +347 -108
- pyxecm/otmm.py +808 -0
- pyxecm/otpd.py +13 -10
- {pyxecm-1.4.dist-info → pyxecm-1.5.dist-info}/METADATA +3 -1
- pyxecm-1.5.dist-info/RECORD +30 -0
- {pyxecm-1.4.dist-info → pyxecm-1.5.dist-info}/WHEEL +1 -1
- pyxecm-1.4.dist-info/RECORD +0 -24
- {pyxecm-1.4.dist-info → pyxecm-1.5.dist-info}/LICENSE +0 -0
- {pyxecm-1.4.dist-info → pyxecm-1.5.dist-info}/top_level.txt +0 -0
pyxecm/customizer/m365.py
CHANGED
|
@@ -30,6 +30,7 @@ update_user: Update selected properties of an M365 user
|
|
|
30
30
|
get_user_licenses: Get the assigned license SKUs of a user
|
|
31
31
|
assign_license_to_user: Add an M365 license to a user (e.g. to use Office 365)
|
|
32
32
|
get_user_photo: Get the photo of a M365 user
|
|
33
|
+
download_user_photo: Download the M365 user photo and save it to the local file system
|
|
33
34
|
update_user_photo: Update a user with a profile photo (which must be in local file system)
|
|
34
35
|
|
|
35
36
|
get_groups: Get list all all groups in M365 tenant
|
|
@@ -63,6 +64,7 @@ upload_teams_app: Upload a new app package to the catalog of MS Teams apps
|
|
|
63
64
|
remove_teams_app: Remove MS Teams App for the app catalog
|
|
64
65
|
assign_teams_app_to_user: Assign (add) a MS Teams app to a M365 user.
|
|
65
66
|
upgrade_teams_app_of_user: Upgrade a MS teams app for a user.
|
|
67
|
+
remove_teams_app_from_user: Remove a M365 Teams app from a M365 user.
|
|
66
68
|
assign_teams_app_to_team: Assign (add) a MS Teams app to a M365 team
|
|
67
69
|
(so that it afterwards can be added as a Tab in a M365 Teams Channel)
|
|
68
70
|
upgrade_teams_app_of_team: Upgrade a MS teams app for a specific team.
|
|
@@ -74,6 +76,17 @@ add_sensitivity_label: Assign a existing sensitivity label to a user.
|
|
|
74
76
|
THIS IS CURRENTLY NOT WORKING!
|
|
75
77
|
assign_sensitivity_label_to_user: Create a new sensitivity label in M365
|
|
76
78
|
THIS IS CURRENTLY NOT WORKING!
|
|
79
|
+
|
|
80
|
+
upload_outlook_app: Upload the M365 Outlook Add-In as "Integrated" App to M365 Admin Center. (NOT WORKING)
|
|
81
|
+
get_app_registration: Find an Azure App Registration based on its name
|
|
82
|
+
add_app_registration: Add an Azure App Registration
|
|
83
|
+
update_app_registration: Update an Azure App Registration
|
|
84
|
+
|
|
85
|
+
get_mail: Get email from inbox of a given user and a given sender (from)
|
|
86
|
+
get_mail_body: Get full email body for a given email ID
|
|
87
|
+
extract_url_from_message_body: Parse the email body to extract (a potentially multi-line) URL from the body.
|
|
88
|
+
delete_mail: Delete email from inbox of a given user and a given email ID.
|
|
89
|
+
email_verification: Process email verification
|
|
77
90
|
"""
|
|
78
91
|
|
|
79
92
|
__author__ = "Dr. Marc Diefenbruch"
|
|
@@ -90,9 +103,13 @@ import time
|
|
|
90
103
|
import urllib.parse
|
|
91
104
|
import zipfile
|
|
92
105
|
from urllib.parse import quote
|
|
106
|
+
from datetime import datetime
|
|
93
107
|
|
|
94
108
|
import requests
|
|
95
109
|
|
|
110
|
+
from pyxecm.helper.web import HTTP
|
|
111
|
+
from pyxecm.customizer.browser_automation import BrowserAutomation
|
|
112
|
+
|
|
96
113
|
logger = logging.getLogger("pyxecm.customizer.m365")
|
|
97
114
|
|
|
98
115
|
request_login_headers = {
|
|
@@ -100,6 +117,7 @@ request_login_headers = {
|
|
|
100
117
|
"Accept": "application/json",
|
|
101
118
|
}
|
|
102
119
|
|
|
120
|
+
REQUEST_TIMEOUT = 60
|
|
103
121
|
|
|
104
122
|
class M365(object):
|
|
105
123
|
"""Used to automate stettings in Microsoft 365 via the Graph API."""
|
|
@@ -107,6 +125,7 @@ class M365(object):
|
|
|
107
125
|
_config: dict
|
|
108
126
|
_access_token = None
|
|
109
127
|
_user_access_token = None
|
|
128
|
+
_http_object: HTTP | None = None
|
|
110
129
|
|
|
111
130
|
def __init__(
|
|
112
131
|
self,
|
|
@@ -116,6 +135,7 @@ class M365(object):
|
|
|
116
135
|
domain: str,
|
|
117
136
|
sku_id: str,
|
|
118
137
|
teams_app_name: str,
|
|
138
|
+
teams_app_external_id: str,
|
|
119
139
|
):
|
|
120
140
|
"""Initialize the M365 object
|
|
121
141
|
|
|
@@ -126,6 +146,7 @@ class M365(object):
|
|
|
126
146
|
domain (str): M365 domain
|
|
127
147
|
sku_id (str): License SKU for M365 users
|
|
128
148
|
teams_app_name (str): name of the Extended ECM app for MS Teams
|
|
149
|
+
teams_app_external_id (str): external ID of the Extended ECM app for MS Teams
|
|
129
150
|
"""
|
|
130
151
|
|
|
131
152
|
m365_config = {}
|
|
@@ -137,6 +158,10 @@ class M365(object):
|
|
|
137
158
|
m365_config["domain"] = domain
|
|
138
159
|
m365_config["skuId"] = sku_id
|
|
139
160
|
m365_config["teamsAppName"] = teams_app_name
|
|
161
|
+
m365_config["teamsAppExternalId"] = (
|
|
162
|
+
teams_app_external_id # this is the external App ID
|
|
163
|
+
)
|
|
164
|
+
m365_config["teamsAppInternalId"] = None # will be set later...
|
|
140
165
|
m365_config[
|
|
141
166
|
"authenticationUrl"
|
|
142
167
|
] = "https://login.microsoftonline.com/{}/oauth2/v2.0/token".format(tenant_id)
|
|
@@ -162,6 +187,7 @@ class M365(object):
|
|
|
162
187
|
m365_config["applicationsUrl"] = m365_config["graphUrl"] + "applications"
|
|
163
188
|
|
|
164
189
|
self._config = m365_config
|
|
190
|
+
self._http_object = HTTP()
|
|
165
191
|
|
|
166
192
|
def config(self) -> dict:
|
|
167
193
|
"""Returns the configuration dictionary
|
|
@@ -396,7 +422,7 @@ class M365(object):
|
|
|
396
422
|
|
|
397
423
|
# Already authenticated and session still valid?
|
|
398
424
|
if self._access_token and not revalidate:
|
|
399
|
-
logger.
|
|
425
|
+
logger.debug(
|
|
400
426
|
"Session still valid - return existing access token -> %s",
|
|
401
427
|
str(self._access_token),
|
|
402
428
|
)
|
|
@@ -405,7 +431,7 @@ class M365(object):
|
|
|
405
431
|
request_url = self.config()["authenticationUrl"]
|
|
406
432
|
request_header = request_login_headers
|
|
407
433
|
|
|
408
|
-
logger.
|
|
434
|
+
logger.debug("Requesting M365 Access Token from -> %s", request_url)
|
|
409
435
|
|
|
410
436
|
authenticate_post_body = self.credentials()
|
|
411
437
|
authenticate_response = None
|
|
@@ -415,7 +441,7 @@ class M365(object):
|
|
|
415
441
|
request_url,
|
|
416
442
|
data=authenticate_post_body,
|
|
417
443
|
headers=request_header,
|
|
418
|
-
timeout=
|
|
444
|
+
timeout=REQUEST_TIMEOUT,
|
|
419
445
|
)
|
|
420
446
|
except requests.exceptions.ConnectionError as exception:
|
|
421
447
|
logger.warning(
|
|
@@ -459,7 +485,7 @@ class M365(object):
|
|
|
459
485
|
request_url = self.config()["authenticationUrl"]
|
|
460
486
|
request_header = request_login_headers
|
|
461
487
|
|
|
462
|
-
logger.
|
|
488
|
+
logger.debug(
|
|
463
489
|
"Requesting M365 Access Token for user -> %s from -> %s",
|
|
464
490
|
username,
|
|
465
491
|
request_url,
|
|
@@ -473,7 +499,7 @@ class M365(object):
|
|
|
473
499
|
request_url,
|
|
474
500
|
data=authenticate_post_body,
|
|
475
501
|
headers=request_header,
|
|
476
|
-
timeout=
|
|
502
|
+
timeout=REQUEST_TIMEOUT,
|
|
477
503
|
)
|
|
478
504
|
except requests.exceptions.ConnectionError as exception:
|
|
479
505
|
logger.warning(
|
|
@@ -515,16 +541,18 @@ class M365(object):
|
|
|
515
541
|
request_url = self.config()["usersUrl"]
|
|
516
542
|
request_header = self.request_header()
|
|
517
543
|
|
|
518
|
-
logger.
|
|
544
|
+
logger.debug("Get list of all users; calling -> %s", request_url)
|
|
519
545
|
|
|
520
546
|
retries = 0
|
|
521
547
|
while True:
|
|
522
|
-
response = requests.get(
|
|
548
|
+
response = requests.get(
|
|
549
|
+
request_url, headers=request_header, timeout=REQUEST_TIMEOUT
|
|
550
|
+
)
|
|
523
551
|
if response.ok:
|
|
524
552
|
return self.parse_request_response(response)
|
|
525
553
|
# Check if Session has expired - then re-authenticate and try once more
|
|
526
554
|
elif response.status_code == 401 and retries == 0:
|
|
527
|
-
logger.
|
|
555
|
+
logger.debug("Session has expired - try to re-authenticate...")
|
|
528
556
|
self.authenticate(revalidate=True)
|
|
529
557
|
request_header = self.request_header()
|
|
530
558
|
retries += 1
|
|
@@ -573,19 +601,46 @@ class M365(object):
|
|
|
573
601
|
}
|
|
574
602
|
"""
|
|
575
603
|
|
|
604
|
+
# Some sanity checks:
|
|
605
|
+
if not "@" in user_email or not "." in user_email:
|
|
606
|
+
logger.error("User email -> %s is not a valid email address", user_email)
|
|
607
|
+
return None
|
|
608
|
+
|
|
609
|
+
# if there's an alias in the E-Mail Adress we remove it as
|
|
610
|
+
# MS Graph seems to not support an alias to lookup a user object.
|
|
611
|
+
if "+" in user_email:
|
|
612
|
+
logger.info(
|
|
613
|
+
"Removing Alias from email address -> %s to determine M365 principal name...",
|
|
614
|
+
user_email,
|
|
615
|
+
)
|
|
616
|
+
# Find the index of the '+' character
|
|
617
|
+
alias_index = user_email.find("+")
|
|
618
|
+
|
|
619
|
+
# Find the index of the '@' character
|
|
620
|
+
domain_index = user_email.find("@")
|
|
621
|
+
|
|
622
|
+
# Construct the email address without the alias
|
|
623
|
+
user_email = user_email[:alias_index] + user_email[domain_index:]
|
|
624
|
+
logger.info(
|
|
625
|
+
"M365 user principal name -> %s",
|
|
626
|
+
user_email,
|
|
627
|
+
)
|
|
628
|
+
|
|
576
629
|
request_url = self.config()["usersUrl"] + "/" + user_email
|
|
577
630
|
request_header = self.request_header()
|
|
578
631
|
|
|
579
|
-
logger.
|
|
632
|
+
logger.debug("Get M365 user -> %s; calling -> %s", user_email, request_url)
|
|
580
633
|
|
|
581
634
|
retries = 0
|
|
582
635
|
while True:
|
|
583
|
-
response = requests.get(
|
|
636
|
+
response = requests.get(
|
|
637
|
+
request_url, headers=request_header, timeout=REQUEST_TIMEOUT
|
|
638
|
+
)
|
|
584
639
|
if response.ok:
|
|
585
640
|
return self.parse_request_response(response)
|
|
586
641
|
# Check if Session has expired - then re-authenticate and try once more
|
|
587
642
|
elif response.status_code == 401 and retries == 0:
|
|
588
|
-
logger.
|
|
643
|
+
logger.debug("Session has expired - try to re-authenticate...")
|
|
589
644
|
self.authenticate(revalidate=True)
|
|
590
645
|
request_header = self.request_header()
|
|
591
646
|
retries += 1
|
|
@@ -606,7 +661,7 @@ class M365(object):
|
|
|
606
661
|
response.text,
|
|
607
662
|
)
|
|
608
663
|
else:
|
|
609
|
-
logger.
|
|
664
|
+
logger.debug("M365 User -> %s not found.", user_email)
|
|
610
665
|
return None
|
|
611
666
|
|
|
612
667
|
# end method definition
|
|
@@ -657,7 +712,7 @@ class M365(object):
|
|
|
657
712
|
request_url = self.config()["usersUrl"]
|
|
658
713
|
request_header = self.request_header()
|
|
659
714
|
|
|
660
|
-
logger.
|
|
715
|
+
logger.debug("Adding M365 user -> %s; calling -> %s", email, request_url)
|
|
661
716
|
|
|
662
717
|
retries = 0
|
|
663
718
|
while True:
|
|
@@ -665,13 +720,13 @@ class M365(object):
|
|
|
665
720
|
request_url,
|
|
666
721
|
data=json.dumps(user_post_body),
|
|
667
722
|
headers=request_header,
|
|
668
|
-
timeout=
|
|
723
|
+
timeout=REQUEST_TIMEOUT,
|
|
669
724
|
)
|
|
670
725
|
if response.ok:
|
|
671
726
|
return self.parse_request_response(response)
|
|
672
727
|
# Check if Session has expired - then re-authenticate and try once more
|
|
673
728
|
elif response.status_code == 401 and retries == 0:
|
|
674
|
-
logger.
|
|
729
|
+
logger.debug("Session has expired - try to re-authenticate...")
|
|
675
730
|
self.authenticate(revalidate=True)
|
|
676
731
|
request_header = self.request_header()
|
|
677
732
|
retries += 1
|
|
@@ -698,6 +753,9 @@ class M365(object):
|
|
|
698
753
|
"""Update selected properties of an M365 user. Documentation
|
|
699
754
|
on user properties is here: https://learn.microsoft.com/en-us/graph/api/user-update
|
|
700
755
|
|
|
756
|
+
Args:
|
|
757
|
+
user_id (str): ID of the user (can also be email). This is also the unique identifier
|
|
758
|
+
updated_settings (dict): new data to update the user with
|
|
701
759
|
Returns:
|
|
702
760
|
dict | None: Response of the M365 Graph API or None if the call fails.
|
|
703
761
|
"""
|
|
@@ -705,8 +763,8 @@ class M365(object):
|
|
|
705
763
|
request_url = self.config()["usersUrl"] + "/" + user_id
|
|
706
764
|
request_header = self.request_header()
|
|
707
765
|
|
|
708
|
-
logger.
|
|
709
|
-
"Updating M365 user -> %s with -> %s; calling -> %s",
|
|
766
|
+
logger.debug(
|
|
767
|
+
"Updating M365 user with ID -> %s with -> %s; calling -> %s",
|
|
710
768
|
user_id,
|
|
711
769
|
str(updated_settings),
|
|
712
770
|
request_url,
|
|
@@ -718,13 +776,13 @@ class M365(object):
|
|
|
718
776
|
request_url,
|
|
719
777
|
json=updated_settings,
|
|
720
778
|
headers=request_header,
|
|
721
|
-
timeout=
|
|
779
|
+
timeout=REQUEST_TIMEOUT,
|
|
722
780
|
)
|
|
723
781
|
if response.ok:
|
|
724
782
|
return self.parse_request_response(response)
|
|
725
783
|
# Check if Session has expired - then re-authenticate and try once more
|
|
726
784
|
elif response.status_code == 401 and retries == 0:
|
|
727
|
-
logger.
|
|
785
|
+
logger.debug("Session has expired - try to re-authenticate...")
|
|
728
786
|
self.authenticate(revalidate=True)
|
|
729
787
|
request_header = self.request_header()
|
|
730
788
|
retries += 1
|
|
@@ -775,12 +833,14 @@ class M365(object):
|
|
|
775
833
|
|
|
776
834
|
retries = 0
|
|
777
835
|
while True:
|
|
778
|
-
response = requests.get(
|
|
836
|
+
response = requests.get(
|
|
837
|
+
request_url, headers=request_header, timeout=REQUEST_TIMEOUT
|
|
838
|
+
)
|
|
779
839
|
if response.ok:
|
|
780
840
|
return self.parse_request_response(response)
|
|
781
841
|
# Check if Session has expired - then re-authenticate and try once more
|
|
782
842
|
elif response.status_code == 401 and retries == 0:
|
|
783
|
-
logger.
|
|
843
|
+
logger.debug("Session has expired - try to re-authenticate...")
|
|
784
844
|
self.authenticate(revalidate=True)
|
|
785
845
|
request_header = self.request_header()
|
|
786
846
|
retries += 1
|
|
@@ -830,7 +890,7 @@ class M365(object):
|
|
|
830
890
|
"removeLicenses": [],
|
|
831
891
|
}
|
|
832
892
|
|
|
833
|
-
logger.
|
|
893
|
+
logger.debug(
|
|
834
894
|
"Assign M365 license -> %s to M365 user -> %s; calling -> %s",
|
|
835
895
|
sku_id,
|
|
836
896
|
user_id,
|
|
@@ -840,13 +900,16 @@ class M365(object):
|
|
|
840
900
|
retries = 0
|
|
841
901
|
while True:
|
|
842
902
|
response = requests.post(
|
|
843
|
-
request_url,
|
|
903
|
+
request_url,
|
|
904
|
+
json=license_post_body,
|
|
905
|
+
headers=request_header,
|
|
906
|
+
timeout=REQUEST_TIMEOUT,
|
|
844
907
|
)
|
|
845
908
|
if response.ok:
|
|
846
909
|
return self.parse_request_response(response)
|
|
847
910
|
# Check if Session has expired - then re-authenticate and try once more
|
|
848
911
|
elif response.status_code == 401 and retries == 0:
|
|
849
|
-
logger.
|
|
912
|
+
logger.debug("Session has expired - try to re-authenticate...")
|
|
850
913
|
self.authenticate(revalidate=True)
|
|
851
914
|
request_header = self.request_header()
|
|
852
915
|
retries += 1
|
|
@@ -885,16 +948,18 @@ class M365(object):
|
|
|
885
948
|
# Set image as content type:
|
|
886
949
|
request_header = self.request_header("image/*")
|
|
887
950
|
|
|
888
|
-
logger.
|
|
951
|
+
logger.debug("Get photo of user -> %s; calling -> %s", user_id, request_url)
|
|
889
952
|
|
|
890
953
|
retries = 0
|
|
891
954
|
while True:
|
|
892
|
-
response = requests.get(
|
|
955
|
+
response = requests.get(
|
|
956
|
+
request_url, headers=request_header, timeout=REQUEST_TIMEOUT
|
|
957
|
+
)
|
|
893
958
|
if response.ok:
|
|
894
959
|
return response.content # this is the actual image - not json!
|
|
895
960
|
# Check if Session has expired - then re-authenticate and try once more
|
|
896
961
|
elif response.status_code == 401 and retries == 0:
|
|
897
|
-
logger.
|
|
962
|
+
logger.debug("Session has expired - try to re-authenticate...")
|
|
898
963
|
self.authenticate(revalidate=True)
|
|
899
964
|
request_header = self.request_header()
|
|
900
965
|
retries += 1
|
|
@@ -915,7 +980,88 @@ class M365(object):
|
|
|
915
980
|
response.text,
|
|
916
981
|
)
|
|
917
982
|
else:
|
|
918
|
-
logger.
|
|
983
|
+
logger.debug("M365 User -> %s does not yet have a photo.", user_id)
|
|
984
|
+
return None
|
|
985
|
+
|
|
986
|
+
# end method definition
|
|
987
|
+
|
|
988
|
+
def download_user_photo(self, user_id: str, photo_path: str) -> str:
|
|
989
|
+
"""Download the M365 user photo and save it to the local file system
|
|
990
|
+
|
|
991
|
+
Args:
|
|
992
|
+
user_id (str): M365 GUID of the user (can also be the M365 email of the user)
|
|
993
|
+
photo_path (str): Directory where the photo should be saved
|
|
994
|
+
Returns:
|
|
995
|
+
str: name of the photo file in the file system (with full path) or None if
|
|
996
|
+
the call of the REST API fails.
|
|
997
|
+
"""
|
|
998
|
+
|
|
999
|
+
request_url = self.config()["usersUrl"] + "/" + user_id + "/photo/$value"
|
|
1000
|
+
request_header = self.request_header("application/json")
|
|
1001
|
+
|
|
1002
|
+
logger.debug(
|
|
1003
|
+
"Downloading photo for M365 user with ID -> %s; calling -> %s",
|
|
1004
|
+
user_id,
|
|
1005
|
+
request_url,
|
|
1006
|
+
)
|
|
1007
|
+
|
|
1008
|
+
retries = 0
|
|
1009
|
+
while True:
|
|
1010
|
+
response = requests.get(
|
|
1011
|
+
request_url,
|
|
1012
|
+
headers=request_header,
|
|
1013
|
+
timeout=REQUEST_TIMEOUT,
|
|
1014
|
+
stream=True,
|
|
1015
|
+
)
|
|
1016
|
+
if response.ok:
|
|
1017
|
+
content_type = response.headers.get("Content-Type", "image/png")
|
|
1018
|
+
if content_type == "image/jpeg":
|
|
1019
|
+
file_extension = "jpg"
|
|
1020
|
+
elif content_type == "image/png":
|
|
1021
|
+
file_extension = "png"
|
|
1022
|
+
else:
|
|
1023
|
+
file_extension = "img" # Default extension if type is unknown
|
|
1024
|
+
file_path = os.path.join(
|
|
1025
|
+
photo_path, "{}.{}".format(user_id, file_extension)
|
|
1026
|
+
)
|
|
1027
|
+
|
|
1028
|
+
try:
|
|
1029
|
+
with open(file_path, "wb") as file:
|
|
1030
|
+
for chunk in response.iter_content(chunk_size=8192):
|
|
1031
|
+
file.write(chunk)
|
|
1032
|
+
logger.info(
|
|
1033
|
+
"Photo for M365 user with ID -> %s saved to %s",
|
|
1034
|
+
user_id,
|
|
1035
|
+
file_path,
|
|
1036
|
+
)
|
|
1037
|
+
return file_path
|
|
1038
|
+
except OSError as exception:
|
|
1039
|
+
logger.error(
|
|
1040
|
+
"Error saving photo for user with ID -> %s; error -> %s",
|
|
1041
|
+
user_id,
|
|
1042
|
+
exception,
|
|
1043
|
+
)
|
|
1044
|
+
return None
|
|
1045
|
+
elif response.status_code == 401 and retries == 0:
|
|
1046
|
+
logger.debug("Session has expired - try to re-authenticate...")
|
|
1047
|
+
self.authenticate(revalidate=True)
|
|
1048
|
+
request_header = self.request_header("application/json")
|
|
1049
|
+
retries += 1
|
|
1050
|
+
elif response.status_code in [502, 503, 504] and retries < 3:
|
|
1051
|
+
logger.warning(
|
|
1052
|
+
"M365 Graph API delivered server side error -> %s; retrying in %s seconds...",
|
|
1053
|
+
response.status_code,
|
|
1054
|
+
(retries + 1) * 60,
|
|
1055
|
+
)
|
|
1056
|
+
time.sleep((retries + 1) * 60)
|
|
1057
|
+
retries += 1
|
|
1058
|
+
else:
|
|
1059
|
+
logger.error(
|
|
1060
|
+
"Failed to download photo for user with ID -> %s; status -> %s; error -> %s",
|
|
1061
|
+
user_id,
|
|
1062
|
+
response.status_code,
|
|
1063
|
+
response.text,
|
|
1064
|
+
)
|
|
919
1065
|
return None
|
|
920
1066
|
|
|
921
1067
|
# end method definition
|
|
@@ -952,8 +1098,8 @@ class M365(object):
|
|
|
952
1098
|
|
|
953
1099
|
data = photo_data
|
|
954
1100
|
|
|
955
|
-
logger.
|
|
956
|
-
"Update M365 user -> %s with photo -> %s; calling -> %s",
|
|
1101
|
+
logger.debug(
|
|
1102
|
+
"Update M365 user with ID -> %s with photo -> %s; calling -> %s",
|
|
957
1103
|
user_id,
|
|
958
1104
|
photo_path,
|
|
959
1105
|
request_url,
|
|
@@ -962,13 +1108,13 @@ class M365(object):
|
|
|
962
1108
|
retries = 0
|
|
963
1109
|
while True:
|
|
964
1110
|
response = requests.put(
|
|
965
|
-
request_url, headers=request_header, data=data, timeout=
|
|
1111
|
+
request_url, headers=request_header, data=data, timeout=REQUEST_TIMEOUT
|
|
966
1112
|
)
|
|
967
1113
|
if response.ok:
|
|
968
1114
|
return self.parse_request_response(response)
|
|
969
1115
|
# Check if Session has expired - then re-authenticate and try once more
|
|
970
1116
|
elif response.status_code == 401 and retries == 0:
|
|
971
|
-
logger.
|
|
1117
|
+
logger.debug("Session has expired - try to re-authenticate...")
|
|
972
1118
|
self.authenticate(revalidate=True)
|
|
973
1119
|
request_header = self.request_header()
|
|
974
1120
|
retries += 1
|
|
@@ -982,7 +1128,7 @@ class M365(object):
|
|
|
982
1128
|
retries += 1
|
|
983
1129
|
else:
|
|
984
1130
|
logger.error(
|
|
985
|
-
"Failed to update user -> %s with photo -> %s; status -> %s; error -> %s",
|
|
1131
|
+
"Failed to update user with ID -> %s with photo -> %s; status -> %s; error -> %s",
|
|
986
1132
|
user_id,
|
|
987
1133
|
photo_path,
|
|
988
1134
|
response.status_code,
|
|
@@ -1004,7 +1150,7 @@ class M365(object):
|
|
|
1004
1150
|
request_url = self.config()["groupsUrl"]
|
|
1005
1151
|
request_header = self.request_header()
|
|
1006
1152
|
|
|
1007
|
-
logger.
|
|
1153
|
+
logger.debug("Get list of all M365 groups; calling -> %s", request_url)
|
|
1008
1154
|
|
|
1009
1155
|
retries = 0
|
|
1010
1156
|
while True:
|
|
@@ -1012,13 +1158,13 @@ class M365(object):
|
|
|
1012
1158
|
request_url,
|
|
1013
1159
|
headers=request_header,
|
|
1014
1160
|
params={"$top": str(max_number)},
|
|
1015
|
-
timeout=
|
|
1161
|
+
timeout=REQUEST_TIMEOUT,
|
|
1016
1162
|
)
|
|
1017
1163
|
if response.ok:
|
|
1018
1164
|
return self.parse_request_response(response)
|
|
1019
1165
|
# Check if Session has expired - then re-authenticate and try once more
|
|
1020
1166
|
elif response.status_code == 401 and retries == 0:
|
|
1021
|
-
logger.
|
|
1167
|
+
logger.debug("Session has expired - try to re-authenticate...")
|
|
1022
1168
|
self.authenticate(revalidate=True)
|
|
1023
1169
|
request_header = self.request_header()
|
|
1024
1170
|
retries += 1
|
|
@@ -1101,16 +1247,18 @@ class M365(object):
|
|
|
1101
1247
|
request_url = self.config()["groupsUrl"] + "?" + encoded_query
|
|
1102
1248
|
request_header = self.request_header()
|
|
1103
1249
|
|
|
1104
|
-
logger.
|
|
1250
|
+
logger.debug("Get M365 group -> %s; calling -> %s", group_name, request_url)
|
|
1105
1251
|
|
|
1106
1252
|
retries = 0
|
|
1107
1253
|
while True:
|
|
1108
|
-
response = requests.get(
|
|
1254
|
+
response = requests.get(
|
|
1255
|
+
request_url, headers=request_header, timeout=REQUEST_TIMEOUT
|
|
1256
|
+
)
|
|
1109
1257
|
if response.ok:
|
|
1110
1258
|
return self.parse_request_response(response)
|
|
1111
1259
|
# Check if Session has expired - then re-authenticate and try once more
|
|
1112
1260
|
elif response.status_code == 401 and retries == 0:
|
|
1113
|
-
logger.
|
|
1261
|
+
logger.debug("Session has expired - try to re-authenticate...")
|
|
1114
1262
|
self.authenticate(revalidate=True)
|
|
1115
1263
|
request_header = self.request_header()
|
|
1116
1264
|
retries += 1
|
|
@@ -1131,7 +1279,7 @@ class M365(object):
|
|
|
1131
1279
|
response.text,
|
|
1132
1280
|
)
|
|
1133
1281
|
else:
|
|
1134
|
-
logger.
|
|
1282
|
+
logger.debug("M365 Group -> %s not found.", group_name)
|
|
1135
1283
|
return None
|
|
1136
1284
|
|
|
1137
1285
|
# end method definition
|
|
@@ -1197,7 +1345,7 @@ class M365(object):
|
|
|
1197
1345
|
request_url = self.config()["groupsUrl"]
|
|
1198
1346
|
request_header = self.request_header()
|
|
1199
1347
|
|
|
1200
|
-
logger.
|
|
1348
|
+
logger.debug("Adding M365 group -> %s; calling -> %s", name, request_url)
|
|
1201
1349
|
logger.debug("M365 group attributes -> %s", group_post_body)
|
|
1202
1350
|
|
|
1203
1351
|
retries = 0
|
|
@@ -1206,13 +1354,13 @@ class M365(object):
|
|
|
1206
1354
|
request_url,
|
|
1207
1355
|
data=json.dumps(group_post_body),
|
|
1208
1356
|
headers=request_header,
|
|
1209
|
-
timeout=
|
|
1357
|
+
timeout=REQUEST_TIMEOUT,
|
|
1210
1358
|
)
|
|
1211
1359
|
if response.ok:
|
|
1212
1360
|
return self.parse_request_response(response)
|
|
1213
1361
|
# Check if Session has expired - then re-authenticate and try once more
|
|
1214
1362
|
elif response.status_code == 401 and retries == 0:
|
|
1215
|
-
logger.
|
|
1363
|
+
logger.debug("Session has expired - try to re-authenticate...")
|
|
1216
1364
|
self.authenticate(revalidate=True)
|
|
1217
1365
|
request_header = self.request_header()
|
|
1218
1366
|
retries += 1
|
|
@@ -1261,7 +1409,7 @@ class M365(object):
|
|
|
1261
1409
|
)
|
|
1262
1410
|
request_header = self.request_header()
|
|
1263
1411
|
|
|
1264
|
-
logger.
|
|
1412
|
+
logger.debug(
|
|
1265
1413
|
"Get members of M365 group -> %s (%s); calling -> %s",
|
|
1266
1414
|
group_name,
|
|
1267
1415
|
group_id,
|
|
@@ -1270,12 +1418,14 @@ class M365(object):
|
|
|
1270
1418
|
|
|
1271
1419
|
retries = 0
|
|
1272
1420
|
while True:
|
|
1273
|
-
response = requests.get(
|
|
1421
|
+
response = requests.get(
|
|
1422
|
+
request_url, headers=request_header, timeout=REQUEST_TIMEOUT
|
|
1423
|
+
)
|
|
1274
1424
|
if response.ok:
|
|
1275
1425
|
return self.parse_request_response(response)
|
|
1276
1426
|
# Check if Session has expired - then re-authenticate and try once more
|
|
1277
1427
|
elif response.status_code == 401 and retries == 0:
|
|
1278
|
-
logger.
|
|
1428
|
+
logger.debug("Session has expired - try to re-authenticate...")
|
|
1279
1429
|
self.authenticate(revalidate=True)
|
|
1280
1430
|
request_header = self.request_header()
|
|
1281
1431
|
retries += 1
|
|
@@ -1316,7 +1466,7 @@ class M365(object):
|
|
|
1316
1466
|
"@odata.id": self.config()["directoryObjects"] + "/" + member_id
|
|
1317
1467
|
}
|
|
1318
1468
|
|
|
1319
|
-
logger.
|
|
1469
|
+
logger.debug(
|
|
1320
1470
|
"Adding member -> %s to group -> %s; calling -> %s",
|
|
1321
1471
|
member_id,
|
|
1322
1472
|
group_id,
|
|
@@ -1329,14 +1479,14 @@ class M365(object):
|
|
|
1329
1479
|
request_url,
|
|
1330
1480
|
headers=request_header,
|
|
1331
1481
|
data=json.dumps(group_member_post_body),
|
|
1332
|
-
timeout=
|
|
1482
|
+
timeout=REQUEST_TIMEOUT,
|
|
1333
1483
|
)
|
|
1334
1484
|
if response.ok:
|
|
1335
1485
|
return self.parse_request_response(response)
|
|
1336
1486
|
|
|
1337
1487
|
# Check if Session has expired - then re-authenticate and try once more
|
|
1338
1488
|
if response.status_code == 401 and retries == 0:
|
|
1339
|
-
logger.
|
|
1489
|
+
logger.debug("Session has expired - try to re-authenticate...")
|
|
1340
1490
|
self.authenticate(revalidate=True)
|
|
1341
1491
|
request_header = self.request_header()
|
|
1342
1492
|
retries += 1
|
|
@@ -1379,7 +1529,7 @@ class M365(object):
|
|
|
1379
1529
|
)
|
|
1380
1530
|
request_header = self.request_header()
|
|
1381
1531
|
|
|
1382
|
-
logger.
|
|
1532
|
+
logger.debug(
|
|
1383
1533
|
"Check if user -> %s is in group -> %s; calling -> %s",
|
|
1384
1534
|
member_id,
|
|
1385
1535
|
group_id,
|
|
@@ -1388,7 +1538,9 @@ class M365(object):
|
|
|
1388
1538
|
|
|
1389
1539
|
retries = 0
|
|
1390
1540
|
while True:
|
|
1391
|
-
response = requests.get(
|
|
1541
|
+
response = requests.get(
|
|
1542
|
+
request_url, headers=request_header, timeout=REQUEST_TIMEOUT
|
|
1543
|
+
)
|
|
1392
1544
|
if response.ok:
|
|
1393
1545
|
response = self.parse_request_response(response)
|
|
1394
1546
|
if not "value" in response or len(response["value"]) == 0:
|
|
@@ -1396,7 +1548,7 @@ class M365(object):
|
|
|
1396
1548
|
return True
|
|
1397
1549
|
# Check if Session has expired - then re-authenticate and try once more
|
|
1398
1550
|
elif response.status_code == 401 and retries == 0:
|
|
1399
|
-
logger.
|
|
1551
|
+
logger.debug("Session has expired - try to re-authenticate...")
|
|
1400
1552
|
self.authenticate(revalidate=True)
|
|
1401
1553
|
request_header = self.request_header()
|
|
1402
1554
|
retries += 1
|
|
@@ -1449,7 +1601,7 @@ class M365(object):
|
|
|
1449
1601
|
)
|
|
1450
1602
|
request_header = self.request_header()
|
|
1451
1603
|
|
|
1452
|
-
logger.
|
|
1604
|
+
logger.debug(
|
|
1453
1605
|
"Get owners of M365 group -> %s (%s); calling -> %s",
|
|
1454
1606
|
group_name,
|
|
1455
1607
|
group_id,
|
|
@@ -1458,12 +1610,14 @@ class M365(object):
|
|
|
1458
1610
|
|
|
1459
1611
|
retries = 0
|
|
1460
1612
|
while True:
|
|
1461
|
-
response = requests.get(
|
|
1613
|
+
response = requests.get(
|
|
1614
|
+
request_url, headers=request_header, timeout=REQUEST_TIMEOUT
|
|
1615
|
+
)
|
|
1462
1616
|
if response.ok:
|
|
1463
1617
|
return self.parse_request_response(response)
|
|
1464
1618
|
# Check if Session has expired - then re-authenticate and try once more
|
|
1465
1619
|
elif response.status_code == 401 and retries == 0:
|
|
1466
|
-
logger.
|
|
1620
|
+
logger.debug("Session has expired - try to re-authenticate...")
|
|
1467
1621
|
self.authenticate(revalidate=True)
|
|
1468
1622
|
request_header = self.request_header()
|
|
1469
1623
|
retries += 1
|
|
@@ -1504,7 +1658,7 @@ class M365(object):
|
|
|
1504
1658
|
"@odata.id": self.config()["directoryObjects"] + "/" + owner_id
|
|
1505
1659
|
}
|
|
1506
1660
|
|
|
1507
|
-
logger.
|
|
1661
|
+
logger.debug(
|
|
1508
1662
|
"Adding owner -> %s to M365 group -> %s; calling -> %s",
|
|
1509
1663
|
owner_id,
|
|
1510
1664
|
group_id,
|
|
@@ -1517,13 +1671,13 @@ class M365(object):
|
|
|
1517
1671
|
request_url,
|
|
1518
1672
|
headers=request_header,
|
|
1519
1673
|
data=json.dumps(group_member_post_body),
|
|
1520
|
-
timeout=
|
|
1674
|
+
timeout=REQUEST_TIMEOUT,
|
|
1521
1675
|
)
|
|
1522
1676
|
if response.ok:
|
|
1523
1677
|
return self.parse_request_response(response)
|
|
1524
1678
|
# Check if Session has expired - then re-authenticate and try once more
|
|
1525
1679
|
elif response.status_code == 401 and retries == 0:
|
|
1526
|
-
logger.
|
|
1680
|
+
logger.debug("Session has expired - try to re-authenticate...")
|
|
1527
1681
|
self.authenticate(revalidate=True)
|
|
1528
1682
|
request_header = self.request_header()
|
|
1529
1683
|
retries += 1
|
|
@@ -1558,7 +1712,9 @@ class M365(object):
|
|
|
1558
1712
|
request_url = (
|
|
1559
1713
|
self.config()["directoryUrl"] + "/deletedItems/microsoft.graph.group"
|
|
1560
1714
|
)
|
|
1561
|
-
response = requests.get(
|
|
1715
|
+
response = requests.get(
|
|
1716
|
+
request_url, headers=request_header, timeout=REQUEST_TIMEOUT
|
|
1717
|
+
)
|
|
1562
1718
|
deleted_groups = self.parse_request_response(response)
|
|
1563
1719
|
|
|
1564
1720
|
for group in deleted_groups["value"]:
|
|
@@ -1568,7 +1724,9 @@ class M365(object):
|
|
|
1568
1724
|
request_url = (
|
|
1569
1725
|
self.config()["directoryUrl"] + "/deletedItems/microsoft.graph.user"
|
|
1570
1726
|
)
|
|
1571
|
-
response = requests.get(
|
|
1727
|
+
response = requests.get(
|
|
1728
|
+
request_url, headers=request_header, timeout=REQUEST_TIMEOUT
|
|
1729
|
+
)
|
|
1572
1730
|
deleted_users = self.parse_request_response(response)
|
|
1573
1731
|
|
|
1574
1732
|
for user in deleted_users["value"]:
|
|
@@ -1591,16 +1749,18 @@ class M365(object):
|
|
|
1591
1749
|
request_url = self.config()["directoryUrl"] + "/deletedItems/" + item_id
|
|
1592
1750
|
request_header = self.request_header()
|
|
1593
1751
|
|
|
1594
|
-
logger.
|
|
1752
|
+
logger.debug("Purging deleted item -> %s; calling -> %s", item_id, request_url)
|
|
1595
1753
|
|
|
1596
1754
|
retries = 0
|
|
1597
1755
|
while True:
|
|
1598
|
-
response = requests.delete(
|
|
1756
|
+
response = requests.delete(
|
|
1757
|
+
request_url, headers=request_header, timeout=REQUEST_TIMEOUT
|
|
1758
|
+
)
|
|
1599
1759
|
if response.ok:
|
|
1600
1760
|
return self.parse_request_response(response)
|
|
1601
1761
|
# Check if Session has expired - then re-authenticate and try once more
|
|
1602
1762
|
elif response.status_code == 401 and retries == 0:
|
|
1603
|
-
logger.
|
|
1763
|
+
logger.debug("Session has expired - try to re-authenticate...")
|
|
1604
1764
|
self.authenticate(revalidate=True)
|
|
1605
1765
|
request_header = self.request_header()
|
|
1606
1766
|
retries += 1
|
|
@@ -1644,7 +1804,7 @@ class M365(object):
|
|
|
1644
1804
|
request_url = self.config()["groupsUrl"] + "/" + group_id + "/team"
|
|
1645
1805
|
request_header = self.request_header()
|
|
1646
1806
|
|
|
1647
|
-
logger.
|
|
1807
|
+
logger.debug(
|
|
1648
1808
|
"Check if M365 Group -> %s has a M365 Team connected; calling -> %s",
|
|
1649
1809
|
group_name,
|
|
1650
1810
|
request_url,
|
|
@@ -1652,16 +1812,18 @@ class M365(object):
|
|
|
1652
1812
|
|
|
1653
1813
|
retries = 0
|
|
1654
1814
|
while True:
|
|
1655
|
-
response = requests.get(
|
|
1815
|
+
response = requests.get(
|
|
1816
|
+
request_url, headers=request_header, timeout=REQUEST_TIMEOUT
|
|
1817
|
+
)
|
|
1656
1818
|
|
|
1657
1819
|
if response.status_code == 200: # Group has a Team assigned!
|
|
1658
|
-
logger.
|
|
1820
|
+
logger.debug("Group -> %s has a M365 Team connected.", group_name)
|
|
1659
1821
|
return True
|
|
1660
1822
|
elif response.status_code == 404: # Group does not have a Team assigned!
|
|
1661
|
-
logger.
|
|
1823
|
+
logger.debug("Group -> %s has no M365 Team connected.", group_name)
|
|
1662
1824
|
return False
|
|
1663
1825
|
elif response.status_code == 401 and retries == 0:
|
|
1664
|
-
logger.
|
|
1826
|
+
logger.debug("Session has expired - try to re-authenticate...")
|
|
1665
1827
|
self.authenticate(revalidate=True)
|
|
1666
1828
|
request_header = self.request_header()
|
|
1667
1829
|
retries += 1
|
|
@@ -1734,20 +1896,22 @@ class M365(object):
|
|
|
1734
1896
|
|
|
1735
1897
|
request_header = self.request_header()
|
|
1736
1898
|
|
|
1737
|
-
logger.
|
|
1738
|
-
"Lookup Microsoft 365 Teams with name -> %s; calling -> %s",
|
|
1899
|
+
logger.debug(
|
|
1900
|
+
"Lookup Microsoft 365 Teams with name -> '%s'; calling -> %s",
|
|
1739
1901
|
name,
|
|
1740
1902
|
request_url,
|
|
1741
1903
|
)
|
|
1742
1904
|
|
|
1743
1905
|
retries = 0
|
|
1744
1906
|
while True:
|
|
1745
|
-
response = requests.get(
|
|
1907
|
+
response = requests.get(
|
|
1908
|
+
request_url, headers=request_header, timeout=REQUEST_TIMEOUT
|
|
1909
|
+
)
|
|
1746
1910
|
if response.ok:
|
|
1747
1911
|
return self.parse_request_response(response)
|
|
1748
1912
|
# Check if Session has expired - then re-authenticate and try once more
|
|
1749
1913
|
elif response.status_code == 401 and retries == 0:
|
|
1750
|
-
logger.
|
|
1914
|
+
logger.debug("Session has expired - try to re-authenticate...")
|
|
1751
1915
|
self.authenticate(revalidate=True)
|
|
1752
1916
|
request_header = self.request_header()
|
|
1753
1917
|
retries += 1
|
|
@@ -1785,7 +1949,7 @@ class M365(object):
|
|
|
1785
1949
|
group_id = self.get_result_value(response, "id", 0)
|
|
1786
1950
|
if not group_id:
|
|
1787
1951
|
logger.error(
|
|
1788
|
-
"M365 Group -> %s not found. It is required for creating a corresponding M365 Team.",
|
|
1952
|
+
"M365 Group -> '%s' not found. It is required for creating a corresponding M365 Team.",
|
|
1789
1953
|
name,
|
|
1790
1954
|
)
|
|
1791
1955
|
return None
|
|
@@ -1793,7 +1957,7 @@ class M365(object):
|
|
|
1793
1957
|
response = self.get_group_owners(name)
|
|
1794
1958
|
if response is None or not "value" in response or not response["value"]:
|
|
1795
1959
|
logger.warning(
|
|
1796
|
-
"M365 Group -> %s has no owners. This is required for creating a corresponding M365 Team.",
|
|
1960
|
+
"M365 Group -> '%s' has no owners. This is required for creating a corresponding M365 Team.",
|
|
1797
1961
|
name,
|
|
1798
1962
|
)
|
|
1799
1963
|
return None
|
|
@@ -1808,7 +1972,7 @@ class M365(object):
|
|
|
1808
1972
|
request_url = self.config()["teamsUrl"]
|
|
1809
1973
|
request_header = self.request_header()
|
|
1810
1974
|
|
|
1811
|
-
logger.
|
|
1975
|
+
logger.debug("Adding M365 Team -> %s; calling -> %s", name, request_url)
|
|
1812
1976
|
logger.debug("M365 Team attributes -> %s", team_post_body)
|
|
1813
1977
|
|
|
1814
1978
|
retries = 0
|
|
@@ -1817,13 +1981,13 @@ class M365(object):
|
|
|
1817
1981
|
request_url,
|
|
1818
1982
|
data=json.dumps(team_post_body),
|
|
1819
1983
|
headers=request_header,
|
|
1820
|
-
timeout=
|
|
1984
|
+
timeout=REQUEST_TIMEOUT,
|
|
1821
1985
|
)
|
|
1822
1986
|
if response.ok:
|
|
1823
1987
|
return self.parse_request_response(response)
|
|
1824
1988
|
# Check if Session has expired - then re-authenticate and try once more
|
|
1825
1989
|
elif response.status_code == 401 and retries == 0:
|
|
1826
|
-
logger.
|
|
1990
|
+
logger.debug("Session has expired - try to re-authenticate...")
|
|
1827
1991
|
self.authenticate(revalidate=True)
|
|
1828
1992
|
request_header = self.request_header()
|
|
1829
1993
|
retries += 1
|
|
@@ -1837,7 +2001,7 @@ class M365(object):
|
|
|
1837
2001
|
retries += 1
|
|
1838
2002
|
else:
|
|
1839
2003
|
logger.error(
|
|
1840
|
-
"Failed to add M365 Team -> %s; status -> %s; error -> %s",
|
|
2004
|
+
"Failed to add M365 Team -> '%s'; status -> %s; error -> %s",
|
|
1841
2005
|
name,
|
|
1842
2006
|
response.status_code,
|
|
1843
2007
|
response.text,
|
|
@@ -1859,7 +2023,7 @@ class M365(object):
|
|
|
1859
2023
|
|
|
1860
2024
|
request_header = self.request_header()
|
|
1861
2025
|
|
|
1862
|
-
logger.
|
|
2026
|
+
logger.debug(
|
|
1863
2027
|
"Delete Microsoft 365 Teams with ID -> %s; calling -> %s",
|
|
1864
2028
|
team_id,
|
|
1865
2029
|
request_url,
|
|
@@ -1867,12 +2031,14 @@ class M365(object):
|
|
|
1867
2031
|
|
|
1868
2032
|
retries = 0
|
|
1869
2033
|
while True:
|
|
1870
|
-
response = requests.delete(
|
|
2034
|
+
response = requests.delete(
|
|
2035
|
+
request_url, headers=request_header, timeout=REQUEST_TIMEOUT
|
|
2036
|
+
)
|
|
1871
2037
|
if response.ok:
|
|
1872
2038
|
return self.parse_request_response(response)
|
|
1873
2039
|
# Check if Session has expired - then re-authenticate and try once more
|
|
1874
2040
|
elif response.status_code == 401 and retries == 0:
|
|
1875
|
-
logger.
|
|
2041
|
+
logger.debug("Session has expired - try to re-authenticate...")
|
|
1876
2042
|
self.authenticate(revalidate=True)
|
|
1877
2043
|
request_header = self.request_header()
|
|
1878
2044
|
retries += 1
|
|
@@ -1911,21 +2077,23 @@ class M365(object):
|
|
|
1911
2077
|
|
|
1912
2078
|
request_header = self.request_header()
|
|
1913
2079
|
|
|
1914
|
-
logger.
|
|
1915
|
-
"Delete all Microsoft 365 Teams with name -> %s; calling -> %s",
|
|
2080
|
+
logger.debug(
|
|
2081
|
+
"Delete all Microsoft 365 Teams with name -> '%s'; calling -> %s",
|
|
1916
2082
|
name,
|
|
1917
2083
|
request_url,
|
|
1918
2084
|
)
|
|
1919
2085
|
|
|
1920
2086
|
retries = 0
|
|
1921
2087
|
while True:
|
|
1922
|
-
response = requests.get(
|
|
2088
|
+
response = requests.get(
|
|
2089
|
+
request_url, headers=request_header, timeout=REQUEST_TIMEOUT
|
|
2090
|
+
)
|
|
1923
2091
|
if response.ok:
|
|
1924
2092
|
existing_teams = self.parse_request_response(response)
|
|
1925
2093
|
break
|
|
1926
2094
|
# Check if Session has expired - then re-authenticate and try once more
|
|
1927
2095
|
elif response.status_code == 401 and retries == 0:
|
|
1928
|
-
logger.
|
|
2096
|
+
logger.debug("Session has expired - try to re-authenticate...")
|
|
1929
2097
|
self.authenticate(revalidate=True)
|
|
1930
2098
|
request_header = self.request_header()
|
|
1931
2099
|
retries += 1
|
|
@@ -1962,16 +2130,16 @@ class M365(object):
|
|
|
1962
2130
|
counter += 1
|
|
1963
2131
|
|
|
1964
2132
|
logger.info(
|
|
1965
|
-
"%s M365 Teams with name -> %s have been deleted.",
|
|
2133
|
+
"%s M365 Teams with name -> '%s' have been deleted.",
|
|
1966
2134
|
str(counter),
|
|
1967
2135
|
name,
|
|
1968
2136
|
)
|
|
1969
2137
|
return True
|
|
1970
2138
|
else:
|
|
1971
|
-
logger.info("No M365 Teams with name -> %s found.", name)
|
|
2139
|
+
logger.info("No M365 Teams with name -> '%s' found.", name)
|
|
1972
2140
|
return False
|
|
1973
2141
|
else:
|
|
1974
|
-
logger.error("Failed to retrieve M365 Teams with name -> %s", name)
|
|
2142
|
+
logger.error("Failed to retrieve M365 Teams with name -> '%s'", name)
|
|
1975
2143
|
return False
|
|
1976
2144
|
|
|
1977
2145
|
# end method definition
|
|
@@ -1995,19 +2163,20 @@ class M365(object):
|
|
|
1995
2163
|
if not "value" in response or not response["value"]:
|
|
1996
2164
|
return False
|
|
1997
2165
|
groups = response["value"]
|
|
2166
|
+
|
|
1998
2167
|
logger.info(
|
|
1999
2168
|
"Found -> %s existing M365 groups. Checking which ones should be deleted...",
|
|
2000
2169
|
len(groups),
|
|
2001
2170
|
)
|
|
2002
2171
|
|
|
2003
|
-
# Process all groups and check if
|
|
2172
|
+
# Process all groups and check if they should be
|
|
2004
2173
|
# deleted:
|
|
2005
2174
|
for group in groups:
|
|
2006
2175
|
group_name = group["displayName"]
|
|
2007
2176
|
# Check if group is in exception list:
|
|
2008
2177
|
if group_name in exception_list:
|
|
2009
2178
|
logger.info(
|
|
2010
|
-
"M365 Group name -> %s is on the exception list. Skipping...",
|
|
2179
|
+
"M365 Group name -> '%s' is on the exception list. Skipping...",
|
|
2011
2180
|
group_name,
|
|
2012
2181
|
)
|
|
2013
2182
|
continue
|
|
@@ -2016,7 +2185,7 @@ class M365(object):
|
|
|
2016
2185
|
result = re.search(pattern, group_name)
|
|
2017
2186
|
if result:
|
|
2018
2187
|
logger.info(
|
|
2019
|
-
"M365 Group name -> %s is matching pattern -> %s. Delete it now...",
|
|
2188
|
+
"M365 Group name -> '%s' is matching pattern -> %s. Delete it now...",
|
|
2020
2189
|
group_name,
|
|
2021
2190
|
pattern,
|
|
2022
2191
|
)
|
|
@@ -2024,7 +2193,7 @@ class M365(object):
|
|
|
2024
2193
|
break
|
|
2025
2194
|
else:
|
|
2026
2195
|
logger.info(
|
|
2027
|
-
"M365 Group name -> %s is not matching any delete pattern. Skipping...",
|
|
2196
|
+
"M365 Group name -> '%s' is not matching any delete pattern. Skipping...",
|
|
2028
2197
|
group_name,
|
|
2029
2198
|
)
|
|
2030
2199
|
return True
|
|
@@ -2068,20 +2237,22 @@ class M365(object):
|
|
|
2068
2237
|
|
|
2069
2238
|
request_header = self.request_header()
|
|
2070
2239
|
|
|
2071
|
-
logger.
|
|
2072
|
-
"Retrieve channels of Microsoft 365 Team -> %s; calling -> %s",
|
|
2240
|
+
logger.debug(
|
|
2241
|
+
"Retrieve channels of Microsoft 365 Team -> '%s'; calling -> %s",
|
|
2073
2242
|
name,
|
|
2074
2243
|
request_url,
|
|
2075
2244
|
)
|
|
2076
2245
|
|
|
2077
2246
|
retries = 0
|
|
2078
2247
|
while True:
|
|
2079
|
-
response = requests.get(
|
|
2248
|
+
response = requests.get(
|
|
2249
|
+
request_url, headers=request_header, timeout=REQUEST_TIMEOUT
|
|
2250
|
+
)
|
|
2080
2251
|
if response.ok:
|
|
2081
2252
|
return self.parse_request_response(response)
|
|
2082
2253
|
# Check if Session has expired - then re-authenticate and try once more
|
|
2083
2254
|
elif response.status_code == 401 and retries == 0:
|
|
2084
|
-
logger.
|
|
2255
|
+
logger.debug("Session has expired - try to re-authenticate...")
|
|
2085
2256
|
self.authenticate(revalidate=True)
|
|
2086
2257
|
request_header = self.request_header()
|
|
2087
2258
|
retries += 1
|
|
@@ -2151,7 +2322,7 @@ class M365(object):
|
|
|
2151
2322
|
None,
|
|
2152
2323
|
)
|
|
2153
2324
|
if not channel:
|
|
2154
|
-
logger.
|
|
2325
|
+
logger.error(
|
|
2155
2326
|
"Cannot find Channel -> %s on M365 Team -> %s", channel_name, team_name
|
|
2156
2327
|
)
|
|
2157
2328
|
return None
|
|
@@ -2168,7 +2339,7 @@ class M365(object):
|
|
|
2168
2339
|
|
|
2169
2340
|
request_header = self.request_header()
|
|
2170
2341
|
|
|
2171
|
-
logger.
|
|
2342
|
+
logger.debug(
|
|
2172
2343
|
"Retrieve Tabs of Microsoft 365 Teams -> %s and Channel -> %s; calling -> %s",
|
|
2173
2344
|
team_name,
|
|
2174
2345
|
channel_name,
|
|
@@ -2177,12 +2348,14 @@ class M365(object):
|
|
|
2177
2348
|
|
|
2178
2349
|
retries = 0
|
|
2179
2350
|
while True:
|
|
2180
|
-
response = requests.get(
|
|
2351
|
+
response = requests.get(
|
|
2352
|
+
request_url, headers=request_header, timeout=REQUEST_TIMEOUT
|
|
2353
|
+
)
|
|
2181
2354
|
if response.ok:
|
|
2182
2355
|
return self.parse_request_response(response)
|
|
2183
2356
|
# Check if Session has expired - then re-authenticate and try once more
|
|
2184
2357
|
elif response.status_code == 401 and retries == 0:
|
|
2185
|
-
logger.
|
|
2358
|
+
logger.debug("Session has expired - try to re-authenticate...")
|
|
2186
2359
|
self.authenticate(revalidate=True)
|
|
2187
2360
|
request_header = self.request_header()
|
|
2188
2361
|
retries += 1
|
|
@@ -2257,24 +2430,26 @@ class M365(object):
|
|
|
2257
2430
|
request_url = self.config()["teamsAppsUrl"] + "?" + encoded_query
|
|
2258
2431
|
|
|
2259
2432
|
if filter_expression:
|
|
2260
|
-
logger.
|
|
2433
|
+
logger.debug(
|
|
2261
2434
|
"Get list of MS Teams Apps using filter -> %s; calling -> %s",
|
|
2262
2435
|
filter_expression,
|
|
2263
2436
|
request_url,
|
|
2264
2437
|
)
|
|
2265
2438
|
else:
|
|
2266
|
-
logger.
|
|
2439
|
+
logger.debug("Get list of all MS Teams Apps; calling -> %s", request_url)
|
|
2267
2440
|
|
|
2268
2441
|
request_header = self.request_header()
|
|
2269
2442
|
|
|
2270
2443
|
retries = 0
|
|
2271
2444
|
while True:
|
|
2272
|
-
response = requests.get(
|
|
2445
|
+
response = requests.get(
|
|
2446
|
+
request_url, headers=request_header, timeout=REQUEST_TIMEOUT
|
|
2447
|
+
)
|
|
2273
2448
|
if response.ok:
|
|
2274
2449
|
return self.parse_request_response(response)
|
|
2275
2450
|
# Check if Session has expired - then re-authenticate and try once more
|
|
2276
2451
|
elif response.status_code == 401 and retries == 0:
|
|
2277
|
-
logger.
|
|
2452
|
+
logger.debug("Session has expired - try to re-authenticate...")
|
|
2278
2453
|
self.authenticate(revalidate=True)
|
|
2279
2454
|
request_header = self.request_header()
|
|
2280
2455
|
retries += 1
|
|
@@ -2297,34 +2472,58 @@ class M365(object):
|
|
|
2297
2472
|
# end method definition
|
|
2298
2473
|
|
|
2299
2474
|
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
|
|
2475
|
+
"""Get a specific MS Teams app in catalog based on the known (internal) app ID
|
|
2301
2476
|
|
|
2302
2477
|
Args:
|
|
2303
|
-
app_id (str): ID of the app
|
|
2478
|
+
app_id (str): ID of the app (this is NOT the external ID but the internal ID)
|
|
2304
2479
|
Returns:
|
|
2305
2480
|
dict: response of the MS Graph API call or None if the call fails.
|
|
2481
|
+
|
|
2482
|
+
Examle response:
|
|
2483
|
+
{
|
|
2484
|
+
'@odata.context': 'https://graph.microsoft.com/v1.0/$metadata#appCatalogs/teamsApps(appDefinitions())/$entity',
|
|
2485
|
+
'id': 'ccabe3fb-316f-40e0-a486-1659682cb8cd',
|
|
2486
|
+
'externalId': 'dd4af790-d8ff-47a0-87ad-486318272c7a',
|
|
2487
|
+
'displayName': 'Extended ECM',
|
|
2488
|
+
'distributionMethod': 'organization',
|
|
2489
|
+
'appDefinitions@odata.context': "https://graph.microsoft.com/v1.0/$metadata#appCatalogs/teamsApps('ccabe3fb-316f-40e0-a486-1659682cb8cd')/appDefinitions",
|
|
2490
|
+
'appDefinitions': [
|
|
2491
|
+
{
|
|
2492
|
+
'id': 'Y2NhYmUzZmItMzE2Zi00MGUwLWE0ODYtMTY1OTY4MmNiOGNkIyMyNC4yLjAjI1B1Ymxpc2hlZA==',
|
|
2493
|
+
'teamsAppId': 'ccabe3fb-316f-40e0-a486-1659682cb8cd',
|
|
2494
|
+
'displayName': 'Extended ECM',
|
|
2495
|
+
'version': '24.2.0',
|
|
2496
|
+
'publishingState': 'published',
|
|
2497
|
+
'shortDescription': 'Add a tab for an Extended ECM business workspace.',
|
|
2498
|
+
'description': 'View and interact with OpenText Extended ECM business workspaces',
|
|
2499
|
+
'lastModifiedDateTime': None,
|
|
2500
|
+
'createdBy': None,
|
|
2501
|
+
'authorization': {...}
|
|
2502
|
+
}
|
|
2503
|
+
]
|
|
2504
|
+
}
|
|
2306
2505
|
"""
|
|
2307
2506
|
|
|
2308
2507
|
query = {"$expand": "AppDefinitions"}
|
|
2309
2508
|
encoded_query = urllib.parse.urlencode(query, doseq=True)
|
|
2310
2509
|
request_url = self.config()["teamsAppsUrl"] + "/" + app_id + "?" + encoded_query
|
|
2311
2510
|
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
logger.info(
|
|
2315
|
-
"Get MS Teams App with ID -> %s; calling -> %s", app_id, request_url
|
|
2511
|
+
logger.debug(
|
|
2512
|
+
"Get M365 Teams App with ID -> %s; calling -> %s", app_id, request_url
|
|
2316
2513
|
)
|
|
2317
2514
|
|
|
2318
2515
|
request_header = self.request_header()
|
|
2319
2516
|
|
|
2320
2517
|
retries = 0
|
|
2321
2518
|
while True:
|
|
2322
|
-
response = requests.get(
|
|
2519
|
+
response = requests.get(
|
|
2520
|
+
request_url, headers=request_header, timeout=REQUEST_TIMEOUT
|
|
2521
|
+
)
|
|
2323
2522
|
if response.ok:
|
|
2324
2523
|
return self.parse_request_response(response)
|
|
2325
2524
|
# Check if Session has expired - then re-authenticate and try once more
|
|
2326
2525
|
elif response.status_code == 401 and retries == 0:
|
|
2327
|
-
logger.
|
|
2526
|
+
logger.debug("Session has expired - try to re-authenticate...")
|
|
2328
2527
|
self.authenticate(revalidate=True)
|
|
2329
2528
|
request_header = self.request_header()
|
|
2330
2529
|
retries += 1
|
|
@@ -2370,7 +2569,8 @@ class M365(object):
|
|
|
2370
2569
|
+ "/teamwork/installedApps?"
|
|
2371
2570
|
+ encoded_query
|
|
2372
2571
|
)
|
|
2373
|
-
|
|
2572
|
+
|
|
2573
|
+
logger.debug(
|
|
2374
2574
|
"Get list of M365 Teams Apps for user -> %s using query -> %s; calling -> %s",
|
|
2375
2575
|
user_id,
|
|
2376
2576
|
query,
|
|
@@ -2381,12 +2581,14 @@ class M365(object):
|
|
|
2381
2581
|
|
|
2382
2582
|
retries = 0
|
|
2383
2583
|
while True:
|
|
2384
|
-
response = requests.get(
|
|
2584
|
+
response = requests.get(
|
|
2585
|
+
request_url, headers=request_header, timeout=REQUEST_TIMEOUT
|
|
2586
|
+
)
|
|
2385
2587
|
if response.ok:
|
|
2386
2588
|
return self.parse_request_response(response)
|
|
2387
2589
|
# Check if Session has expired - then re-authenticate and try once more
|
|
2388
2590
|
elif response.status_code == 401 and retries == 0:
|
|
2389
|
-
logger.
|
|
2591
|
+
logger.debug("Session has expired - try to re-authenticate...")
|
|
2390
2592
|
self.authenticate(revalidate=True)
|
|
2391
2593
|
request_header = self.request_header()
|
|
2392
2594
|
retries += 1
|
|
@@ -2433,7 +2635,8 @@ class M365(object):
|
|
|
2433
2635
|
+ "/installedApps?"
|
|
2434
2636
|
+ encoded_query
|
|
2435
2637
|
)
|
|
2436
|
-
|
|
2638
|
+
|
|
2639
|
+
logger.debug(
|
|
2437
2640
|
"Get list of M365 Teams Apps for M365 Team -> %s using query -> %s; calling -> %s",
|
|
2438
2641
|
team_id,
|
|
2439
2642
|
query,
|
|
@@ -2444,12 +2647,14 @@ class M365(object):
|
|
|
2444
2647
|
|
|
2445
2648
|
retries = 0
|
|
2446
2649
|
while True:
|
|
2447
|
-
response = requests.get(
|
|
2650
|
+
response = requests.get(
|
|
2651
|
+
request_url, headers=request_header, timeout=REQUEST_TIMEOUT
|
|
2652
|
+
)
|
|
2448
2653
|
if response.ok:
|
|
2449
2654
|
return self.parse_request_response(response)
|
|
2450
2655
|
# Check if Session has expired - then re-authenticate and try once more
|
|
2451
2656
|
elif response.status_code == 401 and retries == 0:
|
|
2452
|
-
logger.
|
|
2657
|
+
logger.debug("Session has expired - try to re-authenticate...")
|
|
2453
2658
|
self.authenticate(revalidate=True)
|
|
2454
2659
|
request_header = self.request_header()
|
|
2455
2660
|
retries += 1
|
|
@@ -2501,6 +2706,7 @@ class M365(object):
|
|
|
2501
2706
|
but requires a token of a user authenticated with username + password.
|
|
2502
2707
|
See https://learn.microsoft.com/en-us/graph/api/teamsapp-publish
|
|
2503
2708
|
(permissions table on that page)
|
|
2709
|
+
For updates see: https://learn.microsoft.com/en-us/graph/api/teamsapp-update?view=graph-rest-1.0&tabs=http
|
|
2504
2710
|
|
|
2505
2711
|
Args:
|
|
2506
2712
|
app_path (str): file path (with directory) to the app package to upload
|
|
@@ -2511,6 +2717,34 @@ class M365(object):
|
|
|
2511
2717
|
after installation (which is tenant specific)
|
|
2512
2718
|
Returns:
|
|
2513
2719
|
dict: Response of the MS GRAPH API REST call or None if the request fails
|
|
2720
|
+
|
|
2721
|
+
The responses are different depending if it is an install or upgrade!!
|
|
2722
|
+
|
|
2723
|
+
Example return for upgrades ("teamsAppId" is the "internal" ID of the app):
|
|
2724
|
+
{
|
|
2725
|
+
'@odata.context': "https://graph.microsoft.com/v1.0/$metadata#appCatalogs/teamsApps('3f749cca-8cb0-4925-9fa0-ba7aca2014af')/appDefinitions/$entity",
|
|
2726
|
+
'id': 'M2Y3NDljY2EtOGNiMC00OTI1LTlmYTAtYmE3YWNhMjAxNGFmIyMyNC4yLjAjI1B1Ymxpc2hlZA==',
|
|
2727
|
+
'teamsAppId': '3f749cca-8cb0-4925-9fa0-ba7aca2014af',
|
|
2728
|
+
'displayName': 'IDEA-TE - Extended ECM 24.2.0',
|
|
2729
|
+
'version': '24.2.0',
|
|
2730
|
+
'publishingState': 'published',
|
|
2731
|
+
'shortDescription': 'Add a tab for an Extended ECM business workspace.',
|
|
2732
|
+
'description': 'View and interact with OpenText Extended ECM business workspaces',
|
|
2733
|
+
'lastModifiedDateTime': None,
|
|
2734
|
+
'createdBy': None,
|
|
2735
|
+
'authorization': {
|
|
2736
|
+
'requiredPermissionSet': {...}
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2739
|
+
|
|
2740
|
+
Example return for new installations ("id" is the "internal" ID of the app):
|
|
2741
|
+
{
|
|
2742
|
+
'@odata.context': 'https://graph.microsoft.com/v1.0/$metadata#appCatalogs/teamsApps/$entity',
|
|
2743
|
+
'id': '6c672afd-37fc-46c6-8365-d499aba3808b',
|
|
2744
|
+
'externalId': 'dd4af790-d8ff-47a0-87ad-486318272c7a',
|
|
2745
|
+
'displayName': 'OpenText Extended ECM',
|
|
2746
|
+
'distributionMethod': 'organization'
|
|
2747
|
+
}
|
|
2514
2748
|
"""
|
|
2515
2749
|
|
|
2516
2750
|
if update_existing_app and not app_catalog_id:
|
|
@@ -2520,12 +2754,12 @@ class M365(object):
|
|
|
2520
2754
|
return None
|
|
2521
2755
|
|
|
2522
2756
|
if not os.path.exists(app_path):
|
|
2523
|
-
logger.error("M365 Teams app file ->
|
|
2757
|
+
logger.error("M365 Teams app file -> %s does not exist!", app_path)
|
|
2524
2758
|
return None
|
|
2525
2759
|
|
|
2526
2760
|
# Ensure that the app file is a zip file
|
|
2527
2761
|
if not app_path.endswith(".zip"):
|
|
2528
|
-
logger.error("M365 Teams app file ->
|
|
2762
|
+
logger.error("M365 Teams app file -> %s must be a zip file!", app_path)
|
|
2529
2763
|
return None
|
|
2530
2764
|
|
|
2531
2765
|
request_url = self.config()["teamsAppsUrl"]
|
|
@@ -2545,12 +2779,13 @@ class M365(object):
|
|
|
2545
2779
|
# Ensure that the app file contains a manifest.json file
|
|
2546
2780
|
if "manifest.json" not in z.namelist():
|
|
2547
2781
|
logger.error(
|
|
2548
|
-
"M365 Teams app file ->
|
|
2782
|
+
"M365 Teams app file -> '%s' does not contain a manifest.json file!",
|
|
2783
|
+
app_path,
|
|
2549
2784
|
)
|
|
2550
2785
|
return None
|
|
2551
2786
|
|
|
2552
|
-
logger.
|
|
2553
|
-
"Upload M365 Teams app -> %s to the MS Teams catalog; calling -> %s",
|
|
2787
|
+
logger.debug(
|
|
2788
|
+
"Upload M365 Teams app -> '%s' to the MS Teams catalog; calling -> %s",
|
|
2554
2789
|
app_path,
|
|
2555
2790
|
request_url,
|
|
2556
2791
|
)
|
|
@@ -2558,14 +2793,17 @@ class M365(object):
|
|
|
2558
2793
|
retries = 0
|
|
2559
2794
|
while True:
|
|
2560
2795
|
response = requests.post(
|
|
2561
|
-
request_url,
|
|
2796
|
+
request_url,
|
|
2797
|
+
headers=request_header,
|
|
2798
|
+
data=app_data,
|
|
2799
|
+
timeout=REQUEST_TIMEOUT,
|
|
2562
2800
|
)
|
|
2563
2801
|
if response.ok:
|
|
2564
2802
|
return self.parse_request_response(response)
|
|
2565
2803
|
|
|
2566
2804
|
# Check if Session has expired - then re-authenticate and try once more
|
|
2567
2805
|
if response.status_code == 401 and retries == 0:
|
|
2568
|
-
logger.
|
|
2806
|
+
logger.debug("Session has expired - try to re-authenticate...")
|
|
2569
2807
|
self.authenticate(revalidate=True)
|
|
2570
2808
|
request_header = self.request_header()
|
|
2571
2809
|
retries += 1
|
|
@@ -2580,7 +2818,7 @@ class M365(object):
|
|
|
2580
2818
|
else:
|
|
2581
2819
|
if update_existing_app:
|
|
2582
2820
|
logger.warning(
|
|
2583
|
-
"Failed to update existing M365 Teams app -> %s (may be because it is not a new version); status -> %s; error -> %s",
|
|
2821
|
+
"Failed to update existing M365 Teams app -> '%s' (may be because it is not a new version); status -> %s; error -> %s",
|
|
2584
2822
|
app_path,
|
|
2585
2823
|
response.status_code,
|
|
2586
2824
|
response.text,
|
|
@@ -2588,7 +2826,7 @@ class M365(object):
|
|
|
2588
2826
|
|
|
2589
2827
|
else:
|
|
2590
2828
|
logger.error(
|
|
2591
|
-
"Failed to upload new M365 Teams app -> %s; status -> %s; error -> %s",
|
|
2829
|
+
"Failed to upload new M365 Teams app -> '%s'; status -> %s; error -> %s",
|
|
2592
2830
|
app_path,
|
|
2593
2831
|
response.status_code,
|
|
2594
2832
|
response.text,
|
|
@@ -2610,11 +2848,13 @@ class M365(object):
|
|
|
2610
2848
|
request_header = self.request_header_user()
|
|
2611
2849
|
|
|
2612
2850
|
# Make the DELETE request to remove the app from the app catalog
|
|
2613
|
-
response = requests.delete(
|
|
2851
|
+
response = requests.delete(
|
|
2852
|
+
request_url, headers=request_header, timeout=REQUEST_TIMEOUT
|
|
2853
|
+
)
|
|
2614
2854
|
|
|
2615
2855
|
# Check the status code of the response
|
|
2616
2856
|
if response.status_code == 204:
|
|
2617
|
-
logger.
|
|
2857
|
+
logger.debug(
|
|
2618
2858
|
"The M365 Teams app with ID -> %s has been successfully removed from the app catalog.",
|
|
2619
2859
|
app_id,
|
|
2620
2860
|
)
|
|
@@ -2627,35 +2867,160 @@ class M365(object):
|
|
|
2627
2867
|
|
|
2628
2868
|
# end method definition
|
|
2629
2869
|
|
|
2630
|
-
def assign_teams_app_to_user(
|
|
2870
|
+
def assign_teams_app_to_user(
|
|
2871
|
+
self,
|
|
2872
|
+
user_id: str,
|
|
2873
|
+
app_name: str = "",
|
|
2874
|
+
app_internal_id: str = "",
|
|
2875
|
+
show_error: bool = False,
|
|
2876
|
+
) -> dict | None:
|
|
2631
2877
|
"""Assigns (adds) a M365 Teams app to a M365 user.
|
|
2632
2878
|
|
|
2879
|
+
See: https://learn.microsoft.com/en-us/graph/api/userteamwork-post-installedapps?view=graph-rest-1.0&tabs=http
|
|
2880
|
+
|
|
2633
2881
|
Args:
|
|
2634
2882
|
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
|
|
2883
|
+
app_name (str, optional): exact name of the app. Not needed if app_internal_id is provided
|
|
2884
|
+
app_internal_id (str, optional): internal ID of the app. If not provided it will be derived from app_name
|
|
2885
|
+
show_error (bool): whether or not an error should be displayed if the
|
|
2886
|
+
user is not found.
|
|
2636
2887
|
Returns:
|
|
2637
2888
|
dict: response of the MS Graph API call or None if the call fails.
|
|
2638
2889
|
"""
|
|
2639
2890
|
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2891
|
+
if not app_internal_id and not app_name:
|
|
2892
|
+
logger.error(
|
|
2893
|
+
"Either the internal App ID or the App name need to be provided!"
|
|
2894
|
+
)
|
|
2644
2895
|
return None
|
|
2645
2896
|
|
|
2897
|
+
if not app_internal_id:
|
|
2898
|
+
response = self.get_teams_apps(
|
|
2899
|
+
filter_expression="contains(displayName, '{}')".format(app_name)
|
|
2900
|
+
)
|
|
2901
|
+
app_internal_id = self.get_result_value(
|
|
2902
|
+
response=response, key="id", index=0
|
|
2903
|
+
)
|
|
2904
|
+
if not app_internal_id:
|
|
2905
|
+
logger.error(
|
|
2906
|
+
"M365 Teams App -> '%s' not found! Cannot assign App to user -> %s.",
|
|
2907
|
+
app_name,
|
|
2908
|
+
user_id,
|
|
2909
|
+
)
|
|
2910
|
+
return None
|
|
2911
|
+
|
|
2646
2912
|
request_url = (
|
|
2647
2913
|
self.config()["usersUrl"] + "/" + user_id + "/teamwork/installedApps"
|
|
2648
2914
|
)
|
|
2649
2915
|
request_header = self.request_header()
|
|
2650
2916
|
|
|
2651
2917
|
post_body = {
|
|
2652
|
-
"teamsApp@odata.bind": self.config()["teamsAppsUrl"] + "/" +
|
|
2918
|
+
"teamsApp@odata.bind": self.config()["teamsAppsUrl"] + "/" + app_internal_id
|
|
2653
2919
|
}
|
|
2654
2920
|
|
|
2655
|
-
logger.
|
|
2656
|
-
"Assign M365 Teams app -> %s (%s) to M365 user -> %s; calling -> %s",
|
|
2921
|
+
logger.debug(
|
|
2922
|
+
"Assign M365 Teams app -> '%s' (%s) to M365 user -> %s; calling -> %s",
|
|
2657
2923
|
app_name,
|
|
2658
|
-
|
|
2924
|
+
app_internal_id,
|
|
2925
|
+
user_id,
|
|
2926
|
+
request_url,
|
|
2927
|
+
)
|
|
2928
|
+
|
|
2929
|
+
retries = 0
|
|
2930
|
+
while True:
|
|
2931
|
+
response = requests.post(
|
|
2932
|
+
request_url,
|
|
2933
|
+
json=post_body,
|
|
2934
|
+
headers=request_header,
|
|
2935
|
+
timeout=REQUEST_TIMEOUT,
|
|
2936
|
+
)
|
|
2937
|
+
if response.ok:
|
|
2938
|
+
return self.parse_request_response(response)
|
|
2939
|
+
# Check if Session has expired - then re-authenticate and try once more
|
|
2940
|
+
elif response.status_code == 401 and retries == 0:
|
|
2941
|
+
logger.debug("Session has expired - try to re-authenticate...")
|
|
2942
|
+
self.authenticate(revalidate=True)
|
|
2943
|
+
request_header = self.request_header()
|
|
2944
|
+
retries += 1
|
|
2945
|
+
elif response.status_code in [502, 503, 504] and retries < 3:
|
|
2946
|
+
logger.warning(
|
|
2947
|
+
"M365 Graph API delivered server side error -> %s; retrying in %s seconds...",
|
|
2948
|
+
response.status_code,
|
|
2949
|
+
(retries + 1) * 60,
|
|
2950
|
+
)
|
|
2951
|
+
time.sleep((retries + 1) * 60)
|
|
2952
|
+
retries += 1
|
|
2953
|
+
else:
|
|
2954
|
+
if show_error:
|
|
2955
|
+
logger.error(
|
|
2956
|
+
"Failed to assign M365 Teams app -> '%s' (%s) to M365 user -> %s; status -> %s; error -> %s",
|
|
2957
|
+
app_name,
|
|
2958
|
+
app_internal_id,
|
|
2959
|
+
user_id,
|
|
2960
|
+
response.status_code,
|
|
2961
|
+
response.text,
|
|
2962
|
+
)
|
|
2963
|
+
else:
|
|
2964
|
+
logger.warning(
|
|
2965
|
+
"Failed to assign M365 Teams app -> '%s' (%s) to M365 user -> %s (could be because the app is assigned organization-wide); status -> %s; warning -> %s",
|
|
2966
|
+
app_name,
|
|
2967
|
+
app_internal_id,
|
|
2968
|
+
user_id,
|
|
2969
|
+
response.status_code,
|
|
2970
|
+
response.text,
|
|
2971
|
+
)
|
|
2972
|
+
return None
|
|
2973
|
+
|
|
2974
|
+
# end method definition
|
|
2975
|
+
|
|
2976
|
+
def upgrade_teams_app_of_user(
|
|
2977
|
+
self, user_id: str, app_name: str, app_installation_id: str | None = None
|
|
2978
|
+
) -> dict | None:
|
|
2979
|
+
"""Upgrade a MS teams app for a user. The call will fail if the user does not
|
|
2980
|
+
already have the app assigned. So this needs to be checked before
|
|
2981
|
+
calling this method.
|
|
2982
|
+
See: https://learn.microsoft.com/en-us/graph/api/userteamwork-teamsappinstallation-upgrade?view=graph-rest-1.0&tabs=http
|
|
2983
|
+
|
|
2984
|
+
Args:
|
|
2985
|
+
user_id (str): M365 GUID of the user (can also be the M365 email of the user)
|
|
2986
|
+
app_name (str): exact name of the app
|
|
2987
|
+
app_installation_id (str): ID of the app installation for the user. This is neither the internal nor
|
|
2988
|
+
external app ID. It is specific for each user and app.
|
|
2989
|
+
Returns:
|
|
2990
|
+
dict: response of the MS Graph API call or None if the call fails.
|
|
2991
|
+
"""
|
|
2992
|
+
|
|
2993
|
+
if not app_installation_id:
|
|
2994
|
+
response = self.get_teams_apps_of_user(
|
|
2995
|
+
user_id=user_id,
|
|
2996
|
+
filter_expression="contains(teamsAppDefinition/displayName, '{}')".format(
|
|
2997
|
+
app_name
|
|
2998
|
+
),
|
|
2999
|
+
)
|
|
3000
|
+
# Retrieve the installation specific App ID - this is different from thew App catalalog ID!!
|
|
3001
|
+
app_installation_id = self.get_result_value(response, "id", 0)
|
|
3002
|
+
if not app_installation_id:
|
|
3003
|
+
logger.error(
|
|
3004
|
+
"M365 Teams app -> '%s' not found for user with ID -> %s. Cannot upgrade app for this user!",
|
|
3005
|
+
app_name,
|
|
3006
|
+
user_id,
|
|
3007
|
+
)
|
|
3008
|
+
return None
|
|
3009
|
+
|
|
3010
|
+
request_url = (
|
|
3011
|
+
self.config()["usersUrl"]
|
|
3012
|
+
+ "/"
|
|
3013
|
+
+ user_id
|
|
3014
|
+
+ "/teamwork/installedApps/"
|
|
3015
|
+
+ app_installation_id
|
|
3016
|
+
+ "/upgrade"
|
|
3017
|
+
)
|
|
3018
|
+
request_header = self.request_header()
|
|
3019
|
+
|
|
3020
|
+
logger.debug(
|
|
3021
|
+
"Upgrade M365 Teams app -> '%s' (%s) of M365 user with ID -> %s; calling -> %s",
|
|
3022
|
+
app_name,
|
|
3023
|
+
app_installation_id,
|
|
2659
3024
|
user_id,
|
|
2660
3025
|
request_url,
|
|
2661
3026
|
)
|
|
@@ -2663,13 +3028,13 @@ class M365(object):
|
|
|
2663
3028
|
retries = 0
|
|
2664
3029
|
while True:
|
|
2665
3030
|
response = requests.post(
|
|
2666
|
-
request_url,
|
|
3031
|
+
request_url, headers=request_header, timeout=REQUEST_TIMEOUT
|
|
2667
3032
|
)
|
|
2668
3033
|
if response.ok:
|
|
2669
3034
|
return self.parse_request_response(response)
|
|
2670
3035
|
# Check if Session has expired - then re-authenticate and try once more
|
|
2671
3036
|
elif response.status_code == 401 and retries == 0:
|
|
2672
|
-
logger.
|
|
3037
|
+
logger.debug("Session has expired - try to re-authenticate...")
|
|
2673
3038
|
self.authenticate(revalidate=True)
|
|
2674
3039
|
request_header = self.request_header()
|
|
2675
3040
|
retries += 1
|
|
@@ -2683,9 +3048,9 @@ class M365(object):
|
|
|
2683
3048
|
retries += 1
|
|
2684
3049
|
else:
|
|
2685
3050
|
logger.error(
|
|
2686
|
-
"Failed to
|
|
3051
|
+
"Failed to upgrade M365 Teams app -> '%s' (%s) of M365 user -> %s; status -> %s; error -> %s",
|
|
2687
3052
|
app_name,
|
|
2688
|
-
|
|
3053
|
+
app_installation_id,
|
|
2689
3054
|
user_id,
|
|
2690
3055
|
response.status_code,
|
|
2691
3056
|
response.text,
|
|
@@ -2694,10 +3059,12 @@ class M365(object):
|
|
|
2694
3059
|
|
|
2695
3060
|
# end method definition
|
|
2696
3061
|
|
|
2697
|
-
def
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
3062
|
+
def remove_teams_app_from_user(
|
|
3063
|
+
self, user_id: str, app_name: str, app_installation_id: str | None = None
|
|
3064
|
+
) -> dict | None:
|
|
3065
|
+
"""Remove a M365 Teams app from a M365 user.
|
|
3066
|
+
|
|
3067
|
+
See: https://learn.microsoft.com/en-us/graph/api/userteamwork-delete-installedapps?view=graph-rest-1.0&tabs=http
|
|
2701
3068
|
|
|
2702
3069
|
Args:
|
|
2703
3070
|
user_id (str): M365 GUID of the user (can also be the M365 email of the user)
|
|
@@ -2706,14 +3073,18 @@ class M365(object):
|
|
|
2706
3073
|
dict: response of the MS Graph API call or None if the call fails.
|
|
2707
3074
|
"""
|
|
2708
3075
|
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
3076
|
+
if not app_installation_id:
|
|
3077
|
+
response = self.get_teams_apps_of_user(
|
|
3078
|
+
user_id=user_id,
|
|
3079
|
+
filter_expression="contains(teamsAppDefinition/displayName, '{}')".format(
|
|
3080
|
+
app_name
|
|
3081
|
+
),
|
|
3082
|
+
)
|
|
3083
|
+
# Retrieve the installation specific App ID - this is different from thew App catalalog ID!!
|
|
3084
|
+
app_installation_id = self.get_result_value(response, "id", 0)
|
|
2714
3085
|
if not app_installation_id:
|
|
2715
3086
|
logger.error(
|
|
2716
|
-
"M365 Teams app -> %s not found for user with ID -> %s. Cannot
|
|
3087
|
+
"M365 Teams app -> '%s' not found for user with ID -> %s. Cannot remove app from this user!",
|
|
2717
3088
|
app_name,
|
|
2718
3089
|
user_id,
|
|
2719
3090
|
)
|
|
@@ -2725,12 +3096,11 @@ class M365(object):
|
|
|
2725
3096
|
+ user_id
|
|
2726
3097
|
+ "/teamwork/installedApps/"
|
|
2727
3098
|
+ app_installation_id
|
|
2728
|
-
+ "/upgrade"
|
|
2729
3099
|
)
|
|
2730
3100
|
request_header = self.request_header()
|
|
2731
3101
|
|
|
2732
|
-
logger.
|
|
2733
|
-
"
|
|
3102
|
+
logger.debug(
|
|
3103
|
+
"Remove M365 Teams app -> '%s' (%s) from M365 user with ID -> %s; calling -> %s",
|
|
2734
3104
|
app_name,
|
|
2735
3105
|
app_installation_id,
|
|
2736
3106
|
user_id,
|
|
@@ -2739,12 +3109,14 @@ class M365(object):
|
|
|
2739
3109
|
|
|
2740
3110
|
retries = 0
|
|
2741
3111
|
while True:
|
|
2742
|
-
response = requests.
|
|
3112
|
+
response = requests.delete(
|
|
3113
|
+
request_url, headers=request_header, timeout=REQUEST_TIMEOUT
|
|
3114
|
+
)
|
|
2743
3115
|
if response.ok:
|
|
2744
3116
|
return self.parse_request_response(response)
|
|
2745
3117
|
# Check if Session has expired - then re-authenticate and try once more
|
|
2746
3118
|
elif response.status_code == 401 and retries == 0:
|
|
2747
|
-
logger.
|
|
3119
|
+
logger.debug("Session has expired - try to re-authenticate...")
|
|
2748
3120
|
self.authenticate(revalidate=True)
|
|
2749
3121
|
request_header = self.request_header()
|
|
2750
3122
|
retries += 1
|
|
@@ -2758,7 +3130,7 @@ class M365(object):
|
|
|
2758
3130
|
retries += 1
|
|
2759
3131
|
else:
|
|
2760
3132
|
logger.error(
|
|
2761
|
-
"Failed to
|
|
3133
|
+
"Failed to remove M365 Teams app -> '%s' (%s) from M365 user -> %s; status -> %s; error -> %s",
|
|
2762
3134
|
app_name,
|
|
2763
3135
|
app_installation_id,
|
|
2764
3136
|
user_id,
|
|
@@ -2788,8 +3160,9 @@ class M365(object):
|
|
|
2788
3160
|
"teamsApp@odata.bind": self.config()["teamsAppsUrl"] + "/" + app_id
|
|
2789
3161
|
}
|
|
2790
3162
|
|
|
2791
|
-
logger.
|
|
2792
|
-
"Assign M365 Teams app -> %s to M365 Team -> %s; calling -> %s",
|
|
3163
|
+
logger.debug(
|
|
3164
|
+
"Assign M365 Teams app -> '%s' (%s) to M365 Team -> %s; calling -> %s",
|
|
3165
|
+
self.config()["teamsAppName"],
|
|
2793
3166
|
app_id,
|
|
2794
3167
|
team_id,
|
|
2795
3168
|
request_url,
|
|
@@ -2798,13 +3171,16 @@ class M365(object):
|
|
|
2798
3171
|
retries = 0
|
|
2799
3172
|
while True:
|
|
2800
3173
|
response = requests.post(
|
|
2801
|
-
request_url,
|
|
3174
|
+
request_url,
|
|
3175
|
+
json=post_body,
|
|
3176
|
+
headers=request_header,
|
|
3177
|
+
timeout=REQUEST_TIMEOUT,
|
|
2802
3178
|
)
|
|
2803
3179
|
if response.ok:
|
|
2804
3180
|
return self.parse_request_response(response)
|
|
2805
3181
|
# Check if Session has expired - then re-authenticate and try once more
|
|
2806
3182
|
elif response.status_code == 401 and retries == 0:
|
|
2807
|
-
logger.
|
|
3183
|
+
logger.debug("Session has expired - try to re-authenticate...")
|
|
2808
3184
|
self.authenticate(revalidate=True)
|
|
2809
3185
|
request_header = self.request_header()
|
|
2810
3186
|
retries += 1
|
|
@@ -2818,7 +3194,8 @@ class M365(object):
|
|
|
2818
3194
|
retries += 1
|
|
2819
3195
|
else:
|
|
2820
3196
|
logger.error(
|
|
2821
|
-
"Failed to assign M365 Teams app -> %s to M365 Team -> %s; status -> %s; error -> %s",
|
|
3197
|
+
"Failed to assign M365 Teams app -> '%s' (%s) to M365 Team -> %s; status -> %s; error -> %s",
|
|
3198
|
+
self.config()["teamsAppName"],
|
|
2822
3199
|
app_id,
|
|
2823
3200
|
team_id,
|
|
2824
3201
|
response.status_code,
|
|
@@ -2848,7 +3225,7 @@ class M365(object):
|
|
|
2848
3225
|
app_installation_id = self.get_result_value(response, "id", 0)
|
|
2849
3226
|
if not app_installation_id:
|
|
2850
3227
|
logger.error(
|
|
2851
|
-
"M365 Teams app -> %s not found for M365 Team with ID -> %s. Cannot upgrade app for this team!",
|
|
3228
|
+
"M365 Teams app -> '%s' not found for M365 Team with ID -> %s. Cannot upgrade app for this team!",
|
|
2852
3229
|
app_name,
|
|
2853
3230
|
team_id,
|
|
2854
3231
|
)
|
|
@@ -2864,8 +3241,8 @@ class M365(object):
|
|
|
2864
3241
|
)
|
|
2865
3242
|
request_header = self.request_header()
|
|
2866
3243
|
|
|
2867
|
-
logger.
|
|
2868
|
-
"Upgrade app -> %s (%s) of M365 team with ID -> %s; calling -> %s",
|
|
3244
|
+
logger.debug(
|
|
3245
|
+
"Upgrade app -> '%s' (%s) of M365 team with ID -> %s; calling -> %s",
|
|
2869
3246
|
app_name,
|
|
2870
3247
|
app_installation_id,
|
|
2871
3248
|
team_id,
|
|
@@ -2874,12 +3251,14 @@ class M365(object):
|
|
|
2874
3251
|
|
|
2875
3252
|
retries = 0
|
|
2876
3253
|
while True:
|
|
2877
|
-
response = requests.post(
|
|
3254
|
+
response = requests.post(
|
|
3255
|
+
request_url, headers=request_header, timeout=REQUEST_TIMEOUT
|
|
3256
|
+
)
|
|
2878
3257
|
if response.ok:
|
|
2879
3258
|
return self.parse_request_response(response)
|
|
2880
3259
|
# Check if Session has expired - then re-authenticate and try once more
|
|
2881
3260
|
elif response.status_code == 401 and retries == 0:
|
|
2882
|
-
logger.
|
|
3261
|
+
logger.debug("Session has expired - try to re-authenticate...")
|
|
2883
3262
|
self.authenticate(revalidate=True)
|
|
2884
3263
|
request_header = self.request_header()
|
|
2885
3264
|
retries += 1
|
|
@@ -2893,7 +3272,7 @@ class M365(object):
|
|
|
2893
3272
|
retries += 1
|
|
2894
3273
|
else:
|
|
2895
3274
|
logger.error(
|
|
2896
|
-
"Failed to upgrade app -> %s (%s) of M365 team with ID -> %s; status -> %s; error -> %s",
|
|
3275
|
+
"Failed to upgrade M365 Teams app -> '%s' (%s) of M365 team with ID -> %s; status -> %s; error -> %s",
|
|
2897
3276
|
app_name,
|
|
2898
3277
|
app_installation_id,
|
|
2899
3278
|
team_id,
|
|
@@ -2945,8 +3324,10 @@ class M365(object):
|
|
|
2945
3324
|
None,
|
|
2946
3325
|
)
|
|
2947
3326
|
if not channel:
|
|
2948
|
-
logger.
|
|
2949
|
-
"Cannot find Channel -> %s on M365 Team -> %s",
|
|
3327
|
+
logger.error(
|
|
3328
|
+
"Cannot find Channel -> '%s' on M365 Team -> '%s'",
|
|
3329
|
+
channel_name,
|
|
3330
|
+
team_name,
|
|
2950
3331
|
)
|
|
2951
3332
|
return None
|
|
2952
3333
|
channel_id = channel["id"]
|
|
@@ -2974,8 +3355,8 @@ class M365(object):
|
|
|
2974
3355
|
},
|
|
2975
3356
|
}
|
|
2976
3357
|
|
|
2977
|
-
logger.
|
|
2978
|
-
"Add Tab -> %s with App ID -> %s to Channel -> %s of Microsoft 365 Team -> %s; calling -> %s",
|
|
3358
|
+
logger.debug(
|
|
3359
|
+
"Add Tab -> '%s' with App ID -> %s to Channel -> '%s' of Microsoft 365 Team -> '%s'; calling -> %s",
|
|
2979
3360
|
tab_name,
|
|
2980
3361
|
app_id,
|
|
2981
3362
|
channel_name,
|
|
@@ -2986,13 +3367,16 @@ class M365(object):
|
|
|
2986
3367
|
retries = 0
|
|
2987
3368
|
while True:
|
|
2988
3369
|
response = requests.post(
|
|
2989
|
-
request_url,
|
|
3370
|
+
request_url,
|
|
3371
|
+
headers=request_header,
|
|
3372
|
+
json=tab_config,
|
|
3373
|
+
timeout=REQUEST_TIMEOUT,
|
|
2990
3374
|
)
|
|
2991
3375
|
if response.ok:
|
|
2992
3376
|
return self.parse_request_response(response)
|
|
2993
3377
|
# Check if Session has expired - then re-authenticate and try once more
|
|
2994
3378
|
elif response.status_code == 401 and retries == 0:
|
|
2995
|
-
logger.
|
|
3379
|
+
logger.debug("Session has expired - try to re-authenticate...")
|
|
2996
3380
|
self.authenticate(revalidate=True)
|
|
2997
3381
|
request_header = self.request_header()
|
|
2998
3382
|
retries += 1
|
|
@@ -3006,7 +3390,7 @@ class M365(object):
|
|
|
3006
3390
|
retries += 1
|
|
3007
3391
|
else:
|
|
3008
3392
|
logger.error(
|
|
3009
|
-
"Failed to add Tab for M365 Team -> %s (%s) and Channel -> %s (%s); status -> %s; error -> %s; tab config -> %s",
|
|
3393
|
+
"Failed to add Tab for M365 Team -> '%s' (%s) and Channel -> '%s' (%s); status -> %s; error -> %s; tab config -> %s",
|
|
3010
3394
|
team_name,
|
|
3011
3395
|
team_id,
|
|
3012
3396
|
channel_name,
|
|
@@ -3058,8 +3442,10 @@ class M365(object):
|
|
|
3058
3442
|
None,
|
|
3059
3443
|
)
|
|
3060
3444
|
if not channel:
|
|
3061
|
-
logger.
|
|
3062
|
-
"Cannot find Channel -> %s for M365 Team -> %s",
|
|
3445
|
+
logger.error(
|
|
3446
|
+
"Cannot find Channel -> '%s' for M365 Team -> '%s'",
|
|
3447
|
+
channel_name,
|
|
3448
|
+
team_name,
|
|
3063
3449
|
)
|
|
3064
3450
|
return None
|
|
3065
3451
|
channel_id = channel["id"]
|
|
@@ -3075,8 +3461,8 @@ class M365(object):
|
|
|
3075
3461
|
None,
|
|
3076
3462
|
)
|
|
3077
3463
|
if not tab:
|
|
3078
|
-
logger.
|
|
3079
|
-
"Cannot find Tab -> %s on M365 Team -> %s (%s) and Channel -> %s (%s)",
|
|
3464
|
+
logger.error(
|
|
3465
|
+
"Cannot find Tab -> '%s' on M365 Team -> '%s' (%s) and Channel -> '%s' (%s)",
|
|
3080
3466
|
tab_name,
|
|
3081
3467
|
team_name,
|
|
3082
3468
|
team_id,
|
|
@@ -3108,8 +3494,8 @@ class M365(object):
|
|
|
3108
3494
|
},
|
|
3109
3495
|
}
|
|
3110
3496
|
|
|
3111
|
-
logger.
|
|
3112
|
-
"Update Tab -> %s (%s) of Channel -> %s (%s) for Microsoft 365 Teams -> %s (%s) with configuration -> %s; calling -> %s",
|
|
3497
|
+
logger.debug(
|
|
3498
|
+
"Update Tab -> '%s' (%s) of Channel -> '%s' (%s) for Microsoft 365 Teams -> '%s' (%s) with configuration -> %s; calling -> %s",
|
|
3113
3499
|
tab_name,
|
|
3114
3500
|
tab_id,
|
|
3115
3501
|
channel_name,
|
|
@@ -3123,13 +3509,16 @@ class M365(object):
|
|
|
3123
3509
|
retries = 0
|
|
3124
3510
|
while True:
|
|
3125
3511
|
response = requests.patch(
|
|
3126
|
-
request_url,
|
|
3512
|
+
request_url,
|
|
3513
|
+
headers=request_header,
|
|
3514
|
+
json=tab_config,
|
|
3515
|
+
timeout=REQUEST_TIMEOUT,
|
|
3127
3516
|
)
|
|
3128
3517
|
if response.ok:
|
|
3129
3518
|
return self.parse_request_response(response)
|
|
3130
3519
|
# Check if Session has expired - then re-authenticate and try once more
|
|
3131
3520
|
elif response.status_code == 401 and retries == 0:
|
|
3132
|
-
logger.
|
|
3521
|
+
logger.debug("Session has expired - try to re-authenticate...")
|
|
3133
3522
|
self.authenticate(revalidate=True)
|
|
3134
3523
|
request_header = self.request_header()
|
|
3135
3524
|
retries += 1
|
|
@@ -3143,7 +3532,7 @@ class M365(object):
|
|
|
3143
3532
|
retries += 1
|
|
3144
3533
|
else:
|
|
3145
3534
|
logger.error(
|
|
3146
|
-
"Failed to update Tab -> %s (%s) for M365 Team -> %s (%s) and Channel -> %s (%s); status -> %s; error -> %s",
|
|
3535
|
+
"Failed to update Tab -> '%s' (%s) for M365 Team -> '%s' (%s) and Channel -> '%s' (%s); status -> %s; error -> %s",
|
|
3147
3536
|
tab_name,
|
|
3148
3537
|
tab_id,
|
|
3149
3538
|
team_name,
|
|
@@ -3189,8 +3578,10 @@ class M365(object):
|
|
|
3189
3578
|
None,
|
|
3190
3579
|
)
|
|
3191
3580
|
if not channel:
|
|
3192
|
-
logger.
|
|
3193
|
-
"Cannot find Channel -> %s for M365 Team -> %s",
|
|
3581
|
+
logger.error(
|
|
3582
|
+
"Cannot find Channel -> '%s' for M365 Team -> '%s'",
|
|
3583
|
+
channel_name,
|
|
3584
|
+
team_name,
|
|
3194
3585
|
)
|
|
3195
3586
|
return False
|
|
3196
3587
|
channel_id = channel["id"]
|
|
@@ -3206,8 +3597,8 @@ class M365(object):
|
|
|
3206
3597
|
item for item in response["value"] if item["displayName"] == tab_name
|
|
3207
3598
|
]
|
|
3208
3599
|
if not tab_list:
|
|
3209
|
-
logger.
|
|
3210
|
-
"Cannot find
|
|
3600
|
+
logger.error(
|
|
3601
|
+
"Cannot find Tab -> '%s' on M365 Team -> '%s' (%s) and Channel -> '%s' (%s)",
|
|
3211
3602
|
tab_name,
|
|
3212
3603
|
team_name,
|
|
3213
3604
|
team_id,
|
|
@@ -3231,8 +3622,8 @@ class M365(object):
|
|
|
3231
3622
|
|
|
3232
3623
|
request_header = self.request_header()
|
|
3233
3624
|
|
|
3234
|
-
logger.
|
|
3235
|
-
"Delete Tab -> %s (%s) from Channel -> %s (%s) of Microsoft 365 Teams -> %s (%s); calling -> %s",
|
|
3625
|
+
logger.debug(
|
|
3626
|
+
"Delete Tab -> '%s' (%s) from Channel -> '%s' (%s) of Microsoft 365 Teams -> '%s' (%s); calling -> %s",
|
|
3236
3627
|
tab_name,
|
|
3237
3628
|
tab_id,
|
|
3238
3629
|
channel_name,
|
|
@@ -3245,11 +3636,11 @@ class M365(object):
|
|
|
3245
3636
|
retries = 0
|
|
3246
3637
|
while True:
|
|
3247
3638
|
response = requests.delete(
|
|
3248
|
-
request_url, headers=request_header, timeout=
|
|
3639
|
+
request_url, headers=request_header, timeout=REQUEST_TIMEOUT
|
|
3249
3640
|
)
|
|
3250
3641
|
if response.ok:
|
|
3251
|
-
logger.
|
|
3252
|
-
"Tab -> %s (%s) has been deleted from Channel -> %s (%s) of Microsoft 365 Teams -> %s (%s)",
|
|
3642
|
+
logger.debug(
|
|
3643
|
+
"Tab -> '%s' (%s) has been deleted from Channel -> '%s' (%s) of Microsoft 365 Teams -> '%s' (%s)",
|
|
3253
3644
|
tab_name,
|
|
3254
3645
|
tab_id,
|
|
3255
3646
|
channel_name,
|
|
@@ -3260,7 +3651,7 @@ class M365(object):
|
|
|
3260
3651
|
break
|
|
3261
3652
|
# Check if Session has expired - then re-authenticate and try once more
|
|
3262
3653
|
elif response.status_code == 401 and retries == 0:
|
|
3263
|
-
logger.
|
|
3654
|
+
logger.debug("Session has expired - try to re-authenticate...")
|
|
3264
3655
|
self.authenticate(revalidate=True)
|
|
3265
3656
|
request_header = self.request_header()
|
|
3266
3657
|
retries += 1
|
|
@@ -3274,7 +3665,7 @@ class M365(object):
|
|
|
3274
3665
|
retries += 1
|
|
3275
3666
|
else:
|
|
3276
3667
|
logger.error(
|
|
3277
|
-
"Failed to delete Tab -> %s (%s) for M365 Team -> %s (%s) and Channel -> %s (%s); status -> %s; error -> %s",
|
|
3668
|
+
"Failed to delete Tab -> '%s' (%s) for M365 Team -> '%s' (%s) and Channel -> '%s' (%s); status -> %s; error -> %s",
|
|
3278
3669
|
tab_name,
|
|
3279
3670
|
tab_id,
|
|
3280
3671
|
team_name,
|
|
@@ -3334,22 +3725,25 @@ class M365(object):
|
|
|
3334
3725
|
request_url = self.config()["securityUrl"] + "/sensitivityLabels"
|
|
3335
3726
|
request_header = self.request_header()
|
|
3336
3727
|
|
|
3337
|
-
logger.
|
|
3338
|
-
"Create M365 sensitivity label -> %s; calling -> %s", name, request_url
|
|
3728
|
+
logger.debug(
|
|
3729
|
+
"Create M365 sensitivity label -> '%s'; calling -> %s", name, request_url
|
|
3339
3730
|
)
|
|
3340
3731
|
|
|
3341
3732
|
# Send the POST request to create the label
|
|
3342
3733
|
response = requests.post(
|
|
3343
|
-
request_url,
|
|
3734
|
+
request_url,
|
|
3735
|
+
headers=request_header,
|
|
3736
|
+
data=json.dumps(payload),
|
|
3737
|
+
timeout=REQUEST_TIMEOUT,
|
|
3344
3738
|
)
|
|
3345
3739
|
|
|
3346
3740
|
# Check the response status code
|
|
3347
3741
|
if response.status_code == 201:
|
|
3348
|
-
logger.
|
|
3742
|
+
logger.debug("Label -> '%s' has been created successfully!", name)
|
|
3349
3743
|
return response
|
|
3350
3744
|
else:
|
|
3351
3745
|
logger.error(
|
|
3352
|
-
"Failed to create the M365 label -> %s! Response status code -> %s",
|
|
3746
|
+
"Failed to create the M365 label -> '%s'! Response status code -> %s",
|
|
3353
3747
|
name,
|
|
3354
3748
|
response.status_code,
|
|
3355
3749
|
)
|
|
@@ -3377,8 +3771,8 @@ class M365(object):
|
|
|
3377
3771
|
)
|
|
3378
3772
|
request_header = self.request_header()
|
|
3379
3773
|
|
|
3380
|
-
logger.
|
|
3381
|
-
"Assign label -> %s to user -> %s; calling -> %s",
|
|
3774
|
+
logger.debug(
|
|
3775
|
+
"Assign label -> '%s' to user -> '%s'; calling -> %s",
|
|
3382
3776
|
label_name,
|
|
3383
3777
|
user_email,
|
|
3384
3778
|
request_url,
|
|
@@ -3387,13 +3781,13 @@ class M365(object):
|
|
|
3387
3781
|
retries = 0
|
|
3388
3782
|
while True:
|
|
3389
3783
|
response = requests.post(
|
|
3390
|
-
request_url, headers=request_header, json=body, timeout=
|
|
3784
|
+
request_url, headers=request_header, json=body, timeout=REQUEST_TIMEOUT
|
|
3391
3785
|
)
|
|
3392
3786
|
if response.ok:
|
|
3393
3787
|
return self.parse_request_response(response)
|
|
3394
3788
|
# Check if Session has expired - then re-authenticate and try once more
|
|
3395
3789
|
elif response.status_code == 401 and retries == 0:
|
|
3396
|
-
logger.
|
|
3790
|
+
logger.debug("Session has expired - try to re-authenticate...")
|
|
3397
3791
|
self.authenticate(revalidate=True)
|
|
3398
3792
|
request_header = self.request_header()
|
|
3399
3793
|
retries += 1
|
|
@@ -3407,7 +3801,7 @@ class M365(object):
|
|
|
3407
3801
|
retries += 1
|
|
3408
3802
|
else:
|
|
3409
3803
|
logger.error(
|
|
3410
|
-
"Failed to assign label -> %s to M365 user -> %s; status -> %s; error -> %s",
|
|
3804
|
+
"Failed to assign label -> '%s' to M365 user -> '%s'; status -> %s; error -> %s",
|
|
3411
3805
|
label_name,
|
|
3412
3806
|
user_email,
|
|
3413
3807
|
response.status_code,
|
|
@@ -3438,7 +3832,7 @@ class M365(object):
|
|
|
3438
3832
|
|
|
3439
3833
|
# request_header = self.request_header()
|
|
3440
3834
|
|
|
3441
|
-
logger.
|
|
3835
|
+
logger.debug("Install Outlook Add-in from '%s' (NOT IMPLEMENTED)", app_path)
|
|
3442
3836
|
|
|
3443
3837
|
response = None
|
|
3444
3838
|
|
|
@@ -3464,20 +3858,22 @@ class M365(object):
|
|
|
3464
3858
|
] + "?$filter=displayName eq '{}'".format(app_registration_name)
|
|
3465
3859
|
request_header = self.request_header()
|
|
3466
3860
|
|
|
3467
|
-
logger.
|
|
3468
|
-
"Get Azure App Registration -> %s; calling -> %s",
|
|
3861
|
+
logger.debug(
|
|
3862
|
+
"Get Azure App Registration -> '%s'; calling -> %s",
|
|
3469
3863
|
app_registration_name,
|
|
3470
3864
|
request_url,
|
|
3471
3865
|
)
|
|
3472
3866
|
|
|
3473
3867
|
retries = 0
|
|
3474
3868
|
while True:
|
|
3475
|
-
response = requests.get(
|
|
3869
|
+
response = requests.get(
|
|
3870
|
+
request_url, headers=request_header, timeout=REQUEST_TIMEOUT
|
|
3871
|
+
)
|
|
3476
3872
|
if response.ok:
|
|
3477
3873
|
return self.parse_request_response(response)
|
|
3478
3874
|
# Check if Session has expired - then re-authenticate and try once more
|
|
3479
3875
|
elif response.status_code == 401 and retries == 0:
|
|
3480
|
-
logger.
|
|
3876
|
+
logger.debug("Session has expired - try to re-authenticate...")
|
|
3481
3877
|
self.authenticate(revalidate=True)
|
|
3482
3878
|
request_header = self.request_header()
|
|
3483
3879
|
retries += 1
|
|
@@ -3491,7 +3887,7 @@ class M365(object):
|
|
|
3491
3887
|
retries += 1
|
|
3492
3888
|
else:
|
|
3493
3889
|
logger.error(
|
|
3494
|
-
"Cannot find Azure App Registration -> %s; status -> %s; error -> %s",
|
|
3890
|
+
"Cannot find Azure App Registration -> '%s'; status -> %s; error -> %s",
|
|
3495
3891
|
app_registration_name,
|
|
3496
3892
|
response.status_code,
|
|
3497
3893
|
response.text,
|
|
@@ -3573,13 +3969,13 @@ class M365(object):
|
|
|
3573
3969
|
request_url,
|
|
3574
3970
|
headers=request_header,
|
|
3575
3971
|
json=app_registration_data,
|
|
3576
|
-
timeout=
|
|
3972
|
+
timeout=REQUEST_TIMEOUT,
|
|
3577
3973
|
)
|
|
3578
3974
|
if response.ok:
|
|
3579
3975
|
return self.parse_request_response(response)
|
|
3580
3976
|
# Check if Session has expired - then re-authenticate and try once more
|
|
3581
3977
|
elif response.status_code == 401 and retries == 0:
|
|
3582
|
-
logger.
|
|
3978
|
+
logger.debug("Session has expired - try to re-authenticate...")
|
|
3583
3979
|
self.authenticate(revalidate=True)
|
|
3584
3980
|
request_header = self.request_header()
|
|
3585
3981
|
retries += 1
|
|
@@ -3593,7 +3989,7 @@ class M365(object):
|
|
|
3593
3989
|
retries += 1
|
|
3594
3990
|
else:
|
|
3595
3991
|
logger.error(
|
|
3596
|
-
"Cannot add App Registration -> %s; status -> %s; error -> %s",
|
|
3992
|
+
"Cannot add App Registration -> '%s'; status -> %s; error -> %s",
|
|
3597
3993
|
app_registration_name,
|
|
3598
3994
|
response.status_code,
|
|
3599
3995
|
response.text,
|
|
@@ -3632,8 +4028,8 @@ class M365(object):
|
|
|
3632
4028
|
request_url = self.config()["applicationsUrl"] + "/" + app_registration_id
|
|
3633
4029
|
request_header = self.request_header()
|
|
3634
4030
|
|
|
3635
|
-
logger.
|
|
3636
|
-
"Update App Registration -> %s (%s); calling -> %s",
|
|
4031
|
+
logger.debug(
|
|
4032
|
+
"Update App Registration -> '%s' (%s); calling -> %s",
|
|
3637
4033
|
app_registration_name,
|
|
3638
4034
|
app_registration_id,
|
|
3639
4035
|
request_url,
|
|
@@ -3645,13 +4041,13 @@ class M365(object):
|
|
|
3645
4041
|
request_url,
|
|
3646
4042
|
headers=request_header,
|
|
3647
4043
|
json=app_registration_data,
|
|
3648
|
-
timeout=
|
|
4044
|
+
timeout=REQUEST_TIMEOUT,
|
|
3649
4045
|
)
|
|
3650
4046
|
if response.ok:
|
|
3651
4047
|
return self.parse_request_response(response)
|
|
3652
4048
|
# Check if Session has expired - then re-authenticate and try once more
|
|
3653
4049
|
elif response.status_code == 401 and retries == 0:
|
|
3654
|
-
logger.
|
|
4050
|
+
logger.debug("Session has expired - try to re-authenticate...")
|
|
3655
4051
|
self.authenticate(revalidate=True)
|
|
3656
4052
|
request_header = self.request_header()
|
|
3657
4053
|
retries += 1
|
|
@@ -3665,7 +4061,7 @@ class M365(object):
|
|
|
3665
4061
|
retries += 1
|
|
3666
4062
|
else:
|
|
3667
4063
|
logger.error(
|
|
3668
|
-
"Cannot update App Registration -> %s (%s); status -> %s; error -> %s",
|
|
4064
|
+
"Cannot update App Registration -> '%s' (%s); status -> %s; error -> %s",
|
|
3669
4065
|
app_registration_name,
|
|
3670
4066
|
app_registration_id,
|
|
3671
4067
|
response.status_code,
|
|
@@ -3674,3 +4070,522 @@ class M365(object):
|
|
|
3674
4070
|
return None
|
|
3675
4071
|
|
|
3676
4072
|
# end method definition
|
|
4073
|
+
|
|
4074
|
+
def get_mail(
|
|
4075
|
+
self,
|
|
4076
|
+
user_id: str,
|
|
4077
|
+
sender: str,
|
|
4078
|
+
subject: str,
|
|
4079
|
+
num_emails: int | None = None,
|
|
4080
|
+
show_error: bool = False,
|
|
4081
|
+
) -> dict | None:
|
|
4082
|
+
"""Get email from inbox of a given user and a given sender (from)
|
|
4083
|
+
This requires Mail.Read Application permissions for the Azure App being used.
|
|
4084
|
+
|
|
4085
|
+
Args:
|
|
4086
|
+
user_id (str): M365 ID of the user
|
|
4087
|
+
sender (str): sender email address to filter for
|
|
4088
|
+
num_emails (int, optional): number of matching emails to retrieve
|
|
4089
|
+
show_error (bool): whether or not an error should be displayed if the
|
|
4090
|
+
user is not found.
|
|
4091
|
+
Returns:
|
|
4092
|
+
dict: Email or None of the request fails.
|
|
4093
|
+
"""
|
|
4094
|
+
|
|
4095
|
+
# Attention: you can easily run in limitation of the MS Graph API. If selection + filtering
|
|
4096
|
+
# is too complex you can get this error: "The restriction or sort order is too complex for this operation."
|
|
4097
|
+
# that's why we first just do the ordering and then do the filtering on sender and subject
|
|
4098
|
+
# separately
|
|
4099
|
+
request_url = (
|
|
4100
|
+
self.config()["usersUrl"]
|
|
4101
|
+
+ "/"
|
|
4102
|
+
+ user_id
|
|
4103
|
+
# + "/messages?$filter=from/emailAddress/address eq '{}' and contains(subject, '{}')&$orderby=receivedDateTime desc".format(
|
|
4104
|
+
+ "/messages?$orderby=receivedDateTime desc"
|
|
4105
|
+
)
|
|
4106
|
+
if num_emails:
|
|
4107
|
+
request_url += "&$top={}".format(num_emails)
|
|
4108
|
+
|
|
4109
|
+
request_header = self.request_header()
|
|
4110
|
+
|
|
4111
|
+
logger.debug(
|
|
4112
|
+
"Retrieve mails for user -> %s from -> '%s' with subject -> '%s'; calling -> %s",
|
|
4113
|
+
user_id,
|
|
4114
|
+
sender,
|
|
4115
|
+
subject,
|
|
4116
|
+
request_url,
|
|
4117
|
+
)
|
|
4118
|
+
|
|
4119
|
+
retries = 0
|
|
4120
|
+
while True:
|
|
4121
|
+
response = requests.get(
|
|
4122
|
+
request_url, headers=request_header, timeout=REQUEST_TIMEOUT
|
|
4123
|
+
)
|
|
4124
|
+
if response.ok:
|
|
4125
|
+
response = self.parse_request_response(response)
|
|
4126
|
+
messages = response["value"] if response else []
|
|
4127
|
+
|
|
4128
|
+
# Filter the messages by sender and subject in code
|
|
4129
|
+
filtered_messages = [
|
|
4130
|
+
msg
|
|
4131
|
+
for msg in messages
|
|
4132
|
+
if msg.get("from", {}).get("emailAddress", {}).get("address")
|
|
4133
|
+
== sender
|
|
4134
|
+
and subject in msg.get("subject", "")
|
|
4135
|
+
]
|
|
4136
|
+
response["value"] = filtered_messages
|
|
4137
|
+
return response
|
|
4138
|
+
|
|
4139
|
+
# Check if Session has expired - then re-authenticate and try once more
|
|
4140
|
+
elif response.status_code == 401 and retries == 0:
|
|
4141
|
+
logger.debug("Session has expired - try to re-authenticate...")
|
|
4142
|
+
self.authenticate(revalidate=True)
|
|
4143
|
+
request_header = self.request_header()
|
|
4144
|
+
retries += 1
|
|
4145
|
+
elif response.status_code in [502, 503, 504] and retries < 3:
|
|
4146
|
+
logger.warning(
|
|
4147
|
+
"M365 Graph API delivered server side error -> %s; retrying in %s seconds...",
|
|
4148
|
+
response.status_code,
|
|
4149
|
+
(retries + 1) * 60,
|
|
4150
|
+
)
|
|
4151
|
+
time.sleep((retries + 1) * 60)
|
|
4152
|
+
retries += 1
|
|
4153
|
+
else:
|
|
4154
|
+
if show_error:
|
|
4155
|
+
logger.error(
|
|
4156
|
+
"Cannot retrieve emails for user -> %s; status -> %s; error -> %s",
|
|
4157
|
+
user_id,
|
|
4158
|
+
response.status_code,
|
|
4159
|
+
response.text,
|
|
4160
|
+
)
|
|
4161
|
+
else:
|
|
4162
|
+
logger.warning(
|
|
4163
|
+
"Cannot retrieve emails for user -> %s; status -> %s; warning -> %s",
|
|
4164
|
+
user_id,
|
|
4165
|
+
response.status_code,
|
|
4166
|
+
response.text,
|
|
4167
|
+
)
|
|
4168
|
+
return None
|
|
4169
|
+
|
|
4170
|
+
# end method definition
|
|
4171
|
+
|
|
4172
|
+
def get_mail_body(self, user_id: str, email_id: str) -> str:
|
|
4173
|
+
"""Get full email body for a given email ID
|
|
4174
|
+
This requires Mail.Read Application permissions for the Azure App being used.
|
|
4175
|
+
|
|
4176
|
+
Args:
|
|
4177
|
+
user_id (str): M365 ID of the user
|
|
4178
|
+
email_id (str): M365 ID of the email
|
|
4179
|
+
Returns:
|
|
4180
|
+
str: Email body or None of the request fails.
|
|
4181
|
+
"""
|
|
4182
|
+
|
|
4183
|
+
request_url = (
|
|
4184
|
+
self.config()["usersUrl"]
|
|
4185
|
+
+ "/"
|
|
4186
|
+
+ user_id
|
|
4187
|
+
+ "/messages/"
|
|
4188
|
+
+ email_id
|
|
4189
|
+
+ "/$value"
|
|
4190
|
+
)
|
|
4191
|
+
|
|
4192
|
+
request_header = self.request_header()
|
|
4193
|
+
|
|
4194
|
+
retries = 0
|
|
4195
|
+
while True:
|
|
4196
|
+
response = requests.get(
|
|
4197
|
+
request_url, headers=request_header, timeout=REQUEST_TIMEOUT
|
|
4198
|
+
)
|
|
4199
|
+
if response.ok:
|
|
4200
|
+
return response.content.decode("utf-8")
|
|
4201
|
+
# Check if Session has expired - then re-authenticate and try once more
|
|
4202
|
+
elif response.status_code == 401 and retries == 0:
|
|
4203
|
+
logger.debug("Session has expired - try to re-authenticate...")
|
|
4204
|
+
self.authenticate(revalidate=True)
|
|
4205
|
+
request_header = self.request_header()
|
|
4206
|
+
retries += 1
|
|
4207
|
+
elif response.status_code in [502, 503, 504] and retries < 3:
|
|
4208
|
+
logger.warning(
|
|
4209
|
+
"M365 Graph API delivered server side error -> %s; retrying in %s seconds...",
|
|
4210
|
+
response.status_code,
|
|
4211
|
+
(retries + 1) * 60,
|
|
4212
|
+
)
|
|
4213
|
+
time.sleep((retries + 1) * 60)
|
|
4214
|
+
retries += 1
|
|
4215
|
+
else:
|
|
4216
|
+
logger.error(
|
|
4217
|
+
"Cannot retrieve emails body for user -> %s and email -> %s; status -> %s; error -> %s",
|
|
4218
|
+
user_id,
|
|
4219
|
+
email_id,
|
|
4220
|
+
response.status_code,
|
|
4221
|
+
response.text,
|
|
4222
|
+
)
|
|
4223
|
+
return None
|
|
4224
|
+
|
|
4225
|
+
# end method definition
|
|
4226
|
+
|
|
4227
|
+
def extract_url_from_message_body(
|
|
4228
|
+
self,
|
|
4229
|
+
message_body: str,
|
|
4230
|
+
search_pattern: str,
|
|
4231
|
+
multi_line: bool = False,
|
|
4232
|
+
multi_line_end_marker: str = "%3D",
|
|
4233
|
+
line_end_marker: str = "=",
|
|
4234
|
+
replacements: list | None = None,
|
|
4235
|
+
) -> str | None:
|
|
4236
|
+
"""Parse the email body to extract (a potentially multi-line) URL from the body.
|
|
4237
|
+
|
|
4238
|
+
Args:
|
|
4239
|
+
message_body (str): Text of the Email body
|
|
4240
|
+
search_pattern (str): Pattern thatneeds to be in first line of the URL. This
|
|
4241
|
+
makes sure it is the right URL we are looking for.
|
|
4242
|
+
multi_line (bool, optional): Is the URL spread over multiple lines?. Defaults to False.
|
|
4243
|
+
multi_line_end_marker (str, optional): If it is a multi-line URL, what marks the end
|
|
4244
|
+
of the URL in the last line? Defaults to "%3D".
|
|
4245
|
+
line_end_marker (str, optional): What makrs the end of lines 1-(n-1)? Defaults to "=".
|
|
4246
|
+
Returns:
|
|
4247
|
+
str: URL text thathas been extracted.
|
|
4248
|
+
"""
|
|
4249
|
+
|
|
4250
|
+
if not message_body:
|
|
4251
|
+
return None
|
|
4252
|
+
|
|
4253
|
+
# Split all the lines after a CRLF:
|
|
4254
|
+
lines = [line.strip() for line in message_body.split("\r\n")]
|
|
4255
|
+
|
|
4256
|
+
# Filter out the complete URL from the extracted URLs
|
|
4257
|
+
found = False
|
|
4258
|
+
|
|
4259
|
+
url = ""
|
|
4260
|
+
|
|
4261
|
+
for line in lines:
|
|
4262
|
+
if found:
|
|
4263
|
+
# Remove line end marker - many times a "="
|
|
4264
|
+
if line.endswith(line_end_marker):
|
|
4265
|
+
line = line[:-1]
|
|
4266
|
+
for replacement in replacements:
|
|
4267
|
+
line = line.replace(replacement["from"], replacement["to"])
|
|
4268
|
+
# We consider an empty line after we found the URL to indicate the end of the URL:
|
|
4269
|
+
if line == "":
|
|
4270
|
+
break
|
|
4271
|
+
url += line
|
|
4272
|
+
if multi_line and line.endswith(multi_line_end_marker):
|
|
4273
|
+
break
|
|
4274
|
+
if not search_pattern in line:
|
|
4275
|
+
continue
|
|
4276
|
+
# Fine https:// in the current line:
|
|
4277
|
+
index = line.find("https://")
|
|
4278
|
+
if index == -1:
|
|
4279
|
+
continue
|
|
4280
|
+
# If there's any text in front of https in that line cut it:
|
|
4281
|
+
line = line[index:]
|
|
4282
|
+
# Remove line end marker - many times a "="
|
|
4283
|
+
if line.endswith(line_end_marker):
|
|
4284
|
+
line = line[:-1]
|
|
4285
|
+
for replacement in replacements:
|
|
4286
|
+
line = line.replace(replacement["from"], replacement["to"])
|
|
4287
|
+
found = True
|
|
4288
|
+
url += line
|
|
4289
|
+
if not multi_line:
|
|
4290
|
+
break
|
|
4291
|
+
|
|
4292
|
+
return url
|
|
4293
|
+
|
|
4294
|
+
# end method definition
|
|
4295
|
+
|
|
4296
|
+
def delete_mail(self, user_id: str, email_id: str) -> dict | None:
|
|
4297
|
+
"""Delete email from inbox of a given user and a given email ID.
|
|
4298
|
+
This requires Mail.ReadWrite Application permissions for the Azure App being used.
|
|
4299
|
+
|
|
4300
|
+
Args:
|
|
4301
|
+
user_id (str): M365 ID of the user
|
|
4302
|
+
email_id (str): M365 ID of the email
|
|
4303
|
+
Returns:
|
|
4304
|
+
dict: Email or None of the request fails.
|
|
4305
|
+
"""
|
|
4306
|
+
|
|
4307
|
+
request_url = (
|
|
4308
|
+
self.config()["usersUrl"] + "/" + user_id + "/messages/" + email_id
|
|
4309
|
+
)
|
|
4310
|
+
|
|
4311
|
+
request_header = self.request_header()
|
|
4312
|
+
|
|
4313
|
+
retries = 0
|
|
4314
|
+
while True:
|
|
4315
|
+
response = requests.delete(
|
|
4316
|
+
request_url, headers=request_header, timeout=REQUEST_TIMEOUT
|
|
4317
|
+
)
|
|
4318
|
+
if response.ok:
|
|
4319
|
+
return self.parse_request_response(response)
|
|
4320
|
+
# Check if Session has expired - then re-authenticate and try once more
|
|
4321
|
+
elif response.status_code == 401 and retries == 0:
|
|
4322
|
+
logger.debug("Session has expired - try to re-authenticate...")
|
|
4323
|
+
self.authenticate(revalidate=True)
|
|
4324
|
+
request_header = self.request_header()
|
|
4325
|
+
retries += 1
|
|
4326
|
+
elif response.status_code in [502, 503, 504] and retries < 3:
|
|
4327
|
+
logger.warning(
|
|
4328
|
+
"M365 Graph API delivered server side error -> %s; retrying in %s seconds...",
|
|
4329
|
+
response.status_code,
|
|
4330
|
+
(retries + 1) * 60,
|
|
4331
|
+
)
|
|
4332
|
+
time.sleep((retries + 1) * 60)
|
|
4333
|
+
retries += 1
|
|
4334
|
+
else:
|
|
4335
|
+
logger.error(
|
|
4336
|
+
"Cannot delete email -> %s from inbox of user -> %s; status -> %s; error -> %s",
|
|
4337
|
+
email_id,
|
|
4338
|
+
user_id,
|
|
4339
|
+
response.status_code,
|
|
4340
|
+
response.text,
|
|
4341
|
+
)
|
|
4342
|
+
return None
|
|
4343
|
+
|
|
4344
|
+
# end method definition
|
|
4345
|
+
|
|
4346
|
+
def email_verification(
|
|
4347
|
+
self,
|
|
4348
|
+
user_email: str,
|
|
4349
|
+
sender: str,
|
|
4350
|
+
subject: str,
|
|
4351
|
+
url_search_pattern: str,
|
|
4352
|
+
line_end_marker: str = "=",
|
|
4353
|
+
multi_line: bool = True,
|
|
4354
|
+
multi_line_end_marker: str = "%3D",
|
|
4355
|
+
replacements: list | None = None,
|
|
4356
|
+
max_retries: int = 6,
|
|
4357
|
+
use_browser_automation: bool = False,
|
|
4358
|
+
password: str = "",
|
|
4359
|
+
password_field_id: str = "",
|
|
4360
|
+
password_confirmation_field_id: str = "",
|
|
4361
|
+
password_submit_xpath: str = "",
|
|
4362
|
+
terms_of_service_xpath: str = "",
|
|
4363
|
+
) -> bool:
|
|
4364
|
+
"""Process email verification
|
|
4365
|
+
|
|
4366
|
+
Args:
|
|
4367
|
+
user_email (str): Email address of user recieving the verification mail.
|
|
4368
|
+
sender (str): Email sender (address)
|
|
4369
|
+
subject (str): Email subject to look for (can be substring)
|
|
4370
|
+
url_search_pattern (str): String the URL needs to contain to identify it.
|
|
4371
|
+
multi_line_end_marker (str): If the URL spans multiple lines this is the "end" marker for the last line.
|
|
4372
|
+
replacements (list): if the URL needs some treatment these replacements can be applied.
|
|
4373
|
+
Result:
|
|
4374
|
+
bool: True = Success, False = Failure
|
|
4375
|
+
"""
|
|
4376
|
+
|
|
4377
|
+
# Determine the M365 user for the current user by
|
|
4378
|
+
# the email address:
|
|
4379
|
+
m365_user = self.get_user(user_email=user_email)
|
|
4380
|
+
m365_user_id = self.get_result_value(m365_user, "id")
|
|
4381
|
+
if not m365_user_id:
|
|
4382
|
+
logger.warning("Cannot find M365 user -> %s", user_email)
|
|
4383
|
+
return False
|
|
4384
|
+
|
|
4385
|
+
if replacements is None:
|
|
4386
|
+
replacements = [{"from": "=3D", "to": "="}]
|
|
4387
|
+
|
|
4388
|
+
retries = 0
|
|
4389
|
+
while retries < max_retries:
|
|
4390
|
+
response = self.get_mail(
|
|
4391
|
+
user_id=m365_user_id,
|
|
4392
|
+
sender=sender,
|
|
4393
|
+
subject=subject,
|
|
4394
|
+
show_error=False,
|
|
4395
|
+
)
|
|
4396
|
+
if response and response["value"]:
|
|
4397
|
+
emails = response["value"]
|
|
4398
|
+
# potentially there may be multiple matching emails,
|
|
4399
|
+
# we want the most recent one (from today):
|
|
4400
|
+
latest_email = max(emails, key=lambda x: x["receivedDateTime"])
|
|
4401
|
+
# Extract just the date:
|
|
4402
|
+
latest_email_date = latest_email["receivedDateTime"].split("T")[0]
|
|
4403
|
+
# Get the current date (today):
|
|
4404
|
+
today_date = datetime.today().strftime("%Y-%m-%d")
|
|
4405
|
+
# We do a sanity check here: the verification mail should be from today,
|
|
4406
|
+
# otherwise we assume it is an old mail and we need to wait for the
|
|
4407
|
+
# new verification mail to yet arrive:
|
|
4408
|
+
if latest_email_date != today_date:
|
|
4409
|
+
logger.info(
|
|
4410
|
+
"Verification email not yet received (latest mail from -> %s). Waiting %s seconds...",
|
|
4411
|
+
latest_email_date,
|
|
4412
|
+
10 * (retries + 1),
|
|
4413
|
+
)
|
|
4414
|
+
time.sleep(10 * (retries + 1))
|
|
4415
|
+
retries += 1
|
|
4416
|
+
continue
|
|
4417
|
+
email_id = latest_email["id"]
|
|
4418
|
+
# The full email body needs to be loaded with a separate REST call:
|
|
4419
|
+
body_text = self.get_mail_body(user_id=m365_user_id, email_id=email_id)
|
|
4420
|
+
# Extract the verification URL.
|
|
4421
|
+
if body_text:
|
|
4422
|
+
url = self.extract_url_from_message_body(
|
|
4423
|
+
message_body=body_text,
|
|
4424
|
+
search_pattern=url_search_pattern,
|
|
4425
|
+
line_end_marker=line_end_marker,
|
|
4426
|
+
multi_line=multi_line,
|
|
4427
|
+
multi_line_end_marker=multi_line_end_marker,
|
|
4428
|
+
replacements=replacements,
|
|
4429
|
+
)
|
|
4430
|
+
else:
|
|
4431
|
+
url = ""
|
|
4432
|
+
if not url:
|
|
4433
|
+
logger.warning("Cannot find verification link in the email body!")
|
|
4434
|
+
return False
|
|
4435
|
+
# Simulate a "click" on this URL:
|
|
4436
|
+
if use_browser_automation:
|
|
4437
|
+
# Core Share needs a full browser:
|
|
4438
|
+
browser_automation_object = BrowserAutomation(
|
|
4439
|
+
take_screenshots=True,
|
|
4440
|
+
automation_name="email-verification",
|
|
4441
|
+
)
|
|
4442
|
+
logger.info(
|
|
4443
|
+
"Open URL -> %s to verify account or email change (using browser automation)",
|
|
4444
|
+
url,
|
|
4445
|
+
)
|
|
4446
|
+
success = browser_automation_object.get_page(url)
|
|
4447
|
+
if success:
|
|
4448
|
+
user_interaction_required = False
|
|
4449
|
+
logger.info(
|
|
4450
|
+
"Successfully opened URL. Browser title is -> '%s'.",
|
|
4451
|
+
browser_automation_object.get_title(),
|
|
4452
|
+
)
|
|
4453
|
+
if password_field_id:
|
|
4454
|
+
password_field = browser_automation_object.find_elem(
|
|
4455
|
+
find_elem=password_field_id, show_error=False
|
|
4456
|
+
)
|
|
4457
|
+
if password_field:
|
|
4458
|
+
# The subsequent processing is only required if
|
|
4459
|
+
# the returned page requests a password change:
|
|
4460
|
+
user_interaction_required = True
|
|
4461
|
+
logger.info(
|
|
4462
|
+
"Found password field on returned page - it seems email verification requests password entry!"
|
|
4463
|
+
)
|
|
4464
|
+
result = browser_automation_object.find_elem_and_set(
|
|
4465
|
+
find_elem=password_field_id,
|
|
4466
|
+
elem_value=password,
|
|
4467
|
+
is_sensitive=True,
|
|
4468
|
+
)
|
|
4469
|
+
if not result:
|
|
4470
|
+
logger.error(
|
|
4471
|
+
"Failed to enter password in field -> '%s'",
|
|
4472
|
+
password_field_id,
|
|
4473
|
+
)
|
|
4474
|
+
success = False
|
|
4475
|
+
else:
|
|
4476
|
+
logger.info(
|
|
4477
|
+
"No user interaction required (no password change or terms of service acceptance)."
|
|
4478
|
+
)
|
|
4479
|
+
if user_interaction_required and password_confirmation_field_id:
|
|
4480
|
+
password_confirm_field = (
|
|
4481
|
+
browser_automation_object.find_elem(
|
|
4482
|
+
find_elem=password_confirmation_field_id,
|
|
4483
|
+
show_error=False,
|
|
4484
|
+
)
|
|
4485
|
+
)
|
|
4486
|
+
if password_confirm_field:
|
|
4487
|
+
logger.info(
|
|
4488
|
+
"Found password confirmation field on returned page - it seems email verification requests consecutive password entry!"
|
|
4489
|
+
)
|
|
4490
|
+
result = browser_automation_object.find_elem_and_set(
|
|
4491
|
+
find_elem=password_confirmation_field_id,
|
|
4492
|
+
elem_value=password,
|
|
4493
|
+
is_sensitive=True,
|
|
4494
|
+
)
|
|
4495
|
+
if not result:
|
|
4496
|
+
logger.error(
|
|
4497
|
+
"Failed to enter password in field -> '%s'",
|
|
4498
|
+
password_confirmation_field_id,
|
|
4499
|
+
)
|
|
4500
|
+
success = False
|
|
4501
|
+
if user_interaction_required and password_submit_xpath:
|
|
4502
|
+
password_submit_button = (
|
|
4503
|
+
browser_automation_object.find_elem(
|
|
4504
|
+
find_elem=password_submit_xpath,
|
|
4505
|
+
find_method="xpath",
|
|
4506
|
+
show_error=False,
|
|
4507
|
+
)
|
|
4508
|
+
)
|
|
4509
|
+
if password_submit_button:
|
|
4510
|
+
logger.info(
|
|
4511
|
+
"Submit password change dialog with button -> '%s' (found with XPath -> %s)",
|
|
4512
|
+
password_submit_button.text,
|
|
4513
|
+
password_submit_xpath,
|
|
4514
|
+
)
|
|
4515
|
+
result = browser_automation_object.find_elem_and_click(
|
|
4516
|
+
find_elem=password_submit_xpath, find_method="xpath"
|
|
4517
|
+
)
|
|
4518
|
+
if not result:
|
|
4519
|
+
logger.error(
|
|
4520
|
+
"Failed to press submit button -> %s",
|
|
4521
|
+
password_submit_xpath,
|
|
4522
|
+
)
|
|
4523
|
+
success = False
|
|
4524
|
+
# TODO is this sleep required? The Terms of service dialog has some weird animation
|
|
4525
|
+
# which may require this. It seems it is rtequired!
|
|
4526
|
+
time.sleep(1)
|
|
4527
|
+
terms_accept_button = browser_automation_object.find_elem(
|
|
4528
|
+
find_elem=terms_of_service_xpath,
|
|
4529
|
+
find_method="xpath",
|
|
4530
|
+
show_error=False,
|
|
4531
|
+
)
|
|
4532
|
+
if terms_accept_button:
|
|
4533
|
+
logger.info(
|
|
4534
|
+
"Accept terms of service with button -> '%s' (found with XPath -> %s)",
|
|
4535
|
+
terms_accept_button.text,
|
|
4536
|
+
terms_of_service_xpath,
|
|
4537
|
+
)
|
|
4538
|
+
result = browser_automation_object.find_elem_and_click(
|
|
4539
|
+
find_elem=terms_of_service_xpath,
|
|
4540
|
+
find_method="xpath",
|
|
4541
|
+
)
|
|
4542
|
+
if not result:
|
|
4543
|
+
logger.error(
|
|
4544
|
+
"Failed to accept terms of service with button -> '%s'",
|
|
4545
|
+
terms_accept_button.text,
|
|
4546
|
+
)
|
|
4547
|
+
success = False
|
|
4548
|
+
else:
|
|
4549
|
+
logger.info("No Terms of Service acceptance required.")
|
|
4550
|
+
# end if use_browser_automation
|
|
4551
|
+
else:
|
|
4552
|
+
# Salesforce (other than Core Share) is OK with the simple HTTP GET request:
|
|
4553
|
+
logger.info("Open URL -> %s to verify account or email change", url)
|
|
4554
|
+
response = self._http_object.http_request(url=url, method="GET")
|
|
4555
|
+
success = response and response.ok
|
|
4556
|
+
|
|
4557
|
+
if success:
|
|
4558
|
+
logger.info("Remove email from inbox of user -> %s...", user_email)
|
|
4559
|
+
response = self.delete_mail(user_id=m365_user_id, email_id=email_id)
|
|
4560
|
+
if not response:
|
|
4561
|
+
logger.warning(
|
|
4562
|
+
"Couldn't remove the mail from the inbox of user -> %s",
|
|
4563
|
+
user_email,
|
|
4564
|
+
)
|
|
4565
|
+
# We have success now and can break from the while loop
|
|
4566
|
+
return True
|
|
4567
|
+
else:
|
|
4568
|
+
logger.error(
|
|
4569
|
+
"Failed to process e-mail verification for user -> %s",
|
|
4570
|
+
user_email,
|
|
4571
|
+
)
|
|
4572
|
+
return False
|
|
4573
|
+
# end if response and response["value"]
|
|
4574
|
+
else:
|
|
4575
|
+
logger.info(
|
|
4576
|
+
"Verification email not yet received (no mails with sender -> %s and subject -> '%s' found). Waiting %s seconds...",
|
|
4577
|
+
sender,
|
|
4578
|
+
subject,
|
|
4579
|
+
10 * (retries + 1),
|
|
4580
|
+
)
|
|
4581
|
+
time.sleep(10 * (retries + 1))
|
|
4582
|
+
retries += 1
|
|
4583
|
+
# end while
|
|
4584
|
+
|
|
4585
|
+
logger.warning(
|
|
4586
|
+
"Verification mail for user -> %s has not arrived in time.", user_email
|
|
4587
|
+
)
|
|
4588
|
+
|
|
4589
|
+
return False
|
|
4590
|
+
|
|
4591
|
+
# end method definition
|