diff --git a/docs/rest-api.yml b/docs/rest-api.yml index 9e94ed1a..790e24b2 100644 --- a/docs/rest-api.yml +++ b/docs/rest-api.yml @@ -17,35 +17,37 @@ securityDefinitions: flow: accessCode authorizationUrl: "/hub/api/oauth2/authorize" # what are the absolute URIs here? is oauth2 correct here or shall we use just authorizations? tokenUrl: "/hub/api/oauth2/token" - scopes: + scopes: # Todo: Generate based on scope table + (noscope): Allows only to identify the requesting entity self: Metascope, grants access to user's own resources; resolves to (no scope) for services. - all: Metascope, valid for tokens only. Grants access to everything that the token's owning entity can do. + all: Metascope, valid for tokens only. Grants access to all resources of the token-owning entity. admin:users: Grants read, write, create and delete access to users and their authentication state but not their servers or tokens. - admin:users:auth_state: Grants access to users' authentication state only. + admin:auth_state: Grants access to users' authentication state only. users: Grants read and write permissions to users' models apart from servers, tokens and authentication state. users:activity: Grants access to read and post users' activity only. users:activity!user=username: Update a single user's activity (example horizontal filter). read:users: Read-only access to users' models apart from servers, tokens and authentication state. read:users!user=username: As above limited to a specific user (example horizontal filter). - read:users:name: Read-only access to users' names. - read:users:roles: Read-only access to a list of users' roles names. + read:users:name: Read-only access to user names. + read:roles:users: Read-only access to user role assignments. read:users:groups: Read-only access to a list of users' group names. read:users:activity: Read-only access to users' activity. read:users:activity!group=groupname: Read-only access to specific group's users' activity (example horizontal filter). - admin:users:servers: Grants read, start/stop, create and delete permissions to users' servers and their state. - admin:users:server_state: Grants access to servers' state only. - users:servers: Allows for starting/stopping users' servers in addition to read access to their models. Does not include the server state. - users:servers!server=servername: Limits the above to a specific server (example horizontal filter). - read:users:servers: Read-only access to users' server models. Does not include the server state. - users:tokens: Grants read, write, create and delete permissions to users' tokens. - read:users:tokens: Read-only access to users' tokens. + admin:servers: Grants read, start/stop, create and delete permissions to users' servers and their state. + admin:server_state: Grants access to servers' state only. + servers: Allows for starting/stopping users' servers in addition to read access to their models. Does not include the server state. + servers!server=servername: Limits the above to a specific server (example horizontal filter). + read:servers: Read-only access to users' server models. Does not include the server state. + tokens: Grants read, write, create and delete permissions to users' tokens. + read:tokens: Read-only access to users' tokens. admin:groups: Grants read, write, create and delete access to groups. groups: Grants read and write permissions to groups, including adding/removing users to/from groups. + read:roles:groups: Read-only access to group roles assignments groups!group=groupname: As above limited to a specific group only (example horizontal filter) read:groups: Read-only access to groups. read:services: Read-only access to service models. read:services:name: Read-only access to service names. - read:services:roles: Read-only access to a list of service roles names. + read:roles:services: Read-only access to a list of service roles names. read:hub: Read-only access to detailed information about JupyterHub. proxy: Allows for obtaining information about the proxy's routing table, for syncing the Hub with proxy and notifying the Hub about a new proxy. shutdown: Grants access to shutdown the Hub. @@ -126,8 +128,10 @@ paths: - read:users:name - read:users:groups - read:users:activity - - read:users:servers - #TODO: add admin:users:auth_state/server_state? + - read:servers + - read:roles:users + - admin:auth_state + - admin:server_state parameters: - name: state in: query @@ -203,9 +207,10 @@ paths: - read:users:name - read:users:groups - read:users:activity - - read:users:servers - - admin:users:auth_state - - admin:users:server_state + - read:servers + - read:roles:users + - admin:auth_state + - admin:server_state parameters: - name: name description: username @@ -340,7 +345,7 @@ paths: summary: Start a user's single-user notebook server security: - oauth2: - - users:servers + - servers parameters: - name: name description: username @@ -369,7 +374,7 @@ paths: summary: Stop a user's server security: - oauth2: - - users:servers + - servers parameters: - name: name description: username @@ -386,7 +391,7 @@ paths: summary: Start a user's single-user named-server notebook server security: - oauth2: - - users:servers + - servers parameters: - name: name description: username @@ -420,7 +425,7 @@ paths: summary: Stop a user's named-server security: - oauth2: - - users:servers + - servers parameters: - name: name description: username @@ -460,7 +465,7 @@ paths: summary: List tokens for the user security: - oauth2: - - read:users:tokens + - read:tokens responses: "200": description: The list of tokens @@ -476,7 +481,7 @@ paths: summary: Create a new token for the user security: - oauth2: - - users:tokens + - tokens parameters: - name: token_params in: body @@ -519,7 +524,7 @@ paths: summary: Get the model for a token by id security: - oauth2: - - read:users:tokens + - read:tokens responses: "200": description: The info for the new token @@ -529,7 +534,7 @@ paths: summary: Delete (revoke) a token by id security: - oauth2: - - users:tokens + - tokens responses: "204": description: The token has been deleted @@ -542,9 +547,10 @@ paths: - read:users:name - read:users:groups - read:users:activity - - read:users:servers - - admin:users:auth_state - - admin:users:server_state + - read:servers + - read:roles:users + - admin:auth_state + - admin:server_state responses: "200": description: The authenticated user's model is returned. @@ -556,6 +562,8 @@ paths: security: - oauth2: - read:groups + - read:groups:name + - read:roles:groups parameters: - name: offset in: query @@ -586,6 +594,8 @@ paths: security: - oauth2: - read:groups + - read:groups:name + - read:roles:groups parameters: - name: name description: group name @@ -688,6 +698,8 @@ paths: security: - oauth2: - read:services + - read:services:name + - read:roles:services responses: "200": description: The service list @@ -701,6 +713,8 @@ paths: security: - oauth2: - read:services + - read:services:name + - read:roles:services parameters: - name: name description: service name @@ -790,7 +804,7 @@ paths: accepts passwords (e.g. not OAuth). security: - oauth2: - - users:tokens # minrk: this is a deprecated alias to POST /users/{name}/tokens, either remove it or use the same scope + - tokens parameters: - name: credentials in: body @@ -817,7 +831,7 @@ paths: summary: Identify a user or service from an API token security: - oauth2: - - read:users:tokens # minrk: is it really necessary to have a scope for this, or use self handler for token whoami? + - (noscope) parameters: - name: token in: path diff --git a/docs/source/rbac/index.md b/docs/source/rbac/index.md index 3f9ed98f..6fb102f9 100644 --- a/docs/source/rbac/index.md +++ b/docs/source/rbac/index.md @@ -17,7 +17,7 @@ To remedy situations like this, JupyterHub is transitioning to an RBAC system. B ## Definitions -**Scopes** are specific permissions used to evaluate API requests. For example: the API endpoint `users/servers`, which enables starting or stopping user servers, is guarded by the scope `users:servers`. +**Scopes** are specific permissions used to evaluate API requests. For example: the API endpoint `users/servers`, which enables starting or stopping user servers, is guarded by the scope `servers`. Scopes are not directly assigned to requesters. Rather, when a client performs an API call, their access will be evaluated based on their assigned roles. diff --git a/docs/source/rbac/roles.md b/docs/source/rbac/roles.md index a8bb35a9..afff5f55 100644 --- a/docs/source/rbac/roles.md +++ b/docs/source/rbac/roles.md @@ -55,7 +55,7 @@ c.JupyterHub.load_roles = [  {    'name': 'server-rights',    'description': 'Allows parties to start and stop user servers', -   'scopes': ['users:servers'], +   'scopes': ['servers'],    'users': ['alice', 'bob'],    'services': ['idle-culler'],    'groups': ['admin-group'], diff --git a/docs/source/rbac/scopes.md b/docs/source/rbac/scopes.md index e97aefe8..f4ed13e1 100644 --- a/docs/source/rbac/scopes.md +++ b/docs/source/rbac/scopes.md @@ -7,7 +7,7 @@ A scope has a syntax-based design that reveals which resources it provides acces ## Scope conventions - `` \ - The `` scopes, such as `users` or `groups`, grant read and write permissions to the resource itself and all its sub-resources. E.g., the scope `users:servers` is included within the scope `users`. + The top-level `` scopes, such as `users` or `groups`, grant read and write permissions to the resource itself as well as its sub-resources. For example, the scope `users:activity` is included in the scope `users`. +++ - `read:` \ @@ -19,7 +19,7 @@ A scope has a syntax-based design that reveals which resources it provides acces +++ - `:` \ - The {ref}`vertically filtered ` scopes provide access to a subset of the information granted by the `` scope. E.g., the scope `users:servers` allows for accessing user servers only. + The {ref}`vertically filtered ` scopes provide access to a subset of the information granted by the `` scope. E.g., the scope `users:activity` only provides permission to post user activity. +++ - `!=` \ @@ -42,8 +42,8 @@ Metascopes do not follow the general scope syntax. Instead, a metascope resolves Access to the user's own resources and subresources is covered by metascope `self`. This metascope includes the user's model, activity, servers and tokens. For example, `self` for a user named "gerard" includes: - `users!user=gerard` where the `users` scope provides access to the full user model and activity. The filter restricts this access to the user's own resources. -- `users:servers!user=gerard` which grants the user access to their own servers without being able to create/delete any. -- `users:tokens!user=gerard` which allows the user to access, request and delete their own tokens. +- `servers!user=gerard` which grants the user access to their own servers without being able to create/delete any. +- `tokens!user=gerard` which allows the user to access, request and delete their own tokens. The `self` scope is only valid for user entities. In other cases (e.g., for services) it resolves to an empty set of scopes. diff --git a/docs/source/rbac/use-cases.md b/docs/source/rbac/use-cases.md index 4b31ba63..d0858ae3 100644 --- a/docs/source/rbac/use-cases.md +++ b/docs/source/rbac/use-cases.md @@ -38,7 +38,7 @@ Below follows a short tutorial on how to add a cull-idle service in the RBAC sys { "name": "idle-culler", "description": "Culls idle servers", - "scopes": ["read:users:name", "read:users:activity", "users:servers"], + "scopes": ["read:users:name", "read:users:activity", "servers"], "services": ["idle-culler"], } ] @@ -48,7 +48,7 @@ Below follows a short tutorial on how to add a cull-idle service in the RBAC sys Note that in the RBAC system the `admin` field in the `idle-culler` service definition is omitted. Instead, the `idle-culler` role provides the service with only the permissions it needs. If the optional actions of deleting the idle servers and/or removing inactive users are desired, **change the following scopes** in the `idle-culler` role definition: - - `users:servers` to `admin:users:servers` for deleting servers + - `servers` to `admin:servers` for deleting servers - `read:users:name`, `read:users:activity` to `admin:users` for deleting users. ``` @@ -65,8 +65,8 @@ A service capable of creating/removing users and launching multiple servers shou The scopes required to access the API enpoints: 1. `admin:users` -2. `users:servers` -3. `admin:users:servers` +2. `servers` +3. `admin:servers` From the above, the role definition is: @@ -77,7 +77,7 @@ c.JupyterHub.load_roles = [ { "name": "api-launcher", "description": "Manages servers", - "scopes": ["admin:users", "admin:users:servers"], + "scopes": ["admin:users", "admin:servers"], "services": [] } ] @@ -117,7 +117,7 @@ c.JupyterHub.load_roles = [ {    'name': 'teacher',    'description': 'Allows for accessing information about teacher group members and starting/stopping their servers', -   'scopes': [ 'read:users!group=class-B', 'users:servers!group=class-B'], +   'scopes': [ 'read:users!group=class-B', 'servers!group=class-B'],    'users': ['johan'] } ] diff --git a/docs/source/reference/services.md b/docs/source/reference/services.md index a78e8707..7de3a7ea 100644 --- a/docs/source/reference/services.md +++ b/docs/source/reference/services.md @@ -91,7 +91,7 @@ c.JupyterHub.load_roles = [ "name": "idle-culler", "scopes": [ "read:users:activity", # read user last_activity - "users:servers", # start and stop servers + "servers", # start and stop servers # 'admin:users' # needed if culling idle users as well ] } diff --git a/examples/service-fastapi/README.md b/examples/service-fastapi/README.md index 26fb6a7b..b26a586b 100644 --- a/examples/service-fastapi/README.md +++ b/examples/service-fastapi/README.md @@ -45,7 +45,7 @@ $ curl -X GET http://127.0.0.1:8000/services/fastapi/me \ "servers": null, "scopes": [ "access:services", - "access:users:servers!user=test-user", + "access:servers!user=test-user", "...", ] } diff --git a/examples/service-whoami/README.md b/examples/service-whoami/README.md index 2fb59f02..072ff526 100644 --- a/examples/service-whoami/README.md +++ b/examples/service-whoami/README.md @@ -43,19 +43,19 @@ $ curl -H "Authorization: token 8630bbd8ef064c48b22c7f122f0cd8ad" http://127.0.0 ], "scopes": [ "access:services", - "access:users:servers!user=test", + "access:servers!user=test", "read:users!user=test", "read:users:activity!user=test", "read:users:groups!user=test", "read:users:name!user=test", - "read:users:servers!user=test", - "read:users:tokens!user=test", + "read:servers!user=test", + "read:tokens!user=test", "users!user=test", "users:activity!user=test", "users:groups!user=test", "users:name!user=test", - "users:servers!user=test", - "users:tokens!user=test" + "servers!user=test", + "tokens!user=test" ], "server": null } diff --git a/jupyterhub/apihandlers/auth.py b/jupyterhub/apihandlers/auth.py index d25d06c7..c88753ac 100644 --- a/jupyterhub/apihandlers/auth.py +++ b/jupyterhub/apihandlers/auth.py @@ -258,7 +258,7 @@ class OAuthAuthorizeHandler(OAuthHandler, BaseHandler): # check for access to target resource if client.spawner: - scope_filter = self.get_scope_filter("access:users:servers") + scope_filter = self.get_scope_filter("access:servers") allowed = scope_filter(client.spawner, kind='server') elif client.service: scope_filter = self.get_scope_filter("access:services") diff --git a/jupyterhub/apihandlers/base.py b/jupyterhub/apihandlers/base.py index d5d4407e..d792d542 100644 --- a/jupyterhub/apihandlers/base.py +++ b/jupyterhub/apihandlers/base.py @@ -143,7 +143,7 @@ class APIHandler(BaseHandler): 'user_options': spawner.user_options, 'progress_url': spawner._progress_url, } - scope_filter = self.get_scope_filter('admin:users:server_state') + scope_filter = self.get_scope_filter('admin:server_state') if scope_filter(spawner, kind='server'): model['state'] = spawner.get_state() return model @@ -219,9 +219,9 @@ class APIHandler(BaseHandler): 'read:users:name': {'kind', 'name', 'admin'}, 'read:users:groups': {'kind', 'name', 'groups'}, 'read:users:activity': {'kind', 'name', 'last_activity'}, - 'read:users:servers': {'kind', 'name', 'servers'}, - 'read:users:roles': {'kind', 'name', 'roles', 'admin'}, - 'admin:users:auth_state': {'kind', 'name', 'auth_state'}, + 'read:servers': {'kind', 'name', 'servers'}, + 'read:roles:users': {'kind', 'name', 'roles', 'admin'}, + 'admin:auth_state': {'kind', 'name', 'auth_state'}, } self.log.debug( "Asking for user model of %s with scopes [%s]", @@ -237,7 +237,7 @@ class APIHandler(BaseHandler): model['pending'] = user.spawners[''].pending servers = model['servers'] = {} - scope_filter = self.get_scope_filter('read:users:servers') + scope_filter = self.get_scope_filter('read:servers') for name, spawner in user.spawners.items(): # include 'active' servers, not just ready # (this includes pending events) @@ -258,7 +258,7 @@ class APIHandler(BaseHandler): access_map = { 'read:groups': {'kind', 'name', 'users'}, 'read:groups:name': {'kind', 'name'}, - 'read:groups:roles': {'kind', 'name', 'roles'}, + 'read:roles:groups': {'kind', 'name', 'roles'}, } model = self._filter_model(model, access_map, group, 'group') return model @@ -290,7 +290,7 @@ class APIHandler(BaseHandler): 'display', }, 'read:services:name': {'kind', 'name', 'admin'}, - 'read:services:roles': {'kind', 'name', 'roles', 'admin'}, + 'read:roles:services': {'kind', 'name', 'roles', 'admin'}, } model = self._filter_model(model, access_map, service, 'service') return model diff --git a/jupyterhub/apihandlers/groups.py b/jupyterhub/apihandlers/groups.py index df6e197d..84f97a6b 100644 --- a/jupyterhub/apihandlers/groups.py +++ b/jupyterhub/apihandlers/groups.py @@ -34,7 +34,7 @@ class _GroupAPIHandler(APIHandler): class GroupListAPIHandler(_GroupAPIHandler): - @needs_scope('read:groups', 'read:groups:name', 'read:groups:roles') + @needs_scope('read:groups', 'read:groups:name', 'read:roles:groups') def get(self): """List groups""" query = self.db.query(orm.Group) @@ -77,7 +77,7 @@ class GroupListAPIHandler(_GroupAPIHandler): class GroupAPIHandler(_GroupAPIHandler): """View and modify groups by name""" - @needs_scope('read:groups', 'read:groups:name', 'read:groups:roles') + @needs_scope('read:groups', 'read:groups:name', 'read:roles:groups') def get(self, group_name): group = self.find_group(group_name) self.write(json.dumps(self.group_model(group))) diff --git a/jupyterhub/apihandlers/services.py b/jupyterhub/apihandlers/services.py index e2106801..dc751a16 100644 --- a/jupyterhub/apihandlers/services.py +++ b/jupyterhub/apihandlers/services.py @@ -11,7 +11,7 @@ from .base import APIHandler class ServiceListAPIHandler(APIHandler): - @needs_scope('read:services', 'read:services:name', 'read:services:roles') + @needs_scope('read:services', 'read:services:name', 'read:roles:services') def get(self): data = {} for name, service in self.services.items(): @@ -22,7 +22,7 @@ class ServiceListAPIHandler(APIHandler): class ServiceAPIHandler(APIHandler): - @needs_scope('read:services', 'read:services:name', 'read:services:roles') + @needs_scope('read:services', 'read:services:name', 'read:roles:services') def get(self, service_name): service = self.services[service_name] self.write(json.dumps(self.service_model(service))) diff --git a/jupyterhub/apihandlers/users.py b/jupyterhub/apihandlers/users.py index 98a23490..7bca6b02 100644 --- a/jupyterhub/apihandlers/users.py +++ b/jupyterhub/apihandlers/users.py @@ -75,10 +75,10 @@ class UserListAPIHandler(APIHandler): @needs_scope( 'read:users', 'read:users:name', - 'read:users:servers', + 'read:servers', 'read:users:groups', 'read:users:activity', - 'read:users:roles', + 'read:roles:users', ) def get(self): state_filter = self.get_argument("state", None) @@ -195,10 +195,10 @@ class UserAPIHandler(APIHandler): @needs_scope( 'read:users', 'read:users:name', - 'read:users:servers', + 'read:servers', 'read:users:groups', 'read:users:activity', - 'read:users:roles', + 'read:roles:users', ) async def get(self, user_name): user = self.find_user(user_name) @@ -297,7 +297,7 @@ class UserAPIHandler(APIHandler): class UserTokenListAPIHandler(APIHandler): """API endpoint for listing/creating tokens""" - @needs_scope('read:users:tokens') + @needs_scope('read:tokens') def get(self, user_name): """Get tokens for a given user""" user = self.find_user(user_name) @@ -352,7 +352,7 @@ class UserTokenListAPIHandler(APIHandler): self._resolve_roles_and_scopes() user = self.find_user(user_name) kind = 'user' if isinstance(requester, User) else 'service' - scope_filter = self.get_scope_filter('users:tokens') + scope_filter = self.get_scope_filter('tokens') if user is None or not scope_filter(user, kind): raise web.HTTPError( 403, @@ -417,7 +417,7 @@ class UserTokenAPIHandler(APIHandler): raise web.HTTPError(404, "Token not found %s", orm_token) return orm_token - @needs_scope('read:users:tokens') + @needs_scope('read:tokens') def get(self, user_name, token_id): """""" user = self.find_user(user_name) @@ -426,7 +426,7 @@ class UserTokenAPIHandler(APIHandler): token = self.find_token_by_id(user, token_id) self.write(json.dumps(self.token_model(token))) - @needs_scope('users:tokens') + @needs_scope('tokens') def delete(self, user_name, token_id): """Delete a token""" user = self.find_user(user_name) @@ -451,7 +451,7 @@ class UserTokenAPIHandler(APIHandler): class UserServerAPIHandler(APIHandler): """Start and stop single-user servers""" - @needs_scope('users:servers') + @needs_scope('servers') async def post(self, user_name, server_name=''): user = self.find_user(user_name) if server_name: @@ -496,7 +496,7 @@ class UserServerAPIHandler(APIHandler): self.set_header('Content-Type', 'text/plain') self.set_status(status) - @needs_scope('users:servers') + @needs_scope('servers') async def delete(self, user_name, server_name=''): user = self.find_user(user_name) options = self.get_json_body() @@ -569,7 +569,7 @@ class UserAdminAccessAPIHandler(APIHandler): This handler sets the necessary cookie for an admin to login to a single-user server. """ - @needs_scope('users:servers') + @needs_scope('servers') def post(self, user_name): self.log.warning( "Deprecated in JupyterHub 0.8." @@ -625,7 +625,7 @@ class SpawnProgressAPIHandler(APIHandler): await asyncio.wait([self._finish_future], timeout=self.keepalive_interval) - @needs_scope('read:users:servers') + @needs_scope('read:servers') async def get(self, user_name, server_name=''): self.set_header('Cache-Control', 'no-cache') if server_name is None: diff --git a/jupyterhub/handlers/pages.py b/jupyterhub/handlers/pages.py index 77e6b4a3..fd32905e 100644 --- a/jupyterhub/handlers/pages.py +++ b/jupyterhub/handlers/pages.py @@ -456,7 +456,7 @@ class AdminHandler(BaseHandler): @web.authenticated @needs_scope('users') @needs_scope('admin:users') - @needs_scope('admin:users:servers') + @needs_scope('admin:servers') async def get(self): auth_state = await self.current_user.get_auth_state() html = await self.render_template( diff --git a/jupyterhub/roles.py b/jupyterhub/roles.py index fda19fb4..23f220e4 100644 --- a/jupyterhub/roles.py +++ b/jupyterhub/roles.py @@ -33,16 +33,16 @@ def get_default_roles(): 'description': 'Elevated privileges (can do anything)', 'scopes': [ 'admin:users', - 'admin:users:servers', - 'users:tokens', + 'admin:servers', + 'tokens', 'admin:groups', 'read:services', 'read:hub', 'proxy', 'shutdown', 'access:services', - 'access:users:servers', - 'read:services:roles', + 'access:servers', + 'read:roles', ], }, { @@ -50,7 +50,7 @@ def get_default_roles(): 'description': 'Post activity only', 'scopes': [ 'users:activity!user', - 'access:users:servers!user', + 'access:servers!user', ], }, { @@ -70,9 +70,10 @@ def expand_self_scope(name): users:name users:groups users:activity - users:servers - users:tokens - access:users:servers + tokens + servers + access:servers + Arguments: name (str): user name @@ -87,11 +88,11 @@ def expand_self_scope(name): 'read:users:groups', 'users:activity', 'read:users:activity', - 'users:servers', - 'read:users:servers', - 'users:tokens', - 'read:users:tokens', - 'access:users:servers', + 'servers', + 'read:servers', + 'tokens', + 'read:tokens', + 'access:servers', ] return {"{}!user={}".format(scope, name) for scope in scope_list} diff --git a/jupyterhub/scopes.py b/jupyterhub/scopes.py index 85bbb616..f9ffb035 100644 --- a/jupyterhub/scopes.py +++ b/jupyterhub/scopes.py @@ -34,9 +34,9 @@ scope_definitions = { }, 'admin:users': { 'description': 'Read, write, create and delete users and their authentication state, not including their servers or tokens.', - 'subscopes': ['admin:users:auth_state', 'users', 'read:users:roles'], + 'subscopes': ['admin:auth_state', 'users', 'read:roles:users'], }, - 'admin:users:auth_state': {'description': 'Read a user’s authentication state.'}, + 'admin:auth_state': {'description': 'Read a user’s authentication state.'}, 'users': { 'description': 'Read and write permissions to user models (excluding servers, tokens and authentication state).', 'subscopes': ['read:users', 'users:activity'], @@ -52,32 +52,38 @@ scope_definitions = { 'read:users:name': {'description': 'Read names of users.'}, 'read:users:groups': {'description': 'Read users’ group membership.'}, 'read:users:activity': {'description': 'Read time of last user activity.'}, - 'read:users:roles': {'description': 'Read users’ role assignments.'}, + 'read:roles': { + 'description': 'Read role assignments.', + 'subscopes': ['read:roles:users', 'read:roles:services', 'read:roles:groups'], + }, + 'read:roles:users': {'description': 'Read user role assignments.'}, + 'read:roles:services': {'description': 'Read service role assignments.'}, + 'read:roles:groups': {'description': 'Read group role assignments.'}, 'users:activity': { 'description': 'Update time of last user activity.', 'subscopes': ['read:users:activity'], }, - 'admin:users:servers': { + 'admin:servers': { 'description': 'Read, start, stop, create and delete user servers and their state.', - 'subscopes': ['admin:users:server_state', 'users:servers'], + 'subscopes': ['admin:server_state', 'servers'], }, - 'admin:users:server_state': {'description': 'Read and write users’ server state.'}, - 'users:servers': { + 'admin:server_state': {'description': 'Read and write users’ server state.'}, + 'servers': { 'description': 'Start and stop user servers.', - 'subscopes': ['read:users:servers'], + 'subscopes': ['read:servers'], }, - 'read:users:servers': { + 'read:servers': { 'description': 'Read users’ names and their server models (excluding the server state).', 'subscopes': ['read:users:name'], }, - 'users:tokens': { + 'tokens': { 'description': 'Read, write, create and delete user tokens.', - 'subscopes': ['read:users:tokens'], + 'subscopes': ['read:tokens'], }, - 'read:users:tokens': {'description': 'Read user tokens.'}, + 'read:tokens': {'description': 'Read user tokens.'}, 'admin:groups': { 'description': 'Read and write group information, create and delete groups.', - 'subscopes': ['groups', 'read:groups:roles'], + 'subscopes': ['groups', 'read:roles:groups'], }, 'groups': { 'description': 'Read and write group information, including adding/removing users to/from groups.', @@ -88,15 +94,13 @@ scope_definitions = { 'subscopes': ['read:groups:name'], }, 'read:groups:name': {'description': 'Read group names.'}, - 'read:groups:roles': {'description': 'Read group role assignments.'}, 'read:services': { 'description': 'Read service models.', 'subscopes': ['read:services:name'], }, 'read:services:name': {'description': 'Read service names.'}, - 'read:services:roles': {'description': 'Read service role assignments.'}, 'read:hub': {'description': 'Read detailed information about the Hub.'}, - 'access:users:servers': { + 'access:servers': { 'description': 'Access user servers via API or browser.', }, 'access:services': { @@ -279,7 +283,7 @@ def get_scopes_for(orm_object): spawner = orm_object.oauth_client.spawner if spawner: token_scopes.add( - f"access:users:servers!server={spawner.user.name}/{spawner.name}" + f"access:servers!server={spawner.user.name}/{spawner.name}" ) else: service = orm_object.oauth_client.service @@ -388,7 +392,7 @@ def parse_scopes(scope_list): """ Parses scopes and filters in something akin to JSON style - For instance, scope list ["users", "groups!group=foo", "users:servers!server=user/bar", "users:servers!server=user/baz"] + For instance, scope list ["users", "groups!group=foo", "servers!server=user/bar", "servers!server=user/baz"] would lead to scope model { "users":scope.ALL, @@ -397,7 +401,7 @@ def parse_scopes(scope_list): "alice" ] }, - "users:servers":{ + "servers":{ "server":[ "user/bar", "user/baz" diff --git a/jupyterhub/services/auth.py b/jupyterhub/services/auth.py index 64080723..d95775c1 100644 --- a/jupyterhub/services/auth.py +++ b/jupyterhub/services/auth.py @@ -835,8 +835,8 @@ class HubAuthenticated: which in turn is set by $JUPYTERHUB_OAUTH_SCOPES Default values include: - 'access:services', 'access:services!service={service_name}' for services - - 'access:users:servers', 'access:users:servers!user={user}', - 'access:users:servers!server={user}/{server_name}' + - 'access:servers', 'access:servers!user={user}', + 'access:servers!server={user}/{server_name}' for single-user servers If hub_scopes is not used (e.g. JupyterHub 1.x), diff --git a/jupyterhub/spawner.py b/jupyterhub/spawner.py index 22fbe451..74f9240d 100644 --- a/jupyterhub/spawner.py +++ b/jupyterhub/spawner.py @@ -222,8 +222,8 @@ class Spawner(LoggingConfigurable): @default("oauth_scopes") def _default_oauth_scopes(self): return [ - f"access:users:servers!server={self.user.name}/{self.name}", - f"access:users:servers!user={self.user.name}", + f"access:servers!server={self.user.name}/{self.name}", + f"access:servers!user={self.user.name}", ] handler = Any() diff --git a/jupyterhub/tests/test_pages.py b/jupyterhub/tests/test_pages.py index cf441198..922c00e6 100644 --- a/jupyterhub/tests/test_pages.py +++ b/jupyterhub/tests/test_pages.py @@ -956,7 +956,7 @@ async def test_oauth_page_scope_appearance( [ 'self', 'read:users!user=gawain', - 'read:users:tokens', + 'read:tokens', 'read:groups!group=mythos', ] ) diff --git a/jupyterhub/tests/test_roles.py b/jupyterhub/tests/test_roles.py index e33e936a..26740ec6 100644 --- a/jupyterhub/tests/test_roles.py +++ b/jupyterhub/tests/test_roles.py @@ -180,13 +180,13 @@ def test_orm_roles_delete_cascade(db): ['admin:users'], { 'admin:users', - 'admin:users:auth_state', + 'admin:auth_state', 'users', 'read:users', 'users:activity', 'read:users:name', 'read:users:groups', - 'read:users:roles', + 'read:roles:users', 'read:users:activity', }, ), @@ -210,32 +210,32 @@ def test_orm_roles_delete_cascade(db): 'read:users:activity', }, ), - (['read:users:servers'], {'read:users:servers', 'read:users:name'}), + (['read:servers'], {'read:servers', 'read:users:name'}), ( ['admin:groups'], { 'admin:groups', 'groups', 'read:groups', - 'read:groups:roles', + 'read:roles:groups', 'read:groups:name', }, ), ( - ['admin:groups', 'read:users:servers'], + ['admin:groups', 'read:servers'], { 'admin:groups', 'groups', 'read:groups', - 'read:groups:roles', + 'read:roles:groups', 'read:groups:name', - 'read:users:servers', + 'read:servers', 'read:users:name', }, ), ( - ['users:tokens!group=hobbits'], - {'users:tokens!group=hobbits', 'read:users:tokens!group=hobbits'}, + ['tokens!group=hobbits'], + {'tokens!group=hobbits', 'read:tokens!group=hobbits'}, ), ], ) @@ -398,7 +398,7 @@ async def test_delete_roles(db, role_type, rolename, response_type, response): 'users', 'users!user=charlie', 'admin:groups', - 'read:users:tokens', + 'read:tokens', ], }, 'existing', @@ -501,8 +501,8 @@ async def test_load_roles_services(tmpdir, request): 'scopes': [ 'read:users:name', 'read:users:activity', - 'read:users:servers', - 'users:servers', + 'read:servers', + 'servers', ], 'services': ['idle-culler'], }, diff --git a/jupyterhub/tests/test_scopes.py b/jupyterhub/tests/test_scopes.py index e5687caa..1bc4d0a2 100644 --- a/jupyterhub/tests/test_scopes.py +++ b/jupyterhub/tests/test_scopes.py @@ -79,9 +79,9 @@ def test_scope_multiple_filters(): def test_scope_parse_server_name(): handler = get_handler_with_scopes( - ['users:servers!server=maeby/server1', 'read:users!user=maeby'] + ['servers!server=maeby/server1', 'read:users!user=maeby'] ) - assert _check_scope_access(handler, 'users:servers', user='maeby', server='server1') + assert _check_scope_access(handler, 'servers', user='maeby', server='server1') class MockAPIHandler: @@ -99,7 +99,7 @@ class MockAPIHandler: def user_thing(self, user_name): return True - @needs_scope('users:servers') + @needs_scope('servers') def server_thing(self, user_name, server_name): return True @@ -140,18 +140,18 @@ def mock_handler(): (['users!user=george'], 'user_thing', ('fake_user',), False), (['users!user=george'], 'user_thing', ('oscar',), False), (['users!user=george', 'users!user=oscar'], 'user_thing', ('oscar',), True), - (['users:servers'], 'server_thing', ('user1', 'server_1'), True), - (['users:servers'], 'server_thing', ('user1', ''), True), - (['users:servers'], 'server_thing', ('user1', None), True), + (['servers'], 'server_thing', ('user1', 'server_1'), True), + (['servers'], 'server_thing', ('user1', ''), True), + (['servers'], 'server_thing', ('user1', None), True), ( - ['users:servers!server=maeby/bluth'], + ['servers!server=maeby/bluth'], 'server_thing', ('maeby', 'bluth'), True, ), - (['users:servers!server=maeby/bluth'], 'server_thing', ('gob', 'bluth'), False), + (['servers!server=maeby/bluth'], 'server_thing', ('gob', 'bluth'), False), ( - ['users:servers!server=maeby/bluth'], + ['servers!server=maeby/bluth'], 'server_thing', ('maybe', 'bluth2'), False, @@ -488,17 +488,17 @@ async def test_metascope_all_expansion(app, create_user_with_scopes): @mark.parametrize( "scopes, can_stop ,num_servers, keys_in, keys_out", [ - (['read:users:servers!user=almond'], False, 2, {'name'}, {'state'}), + (['read:servers!user=almond'], False, 2, {'name'}, {'state'}), (['admin:users', 'read:users'], False, 0, set(), set()), ( - ['read:users:servers!group=nuts', 'users:servers'], + ['read:servers!group=nuts', 'servers'], True, 2, {'name'}, {'state'}, ), ( - ['admin:users:server_state', 'read:users:servers'], + ['admin:server_state', 'read:servers'], False, 2, {'name', 'state'}, @@ -506,8 +506,8 @@ async def test_metascope_all_expansion(app, create_user_with_scopes): ), ( [ - 'read:users:servers!server=almond/bianca', - 'admin:users:server_state!server=almond/bianca', + 'read:servers!server=almond/bianca', + 'admin:server_state!server=almond/bianca', ], False, 1, @@ -634,8 +634,8 @@ async def test_server_state_access( ), ( 'no_intersection_user_server', - ['users:servers!user=y'], - ['users:servers!server=x'], + ['servers!user=y'], + ['servers!server=x'], set(), ), ( @@ -704,7 +704,7 @@ async def test_resolve_token_permissions( }, ), ( - {'read:services:roles', 'read:services:name'}, + {'read:roles:services', 'read:services:name'}, {'name', 'kind', 'roles', 'admin'}, ), ({'read:services:name'}, {'name', 'kind', 'admin'}), @@ -734,7 +734,7 @@ async def test_service_model_filtering( }, ), ( - {'read:groups:roles', 'read:groups:name'}, + {'read:roles:groups', 'read:groups:name'}, {'name', 'kind', 'roles'}, ), ({'read:groups:name'}, {'name', 'kind'}), @@ -761,7 +761,7 @@ async def test_group_model_filtering( async def test_roles_access(app, create_service_with_scopes, create_user_with_scopes): user = add_user(app.db, name='miranda') - read_user = create_user_with_scopes('read:users:roles') + read_user = create_user_with_scopes('read:roles:users') r = await api_request( app, 'users', user.name, headers=auth_header(app.db, read_user.name) ) @@ -822,15 +822,15 @@ async def test_roles_access(app, create_service_with_scopes, create_user_with_sc ), # resolves server under user, without warning ( - set(["read:users:servers!user=abc"]), - set(["read:users:servers!server=abc/xyz"]), - set(["read:users:servers!server=abc/xyz"]), + set(["read:servers!user=abc"]), + set(["read:servers!server=abc/xyz"]), + set(["read:servers!server=abc/xyz"]), False, ), # user->server, no match ( - set(["read:users:servers!user=abc"]), - set(["read:users:servers!server=abcd/xyz"]), + set(["read:servers!user=abc"]), + set(["read:servers!server=abcd/xyz"]), set([]), False, ), diff --git a/jupyterhub/tests/test_services_auth.py b/jupyterhub/tests/test_services_auth.py index b3b2838f..c402c6be 100644 --- a/jupyterhub/tests/test_services_auth.py +++ b/jupyterhub/tests/test_services_auth.py @@ -128,7 +128,7 @@ async def test_hubauth_token(app, mockservice_url, create_user_with_scopes): ), ( [ - "access:users:servers!user=$service", + "access:servers!user=$service", ], False, ), diff --git a/jupyterhub/tests/test_singleuser.py b/jupyterhub/tests/test_singleuser.py index bf3b0905..7d6c860c 100644 --- a/jupyterhub/tests/test_singleuser.py +++ b/jupyterhub/tests/test_singleuser.py @@ -17,18 +17,18 @@ from .utils import AsyncSession @pytest.mark.parametrize( "access_scopes, server_name, expect_success", [ - (["access:users:servers!group=$group"], "", True), - (["access:users:servers!group=other-group"], "", False), - (["access:users:servers"], "", True), - (["access:users:servers"], "named", True), - (["access:users:servers!user=$user"], "", True), - (["access:users:servers!user=$user"], "named", True), - (["access:users:servers!server=$server"], "", True), - (["access:users:servers!server=$server"], "named-server", True), - (["access:users:servers!server=$user/other"], "", False), - (["access:users:servers!server=$user/other"], "some-name", False), - (["access:users:servers!user=$other"], "", False), - (["access:users:servers!user=$other"], "named", False), + (["access:servers!group=$group"], "", True), + (["access:servers!group=other-group"], "", False), + (["access:servers"], "", True), + (["access:servers"], "named", True), + (["access:servers!user=$user"], "", True), + (["access:servers!user=$user"], "named", True), + (["access:servers!server=$server"], "", True), + (["access:servers!server=$server"], "named-server", True), + (["access:servers!server=$user/other"], "", False), + (["access:servers!server=$user/other"], "some-name", False), + (["access:servers!user=$other"], "", False), + (["access:servers!user=$other"], "named", False), (["access:services"], "", False), (["self"], "named", False), ([], "", False),