skypilot-nightly 1.0.0.dev20250607__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.
Files changed (79) hide show
  1. sky/__init__.py +2 -2
  2. sky/backends/backend_utils.py +18 -2
  3. sky/check.py +4 -3
  4. sky/cli.py +5 -6
  5. sky/client/cli.py +5 -6
  6. sky/core.py +3 -2
  7. sky/dashboard/out/404.html +1 -1
  8. sky/dashboard/out/_next/static/chunks/470-680c19413b8f808b.js +1 -0
  9. sky/dashboard/out/_next/static/chunks/{614-635a84e87800f99e.js → 63-e2d7b1e75e67c713.js} +8 -8
  10. sky/dashboard/out/_next/static/chunks/843-16c7194621b2b512.js +11 -0
  11. sky/dashboard/out/_next/static/chunks/pages/clusters/[cluster]/{[job]-18aed9b56247d074.js → [job]-d31688d3e52736dd.js} +1 -1
  12. sky/dashboard/out/_next/static/chunks/pages/clusters/{[cluster]-b919a73aecdfa78f.js → [cluster]-e7d8710a9b0491e5.js} +1 -1
  13. sky/dashboard/out/_next/static/chunks/pages/{clusters-4f6b9dd9abcb33ad.js → clusters-3c674e5d970e05cb.js} +1 -1
  14. sky/dashboard/out/_next/static/chunks/pages/config-3aac7a015c6eede1.js +6 -0
  15. sky/dashboard/out/_next/static/chunks/pages/infra/{[context]-3a18d0eeb5119fe4.js → [context]-46d2e4ad6c487260.js} +1 -1
  16. sky/dashboard/out/_next/static/chunks/pages/{infra-a1a6abeeb58c1051.js → infra-7013d816a2a0e76c.js} +1 -1
  17. sky/dashboard/out/_next/static/chunks/pages/jobs/{[job]-1354e28c81eeb686.js → [job]-f7f0c9e156d328bc.js} +1 -1
  18. sky/dashboard/out/_next/static/chunks/pages/{jobs-23bfc8bf373423db.js → jobs-87e60396c376292f.js} +1 -1
  19. sky/dashboard/out/_next/static/chunks/pages/users-9355a0f13d1db61d.js +16 -0
  20. sky/dashboard/out/_next/static/chunks/pages/workspace/{new-e1f9c0c3ff7ac4bd.js → new-9a749cca1813bd27.js} +1 -1
  21. sky/dashboard/out/_next/static/chunks/pages/workspaces/{[name]-686590e0ee4b2412.js → [name]-8eeb628e03902f1b.js} +1 -1
  22. sky/dashboard/out/_next/static/chunks/pages/workspaces-8fbcc5ab4af316d0.js +1 -0
  23. sky/dashboard/out/_next/static/css/8b1c8321d4c02372.css +3 -0
  24. sky/dashboard/out/_next/static/xos0euNCptbGAM7_Q3Acl/_buildManifest.js +1 -0
  25. sky/dashboard/out/clusters/[cluster]/[job].html +1 -1
  26. sky/dashboard/out/clusters/[cluster].html +1 -1
  27. sky/dashboard/out/clusters.html +1 -1
  28. sky/dashboard/out/config.html +1 -1
  29. sky/dashboard/out/index.html +1 -1
  30. sky/dashboard/out/infra/[context].html +1 -1
  31. sky/dashboard/out/infra.html +1 -1
  32. sky/dashboard/out/jobs/[job].html +1 -1
  33. sky/dashboard/out/jobs.html +1 -1
  34. sky/dashboard/out/users.html +1 -1
  35. sky/dashboard/out/workspace/new.html +1 -1
  36. sky/dashboard/out/workspaces/[name].html +1 -1
  37. sky/dashboard/out/workspaces.html +1 -1
  38. sky/exceptions.py +5 -0
  39. sky/global_user_state.py +11 -6
  40. sky/jobs/server/core.py +9 -1
  41. sky/jobs/server/server.py +0 -95
  42. sky/jobs/utils.py +2 -1
  43. sky/models.py +18 -0
  44. sky/serve/server/core.py +1 -1
  45. sky/server/common.py +4 -2
  46. sky/server/constants.py +0 -2
  47. sky/server/requests/executor.py +10 -2
  48. sky/server/requests/requests.py +4 -3
  49. sky/server/server.py +22 -5
  50. sky/skylet/constants.py +3 -0
  51. sky/skylet/job_lib.py +2 -1
  52. sky/skypilot_config.py +9 -0
  53. sky/users/model.conf +1 -1
  54. sky/users/permission.py +148 -31
  55. sky/users/rbac.py +26 -0
  56. sky/users/server.py +14 -13
  57. sky/utils/common.py +6 -1
  58. sky/utils/common_utils.py +21 -3
  59. sky/utils/schemas.py +9 -0
  60. sky/workspaces/core.py +100 -8
  61. sky/workspaces/server.py +15 -2
  62. sky/workspaces/utils.py +56 -0
  63. {skypilot_nightly-1.0.0.dev20250607.dist-info → skypilot_nightly-1.0.0.dev20250609.dist-info}/METADATA +1 -1
  64. {skypilot_nightly-1.0.0.dev20250607.dist-info → skypilot_nightly-1.0.0.dev20250609.dist-info}/RECORD +72 -71
  65. sky/dashboard/out/_next/static/1qG0HTmVilJPxQdBk0fX5/_buildManifest.js +0 -1
  66. sky/dashboard/out/_next/static/chunks/470-ad1e0db3afcbd9c9.js +0 -1
  67. sky/dashboard/out/_next/static/chunks/843-c296541442d4af88.js +0 -11
  68. sky/dashboard/out/_next/static/chunks/pages/config-fe375a56342cf609.js +0 -6
  69. sky/dashboard/out/_next/static/chunks/pages/users-5800045bd04e69c2.js +0 -16
  70. sky/dashboard/out/_next/static/chunks/pages/workspaces-76b07aa5da91b0df.js +0 -1
  71. sky/dashboard/out/_next/static/css/667d941a2888ce6e.css +0 -3
  72. /sky/dashboard/out/_next/static/chunks/{856-3a32da4b84176f6d.js → 856-affc52adf5403a3a.js} +0 -0
  73. /sky/dashboard/out/_next/static/chunks/{973-6d78a0814682d771.js → 973-aed916d5b02d2d63.js} +0 -0
  74. /sky/dashboard/out/_next/static/chunks/pages/{_app-cb81dc4d27f4d009.js → _app-5f16aba5794ee8e7.js} +0 -0
  75. /sky/dashboard/out/_next/static/{1qG0HTmVilJPxQdBk0fX5 → xos0euNCptbGAM7_Q3Acl}/_ssgManifest.js +0 -0
  76. {skypilot_nightly-1.0.0.dev20250607.dist-info → skypilot_nightly-1.0.0.dev20250609.dist-info}/WHEEL +0 -0
  77. {skypilot_nightly-1.0.0.dev20250607.dist-info → skypilot_nightly-1.0.0.dev20250609.dist-info}/entry_points.txt +0 -0
  78. {skypilot_nightly-1.0.0.dev20250607.dist-info → skypilot_nightly-1.0.0.dev20250609.dist-info}/licenses/LICENSE +0 -0
  79. {skypilot_nightly-1.0.0.dev20250607.dist-info → skypilot_nightly-1.0.0.dev20250609.dist-info}/top_level.txt +0 -0
