skypilot-nightly 1.0.0.dev20250627__py3-none-any.whl → 1.0.0.dev20250628__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/adaptors/kubernetes.py +7 -0
- sky/adaptors/nebius.py +2 -2
- sky/authentication.py +12 -5
- sky/backends/backend_utils.py +92 -26
- sky/check.py +5 -2
- sky/client/cli/command.py +38 -6
- sky/client/sdk.py +217 -167
- sky/client/service_account_auth.py +47 -0
- sky/clouds/aws.py +10 -4
- sky/clouds/azure.py +5 -2
- sky/clouds/cloud.py +5 -2
- sky/clouds/gcp.py +31 -18
- sky/clouds/kubernetes.py +54 -34
- sky/clouds/nebius.py +8 -2
- sky/clouds/ssh.py +5 -2
- sky/clouds/utils/aws_utils.py +10 -4
- sky/clouds/utils/gcp_utils.py +22 -7
- sky/clouds/utils/oci_utils.py +62 -14
- sky/dashboard/out/404.html +1 -1
- sky/dashboard/out/_next/static/{HudU4f4Xsy-cP51JvXSZ- → ZYLkkWSYZjJhLVsObh20y}/_buildManifest.js +1 -1
- sky/dashboard/out/_next/static/chunks/43-f38a531f6692f281.js +1 -0
- sky/dashboard/out/_next/static/chunks/601-111d06d9ded11d00.js +1 -0
- sky/dashboard/out/_next/static/chunks/{616-d6128fa9e7cae6e6.js → 616-50a620ac4a23deb4.js} +1 -1
- sky/dashboard/out/_next/static/chunks/691.fd9292250ab089af.js +21 -0
- sky/dashboard/out/_next/static/chunks/{785.dc2686c3c1235554.js → 785.3446c12ffdf3d188.js} +1 -1
- sky/dashboard/out/_next/static/chunks/871-e547295e7e21399c.js +6 -0
- sky/dashboard/out/_next/static/chunks/937.72796f7afe54075b.js +1 -0
- sky/dashboard/out/_next/static/chunks/938-0a770415b5ce4649.js +1 -0
- sky/dashboard/out/_next/static/chunks/982.d7bd80ed18cad4cc.js +1 -0
- sky/dashboard/out/_next/static/chunks/pages/clusters/[cluster]/[job]-21080826c6095f21.js +6 -0
- sky/dashboard/out/_next/static/chunks/pages/clusters/[cluster]-77d4816945b04793.js +6 -0
- sky/dashboard/out/_next/static/chunks/pages/{clusters-f119a5630a1efd61.js → clusters-65b2c90320b8afb8.js} +1 -1
- sky/dashboard/out/_next/static/chunks/pages/jobs/[job]-64bdc0b2d3a44709.js +16 -0
- sky/dashboard/out/_next/static/chunks/pages/{jobs-0a5695ff3075d94a.js → jobs-df7407b5e37d3750.js} +1 -1
- sky/dashboard/out/_next/static/chunks/pages/{users-4978cbb093e141e7.js → users-d7684eaa04c4f58f.js} +1 -1
- sky/dashboard/out/_next/static/chunks/pages/workspaces/{[name]-cb7e720b739de53a.js → [name]-04e1b3ad4207b1e9.js} +1 -1
- sky/dashboard/out/_next/static/chunks/pages/{workspaces-50e230828730cfb3.js → workspaces-c470366a6179f16e.js} +1 -1
- sky/dashboard/out/_next/static/chunks/{webpack-08fdb9e6070127fc.js → webpack-75a3310ef922a299.js} +1 -1
- sky/dashboard/out/_next/static/css/605ac87514049058.css +3 -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/volumes.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/data/storage.py +8 -3
- sky/global_user_state.py +257 -9
- sky/jobs/client/sdk.py +20 -25
- sky/models.py +16 -0
- sky/provision/kubernetes/config.py +1 -1
- sky/provision/kubernetes/instance.py +7 -4
- sky/provision/kubernetes/network.py +15 -9
- sky/provision/kubernetes/network_utils.py +42 -23
- sky/provision/kubernetes/utils.py +73 -35
- sky/provision/nebius/utils.py +10 -4
- sky/resources.py +10 -4
- sky/serve/client/sdk.py +28 -34
- sky/server/common.py +51 -3
- sky/server/constants.py +3 -0
- sky/server/requests/executor.py +4 -0
- sky/server/requests/payloads.py +33 -0
- sky/server/requests/requests.py +19 -0
- sky/server/rest.py +6 -15
- sky/server/server.py +121 -6
- sky/skylet/constants.py +6 -0
- sky/skypilot_config.py +32 -4
- sky/users/permission.py +29 -0
- sky/users/server.py +384 -5
- sky/users/token_service.py +196 -0
- sky/utils/common_utils.py +4 -5
- sky/utils/config_utils.py +41 -0
- sky/utils/controller_utils.py +5 -1
- sky/utils/resource_checker.py +153 -0
- sky/utils/resources_utils.py +12 -4
- sky/utils/schemas.py +87 -60
- sky/utils/subprocess_utils.py +2 -6
- sky/workspaces/core.py +9 -117
- {skypilot_nightly-1.0.0.dev20250627.dist-info → skypilot_nightly-1.0.0.dev20250628.dist-info}/METADATA +1 -1
- {skypilot_nightly-1.0.0.dev20250627.dist-info → skypilot_nightly-1.0.0.dev20250628.dist-info}/RECORD +94 -91
- sky/dashboard/out/_next/static/chunks/43-36177d00f6956ab2.js +0 -1
- sky/dashboard/out/_next/static/chunks/690.55f9eed3be903f56.js +0 -16
- sky/dashboard/out/_next/static/chunks/871-3db673be3ee3750b.js +0 -6
- sky/dashboard/out/_next/static/chunks/937.3759f538f11a0953.js +0 -1
- sky/dashboard/out/_next/static/chunks/938-068520cc11738deb.js +0 -1
- sky/dashboard/out/_next/static/chunks/973-81b2d057178adb76.js +0 -1
- sky/dashboard/out/_next/static/chunks/982.1b61658204416b0f.js +0 -1
- sky/dashboard/out/_next/static/chunks/pages/clusters/[cluster]/[job]-aff040d7bc5d0086.js +0 -6
- sky/dashboard/out/_next/static/chunks/pages/clusters/[cluster]-8040f2483897ed0c.js +0 -6
- sky/dashboard/out/_next/static/chunks/pages/jobs/[job]-e4b23128db0774cd.js +0 -16
- sky/dashboard/out/_next/static/css/52082cf558ec9705.css +0 -3
- /sky/dashboard/out/_next/static/{HudU4f4Xsy-cP51JvXSZ- → ZYLkkWSYZjJhLVsObh20y}/_ssgManifest.js +0 -0
- /sky/dashboard/out/_next/static/chunks/pages/{_app-9a3ce3170d2edcec.js → _app-050a9e637b057b24.js} +0 -0
- {skypilot_nightly-1.0.0.dev20250627.dist-info → skypilot_nightly-1.0.0.dev20250628.dist-info}/WHEEL +0 -0
- {skypilot_nightly-1.0.0.dev20250627.dist-info → skypilot_nightly-1.0.0.dev20250628.dist-info}/entry_points.txt +0 -0
- {skypilot_nightly-1.0.0.dev20250627.dist-info → skypilot_nightly-1.0.0.dev20250628.dist-info}/licenses/LICENSE +0 -0
- {skypilot_nightly-1.0.0.dev20250627.dist-info → skypilot_nightly-1.0.0.dev20250628.dist-info}/top_level.txt +0 -0
sky/server/requests/requests.py
CHANGED
@@ -375,10 +375,29 @@ def managed_job_status_refresh_event():
|
|
375
375
|
|
376
376
|
@dataclasses.dataclass
|
377
377
|
class InternalRequestDaemon:
|
378
|
+
"""Internal daemon that runs an event in the background."""
|
379
|
+
|
378
380
|
id: str
|
379
381
|
name: str
|
380
382
|
event_fn: Callable[[], None]
|
381
383
|
|
384
|
+
def run_event(self):
|
385
|
+
"""Run the event."""
|
386
|
+
while True:
|
387
|
+
with ux_utils.enable_traceback():
|
388
|
+
try:
|
389
|
+
self.event_fn()
|
390
|
+
break
|
391
|
+
except Exception: # pylint: disable=broad-except
|
392
|
+
# It is OK to fail to run the event, as the event is not
|
393
|
+
# critical, but we should log the error.
|
394
|
+
logger.exception(
|
395
|
+
f'Error running {self.name} event. '
|
396
|
+
f'Restarting in '
|
397
|
+
f'{server_constants.DAEMON_RESTART_INTERVAL_SECONDS} '
|
398
|
+
'seconds...')
|
399
|
+
time.sleep(server_constants.DAEMON_RESTART_INTERVAL_SECONDS)
|
400
|
+
|
382
401
|
|
383
402
|
# Register the events to run in the background.
|
384
403
|
INTERNAL_REQUEST_DAEMONS = [
|
sky/server/rest.py
CHANGED
@@ -129,25 +129,16 @@ def handle_server_unavailable(response: 'requests.Response') -> None:
|
|
129
129
|
|
130
130
|
|
131
131
|
@retry_on_server_unavailable()
|
132
|
-
def
|
133
|
-
"""Send a
|
132
|
+
def request(method, url, **kwargs) -> 'requests.Response':
|
133
|
+
"""Send a request to the API server, retry on server temporarily
|
134
134
|
unavailable."""
|
135
|
-
response = requests.
|
135
|
+
response = requests.request(method, url, **kwargs)
|
136
136
|
handle_server_unavailable(response)
|
137
137
|
return response
|
138
138
|
|
139
139
|
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
unavailable."""
|
144
|
-
response = requests.get(url, params=params, **kwargs)
|
145
|
-
handle_server_unavailable(response)
|
146
|
-
return response
|
147
|
-
|
148
|
-
|
149
|
-
def get_without_retry(url, params=None, **kwargs) -> 'requests.Response':
|
150
|
-
"""Send a GET request to the API server without retry."""
|
151
|
-
response = requests.get(url, params=params, **kwargs)
|
140
|
+
def request_without_retry(method, url, **kwargs) -> 'requests.Response':
|
141
|
+
"""Send a request to the API server without retry."""
|
142
|
+
response = requests.request(method, url, **kwargs)
|
152
143
|
handle_server_unavailable(response)
|
153
144
|
return response
|
sky/server/server.py
CHANGED
@@ -119,8 +119,11 @@ def _basic_auth_401_response(content: str):
|
|
119
119
|
# TODO(hailong): Remove this function and use request.state.auth_user instead.
|
120
120
|
async def _override_user_info_in_request_body(request: fastapi.Request,
|
121
121
|
auth_user: Optional[models.User]):
|
122
|
+
if auth_user is None:
|
123
|
+
return
|
124
|
+
|
122
125
|
body = await request.body()
|
123
|
-
if
|
126
|
+
if body:
|
124
127
|
try:
|
125
128
|
original_json = await request.json()
|
126
129
|
except json.JSONDecodeError as e:
|
@@ -228,14 +231,17 @@ class BasicAuthMiddleware(starlette.middleware.base.BaseHTTPMiddleware):
|
|
228
231
|
|
229
232
|
async def dispatch(self, request: fastapi.Request, call_next):
|
230
233
|
if request.url.path.startswith('/api/'):
|
231
|
-
# Try to set the auth user from
|
232
|
-
# following endpoint handlers can leverage the auth_user info
|
234
|
+
# Try to set the auth user from basic auth
|
233
235
|
_try_set_basic_auth_user(request)
|
234
236
|
return await call_next(request)
|
235
237
|
|
236
238
|
auth_header = request.headers.get('authorization')
|
237
|
-
if not auth_header
|
238
|
-
return _basic_auth_401_response('
|
239
|
+
if not auth_header:
|
240
|
+
return _basic_auth_401_response('Authentication required')
|
241
|
+
|
242
|
+
# Only handle basic auth
|
243
|
+
if not auth_header.lower().startswith('basic '):
|
244
|
+
return _basic_auth_401_response('Invalid authentication method')
|
239
245
|
|
240
246
|
# Check username and password
|
241
247
|
encoded = auth_header.split(' ', 1)[1]
|
@@ -267,6 +273,111 @@ class BasicAuthMiddleware(starlette.middleware.base.BaseHTTPMiddleware):
|
|
267
273
|
return await call_next(request)
|
268
274
|
|
269
275
|
|
276
|
+
class BearerTokenMiddleware(starlette.middleware.base.BaseHTTPMiddleware):
|
277
|
+
"""Middleware to handle Bearer Token Auth (Service Accounts)."""
|
278
|
+
|
279
|
+
async def dispatch(self, request: fastapi.Request, call_next):
|
280
|
+
# Only process requests with Bearer token authorization header
|
281
|
+
auth_header = request.headers.get('authorization')
|
282
|
+
if not auth_header or not auth_header.lower().startswith('bearer '):
|
283
|
+
# No Bearer token, continue with normal processing (OAuth2 cookies,
|
284
|
+
# etc.)
|
285
|
+
return await call_next(request)
|
286
|
+
|
287
|
+
# Extract token
|
288
|
+
sa_token = auth_header.split(' ', 1)[1]
|
289
|
+
|
290
|
+
# Handle SkyPilot service account tokens
|
291
|
+
if sa_token.startswith('sky_'):
|
292
|
+
return await self._handle_service_account_token(
|
293
|
+
request, sa_token, call_next)
|
294
|
+
|
295
|
+
# Handle other Bearer tokens (OAuth2 access tokens, etc.)
|
296
|
+
# These requests bypassed OAuth2 proxy, so let the application decide
|
297
|
+
# how to handle them
|
298
|
+
# For now, we'll let them continue through normal processing
|
299
|
+
logger.debug(
|
300
|
+
'Non-SkyPilot Bearer token detected, continuing with normal '
|
301
|
+
'processing')
|
302
|
+
return await call_next(request)
|
303
|
+
|
304
|
+
async def _handle_service_account_token(self, request: fastapi.Request,
|
305
|
+
sa_token: str, call_next):
|
306
|
+
"""Handle SkyPilot service account tokens."""
|
307
|
+
# Check if service account tokens are enabled
|
308
|
+
sa_enabled = os.environ.get(constants.ENV_VAR_ENABLE_SERVICE_ACCOUNTS,
|
309
|
+
'false').lower()
|
310
|
+
if sa_enabled != 'true':
|
311
|
+
return fastapi.responses.JSONResponse(
|
312
|
+
status_code=401,
|
313
|
+
content={'detail': 'Service account authentication disabled'})
|
314
|
+
|
315
|
+
try:
|
316
|
+
# Import here to avoid circular imports
|
317
|
+
# pylint: disable=import-outside-toplevel
|
318
|
+
from sky.users.token_service import token_service
|
319
|
+
|
320
|
+
# Verify and decode JWT token
|
321
|
+
payload = token_service.verify_token(sa_token)
|
322
|
+
|
323
|
+
if payload is None:
|
324
|
+
logger.warning('Service account token verification failed')
|
325
|
+
return fastapi.responses.JSONResponse(
|
326
|
+
status_code=401,
|
327
|
+
content={
|
328
|
+
'detail': 'Invalid or expired service account token'
|
329
|
+
})
|
330
|
+
|
331
|
+
# Extract user information from JWT payload
|
332
|
+
user_id = payload.get('sub')
|
333
|
+
user_name = payload.get('name')
|
334
|
+
token_id = payload.get('token_id')
|
335
|
+
|
336
|
+
if not user_id or not token_id:
|
337
|
+
logger.warning(
|
338
|
+
'Invalid token payload: missing user_id or token_id')
|
339
|
+
return fastapi.responses.JSONResponse(
|
340
|
+
status_code=401,
|
341
|
+
content={'detail': 'Invalid token payload'})
|
342
|
+
|
343
|
+
# Verify user still exists in database
|
344
|
+
user_info = global_user_state.get_user(user_id)
|
345
|
+
if user_info is None:
|
346
|
+
logger.warning(
|
347
|
+
f'Service account user {user_id} no longer exists')
|
348
|
+
return fastapi.responses.JSONResponse(
|
349
|
+
status_code=401,
|
350
|
+
content={'detail': 'Service account user no longer exists'})
|
351
|
+
|
352
|
+
# Update last used timestamp for token tracking
|
353
|
+
try:
|
354
|
+
global_user_state.update_service_account_token_last_used(
|
355
|
+
token_id)
|
356
|
+
except Exception as e: # pylint: disable=broad-except
|
357
|
+
logger.debug(f'Failed to update token last used time: {e}')
|
358
|
+
|
359
|
+
# Set the authenticated user
|
360
|
+
auth_user = models.User(id=user_id,
|
361
|
+
name=user_name or user_info.name)
|
362
|
+
request.state.auth_user = auth_user
|
363
|
+
|
364
|
+
# Override user info in request body for service account requests
|
365
|
+
await _override_user_info_in_request_body(request, auth_user)
|
366
|
+
|
367
|
+
logger.debug(f'Authenticated service account: {user_id}')
|
368
|
+
|
369
|
+
except Exception as e: # pylint: disable=broad-except
|
370
|
+
logger.error(f'Service account authentication failed: {e}',
|
371
|
+
exc_info=True)
|
372
|
+
return fastapi.responses.JSONResponse(
|
373
|
+
status_code=401,
|
374
|
+
content={
|
375
|
+
'detail': f'Service account authentication failed: {str(e)}'
|
376
|
+
})
|
377
|
+
|
378
|
+
return await call_next(request)
|
379
|
+
|
380
|
+
|
270
381
|
class AuthProxyMiddleware(starlette.middleware.base.BaseHTTPMiddleware):
|
271
382
|
"""Middleware to handle auth proxy."""
|
272
383
|
|
@@ -330,7 +441,7 @@ async def lifespan(app: fastapi.FastAPI): # pylint: disable=redefined-outer-nam
|
|
330
441
|
request_id=event.id,
|
331
442
|
request_name=event.name,
|
332
443
|
request_body=payloads.RequestBody(),
|
333
|
-
func=event.
|
444
|
+
func=event.run_event,
|
334
445
|
schedule_type=requests_lib.ScheduleType.SHORT,
|
335
446
|
is_skypilot_system=True,
|
336
447
|
)
|
@@ -424,6 +535,9 @@ app.add_middleware(
|
|
424
535
|
enable_basic_auth = os.environ.get(constants.ENV_VAR_ENABLE_BASIC_AUTH, 'false')
|
425
536
|
if str(enable_basic_auth).lower() == 'true':
|
426
537
|
app.add_middleware(BasicAuthMiddleware)
|
538
|
+
# Bearer token middleware should always be present to handle service account
|
539
|
+
# authentication
|
540
|
+
app.add_middleware(BearerTokenMiddleware)
|
427
541
|
app.add_middleware(AuthProxyMiddleware)
|
428
542
|
app.add_middleware(RequestIDMiddleware)
|
429
543
|
app.include_router(jobs_rest.router, prefix='/jobs', tags=['jobs'])
|
@@ -1339,6 +1453,7 @@ async def health(request: fastapi.Request) -> Dict[str, Any]:
|
|
1339
1453
|
- commit: str; The commit hash of SkyPilot used for API server.
|
1340
1454
|
"""
|
1341
1455
|
user = request.state.auth_user
|
1456
|
+
logger.info(f'Health endpoint: request.state.auth_user = {user}')
|
1342
1457
|
return {
|
1343
1458
|
'status': common.ApiServerStatus.HEALTHY.value,
|
1344
1459
|
'api_version': server_constants.API_VERSION,
|
sky/skylet/constants.py
CHANGED
@@ -346,6 +346,11 @@ API_SERVER_CREATION_LOCK_PATH = '~/.sky/api_server/.creation.lock'
|
|
346
346
|
# API server.
|
347
347
|
SKY_API_SERVER_URL_ENV_VAR = f'{SKYPILOT_ENV_VAR_PREFIX}API_SERVER_ENDPOINT'
|
348
348
|
|
349
|
+
# The name for the environment variable that stores the SkyPilot service
|
350
|
+
# account token on client side.
|
351
|
+
SERVICE_ACCOUNT_TOKEN_ENV_VAR = (
|
352
|
+
f'{SKYPILOT_ENV_VAR_PREFIX}SERVICE_ACCOUNT_TOKEN')
|
353
|
+
|
349
354
|
# SkyPilot environment variables
|
350
355
|
SKYPILOT_NUM_NODES = f'{SKYPILOT_ENV_VAR_PREFIX}NUM_NODES'
|
351
356
|
SKYPILOT_NODE_IPS = f'{SKYPILOT_ENV_VAR_PREFIX}NODE_IPS'
|
@@ -424,6 +429,7 @@ ENV_VAR_DB_CONNECTION_URI = (f'{SKYPILOT_ENV_VAR_PREFIX}DB_CONNECTION_URI')
|
|
424
429
|
# authentication is enabled in the API server.
|
425
430
|
ENV_VAR_ENABLE_BASIC_AUTH = 'ENABLE_BASIC_AUTH'
|
426
431
|
SKYPILOT_INITIAL_BASIC_AUTH = 'SKYPILOT_INITIAL_BASIC_AUTH'
|
432
|
+
ENV_VAR_ENABLE_SERVICE_ACCOUNTS = 'ENABLE_SERVICE_ACCOUNTS'
|
427
433
|
|
428
434
|
SKYPILOT_DEFAULT_WORKSPACE = 'default'
|
429
435
|
|
sky/skypilot_config.py
CHANGED
@@ -369,6 +369,34 @@ def get_nested(keys: Tuple[str, ...],
|
|
369
369
|
disallowed_override_keys=None)
|
370
370
|
|
371
371
|
|
372
|
+
def get_effective_region_config(
|
373
|
+
cloud: str,
|
374
|
+
keys: Tuple[str, ...],
|
375
|
+
region: Optional[str] = None,
|
376
|
+
default_value: Optional[Any] = None,
|
377
|
+
override_configs: Optional[Dict[str, Any]] = None) -> Any:
|
378
|
+
"""Returns the nested key value by reading from config
|
379
|
+
Order to get the property_name value:
|
380
|
+
1. if region is specified,
|
381
|
+
try to get the value from <cloud>/<region_key>/<region>/keys
|
382
|
+
2. if no region or no override,
|
383
|
+
try to get it at the cloud level <cloud>/keys
|
384
|
+
3. if not found at cloud level,
|
385
|
+
return either default_value if specified or None
|
386
|
+
|
387
|
+
Note: This function currently only supports getting region-specific
|
388
|
+
config from "kubernetes" cloud. For other clouds, this function behaves
|
389
|
+
identically to get_nested().
|
390
|
+
"""
|
391
|
+
return config_utils.get_cloud_config_value_from_dict(
|
392
|
+
dict_config=_get_loaded_config(),
|
393
|
+
cloud=cloud,
|
394
|
+
keys=keys,
|
395
|
+
region=region,
|
396
|
+
default_value=default_value,
|
397
|
+
override_configs=override_configs)
|
398
|
+
|
399
|
+
|
372
400
|
def get_workspace_cloud(cloud: str,
|
373
401
|
workspace: Optional[str] = None) -> config_utils.Config:
|
374
402
|
"""Returns the workspace config."""
|
@@ -477,10 +505,10 @@ def overlay_skypilot_config(
|
|
477
505
|
def safe_reload_config() -> None:
|
478
506
|
"""Reloads the config, safe to be called concurrently."""
|
479
507
|
with filelock.FileLock(get_skypilot_config_lock_path()):
|
480
|
-
|
508
|
+
reload_config()
|
481
509
|
|
482
510
|
|
483
|
-
def
|
511
|
+
def reload_config() -> None:
|
484
512
|
internal_config_path = os.environ.get(ENV_VAR_SKYPILOT_CONFIG)
|
485
513
|
if internal_config_path is not None:
|
486
514
|
# {ENV_VAR_SKYPILOT_CONFIG} is used internally.
|
@@ -641,7 +669,7 @@ def loaded_config_path_serialized() -> Optional[str]:
|
|
641
669
|
|
642
670
|
|
643
671
|
# Load on import, synchronization is guaranteed by python interpreter.
|
644
|
-
|
672
|
+
reload_config()
|
645
673
|
|
646
674
|
|
647
675
|
def loaded() -> bool:
|
@@ -864,4 +892,4 @@ def update_api_server_config_no_lock(config: config_utils.Config) -> None:
|
|
864
892
|
config_map_utils.patch_configmap_with_config(
|
865
893
|
config, global_config_path)
|
866
894
|
|
867
|
-
|
895
|
+
reload_config()
|
sky/users/permission.py
CHANGED
@@ -265,6 +265,35 @@ class PermissionService:
|
|
265
265
|
f'workspace={workspace_name}, result={result}')
|
266
266
|
return result
|
267
267
|
|
268
|
+
def check_service_account_token_permission(self, user_id: str,
|
269
|
+
token_owner_id: str,
|
270
|
+
action: str) -> bool:
|
271
|
+
"""Check service account token permission.
|
272
|
+
|
273
|
+
This method checks if a user has permission to perform an action on
|
274
|
+
a service account token owned by another user.
|
275
|
+
|
276
|
+
Args:
|
277
|
+
user_id: The ID of the user requesting the action
|
278
|
+
token_owner_id: The ID of the user who owns the token
|
279
|
+
action: The action being performed (e.g., 'delete', 'view')
|
280
|
+
|
281
|
+
Returns:
|
282
|
+
True if the user has permission, False otherwise
|
283
|
+
"""
|
284
|
+
del action
|
285
|
+
# Users can always manage their own tokens
|
286
|
+
if user_id == token_owner_id:
|
287
|
+
return True
|
288
|
+
|
289
|
+
# Check if user has admin role (admins can manage any token)
|
290
|
+
user_roles = self.get_user_roles(user_id)
|
291
|
+
if rbac.RoleName.ADMIN.value in user_roles:
|
292
|
+
return True
|
293
|
+
|
294
|
+
# Regular users cannot manage tokens owned by others
|
295
|
+
return False
|
296
|
+
|
268
297
|
def add_workspace_policy(self, workspace_name: str,
|
269
298
|
users: List[str]) -> None:
|
270
299
|
"""Add workspace policy.
|