skypilot-nightly 1.0.0.dev20250606__py3-none-any.whl → 1.0.0.dev20250609__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.
- sky/__init__.py +2 -2
- sky/backends/backend_utils.py +21 -3
- sky/check.py +18 -22
- sky/cli.py +5 -8
- sky/client/cli.py +5 -8
- sky/client/sdk.py +2 -1
- sky/clouds/cloud.py +4 -0
- sky/clouds/nebius.py +44 -4
- sky/core.py +3 -2
- sky/dashboard/out/404.html +1 -1
- sky/dashboard/out/_next/static/chunks/236-619ed0248fb6fdd9.js +6 -0
- sky/dashboard/out/_next/static/chunks/470-680c19413b8f808b.js +1 -0
- sky/dashboard/out/_next/static/chunks/{614-635a84e87800f99e.js → 63-e2d7b1e75e67c713.js} +8 -8
- sky/dashboard/out/_next/static/chunks/843-16c7194621b2b512.js +11 -0
- sky/dashboard/out/_next/static/chunks/969-2c584e28e6b4b106.js +1 -0
- sky/dashboard/out/_next/static/chunks/973-aed916d5b02d2d63.js +1 -0
- sky/dashboard/out/_next/static/chunks/pages/clusters/[cluster]/{[job]-65d04d5d77cbb6b6.js → [job]-d31688d3e52736dd.js} +1 -1
- sky/dashboard/out/_next/static/chunks/pages/clusters/{[cluster]-35cbeb5214fd4036.js → [cluster]-e7d8710a9b0491e5.js} +1 -1
- sky/dashboard/out/_next/static/chunks/pages/{clusters-5549a350f97d7ef3.js → clusters-3c674e5d970e05cb.js} +1 -1
- sky/dashboard/out/_next/static/chunks/pages/config-3aac7a015c6eede1.js +6 -0
- sky/dashboard/out/_next/static/chunks/pages/infra/{[context]-b68ddeed712d45b5.js → [context]-46d2e4ad6c487260.js} +1 -1
- sky/dashboard/out/_next/static/chunks/pages/{infra-13b117a831702196.js → infra-7013d816a2a0e76c.js} +1 -1
- sky/dashboard/out/_next/static/chunks/pages/jobs/[job]-f7f0c9e156d328bc.js +16 -0
- sky/dashboard/out/_next/static/chunks/pages/{jobs-a76b2700eca236f7.js → jobs-87e60396c376292f.js} +1 -1
- sky/dashboard/out/_next/static/chunks/pages/users-9355a0f13d1db61d.js +16 -0
- sky/dashboard/out/_next/static/chunks/pages/workspace/{new-c7516f2b4c3727c0.js → new-9a749cca1813bd27.js} +1 -1
- sky/dashboard/out/_next/static/chunks/pages/workspaces/{[name]-7799de9e691e35d8.js → [name]-8eeb628e03902f1b.js} +1 -1
- sky/dashboard/out/_next/static/chunks/pages/workspaces-8fbcc5ab4af316d0.js +1 -0
- sky/dashboard/out/_next/static/css/8b1c8321d4c02372.css +3 -0
- sky/dashboard/out/_next/static/xos0euNCptbGAM7_Q3Acl/_buildManifest.js +1 -0
- sky/dashboard/out/clusters/[cluster]/[job].html +1 -1
- sky/dashboard/out/clusters/[cluster].html +1 -1
- sky/dashboard/out/clusters.html +1 -1
- sky/dashboard/out/config.html +1 -1
- sky/dashboard/out/index.html +1 -1
- sky/dashboard/out/infra/[context].html +1 -1
- sky/dashboard/out/infra.html +1 -1
- sky/dashboard/out/jobs/[job].html +1 -1
- sky/dashboard/out/jobs.html +1 -1
- sky/dashboard/out/users.html +1 -1
- sky/dashboard/out/workspace/new.html +1 -1
- sky/dashboard/out/workspaces/[name].html +1 -1
- sky/dashboard/out/workspaces.html +1 -1
- sky/exceptions.py +5 -0
- sky/global_user_state.py +11 -6
- sky/jobs/scheduler.py +9 -4
- sky/jobs/server/core.py +23 -2
- sky/jobs/server/server.py +0 -95
- sky/jobs/state.py +18 -15
- sky/jobs/utils.py +2 -1
- sky/models.py +18 -0
- sky/provision/kubernetes/utils.py +12 -5
- sky/provision/nebius/constants.py +47 -0
- sky/provision/nebius/instance.py +2 -1
- sky/provision/nebius/utils.py +28 -7
- sky/serve/server/core.py +1 -1
- sky/server/common.py +4 -2
- sky/server/constants.py +0 -2
- sky/server/requests/executor.py +10 -2
- sky/server/requests/requests.py +4 -3
- sky/server/server.py +22 -5
- sky/skylet/constants.py +4 -0
- sky/skylet/job_lib.py +2 -1
- sky/skypilot_config.py +13 -1
- sky/templates/jobs-controller.yaml.j2 +3 -1
- sky/templates/nebius-ray.yml.j2 +6 -0
- sky/users/model.conf +1 -1
- sky/users/permission.py +148 -31
- sky/users/rbac.py +26 -0
- sky/users/server.py +14 -13
- sky/utils/common.py +6 -1
- sky/utils/common_utils.py +21 -3
- sky/utils/kubernetes/deploy_remote_cluster.py +5 -3
- sky/utils/resources_utils.py +3 -1
- sky/utils/schemas.py +9 -0
- sky/workspaces/core.py +100 -8
- sky/workspaces/server.py +15 -2
- sky/workspaces/utils.py +56 -0
- {skypilot_nightly-1.0.0.dev20250606.dist-info → skypilot_nightly-1.0.0.dev20250609.dist-info}/METADATA +1 -1
- {skypilot_nightly-1.0.0.dev20250606.dist-info → skypilot_nightly-1.0.0.dev20250609.dist-info}/RECORD +89 -87
- sky/dashboard/out/_next/static/99m-BAySO8Q7J-ul1jZVL/_buildManifest.js +0 -1
- sky/dashboard/out/_next/static/chunks/236-a90f0a9753a10420.js +0 -6
- sky/dashboard/out/_next/static/chunks/470-9e7a479cc8303baa.js +0 -1
- sky/dashboard/out/_next/static/chunks/843-c296541442d4af88.js +0 -11
- sky/dashboard/out/_next/static/chunks/969-c7abda31c10440ac.js +0 -1
- sky/dashboard/out/_next/static/chunks/973-1a09cac61cfcc1e1.js +0 -1
- sky/dashboard/out/_next/static/chunks/pages/config-1a1eeb949dab8897.js +0 -6
- sky/dashboard/out/_next/static/chunks/pages/jobs/[job]-2d23a9c7571e6320.js +0 -16
- sky/dashboard/out/_next/static/chunks/pages/users-262aab38b9baaf3a.js +0 -16
- sky/dashboard/out/_next/static/chunks/pages/workspaces-384ea5fa0cea8f28.js +0 -1
- sky/dashboard/out/_next/static/css/667d941a2888ce6e.css +0 -3
- /sky/dashboard/out/_next/static/chunks/{37-beedd583fea84cc8.js → 37-600191c5804dcae2.js} +0 -0
- /sky/dashboard/out/_next/static/chunks/{682-6647f0417d5662f0.js → 682-b60cfdacc15202e8.js} +0 -0
- /sky/dashboard/out/_next/static/chunks/{856-3a32da4b84176f6d.js → 856-affc52adf5403a3a.js} +0 -0
- /sky/dashboard/out/_next/static/chunks/pages/{_app-cb81dc4d27f4d009.js → _app-5f16aba5794ee8e7.js} +0 -0
- /sky/dashboard/out/_next/static/{99m-BAySO8Q7J-ul1jZVL → xos0euNCptbGAM7_Q3Acl}/_ssgManifest.js +0 -0
- {skypilot_nightly-1.0.0.dev20250606.dist-info → skypilot_nightly-1.0.0.dev20250609.dist-info}/WHEEL +0 -0
- {skypilot_nightly-1.0.0.dev20250606.dist-info → skypilot_nightly-1.0.0.dev20250609.dist-info}/entry_points.txt +0 -0
- {skypilot_nightly-1.0.0.dev20250606.dist-info → skypilot_nightly-1.0.0.dev20250609.dist-info}/licenses/LICENSE +0 -0
- {skypilot_nightly-1.0.0.dev20250606.dist-info → skypilot_nightly-1.0.0.dev20250609.dist-info}/top_level.txt +0 -0
sky/workspaces/core.py
CHANGED
@@ -1,20 +1,24 @@
|
|
1
1
|
"""Workspace management core."""
|
2
2
|
|
3
3
|
import concurrent.futures
|
4
|
-
from typing import Any, Callable, Dict
|
4
|
+
from typing import Any, Callable, Dict, List
|
5
5
|
|
6
6
|
import filelock
|
7
7
|
|
8
8
|
from sky import check as sky_check
|
9
9
|
from sky import exceptions
|
10
10
|
from sky import global_user_state
|
11
|
+
from sky import models
|
11
12
|
from sky import sky_logging
|
12
13
|
from sky import skypilot_config
|
13
14
|
from sky.skylet import constants
|
14
15
|
from sky.usage import usage_lib
|
16
|
+
from sky.users import permission
|
17
|
+
from sky.utils import annotations
|
15
18
|
from sky.utils import common_utils
|
16
19
|
from sky.utils import config_utils
|
17
20
|
from sky.utils import schemas
|
21
|
+
from sky.workspaces import utils as workspaces_utils
|
18
22
|
|
19
23
|
logger = sky_logging.init_logger(__name__)
|
20
24
|
|
@@ -28,10 +32,7 @@ _WORKSPACE_CONFIG_LOCK_TIMEOUT_SECONDS = 60
|
|
28
32
|
|
29
33
|
def get_workspaces() -> Dict[str, Any]:
|
30
34
|
"""Returns the workspace config."""
|
31
|
-
|
32
|
-
if constants.SKYPILOT_DEFAULT_WORKSPACE not in workspaces:
|
33
|
-
workspaces[constants.SKYPILOT_DEFAULT_WORKSPACE] = {}
|
34
|
-
return workspaces
|
35
|
+
return workspaces_for_user(common_utils.get_current_user().id)
|
35
36
|
|
36
37
|
|
37
38
|
def _update_workspaces_config(
|
@@ -160,7 +161,7 @@ def _check_workspaces_have_no_active_resources(
|
|
160
161
|
f'{len(workspace_clusters)} active cluster(s): {cluster_list}')
|
161
162
|
|
162
163
|
if workspace_active_jobs:
|
163
|
-
job_names = [job['job_id'] for job in workspace_active_jobs]
|
164
|
+
job_names = [str(job['job_id']) for job in workspace_active_jobs]
|
164
165
|
job_list = ', '.join(job_names)
|
165
166
|
workspace_errors.append(
|
166
167
|
f'{len(workspace_active_jobs)} active managed job(s): '
|
@@ -223,14 +224,19 @@ def update_workspace(workspace_name: str, config: Dict[str,
|
|
223
224
|
FileNotFoundError: If the config file cannot be found.
|
224
225
|
PermissionError: If the config file cannot be written.
|
225
226
|
"""
|
227
|
+
_validate_workspace_config(workspace_name, config)
|
228
|
+
|
226
229
|
# Check for active clusters and managed jobs in the workspace
|
230
|
+
# TODO(zhwu): we should allow the edits that only contain changes to
|
231
|
+
# allowed_users or private.
|
227
232
|
_check_workspace_has_no_active_resources(workspace_name, 'update')
|
228
233
|
|
229
|
-
_validate_workspace_config(workspace_name, config)
|
230
|
-
|
231
234
|
def update_workspace_fn(workspaces: Dict[str, Any]) -> None:
|
232
235
|
"""Function to update workspace inside the lock."""
|
233
236
|
workspaces[workspace_name] = config
|
237
|
+
users = workspaces_utils.get_workspace_users(config)
|
238
|
+
permission_service = permission.permission_service
|
239
|
+
permission_service.update_workspace_policy(workspace_name, users)
|
234
240
|
|
235
241
|
# Use the internal helper function to save
|
236
242
|
result = _update_workspaces_config(update_workspace_fn)
|
@@ -275,6 +281,10 @@ def create_workspace(workspace_name: str, config: Dict[str,
|
|
275
281
|
raise ValueError(f'Workspace {workspace_name!r} already exists. '
|
276
282
|
'Use update instead.')
|
277
283
|
workspaces[workspace_name] = config
|
284
|
+
# Add policy for the workspace and allowed users
|
285
|
+
users = workspaces_utils.get_workspace_users(config)
|
286
|
+
permission_service = permission.permission_service
|
287
|
+
permission_service.add_workspace_policy(workspace_name, users)
|
278
288
|
|
279
289
|
# Use the internal helper function to save
|
280
290
|
result = _update_workspaces_config(create_workspace_fn)
|
@@ -324,6 +334,8 @@ def delete_workspace(workspace_name: str) -> Dict[str, Any]:
|
|
324
334
|
if workspace_name not in workspaces:
|
325
335
|
raise ValueError(f'Workspace {workspace_name!r} does not exist.')
|
326
336
|
del workspaces[workspace_name]
|
337
|
+
permission_service = permission.permission_service
|
338
|
+
permission_service.remove_workspace_policy(workspace_name)
|
327
339
|
|
328
340
|
# Use the internal helper function to save
|
329
341
|
return _update_workspaces_config(delete_workspace_fn)
|
@@ -369,6 +381,9 @@ def update_config(config: Dict[str, Any]) -> Dict[str, Any]:
|
|
369
381
|
|
370
382
|
# Check for API server changes and validate them
|
371
383
|
current_config = skypilot_config.to_dict()
|
384
|
+
# If there is no changes to the config, we can return early
|
385
|
+
if current_config == config:
|
386
|
+
return config
|
372
387
|
|
373
388
|
current_endpoint = current_config.get('api_server', {}).get('endpoint')
|
374
389
|
new_endpoint = config.get('api_server', {}).get('endpoint')
|
@@ -382,15 +397,27 @@ def update_config(config: Dict[str, Any]) -> Dict[str, Any]:
|
|
382
397
|
|
383
398
|
# Collect all workspaces that need to be checked for active resources
|
384
399
|
workspaces_to_check = []
|
400
|
+
workspaces_to_check_policy: Dict[str, Dict[str, List[str]]] = {
|
401
|
+
'add': {},
|
402
|
+
'update': {},
|
403
|
+
'delete': {}
|
404
|
+
}
|
385
405
|
|
386
406
|
# Check each workspace that is being modified
|
387
407
|
for workspace_name, new_workspace_config in new_workspaces.items():
|
408
|
+
if workspace_name not in current_workspaces:
|
409
|
+
users = workspaces_utils.get_workspace_users(new_workspace_config)
|
410
|
+
workspaces_to_check_policy['add'][workspace_name] = users
|
411
|
+
continue
|
412
|
+
|
388
413
|
current_workspace_config = current_workspaces.get(workspace_name, {})
|
389
414
|
|
390
415
|
# If workspace configuration is changing, validate and mark for checking
|
391
416
|
if current_workspace_config != new_workspace_config:
|
392
417
|
_validate_workspace_config(workspace_name, new_workspace_config)
|
393
418
|
workspaces_to_check.append((workspace_name, 'update'))
|
419
|
+
users = workspaces_utils.get_workspace_users(new_workspace_config)
|
420
|
+
workspaces_to_check_policy['update'][workspace_name] = users
|
394
421
|
|
395
422
|
# Check for workspace deletions
|
396
423
|
for workspace_name in current_workspaces:
|
@@ -400,6 +427,7 @@ def update_config(config: Dict[str, Any]) -> Dict[str, Any]:
|
|
400
427
|
raise ValueError(f'Cannot delete the default workspace '
|
401
428
|
f'{constants.SKYPILOT_DEFAULT_WORKSPACE!r}.')
|
402
429
|
workspaces_to_check.append((workspace_name, 'delete'))
|
430
|
+
workspaces_to_check_policy['delete'][workspace_name] = ['*']
|
403
431
|
|
404
432
|
# Check all workspaces for active resources in one efficient call
|
405
433
|
_check_workspaces_have_no_active_resources(workspaces_to_check)
|
@@ -412,6 +440,18 @@ def update_config(config: Dict[str, Any]) -> Dict[str, Any]:
|
|
412
440
|
# Convert to config_utils.Config and save
|
413
441
|
config_obj = config_utils.Config.from_dict(config)
|
414
442
|
skypilot_config.update_api_server_config_no_lock(config_obj)
|
443
|
+
permission_service = permission.permission_service
|
444
|
+
for operation, workspaces in workspaces_to_check_policy.items():
|
445
|
+
for workspace_name, users in workspaces.items():
|
446
|
+
if operation == 'add':
|
447
|
+
permission_service.add_workspace_policy(
|
448
|
+
workspace_name, users)
|
449
|
+
elif operation == 'update':
|
450
|
+
permission_service.update_workspace_policy(
|
451
|
+
workspace_name, users)
|
452
|
+
elif operation == 'delete':
|
453
|
+
permission_service.remove_workspace_policy(
|
454
|
+
workspace_name)
|
415
455
|
except filelock.Timeout as e:
|
416
456
|
raise RuntimeError(
|
417
457
|
f'Failed to update configuration due to a timeout '
|
@@ -429,3 +469,55 @@ def update_config(config: Dict[str, Any]) -> Dict[str, Any]:
|
|
429
469
|
# Don't fail the update if the check fails, just warn
|
430
470
|
|
431
471
|
return config
|
472
|
+
|
473
|
+
|
474
|
+
def reject_request_for_unauthorized_workspace(user: models.User) -> None:
|
475
|
+
"""Rejects a request that has no permission to access active workspace.
|
476
|
+
|
477
|
+
Args:
|
478
|
+
user: The user making the request.
|
479
|
+
|
480
|
+
Raises:
|
481
|
+
PermissionDeniedError: If the user does not have permission to access
|
482
|
+
the active workspace.
|
483
|
+
"""
|
484
|
+
active_workspace = skypilot_config.get_active_workspace()
|
485
|
+
if not permission.permission_service.check_workspace_permission(
|
486
|
+
user.id, active_workspace):
|
487
|
+
raise exceptions.PermissionDeniedError(
|
488
|
+
f'User {user.name} ({user.id}) does not have '
|
489
|
+
f'permission to access workspace {active_workspace!r}')
|
490
|
+
|
491
|
+
|
492
|
+
def is_workspace_private(workspace_config: Dict[str, Any]) -> bool:
|
493
|
+
"""Check if a workspace is private.
|
494
|
+
|
495
|
+
Args:
|
496
|
+
workspace_config: The workspace configuration dictionary.
|
497
|
+
|
498
|
+
Returns:
|
499
|
+
True if the workspace is private, False if it's public.
|
500
|
+
"""
|
501
|
+
return workspace_config.get('private', False)
|
502
|
+
|
503
|
+
|
504
|
+
@annotations.lru_cache(scope='request', maxsize=1)
|
505
|
+
def workspaces_for_user(user_id: str) -> Dict[str, Any]:
|
506
|
+
"""Returns the workspaces that the user has access to.
|
507
|
+
|
508
|
+
Args:
|
509
|
+
user_id: The user id to check.
|
510
|
+
|
511
|
+
Returns:
|
512
|
+
A map from workspace name to workspace configuration.
|
513
|
+
"""
|
514
|
+
workspaces = skypilot_config.get_nested(('workspaces',), default_value={})
|
515
|
+
if constants.SKYPILOT_DEFAULT_WORKSPACE not in workspaces:
|
516
|
+
workspaces[constants.SKYPILOT_DEFAULT_WORKSPACE] = {}
|
517
|
+
user_workspaces = {}
|
518
|
+
|
519
|
+
for workspace_name, workspace_config in workspaces.items():
|
520
|
+
if permission.permission_service.check_workspace_permission(
|
521
|
+
user_id, workspace_name):
|
522
|
+
user_workspaces[workspace_name] = workspace_config
|
523
|
+
return user_workspaces
|
sky/workspaces/server.py
CHANGED
@@ -14,10 +14,18 @@ router = fastapi.APIRouter()
|
|
14
14
|
# pylint: disable=redefined-builtin
|
15
15
|
async def get(request: fastapi.Request) -> None:
|
16
16
|
"""Gets workspace config on the server."""
|
17
|
+
# Have to manually inject user info into the request body because the
|
18
|
+
# request body is not available in the GET endpoint.
|
19
|
+
auth_user = request.state.auth_user
|
20
|
+
auth_user_env_vars_kwargs = {
|
21
|
+
'env_vars': auth_user.to_env_vars()
|
22
|
+
} if auth_user else {}
|
23
|
+
request_body = payloads.RequestBody(**auth_user_env_vars_kwargs)
|
24
|
+
|
17
25
|
executor.schedule_request(
|
18
26
|
request_id=request.state.request_id,
|
19
27
|
request_name='workspaces.get',
|
20
|
-
request_body=
|
28
|
+
request_body=request_body,
|
21
29
|
func=core.get_workspaces,
|
22
30
|
schedule_type=api_requests.ScheduleType.SHORT,
|
23
31
|
)
|
@@ -65,10 +73,15 @@ async def delete(request: fastapi.Request,
|
|
65
73
|
@router.get('/config')
|
66
74
|
async def get_config(request: fastapi.Request) -> None:
|
67
75
|
"""Gets the entire SkyPilot configuration."""
|
76
|
+
auth_user = request.state.auth_user
|
77
|
+
auth_user_env_vars_kwargs = {
|
78
|
+
'env_vars': auth_user.to_env_vars()
|
79
|
+
} if auth_user else {}
|
80
|
+
get_config_body = payloads.GetConfigBody(**auth_user_env_vars_kwargs)
|
68
81
|
executor.schedule_request(
|
69
82
|
request_id=request.state.request_id,
|
70
83
|
request_name='workspaces.get_config',
|
71
|
-
request_body=
|
84
|
+
request_body=get_config_body,
|
72
85
|
func=core.get_config,
|
73
86
|
schedule_type=api_requests.ScheduleType.SHORT,
|
74
87
|
)
|
sky/workspaces/utils.py
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
"""Utils for workspaces."""
|
2
|
+
import collections
|
3
|
+
from typing import Any, Dict, List
|
4
|
+
|
5
|
+
from sky import global_user_state
|
6
|
+
from sky import sky_logging
|
7
|
+
|
8
|
+
logger = sky_logging.init_logger(__name__)
|
9
|
+
|
10
|
+
|
11
|
+
def get_workspace_users(workspace_config: Dict[str, Any]) -> List[str]:
|
12
|
+
"""Get the users that should have access to a workspace.
|
13
|
+
|
14
|
+
workspace_config is a dict with the following keys:
|
15
|
+
- private: bool
|
16
|
+
- allowed_users: list of user names or IDs
|
17
|
+
|
18
|
+
This function will automatically resolve the user names to IDs.
|
19
|
+
|
20
|
+
Args:
|
21
|
+
workspace_config: The configuration of the workspace.
|
22
|
+
|
23
|
+
Returns:
|
24
|
+
List of user IDs that should have access to the workspace.
|
25
|
+
For private workspaces, returns specific user IDs.
|
26
|
+
For public workspaces, returns ['*'] to indicate all users.
|
27
|
+
"""
|
28
|
+
if workspace_config.get('private', False):
|
29
|
+
user_ids = []
|
30
|
+
workspace_user_name_or_ids = workspace_config.get('allowed_users', [])
|
31
|
+
all_users = global_user_state.get_all_users()
|
32
|
+
all_user_ids = {user.id for user in all_users}
|
33
|
+
all_user_map = collections.defaultdict(list)
|
34
|
+
for user in all_users:
|
35
|
+
all_user_map[user.name].append(user.id)
|
36
|
+
|
37
|
+
# Resolve user names to IDs
|
38
|
+
for user_name_or_id in workspace_user_name_or_ids:
|
39
|
+
if user_name_or_id in all_user_ids:
|
40
|
+
user_ids.append(user_name_or_id)
|
41
|
+
elif user_name_or_id in all_user_map:
|
42
|
+
if len(all_user_map[user_name_or_id]) > 1:
|
43
|
+
user_ids_str = ', '.join(all_user_map[user_name_or_id])
|
44
|
+
raise ValueError(
|
45
|
+
f'User {user_name_or_id!r} has multiple IDs: '
|
46
|
+
f'{user_ids_str}. Please specify the user '
|
47
|
+
f'ID instead.')
|
48
|
+
user_ids.append(all_user_map[user_name_or_id][0])
|
49
|
+
else:
|
50
|
+
logger.warning(
|
51
|
+
f'User {user_name_or_id!r} not found in all users')
|
52
|
+
continue
|
53
|
+
return user_ids
|
54
|
+
else:
|
55
|
+
# Public workspace - return '*' to indicate all users should have access
|
56
|
+
return ['*']
|