sky/users/rbac.py CHANGED
@@ -5,6 +5,8 @@ from typing import Dict, List
5
5
 
6
6
  from sky import sky_logging
7
7
  from sky import skypilot_config
8
+ from sky.skylet import constants
9
+ from sky.workspaces import utils as workspaces_utils
8
10
 
9
11
  logger = sky_logging.init_logger(__name__)
10
12
 
@@ -84,3 +86,27 @@ def get_role_permissions(
84
86
  }
85
87
  }
86
88
  return config_permissions
89
+
90
+
91
+ def get_workspace_policy_permissions() -> Dict[str, List[str]]:
92
+ """Get workspace policy permissions from config.
93
+
94
+ Returns:
95
+ A dictionary of workspace policy permissions.
96
+ Example:
97
+ {
98
+ 'workspace1': ['user1-id', 'user2-id'],
99
+ 'workspace2': ['user3-id', 'user4-id']
100
+ 'default': ['*']
101
+ }
102
+ """
103
+ current_workspaces = skypilot_config.get_nested(('workspaces',),
104
+ default_value={})
105
+ if constants.SKYPILOT_DEFAULT_WORKSPACE not in current_workspaces:
106
+ current_workspaces[constants.SKYPILOT_DEFAULT_WORKSPACE] = {}
107
+ workspaces_to_policy = {}
108
+ for workspace_name, workspace_config in current_workspaces.items():
109
+ users = workspaces_utils.get_workspace_users(workspace_config)
110
+ workspaces_to_policy[workspace_name] = users
111
+ logger.debug(f'Workspace policy permissions: {workspaces_to_policy}')
112
+ return workspaces_to_policy
sky/users/server.py CHANGED
@@ -1,6 +1,5 @@
1
1
  """REST API for workspace management."""
