skypilot-nightly 1.0.0.dev20250626__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.
Files changed (106) hide show
  1. sky/__init__.py +2 -2
  2. sky/adaptors/kubernetes.py +7 -0
  3. sky/adaptors/nebius.py +2 -2
  4. sky/admin_policy.py +27 -17
  5. sky/authentication.py +12 -5
  6. sky/backends/backend_utils.py +92 -26
  7. sky/check.py +5 -2
  8. sky/client/cli/command.py +38 -6
  9. sky/client/sdk.py +217 -167
  10. sky/client/service_account_auth.py +47 -0
  11. sky/clouds/aws.py +10 -4
  12. sky/clouds/azure.py +5 -2
  13. sky/clouds/cloud.py +5 -2
  14. sky/clouds/gcp.py +31 -18
  15. sky/clouds/kubernetes.py +54 -34
  16. sky/clouds/nebius.py +8 -2
  17. sky/clouds/ssh.py +5 -2
  18. sky/clouds/utils/aws_utils.py +10 -4
  19. sky/clouds/utils/gcp_utils.py +22 -7
  20. sky/clouds/utils/oci_utils.py +62 -14
  21. sky/dashboard/out/404.html +1 -1
  22. sky/dashboard/out/_next/static/{bs6UB9V4Jq10TIZ5x-kBK → ZYLkkWSYZjJhLVsObh20y}/_buildManifest.js +1 -1
  23. sky/dashboard/out/_next/static/chunks/43-f38a531f6692f281.js +1 -0
  24. sky/dashboard/out/_next/static/chunks/601-111d06d9ded11d00.js +1 -0
  25. sky/dashboard/out/_next/static/chunks/{616-d6128fa9e7cae6e6.js → 616-50a620ac4a23deb4.js} +1 -1
  26. sky/dashboard/out/_next/static/chunks/691.fd9292250ab089af.js +21 -0
  27. sky/dashboard/out/_next/static/chunks/{785.dc2686c3c1235554.js → 785.3446c12ffdf3d188.js} +1 -1
  28. sky/dashboard/out/_next/static/chunks/871-e547295e7e21399c.js +6 -0
  29. sky/dashboard/out/_next/static/chunks/937.72796f7afe54075b.js +1 -0
  30. sky/dashboard/out/_next/static/chunks/938-0a770415b5ce4649.js +1 -0
  31. sky/dashboard/out/_next/static/chunks/982.d7bd80ed18cad4cc.js +1 -0
  32. sky/dashboard/out/_next/static/chunks/pages/clusters/[cluster]/[job]-21080826c6095f21.js +6 -0
  33. sky/dashboard/out/_next/static/chunks/pages/clusters/[cluster]-77d4816945b04793.js +6 -0
  34. sky/dashboard/out/_next/static/chunks/pages/{clusters-f119a5630a1efd61.js → clusters-65b2c90320b8afb8.js} +1 -1
  35. sky/dashboard/out/_next/static/chunks/pages/jobs/[job]-64bdc0b2d3a44709.js +16 -0
  36. sky/dashboard/out/_next/static/chunks/pages/{jobs-0a5695ff3075d94a.js → jobs-df7407b5e37d3750.js} +1 -1
  37. sky/dashboard/out/_next/static/chunks/pages/{users-4978cbb093e141e7.js → users-d7684eaa04c4f58f.js} +1 -1
  38. sky/dashboard/out/_next/static/chunks/pages/workspaces/{[name]-cb7e720b739de53a.js → [name]-04e1b3ad4207b1e9.js} +1 -1
  39. sky/dashboard/out/_next/static/chunks/pages/{workspaces-50e230828730cfb3.js → workspaces-c470366a6179f16e.js} +1 -1
  40. sky/dashboard/out/_next/static/chunks/{webpack-08fdb9e6070127fc.js → webpack-75a3310ef922a299.js} +1 -1
  41. sky/dashboard/out/_next/static/css/605ac87514049058.css +3 -0
  42. sky/dashboard/out/clusters/[cluster]/[job].html +1 -1
  43. sky/dashboard/out/clusters/[cluster].html +1 -1
  44. sky/dashboard/out/clusters.html +1 -1
  45. sky/dashboard/out/config.html +1 -1
  46. sky/dashboard/out/index.html +1 -1
  47. sky/dashboard/out/infra/[context].html +1 -1
  48. sky/dashboard/out/infra.html +1 -1
  49. sky/dashboard/out/jobs/[job].html +1 -1
  50. sky/dashboard/out/jobs.html +1 -1
  51. sky/dashboard/out/users.html +1 -1
  52. sky/dashboard/out/volumes.html +1 -1
  53. sky/dashboard/out/workspace/new.html +1 -1
  54. sky/dashboard/out/workspaces/[name].html +1 -1
  55. sky/dashboard/out/workspaces.html +1 -1
  56. sky/data/storage.py +8 -3
  57. sky/global_user_state.py +257 -9
  58. sky/jobs/client/sdk.py +20 -25
  59. sky/models.py +16 -0
  60. sky/provision/kubernetes/config.py +1 -1
  61. sky/provision/kubernetes/instance.py +7 -4
  62. sky/provision/kubernetes/network.py +15 -9
  63. sky/provision/kubernetes/network_utils.py +42 -23
  64. sky/provision/kubernetes/utils.py +73 -35
  65. sky/provision/nebius/utils.py +10 -4
  66. sky/resources.py +10 -4
  67. sky/serve/client/sdk.py +28 -34
  68. sky/server/common.py +51 -3
  69. sky/server/constants.py +3 -0
  70. sky/server/requests/executor.py +4 -0
  71. sky/server/requests/payloads.py +33 -0
  72. sky/server/requests/requests.py +19 -0
  73. sky/server/rest.py +6 -15
  74. sky/server/server.py +121 -6
  75. sky/skylet/constants.py +6 -0
  76. sky/skypilot_config.py +32 -4
  77. sky/users/permission.py +29 -0
  78. sky/users/server.py +384 -5
  79. sky/users/token_service.py +196 -0
  80. sky/utils/common_utils.py +4 -5
  81. sky/utils/config_utils.py +41 -0
  82. sky/utils/controller_utils.py +5 -1
  83. sky/utils/resource_checker.py +153 -0
  84. sky/utils/resources_utils.py +12 -4
  85. sky/utils/schemas.py +87 -60
  86. sky/utils/subprocess_utils.py +2 -6
  87. sky/workspaces/core.py +9 -117
  88. {skypilot_nightly-1.0.0.dev20250626.dist-info → skypilot_nightly-1.0.0.dev20250628.dist-info}/METADATA +1 -1
  89. {skypilot_nightly-1.0.0.dev20250626.dist-info → skypilot_nightly-1.0.0.dev20250628.dist-info}/RECORD +95 -92
  90. sky/dashboard/out/_next/static/chunks/43-36177d00f6956ab2.js +0 -1
  91. sky/dashboard/out/_next/static/chunks/690.55f9eed3be903f56.js +0 -16
  92. sky/dashboard/out/_next/static/chunks/871-3db673be3ee3750b.js +0 -6
  93. sky/dashboard/out/_next/static/chunks/937.3759f538f11a0953.js +0 -1
  94. sky/dashboard/out/_next/static/chunks/938-068520cc11738deb.js +0 -1
  95. sky/dashboard/out/_next/static/chunks/973-81b2d057178adb76.js +0 -1
  96. sky/dashboard/out/_next/static/chunks/982.1b61658204416b0f.js +0 -1
  97. sky/dashboard/out/_next/static/chunks/pages/clusters/[cluster]/[job]-aff040d7bc5d0086.js +0 -6
  98. sky/dashboard/out/_next/static/chunks/pages/clusters/[cluster]-8040f2483897ed0c.js +0 -6
  99. sky/dashboard/out/_next/static/chunks/pages/jobs/[job]-e4b23128db0774cd.js +0 -16
  100. sky/dashboard/out/_next/static/css/52082cf558ec9705.css +0 -3
  101. /sky/dashboard/out/_next/static/{bs6UB9V4Jq10TIZ5x-kBK → ZYLkkWSYZjJhLVsObh20y}/_ssgManifest.js +0 -0
  102. /sky/dashboard/out/_next/static/chunks/pages/{_app-9a3ce3170d2edcec.js → _app-050a9e637b057b24.js} +0 -0
  103. {skypilot_nightly-1.0.0.dev20250626.dist-info → skypilot_nightly-1.0.0.dev20250628.dist-info}/WHEEL +0 -0
  104. {skypilot_nightly-1.0.0.dev20250626.dist-info → skypilot_nightly-1.0.0.dev20250628.dist-info}/entry_points.txt +0 -0
  105. {skypilot_nightly-1.0.0.dev20250626.dist-info → skypilot_nightly-1.0.0.dev20250628.dist-info}/licenses/LICENSE +0 -0
  106. {skypilot_nightly-1.0.0.dev20250626.dist-info → skypilot_nightly-1.0.0.dev20250628.dist-info}/top_level.txt +0 -0
@@ -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 post(url, data=None, json=None, **kwargs) -> 'requests.Response':
133
- """Send a POST request to the API server, retry on server temporarily
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.post(url, data=data, json=json, **kwargs)
135
+ response = requests.request(method, url, **kwargs)
136
136
  handle_server_unavailable(response)
137
137
  return response
138
138
 
139
139
 
140
- @retry_on_server_unavailable()
141
- def get(url, params=None, **kwargs) -> 'requests.Response':
142
- """Send a GET request to the API server, retry on server temporarily
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 auth_user and body:
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 the basic auth header so the
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 or not auth_header.lower().startswith('basic '):
238
- return _basic_auth_401_response('Invalid basic auth')
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.event_fn,
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
- _reload_config()
508
+ reload_config()
481
509
 
482
510
 
483
- def _reload_config() -> None:
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
- _reload_config()
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
- _reload_config()
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.