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.
- sky/__init__.py +2 -2
- sky/backends/backend_utils.py +18 -2
- sky/check.py +4 -3
- sky/cli.py +5 -6
- sky/client/cli.py +5 -6
- sky/core.py +3 -2
- sky/dashboard/out/404.html +1 -1
- 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/pages/clusters/[cluster]/{[job]-18aed9b56247d074.js → [job]-d31688d3e52736dd.js} +1 -1
- sky/dashboard/out/_next/static/chunks/pages/clusters/{[cluster]-b919a73aecdfa78f.js → [cluster]-e7d8710a9b0491e5.js} +1 -1
- sky/dashboard/out/_next/static/chunks/pages/{clusters-4f6b9dd9abcb33ad.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]-3a18d0eeb5119fe4.js → [context]-46d2e4ad6c487260.js} +1 -1
- sky/dashboard/out/_next/static/chunks/pages/{infra-a1a6abeeb58c1051.js → infra-7013d816a2a0e76c.js} +1 -1
- sky/dashboard/out/_next/static/chunks/pages/jobs/{[job]-1354e28c81eeb686.js → [job]-f7f0c9e156d328bc.js} +1 -1
- sky/dashboard/out/_next/static/chunks/pages/{jobs-23bfc8bf373423db.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-e1f9c0c3ff7ac4bd.js → new-9a749cca1813bd27.js} +1 -1
- sky/dashboard/out/_next/static/chunks/pages/workspaces/{[name]-686590e0ee4b2412.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/server/core.py +9 -1
- sky/jobs/server/server.py +0 -95
- sky/jobs/utils.py +2 -1
- sky/models.py +18 -0
- 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 +3 -0
- sky/skylet/job_lib.py +2 -1
- sky/skypilot_config.py +9 -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/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.dev20250607.dist-info → skypilot_nightly-1.0.0.dev20250609.dist-info}/METADATA +1 -1
- {skypilot_nightly-1.0.0.dev20250607.dist-info → skypilot_nightly-1.0.0.dev20250609.dist-info}/RECORD +72 -71
- sky/dashboard/out/_next/static/1qG0HTmVilJPxQdBk0fX5/_buildManifest.js +0 -1
- sky/dashboard/out/_next/static/chunks/470-ad1e0db3afcbd9c9.js +0 -1
- sky/dashboard/out/_next/static/chunks/843-c296541442d4af88.js +0 -11
- sky/dashboard/out/_next/static/chunks/pages/config-fe375a56342cf609.js +0 -6
- sky/dashboard/out/_next/static/chunks/pages/users-5800045bd04e69c2.js +0 -16
- sky/dashboard/out/_next/static/chunks/pages/workspaces-76b07aa5da91b0df.js +0 -1
- sky/dashboard/out/_next/static/css/667d941a2888ce6e.css +0 -3
- /sky/dashboard/out/_next/static/chunks/{856-3a32da4b84176f6d.js → 856-affc52adf5403a3a.js} +0 -0
- /sky/dashboard/out/_next/static/chunks/{973-6d78a0814682d771.js → 973-aed916d5b02d2d63.js} +0 -0
- /sky/dashboard/out/_next/static/chunks/pages/{_app-cb81dc4d27f4d009.js → _app-5f16aba5794ee8e7.js} +0 -0
- /sky/dashboard/out/_next/static/{1qG0HTmVilJPxQdBk0fX5 → xos0euNCptbGAM7_Q3Acl}/_ssgManifest.js +0 -0
- {skypilot_nightly-1.0.0.dev20250607.dist-info → skypilot_nightly-1.0.0.dev20250609.dist-info}/WHEEL +0 -0
- {skypilot_nightly-1.0.0.dev20250607.dist-info → skypilot_nightly-1.0.0.dev20250609.dist-info}/entry_points.txt +0 -0
- {skypilot_nightly-1.0.0.dev20250607.dist-info → skypilot_nightly-1.0.0.dev20250609.dist-info}/licenses/LICENSE +0 -0
- {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
|
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
|
-
|
40
|
+
auth_user = request.state.auth_user
|
41
|
+
if auth_user is None:
|
43
42
|
return {'name': '', 'role': rbac.RoleName.ADMIN.value}
|
44
|
-
|
45
|
-
|
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
|
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(
|
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
|
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
|
262
|
-
|
263
|
-
|
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
|
-
|
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 ['*']
|