2
2
 
3
- import hashlib
4
3
  from typing import Any, Dict, List
5
4
 
6
5
  import fastapi
@@ -8,16 +7,15 @@ import fastapi
8
7
  from sky import global_user_state
9
8
  from sky import sky_logging
10
9
  from sky.server.requests import payloads
10
+ from sky.skylet import constants
11
11
  from sky.users import permission
12
12
  from sky.users import rbac
13
- from sky.utils import common_utils
13
+ from sky.utils import common
14
14
 
15
15
  logger = sky_logging.init_logger(__name__)
16
16
 
17
17
  router = fastapi.APIRouter()
18
18
 
19
- permission_service = permission.PermissionService()
20
-
21
19
 
22
20
  @router.get('')
23
21
  async def users() -> List[Dict[str, Any]]:
@@ -25,7 +23,7 @@ async def users() -> List[Dict[str, Any]]:
25
23
  all_users = []
26
24
  user_list = global_user_state.get_all_users()
27
25
  for user in user_list:
28
- user_roles = permission_service.get_user_roles(user.id)
26
+ user_roles = permission.permission_service.get_user_roles(user.id)
29
27
  all_users.append({
30
28
  'id': user.id,
31
29
  'name': user.name,
@@ -39,13 +37,11 @@ async def get_current_user_role(request: fastapi.Request):
39
37
  """Get current user's role."""
40
38
  # TODO(hailong): is there a reliable way to get the user
41
39
  # hash for the request without 'X-Auth-Request-Email' header?
42
- if 'X-Auth-Request-Email' not in request.headers:
40
+ auth_user = request.state.auth_user
41
+ if auth_user is None:
43
42
  return {'name': '', 'role': rbac.RoleName.ADMIN.value}
44
- user_name = request.headers['X-Auth-Request-Email']
45
- user_hash = hashlib.md5(
46
- user_name.encode()).hexdigest()[:common_utils.USER_HASH_LENGTH]
47
- user_roles = permission_service.get_user_roles(user_hash)
48
- return {'name': user_name, 'role': user_roles[0] if user_roles else ''}
43
+ user_roles = permission.permission_service.get_user_roles(auth_user.id)
44
+ return {'name': auth_user.name, 'role': user_roles[0] if user_roles else ''}
49
45
 
50
46
 
51
47
  @router.post('/update')
@@ -58,9 +54,14 @@ async def user_update(user_update_body: payloads.UserUpdateBody) -> None:
58
54
  raise fastapi.HTTPException(status_code=400,
59
55
  detail=f'Invalid role: {role}')
60
56
  user_info = global_user_state.get_user(user_id)
61
- if not user_info.name:
57
+ if user_info is None:
62
58
  raise fastapi.HTTPException(status_code=400,
63
59
  detail=f'User {user_id} does not exist')
60
+ # Disallow updating roles for the internal users.
61
+ if user_info.id in [common.SERVER_ID, constants.SKYPILOT_SYSTEM_USER_ID]:
62
+ raise fastapi.HTTPException(status_code=400,
63
+ detail=f'Cannot update role for internal '
64
+ f'API server user {user_info.name}')
64
65
 
65
66
  # Update user role in casbin policy
66
- permission_service.update_role(user_id, role)
67
+ permission.permission_service.update_role(user_info.id, role)
sky/utils/common.py CHANGED
@@ -5,6 +5,7 @@ import enum
5
5
  import os
6
6
  from typing import Generator
7
7
 
8
+ from sky import models
8
9
  from sky.skylet import constants
9
10
  from sky.utils import common_utils
10
11
 
@@ -25,10 +26,13 @@ JOB_CONTROLLER_NAME: str = f'{JOB_CONTROLLER_PREFIX}{SERVER_ID}'
25
26
 
26
27
 
27
28
  @contextlib.contextmanager
28
- def with_server_user_hash() -> Generator[None, None, None]:
29
+ def with_server_user() -> Generator[None, None, None]:
29
30
  """Temporarily set the user hash to common.SERVER_ID."""
30
31
  old_env_user_hash = os.getenv(constants.USER_ID_ENV_VAR)
32
+ # TODO(zhwu): once we have fully moved our code to use `get_current_user()`
33
+ # instead of `common_utils.get_user_hash()`, we can remove the env override.
31
34
  os.environ[constants.USER_ID_ENV_VAR] = SERVER_ID
35
+ common_utils.set_current_user(models.User.get_current_user())
32
36
  try:
33
37
  yield
34
38
  finally:
@@ -36,6 +40,7 @@ def with_server_user_hash() -> Generator[None, None, None]:
36
40
  os.environ[constants.USER_ID_ENV_VAR] = old_env_user_hash
37
41
  else:
38
42
  os.environ.pop(constants.USER_ID_ENV_VAR)
43
+ common_utils.set_current_user(models.User.get_current_user())
39
44
 
40
45
 
41
46
  class StatusRefreshMode(enum.Enum):
sky/utils/common_utils.py CHANGED
@@ -20,6 +20,7 @@ import uuid
20
20
  import jsonschema
21
21
 
22
22
  from sky import exceptions
23
+ from sky import models
23
24
  from sky import sky_logging
24
25
  from sky.adaptors import common as adaptors_common
25
26
  from sky.skylet import constants
@@ -256,11 +257,13 @@ class Backoff:
256
257
  _current_command: Optional[str] = None
257
258
  _current_client_entrypoint: Optional[str] = None
258
259
  _using_remote_api_server: Optional[bool] = None
260
+ _current_user: Optional['models.User'] = None
259
261
 
260
262
 
261
- def set_client_status(client_entrypoint: Optional[str],
262
- client_command: Optional[str],
263
- using_remote_api_server: bool):
263
+ def set_request_context(client_entrypoint: Optional[str],
264
+ client_command: Optional[str],
265
+ using_remote_api_server: bool,
266
+ user: Optional['models.User']):
264
267
  """Override the current client entrypoint and command.
265
268
 
266
269
  This is useful when we are on the SkyPilot API server side and we have a
@@ -269,9 +272,11 @@ def set_client_status(client_entrypoint: Optional[str],
269
272
  global _current_command
270
273
  global _current_client_entrypoint
271
274
  global _using_remote_api_server
275
+ global _current_user
272
276
  _current_command = client_command
273
277
  _current_client_entrypoint = client_entrypoint
274
278
  _using_remote_api_server = using_remote_api_server
279
+ _current_user = user
275
280
 
276
281
 
277
282
  def get_current_command() -> str:
@@ -286,6 +291,19 @@ def get_current_command() -> str:
286
291
  return get_pretty_entrypoint_cmd()
287
292
 
288
293
 
294
+ def get_current_user() -> 'models.User':
295
+ """Returns the current user."""
296
+ if _current_user is not None:
297
+ return _current_user
298
+ return models.User.get_current_user()
299
+
300
+
301
+ def set_current_user(user: 'models.User'):
302
+ """Sets the current user."""
303
+ global _current_user
304
+ _current_user = user
305
+
306
+
289
307
  def get_current_client_entrypoint(server_entrypoint: str) -> str:
290
308
  """Returns the current client entrypoint.
291
309
 
sky/utils/schemas.py CHANGED
@@ -1249,6 +1249,15 @@ def get_config_schema():
1249
1249
  'properties': {
1250
1250
  # Explicit definition for GCP allows both project_id and
1251
1251
  # disabled
1252
+ 'private': {
1253
+ 'type': 'boolean',
1254
+ },
1255
+ 'allowed_users': {
1256
+ 'type': 'array',
1257
+ 'items': {
1258
+ 'type': 'string',
1259
+ },
1260
+ },
1252
1261
  'gcp': {
1253
1262
  'type': 'object',
1254
1263
  'properties': {
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
- workspaces = skypilot_config.get_nested(('workspaces',), default_value={})
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=payloads.RequestBody(),
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=payloads.GetConfigBody(),
84
+ request_body=get_config_body,
72
85
  func=core.get_config,
73
86
  schedule_type=api_requests.ScheduleType.SHORT,
74
87
  )
@@ -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 ['*']
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: skypilot-nightly
3
- Version: 1.0.0.dev20250607
3
+ Version: 1.0.0.dev20250609
4
4
  Summary: SkyPilot: Run AI on Any Infra — Unified, Faster, Cheaper.
5
5
  Author: SkyPilot Team
6
6
  License: Apache 2.0