msdev-kit 0.1.0__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.
- msdev_kit/__init__.py +3 -0
- msdev_kit/auth.py +35 -0
- msdev_kit/fabric/__init__.py +11 -0
- msdev_kit/fabric/admin.py +42 -0
- msdev_kit/fabric/capacity.py +186 -0
- msdev_kit/fabric/database.py +94 -0
- msdev_kit/fabric/dataflow.py +1801 -0
- msdev_kit/fabric/dataset.py +617 -0
- msdev_kit/fabric/kql.py +66 -0
- msdev_kit/fabric/notebook.py +88 -0
- msdev_kit/fabric/operations.py +108 -0
- msdev_kit/fabric/pipeline.py +505 -0
- msdev_kit/fabric/report.py +1012 -0
- msdev_kit/fabric/utilities.py +12 -0
- msdev_kit/fabric/workspace.py +516 -0
- msdev_kit/graph/__init__.py +1 -0
- msdev_kit/graph/client.py +91 -0
- msdev_kit/sharepoint/__init__.py +1 -0
- msdev_kit/sharepoint/client.py +105 -0
- msdev_kit-0.1.0.dist-info/METADATA +269 -0
- msdev_kit-0.1.0.dist-info/RECORD +22 -0
- msdev_kit-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import requests
|
|
3
|
+
import pandas as pd
|
|
4
|
+
from pandas.core.frame import DataFrame
|
|
5
|
+
from typing import Dict, List
|
|
6
|
+
from .utilities import create_directory
|
|
7
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Workspace:
|
|
11
|
+
|
|
12
|
+
def __init__(self, token: str):
|
|
13
|
+
"""
|
|
14
|
+
Initialize variables.
|
|
15
|
+
"""
|
|
16
|
+
self.main_url = 'https://api.powerbi.com/v1.0/myorg'
|
|
17
|
+
self.token = token
|
|
18
|
+
self.headers = {'Authorization': f'Bearer {self.token}'}
|
|
19
|
+
|
|
20
|
+
# Directories
|
|
21
|
+
self.workspace_dir = './data/workspaces'
|
|
22
|
+
self.dataflows_dir = './data/dataflows'
|
|
23
|
+
self.datasets_dir = './data/datasets'
|
|
24
|
+
self.reports_dir = './data/reports'
|
|
25
|
+
self.users_dir = './data/users'
|
|
26
|
+
self.directories = [
|
|
27
|
+
self.workspace_dir,
|
|
28
|
+
self.dataflows_dir,
|
|
29
|
+
self.datasets_dir,
|
|
30
|
+
self.reports_dir,
|
|
31
|
+
self.users_dir
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
for dir in self.directories:
|
|
35
|
+
create_directory(dir)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def list_workspaces_for_user(
|
|
39
|
+
self,
|
|
40
|
+
workspace_id: str = '',
|
|
41
|
+
workspace_name: str = '',
|
|
42
|
+
identifier: str = '',
|
|
43
|
+
principal_type: str = 'App',
|
|
44
|
+
filters: str = '') -> Dict:
|
|
45
|
+
"""
|
|
46
|
+
List all workspaces that the user has access to.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
workspace_id (str, optional): workspace id to search for.
|
|
50
|
+
workspace_name (str, optional): workspace name to search for.
|
|
51
|
+
identifier (str, optional): identifier of the service principal.
|
|
52
|
+
principal_type (str, optional): principal type, 'App' for service accounts, 'Users' for usual users. Defaults to 'App'.
|
|
53
|
+
filters (str, optional): filters to be applied.
|
|
54
|
+
- filters example:
|
|
55
|
+
- filters=f"contains(name,'{workspace_to_search}')%20or%20name%20eq%20'Dataflows'")
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Dict: status message and content.
|
|
59
|
+
"""
|
|
60
|
+
# Main URL
|
|
61
|
+
request_url = self.main_url + '/groups'
|
|
62
|
+
|
|
63
|
+
# If no parameter, list all workspaces with access to...
|
|
64
|
+
if (workspace_id == '') & (workspace_name == '') & (filters == ''):
|
|
65
|
+
filename = 'workspaces_all.xlsx'
|
|
66
|
+
|
|
67
|
+
# If workspace ID was informed...
|
|
68
|
+
elif workspace_id != '':
|
|
69
|
+
request_url = f"{request_url}/{workspace_id}"
|
|
70
|
+
filename = f'{workspace_id}.xlsx'
|
|
71
|
+
|
|
72
|
+
# If workspace name was informed...
|
|
73
|
+
elif workspace_name != '':
|
|
74
|
+
request_url = f"{request_url}/?$filter=name%20eq%20'{workspace_name}'"
|
|
75
|
+
filename = f"{workspace_name.replace(' ', '_').upper()}.xlsx"
|
|
76
|
+
|
|
77
|
+
# If any custom (OData) filters were informed...
|
|
78
|
+
# Example: passing -> filters="contains(name,'Databrew')"
|
|
79
|
+
# Filters for workspaces that contain Databrew on it's name.
|
|
80
|
+
elif filters != '':
|
|
81
|
+
request_url = f'{request_url}/{workspace_id}?$filter={filters}'
|
|
82
|
+
filename = 'workspaces_filtered.xlsx'
|
|
83
|
+
else:
|
|
84
|
+
return {'message': 'Missing parameters, please check.', 'content': ''}
|
|
85
|
+
|
|
86
|
+
# Make the request
|
|
87
|
+
r = requests.get(url=request_url, headers=self.headers)
|
|
88
|
+
|
|
89
|
+
# Get HTTP status and content
|
|
90
|
+
status = r.status_code
|
|
91
|
+
response = json.loads(r.content).get('value', '')
|
|
92
|
+
|
|
93
|
+
# If success...
|
|
94
|
+
|
|
95
|
+
if status == 200:
|
|
96
|
+
df = pd.DataFrame(response)
|
|
97
|
+
|
|
98
|
+
if not df.empty and 'id' in df.columns and identifier != '':
|
|
99
|
+
# If identifier was informed, get the role of the app in the workspace
|
|
100
|
+
app_access_rights = [''] * len(df['id'])
|
|
101
|
+
|
|
102
|
+
def _fetch_role(args):
|
|
103
|
+
index, ws_id = args
|
|
104
|
+
role_url = f"{request_url}/{ws_id}/users"
|
|
105
|
+
role_response = requests.get(url=role_url, headers=self.headers)
|
|
106
|
+
if role_response.status_code == 200:
|
|
107
|
+
users_data = role_response.json().get("value", [])
|
|
108
|
+
role = next(
|
|
109
|
+
(
|
|
110
|
+
user["groupUserAccessRight"]
|
|
111
|
+
for user in users_data
|
|
112
|
+
if user.get("identifier") == identifier
|
|
113
|
+
and user.get("principalType") == principal_type
|
|
114
|
+
),
|
|
115
|
+
""
|
|
116
|
+
)
|
|
117
|
+
else:
|
|
118
|
+
role = ""
|
|
119
|
+
return index, role
|
|
120
|
+
|
|
121
|
+
with ThreadPoolExecutor() as executor:
|
|
122
|
+
futures = {executor.submit(_fetch_role, (i, ws_id)): i for i, ws_id in enumerate(df['id'])}
|
|
123
|
+
for future in as_completed(futures):
|
|
124
|
+
index, role = future.result()
|
|
125
|
+
app_access_rights[index] = role
|
|
126
|
+
response[index]["workspaceRole"] = role
|
|
127
|
+
|
|
128
|
+
df["workspaceRole"] = app_access_rights
|
|
129
|
+
|
|
130
|
+
df.to_excel(f'{self.workspace_dir}/{filename}', index=False)
|
|
131
|
+
return {'message': 'Success', 'content': response}
|
|
132
|
+
else:
|
|
133
|
+
response = json.loads(r.content)
|
|
134
|
+
error_message = response['error']['message']
|
|
135
|
+
return {'message': {'error': error_message, 'content': response}}
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def get_workspace_details(
|
|
139
|
+
self,
|
|
140
|
+
workspace_id: str = '') -> Dict:
|
|
141
|
+
"""
|
|
142
|
+
Get details for a specific workspace that the user has access to.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
workspace_id (str, optional): workspace id to search for.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
Dict: status message and content.
|
|
149
|
+
"""
|
|
150
|
+
# Main URL
|
|
151
|
+
request_url = self.main_url + f'/groups'
|
|
152
|
+
|
|
153
|
+
# If no workspace ID was informed...
|
|
154
|
+
if (workspace_id == ''):
|
|
155
|
+
return {'message': 'Missing parameters, please check.', 'content': ''}
|
|
156
|
+
|
|
157
|
+
# If workspace ID was informed...
|
|
158
|
+
elif workspace_id != '':
|
|
159
|
+
request_url = f"{request_url}/{workspace_id}"
|
|
160
|
+
|
|
161
|
+
# Make the request
|
|
162
|
+
r = requests.get(url=request_url, headers=self.headers)
|
|
163
|
+
|
|
164
|
+
# Get HTTP status and content
|
|
165
|
+
status = r.status_code
|
|
166
|
+
response = json.loads(r.content)
|
|
167
|
+
|
|
168
|
+
# If success...
|
|
169
|
+
if status == 200:
|
|
170
|
+
return {'message': 'Success', 'content': response}
|
|
171
|
+
|
|
172
|
+
else:
|
|
173
|
+
# If any error happens, return message.
|
|
174
|
+
response = json.loads(r.content)
|
|
175
|
+
error_message = response['error']['message']
|
|
176
|
+
|
|
177
|
+
return {'message': {'error': error_message, 'content': response}}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def list_users(self, workspace_id: str = '') -> Dict:
|
|
181
|
+
"""
|
|
182
|
+
List all users in a workspace_id that the user has access to.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
workspace_id (str, optional): workspace id to search for.
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
Dict: status message and content.
|
|
189
|
+
"""
|
|
190
|
+
# Main URL
|
|
191
|
+
request_url = self.main_url + '/groups'
|
|
192
|
+
|
|
193
|
+
# If workspace ID was not informed, return error message...
|
|
194
|
+
if workspace_id == '':
|
|
195
|
+
return {'message': 'Missing workspace id, please check.', 'content': ''}
|
|
196
|
+
|
|
197
|
+
# If workspace ID was informed...
|
|
198
|
+
else:
|
|
199
|
+
request_url = f'{request_url}/{workspace_id}/users'
|
|
200
|
+
filename = f'users_{workspace_id}.xlsx'
|
|
201
|
+
|
|
202
|
+
# Make the request
|
|
203
|
+
r = requests.get(url=request_url, headers=self.headers)
|
|
204
|
+
|
|
205
|
+
# Get HTTP status and content
|
|
206
|
+
status = r.status_code
|
|
207
|
+
response = json.loads(r.content).get('value', '')
|
|
208
|
+
|
|
209
|
+
# If success...
|
|
210
|
+
if status == 200:
|
|
211
|
+
# Save to Excel file
|
|
212
|
+
df = pd.DataFrame(response)
|
|
213
|
+
df.to_excel(f'{self.users_dir}/{filename}', index=False)
|
|
214
|
+
|
|
215
|
+
return {'message': 'Success', 'content': response}
|
|
216
|
+
|
|
217
|
+
else:
|
|
218
|
+
# If any error happens, return message.
|
|
219
|
+
response = json.loads(r.content)
|
|
220
|
+
error_message = response['error']['message']
|
|
221
|
+
|
|
222
|
+
return {'message': {'error': error_message, 'content': response}}
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def list_reports(self, workspace_id: str = '') -> Dict:
|
|
226
|
+
"""
|
|
227
|
+
List all reports for a specific workspace.
|
|
228
|
+
Args:
|
|
229
|
+
workspace_id (str, optional): workspace id to search for.
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
Dict: status message and content.
|
|
233
|
+
"""
|
|
234
|
+
# Main URL
|
|
235
|
+
request_url = self.main_url + '/groups'
|
|
236
|
+
|
|
237
|
+
# If workspace ID was not informed, return error message...
|
|
238
|
+
if workspace_id == '':
|
|
239
|
+
return {'message': 'Missing workspace id, please check.', 'content': ''}
|
|
240
|
+
|
|
241
|
+
# If workspace ID was informed...
|
|
242
|
+
else:
|
|
243
|
+
request_url = f'{request_url}/{workspace_id}/reports'
|
|
244
|
+
filename = f'reports_{workspace_id}.xlsx'
|
|
245
|
+
|
|
246
|
+
# Make the request
|
|
247
|
+
r = requests.get(url=request_url, headers=self.headers)
|
|
248
|
+
|
|
249
|
+
# Get HTTP status and content
|
|
250
|
+
status = r.status_code
|
|
251
|
+
response = json.loads(r.content).get('value', '')
|
|
252
|
+
|
|
253
|
+
# If success...
|
|
254
|
+
if status == 200:
|
|
255
|
+
# Save to Excel file
|
|
256
|
+
df = pd.DataFrame(response)
|
|
257
|
+
df.to_excel(f'{self.reports_dir}/{filename}', index=False)
|
|
258
|
+
|
|
259
|
+
return {'message': 'Success', 'content': response}
|
|
260
|
+
|
|
261
|
+
else:
|
|
262
|
+
# If any error happens, return message.
|
|
263
|
+
response = json.loads(r.content)
|
|
264
|
+
error_message = response['error']['message']
|
|
265
|
+
|
|
266
|
+
return {'message': {'error': error_message, 'content': response}}
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def add_user(
|
|
270
|
+
self,
|
|
271
|
+
user_principal_name: str = '',
|
|
272
|
+
workspace_id: str = '',
|
|
273
|
+
access_right: str = 'Viewer',
|
|
274
|
+
user_type: str = 'user') -> Dict:
|
|
275
|
+
"""
|
|
276
|
+
Function to add a user to a workspace.
|
|
277
|
+
Service Principals can also be added to a workspace using the parameter user_type='SP'
|
|
278
|
+
|
|
279
|
+
Args:
|
|
280
|
+
user_principal_name (str): user e-mail or identifier of service principal
|
|
281
|
+
workspace_id (str): workspace id to add the user
|
|
282
|
+
access_right (str, optional): access right type. Defaults to 'Viewer'.
|
|
283
|
+
user_type (str, optional): user type, SP for service accounts. Defaults to 'user'.
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
Dict: status message
|
|
287
|
+
"""
|
|
288
|
+
if user_principal_name == '' or workspace_id == '':
|
|
289
|
+
return {'message': 'Missing parameters, please check.'}
|
|
290
|
+
|
|
291
|
+
print(f'Adding user {user_principal_name} to workspace {workspace_id} as {access_right}...')
|
|
292
|
+
|
|
293
|
+
# Check if user already exists
|
|
294
|
+
current_users = self.list_users(workspace_id=workspace_id)
|
|
295
|
+
if current_users.get('message') != 'Success':
|
|
296
|
+
return {'message': 'Failed to fetch current users.', 'content': current_users}
|
|
297
|
+
|
|
298
|
+
user_role_map = {
|
|
299
|
+
'Admin': 4,
|
|
300
|
+
'Member': 3,
|
|
301
|
+
'Contributor': 2,
|
|
302
|
+
'Viewer': 1
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
current_role = ''
|
|
306
|
+
for user in current_users['content']:
|
|
307
|
+
if (user_type == 'SP' and user.get('identifier', '').lower() == user_principal_name.lower()) or \
|
|
308
|
+
(user_type != 'SP' and user.get('emailAddress', '').lower() == user_principal_name.lower()):
|
|
309
|
+
current_role = user.get('groupUserAccessRight', '')
|
|
310
|
+
break
|
|
311
|
+
|
|
312
|
+
desired_level = user_role_map.get(access_right, 0)
|
|
313
|
+
current_level = user_role_map.get(current_role, 0)
|
|
314
|
+
|
|
315
|
+
if current_level > 0:
|
|
316
|
+
if current_level < desired_level:
|
|
317
|
+
print(f'User {user_principal_name} already exists with lower role ({current_role}). Updating to {access_right}.')
|
|
318
|
+
return self.update_user(
|
|
319
|
+
user_principal_name=user_principal_name,
|
|
320
|
+
workspace_id=workspace_id,
|
|
321
|
+
access_right=access_right
|
|
322
|
+
)
|
|
323
|
+
else:
|
|
324
|
+
# print(f'User {user_principal_name} already has same or higher privilege: {current_role}. No action taken.')
|
|
325
|
+
return {'message': f'User already has same or higher privilege: {current_role}'}
|
|
326
|
+
|
|
327
|
+
request_url = self.main_url + f'/groups/{workspace_id}/users'
|
|
328
|
+
headers = {'Authorization': f'Bearer {self.token}'}
|
|
329
|
+
|
|
330
|
+
# Add user to workspace with the specified access right.
|
|
331
|
+
# https://learn.microsoft.com/en-us/rest/api/power-bi/groups/add-group-user#groupuseraccessright
|
|
332
|
+
|
|
333
|
+
# If service principal account
|
|
334
|
+
if user_type == 'SP':
|
|
335
|
+
data = {
|
|
336
|
+
"identifier": user_principal_name,
|
|
337
|
+
"groupUserAccessRight": access_right
|
|
338
|
+
}
|
|
339
|
+
else:
|
|
340
|
+
data = {
|
|
341
|
+
"emailAddress": user_principal_name,
|
|
342
|
+
"groupUserAccessRight": access_right
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
r = requests.post(url=request_url, headers=headers, json=data)
|
|
346
|
+
status = r.status_code
|
|
347
|
+
|
|
348
|
+
if status == 200:
|
|
349
|
+
print(f'User {user_principal_name} added successfully to workspace {workspace_id} as {access_right}.')
|
|
350
|
+
return {'message': 'Success'}
|
|
351
|
+
else:
|
|
352
|
+
try:
|
|
353
|
+
response = json.loads(r.content)
|
|
354
|
+
error_message = response['error']
|
|
355
|
+
except:
|
|
356
|
+
return {'message': 'Error reading JSON response'}
|
|
357
|
+
return {'message': {'error': error_message, 'content': response}}
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def update_user(
|
|
361
|
+
self,
|
|
362
|
+
user_principal_name: str = '',
|
|
363
|
+
workspace_id: str = '',
|
|
364
|
+
access_right: str = 'Member') -> Dict:
|
|
365
|
+
"""
|
|
366
|
+
Update an user on a workspace.
|
|
367
|
+
|
|
368
|
+
Args:
|
|
369
|
+
user_principal_name (str, optional): user e-mail or identifier of service principal.
|
|
370
|
+
workspace_id (str, optional): workspace id to add the user to.
|
|
371
|
+
access_right (str, optional): access right type. Defaults to 'Member'.
|
|
372
|
+
|
|
373
|
+
Returns:
|
|
374
|
+
Dict: status message.
|
|
375
|
+
"""
|
|
376
|
+
|
|
377
|
+
# If both, user and workspace if are provided...
|
|
378
|
+
if (user_principal_name != '') & (workspace_id != ''):
|
|
379
|
+
|
|
380
|
+
request_url = self.main_url + f'/groups/{workspace_id}/users'
|
|
381
|
+
|
|
382
|
+
headers = {'Authorization': f'Bearer {self.token}'}
|
|
383
|
+
|
|
384
|
+
# Add user to workspace with the specified access right.
|
|
385
|
+
# https://learn.microsoft.com/en-us/rest/api/power-bi/groups/update-group-user
|
|
386
|
+
data = {
|
|
387
|
+
"identifier": user_principal_name,
|
|
388
|
+
"groupUserAccessRight": access_right,
|
|
389
|
+
"principalType": "User"
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
# Make the request
|
|
393
|
+
r = requests.put(url=request_url, headers=headers, json=data)
|
|
394
|
+
|
|
395
|
+
# Get HTTP status and content
|
|
396
|
+
status = r.status_code
|
|
397
|
+
|
|
398
|
+
# If success...
|
|
399
|
+
if status == 200:
|
|
400
|
+
return {'message': 'Success'}
|
|
401
|
+
|
|
402
|
+
elif status == 401:
|
|
403
|
+
return {'message': 'Not enough privileges to update user.'}
|
|
404
|
+
|
|
405
|
+
elif status == 404:
|
|
406
|
+
return {'message': 'User was not found in workspace.'}
|
|
407
|
+
|
|
408
|
+
else:
|
|
409
|
+
|
|
410
|
+
print(f'status={status}, response={response}')
|
|
411
|
+
response = json.loads(r.content)
|
|
412
|
+
# If any error happens, return message.
|
|
413
|
+
error_message = f"Error for workspace_id={workspace_id}: response['error']['code']"
|
|
414
|
+
|
|
415
|
+
return {'message': {'error_status': {r.status_code}, 'error': error_message, 'content': r.content}}
|
|
416
|
+
|
|
417
|
+
else:
|
|
418
|
+
return {'message': 'Missing parameters, please check.'}
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def remove_user(self, user_principal_name: str = '', workspace_id: str = '') -> Dict:
|
|
422
|
+
"""
|
|
423
|
+
Remove an user from a workspace.
|
|
424
|
+
|
|
425
|
+
Args:
|
|
426
|
+
user_principal_name (str, optional): user e-mail or identifier of service principal.
|
|
427
|
+
workspace_id (str, optional): workspace id to add the user to.
|
|
428
|
+
|
|
429
|
+
Returns:
|
|
430
|
+
Dict: status message
|
|
431
|
+
"""
|
|
432
|
+
|
|
433
|
+
# If both, user and workspace if are provided...
|
|
434
|
+
if (user_principal_name != '') & (workspace_id != ''):
|
|
435
|
+
|
|
436
|
+
request_url = self.main_url + f'/groups/{workspace_id}/users/{user_principal_name}'
|
|
437
|
+
|
|
438
|
+
headers = {'Authorization': f'Bearer {self.token}'}
|
|
439
|
+
|
|
440
|
+
# Make the request
|
|
441
|
+
r = requests.delete(url=request_url, headers=headers)
|
|
442
|
+
|
|
443
|
+
# Get HTTP status and content
|
|
444
|
+
status = r.status_code
|
|
445
|
+
response = json.loads(r.content)
|
|
446
|
+
|
|
447
|
+
# If success...
|
|
448
|
+
if status == 200:
|
|
449
|
+
return {'message': 'Success'}
|
|
450
|
+
|
|
451
|
+
elif status == 401:
|
|
452
|
+
return {'message': 'Not enough privileges to remove user.'}
|
|
453
|
+
|
|
454
|
+
elif status == 404:
|
|
455
|
+
return {'message': 'User was not found in workspace.'}
|
|
456
|
+
|
|
457
|
+
else:
|
|
458
|
+
# If any error happens, return message.
|
|
459
|
+
print(f'status={status}, response={response}')
|
|
460
|
+
error_message = response['error']['message']
|
|
461
|
+
|
|
462
|
+
return {'message': {'error': error_message, 'content': response}}
|
|
463
|
+
|
|
464
|
+
else:
|
|
465
|
+
return {'message': 'Missing parameters, please check.'}
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def batch_update_user(self, user: str = '', workspaces_list: List[str] = []) -> DataFrame:
|
|
469
|
+
"""
|
|
470
|
+
Batch update an user on a list of workspaces.
|
|
471
|
+
|
|
472
|
+
Args:
|
|
473
|
+
user (str): user e-mail or identifier of service principal.
|
|
474
|
+
workspaces_list (List[str]): list of workspaces to update an user.
|
|
475
|
+
|
|
476
|
+
Returns:
|
|
477
|
+
DataFrame: table with workspaces and status of the update.
|
|
478
|
+
"""
|
|
479
|
+
|
|
480
|
+
responses = []
|
|
481
|
+
|
|
482
|
+
# If user and list of workspaces were informed...
|
|
483
|
+
if (user != '') & (workspaces_list != []):
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def _update_workspace(workspace):
|
|
487
|
+
id = workspace.get('id', '')
|
|
488
|
+
name = workspace.get('name', '')
|
|
489
|
+
response = self.update_user(user_principal_name=user, workspace_id=id, access_right='Admin')
|
|
490
|
+
try:
|
|
491
|
+
return (id, name, 'Error', response['message']['content'])
|
|
492
|
+
except:
|
|
493
|
+
return (id, name, 'Success', '')
|
|
494
|
+
|
|
495
|
+
with ThreadPoolExecutor() as executor:
|
|
496
|
+
responses = list(executor.map(_update_workspace, workspaces_list))
|
|
497
|
+
|
|
498
|
+
# Create a dataframe with responses
|
|
499
|
+
df1 = pd.DataFrame(responses, columns=['id', 'name', 'status', 'error_message'])
|
|
500
|
+
|
|
501
|
+
# Serialize json from error message as a new dataframe
|
|
502
|
+
df2 = pd.json_normalize(df1['error_message'])
|
|
503
|
+
|
|
504
|
+
# Drop error message column and merge both dataframes
|
|
505
|
+
df1.drop(labels='error_message', axis='columns', inplace=True)
|
|
506
|
+
df = pd.merge(left=df1, right=df2, left_index=True, right_index=True)
|
|
507
|
+
df = df.fillna('')
|
|
508
|
+
|
|
509
|
+
# Save to an Excel file with user name
|
|
510
|
+
df.to_excel(f"./data/workspaces_{user.split('@')[0]}.xlsx", index=False)
|
|
511
|
+
|
|
512
|
+
return df
|
|
513
|
+
|
|
514
|
+
else:
|
|
515
|
+
|
|
516
|
+
return pd.DataFrame([], columns=['id', 'name', 'status', 'error_message'])
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .client import GraphClient
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
from typing import Optional
|
|
3
|
+
from msdev_kit.auth import Auth
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class GraphClient:
|
|
7
|
+
_GRAPH_BASE = 'https://graph.microsoft.com/v1.0'
|
|
8
|
+
|
|
9
|
+
def __init__(self, auth: Auth):
|
|
10
|
+
self._auth = auth
|
|
11
|
+
|
|
12
|
+
def _headers(self) -> dict:
|
|
13
|
+
return {
|
|
14
|
+
'Authorization': f'Bearer {self._auth.get_token("graph")}',
|
|
15
|
+
'Content-Type': 'application/json',
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
def get_user_id(self, email: str) -> Optional[str]:
|
|
19
|
+
"""Return the Entra object ID of a user by UPN/email, or None if not found.
|
|
20
|
+
Falls back to filtering by the mail field when the UPN lookup returns 404."""
|
|
21
|
+
resp = requests.get(
|
|
22
|
+
f'{self._GRAPH_BASE}/users/{email}',
|
|
23
|
+
headers=self._headers(),
|
|
24
|
+
params={'$select': 'id'},
|
|
25
|
+
timeout=30,
|
|
26
|
+
)
|
|
27
|
+
if resp.status_code != 404:
|
|
28
|
+
resp.raise_for_status()
|
|
29
|
+
return resp.json().get('id')
|
|
30
|
+
|
|
31
|
+
resp = requests.get(
|
|
32
|
+
f'{self._GRAPH_BASE}/users',
|
|
33
|
+
headers=self._headers(),
|
|
34
|
+
params={'$filter': f"mail eq '{email}'", '$select': 'id'},
|
|
35
|
+
timeout=30,
|
|
36
|
+
)
|
|
37
|
+
resp.raise_for_status()
|
|
38
|
+
users = resp.json().get('value', [])
|
|
39
|
+
return users[0]['id'] if users else None
|
|
40
|
+
|
|
41
|
+
def get_group_id(self, group_name: str) -> Optional[str]:
|
|
42
|
+
"""Return the Entra object ID of a security group by displayName, or None."""
|
|
43
|
+
resp = requests.get(
|
|
44
|
+
f'{self._GRAPH_BASE}/groups',
|
|
45
|
+
headers=self._headers(),
|
|
46
|
+
params={'$filter': f"displayName eq '{group_name}'", '$select': 'id,displayName'},
|
|
47
|
+
timeout=30,
|
|
48
|
+
)
|
|
49
|
+
resp.raise_for_status()
|
|
50
|
+
groups = resp.json().get('value', [])
|
|
51
|
+
return groups[0]['id'] if groups else None
|
|
52
|
+
|
|
53
|
+
def list_group_members(self, group_id: str) -> list[dict]:
|
|
54
|
+
"""List all members of a group with pagination.
|
|
55
|
+
Returns user dicts with id, displayName, mail, userPrincipalName."""
|
|
56
|
+
members = []
|
|
57
|
+
url = f'{self._GRAPH_BASE}/groups/{group_id}/members/microsoft.graph.user'
|
|
58
|
+
params = {'$select': 'id,displayName,mail,userPrincipalName', '$top': '999'}
|
|
59
|
+
|
|
60
|
+
while url:
|
|
61
|
+
resp = requests.get(url, headers=self._headers(), params=params, timeout=30)
|
|
62
|
+
resp.raise_for_status()
|
|
63
|
+
data = resp.json()
|
|
64
|
+
members.extend(data.get('value', []))
|
|
65
|
+
url = data.get('@odata.nextLink')
|
|
66
|
+
params = None
|
|
67
|
+
|
|
68
|
+
return members
|
|
69
|
+
|
|
70
|
+
def add_group_member(self, group_id: str, user_id: str):
|
|
71
|
+
"""Add user to group. Silently ignores 'already a member' errors."""
|
|
72
|
+
resp = requests.post(
|
|
73
|
+
f'{self._GRAPH_BASE}/groups/{group_id}/members/$ref',
|
|
74
|
+
headers=self._headers(),
|
|
75
|
+
json={'@odata.id': f'{self._GRAPH_BASE}/directoryObjects/{user_id}'},
|
|
76
|
+
timeout=30,
|
|
77
|
+
)
|
|
78
|
+
if resp.status_code == 400 and 'One or more added object references already exist' in resp.text:
|
|
79
|
+
return
|
|
80
|
+
resp.raise_for_status()
|
|
81
|
+
|
|
82
|
+
def remove_group_member(self, group_id: str, user_id: str):
|
|
83
|
+
"""Remove user from group. Silently ignores 404 (not a member) and 403 (insufficient privileges)."""
|
|
84
|
+
resp = requests.delete(
|
|
85
|
+
f'{self._GRAPH_BASE}/groups/{group_id}/members/{user_id}/$ref',
|
|
86
|
+
headers=self._headers(),
|
|
87
|
+
timeout=30,
|
|
88
|
+
)
|
|
89
|
+
if resp.status_code in (403, 404):
|
|
90
|
+
return
|
|
91
|
+
resp.raise_for_status()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .client import SharePointClient
|