Refactored scope names and updated docs to reflect this

This commit is contained in:
0mar
2021-06-15 12:07:07 +02:00
parent 5789806cf7
commit 7a3b237bb3
23 changed files with 180 additions and 161 deletions

View File

@@ -17,35 +17,37 @@ securityDefinitions:
flow: accessCode flow: accessCode
authorizationUrl: "/hub/api/oauth2/authorize" # what are the absolute URIs here? is oauth2 correct here or shall we use just authorizations? 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" 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. 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: 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: 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: Grants access to read and post users' activity only.
users:activity!user=username: Update a single user's activity (example horizontal filter). 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: 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!user=username: As above limited to a specific user (example horizontal filter).
read:users:name: Read-only access to users' names. read:users:name: Read-only access to user names.
read:users:roles: Read-only access to a list of users' roles 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:groups: Read-only access to a list of users' group names.
read:users:activity: Read-only access to users' activity. 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). 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: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. admin: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. 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). 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. read: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. tokens: Grants read, write, create and delete permissions to users' tokens.
read:users:tokens: Read-only access to users' tokens. read:tokens: Read-only access to users' tokens.
admin:groups: Grants read, write, create and delete access to groups. 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. 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) groups!group=groupname: As above limited to a specific group only (example horizontal filter)
read:groups: Read-only access to groups. read:groups: Read-only access to groups.
read:services: Read-only access to service models. read:services: Read-only access to service models.
read:services:name: Read-only access to service names. 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. 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. 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. shutdown: Grants access to shutdown the Hub.
@@ -126,8 +128,10 @@ paths:
- read:users:name - read:users:name
- read:users:groups - read:users:groups
- read:users:activity - read:users:activity
- read:users:servers - read:servers
#TODO: add admin:users:auth_state/server_state? - read:roles:users
- admin:auth_state
- admin:server_state
parameters: parameters:
- name: state - name: state
in: query in: query
@@ -203,9 +207,10 @@ paths:
- read:users:name - read:users:name
- read:users:groups - read:users:groups
- read:users:activity - read:users:activity
- read:users:servers - read:servers
- admin:users:auth_state - read:roles:users
- admin:users:server_state - admin:auth_state
- admin:server_state
parameters: parameters:
- name: name - name: name
description: username description: username
@@ -340,7 +345,7 @@ paths:
summary: Start a user's single-user notebook server summary: Start a user's single-user notebook server
security: security:
- oauth2: - oauth2:
- users:servers - servers
parameters: parameters:
- name: name - name: name
description: username description: username
@@ -369,7 +374,7 @@ paths:
summary: Stop a user's server summary: Stop a user's server
security: security:
- oauth2: - oauth2:
- users:servers - servers
parameters: parameters:
- name: name - name: name
description: username description: username
@@ -386,7 +391,7 @@ paths:
summary: Start a user's single-user named-server notebook server summary: Start a user's single-user named-server notebook server
security: security:
- oauth2: - oauth2:
- users:servers - servers
parameters: parameters:
- name: name - name: name
description: username description: username
@@ -420,7 +425,7 @@ paths:
summary: Stop a user's named-server summary: Stop a user's named-server
security: security:
- oauth2: - oauth2:
- users:servers - servers
parameters: parameters:
- name: name - name: name
description: username description: username
@@ -460,7 +465,7 @@ paths:
summary: List tokens for the user summary: List tokens for the user
security: security:
- oauth2: - oauth2:
- read:users:tokens - read:tokens
responses: responses:
"200": "200":
description: The list of tokens description: The list of tokens
@@ -476,7 +481,7 @@ paths:
summary: Create a new token for the user summary: Create a new token for the user
security: security:
- oauth2: - oauth2:
- users:tokens - tokens
parameters: parameters:
- name: token_params - name: token_params
in: body in: body
@@ -519,7 +524,7 @@ paths:
summary: Get the model for a token by id summary: Get the model for a token by id
security: security:
- oauth2: - oauth2:
- read:users:tokens - read:tokens
responses: responses:
"200": "200":
description: The info for the new token description: The info for the new token
@@ -529,7 +534,7 @@ paths:
summary: Delete (revoke) a token by id summary: Delete (revoke) a token by id
security: security:
- oauth2: - oauth2:
- users:tokens - tokens
responses: responses:
"204": "204":
description: The token has been deleted description: The token has been deleted
@@ -542,9 +547,10 @@ paths:
- read:users:name - read:users:name
- read:users:groups - read:users:groups
- read:users:activity - read:users:activity
- read:users:servers - read:servers
- admin:users:auth_state - read:roles:users
- admin:users:server_state - admin:auth_state
- admin:server_state
responses: responses:
"200": "200":
description: The authenticated user's model is returned. description: The authenticated user's model is returned.
@@ -556,6 +562,8 @@ paths:
security: security:
- oauth2: - oauth2:
- read:groups - read:groups
- read:groups:name
- read:roles:groups
parameters: parameters:
- name: offset - name: offset
in: query in: query
@@ -586,6 +594,8 @@ paths:
security: security:
- oauth2: - oauth2:
- read:groups - read:groups
- read:groups:name
- read:roles:groups
parameters: parameters:
- name: name - name: name
description: group name description: group name
@@ -688,6 +698,8 @@ paths:
security: security:
- oauth2: - oauth2:
- read:services - read:services
- read:services:name
- read:roles:services
responses: responses:
"200": "200":
description: The service list description: The service list
@@ -701,6 +713,8 @@ paths:
security: security:
- oauth2: - oauth2:
- read:services - read:services
- read:services:name
- read:roles:services
parameters: parameters:
- name: name - name: name
description: service name description: service name
@@ -790,7 +804,7 @@ paths:
accepts passwords (e.g. not OAuth). accepts passwords (e.g. not OAuth).
security: security:
- oauth2: - oauth2:
- users:tokens # minrk: this is a deprecated alias to POST /users/{name}/tokens, either remove it or use the same scope - tokens
parameters: parameters:
- name: credentials - name: credentials
in: body in: body
@@ -817,7 +831,7 @@ paths:
summary: Identify a user or service from an API token summary: Identify a user or service from an API token
security: security:
- oauth2: - oauth2:
- read:users:tokens # minrk: is it really necessary to have a scope for this, or use self handler for token whoami? - (noscope)
parameters: parameters:
- name: token - name: token
in: path in: path

View File

@@ -17,7 +17,7 @@ To remedy situations like this, JupyterHub is transitioning to an RBAC system. B
## Definitions ## 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. 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.

View File

@@ -55,7 +55,7 @@ c.JupyterHub.load_roles = [
{ {
'name': 'server-rights', 'name': 'server-rights',
'description': 'Allows parties to start and stop user servers', 'description': 'Allows parties to start and stop user servers',
'scopes': ['users:servers'], 'scopes': ['servers'],
'users': ['alice', 'bob'], 'users': ['alice', 'bob'],
'services': ['idle-culler'], 'services': ['idle-culler'],
'groups': ['admin-group'], 'groups': ['admin-group'],

View File

@@ -7,7 +7,7 @@ A scope has a syntax-based design that reveals which resources it provides acces
## Scope conventions ## Scope conventions
- `<resource>` \ - `<resource>` \
The `<resource>` 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 `<resource>` 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:<resource>` \ - `read:<resource>` \
@@ -19,7 +19,7 @@ A scope has a syntax-based design that reveals which resources it provides acces
+++ +++
- `<resource>:<subresource>` \ - `<resource>:<subresource>` \
The {ref}`vertically filtered <vertical-filtering-target>` scopes provide access to a subset of the information granted by the `<resource>` scope. E.g., the scope `users:servers` allows for accessing user servers only. The {ref}`vertically filtered <vertical-filtering-target>` scopes provide access to a subset of the information granted by the `<resource>` scope. E.g., the scope `users:activity` only provides permission to post user activity.
+++ +++
- `<resource>!<object>=<objectname>` \ - `<resource>!<object>=<objectname>` \
@@ -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: 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!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. - `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. - `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. The `self` scope is only valid for user entities. In other cases (e.g., for services) it resolves to an empty set of scopes.

View File

@@ -38,7 +38,7 @@ Below follows a short tutorial on how to add a cull-idle service in the RBAC sys
{ {
"name": "idle-culler", "name": "idle-culler",
"description": "Culls idle servers", "description": "Culls idle servers",
"scopes": ["read:users:name", "read:users:activity", "users:servers"], "scopes": ["read:users:name", "read:users:activity", "servers"],
"services": ["idle-culler"], "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. 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: 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. - `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: The scopes required to access the API enpoints:
1. `admin:users` 1. `admin:users`
2. `users:servers` 2. `servers`
3. `admin:users:servers` 3. `admin:servers`
From the above, the role definition is: From the above, the role definition is:
@@ -77,7 +77,7 @@ c.JupyterHub.load_roles = [
{ {
"name": "api-launcher", "name": "api-launcher",
"description": "Manages servers", "description": "Manages servers",
"scopes": ["admin:users", "admin:users:servers"], "scopes": ["admin:users", "admin:servers"],
"services": [<service_name>] "services": [<service_name>]
} }
] ]
@@ -117,7 +117,7 @@ c.JupyterHub.load_roles = [
{ {
'name': 'teacher', 'name': 'teacher',
'description': 'Allows for accessing information about teacher group members and starting/stopping their servers', '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'] 'users': ['johan']
} }
] ]

View File

@@ -91,7 +91,7 @@ c.JupyterHub.load_roles = [
"name": "idle-culler", "name": "idle-culler",
"scopes": [ "scopes": [
"read:users:activity", # read user last_activity "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 # 'admin:users' # needed if culling idle users as well
] ]
} }

View File

@@ -45,7 +45,7 @@ $ curl -X GET http://127.0.0.1:8000/services/fastapi/me \
"servers": null, "servers": null,
"scopes": [ "scopes": [
"access:services", "access:services",
"access:users:servers!user=test-user", "access:servers!user=test-user",
"...", "...",
] ]
} }

View File

@@ -43,19 +43,19 @@ $ curl -H "Authorization: token 8630bbd8ef064c48b22c7f122f0cd8ad" http://127.0.0
], ],
"scopes": [ "scopes": [
"access:services", "access:services",
"access:users:servers!user=test", "access:servers!user=test",
"read:users!user=test", "read:users!user=test",
"read:users:activity!user=test", "read:users:activity!user=test",
"read:users:groups!user=test", "read:users:groups!user=test",
"read:users:name!user=test", "read:users:name!user=test",
"read:users:servers!user=test", "read:servers!user=test",
"read:users:tokens!user=test", "read:tokens!user=test",
"users!user=test", "users!user=test",
"users:activity!user=test", "users:activity!user=test",
"users:groups!user=test", "users:groups!user=test",
"users:name!user=test", "users:name!user=test",
"users:servers!user=test", "servers!user=test",
"users:tokens!user=test" "tokens!user=test"
], ],
"server": null "server": null
} }

View File

@@ -258,7 +258,7 @@ class OAuthAuthorizeHandler(OAuthHandler, BaseHandler):
# check for access to target resource # check for access to target resource
if client.spawner: 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') allowed = scope_filter(client.spawner, kind='server')
elif client.service: elif client.service:
scope_filter = self.get_scope_filter("access:services") scope_filter = self.get_scope_filter("access:services")

View File

@@ -143,7 +143,7 @@ class APIHandler(BaseHandler):
'user_options': spawner.user_options, 'user_options': spawner.user_options,
'progress_url': spawner._progress_url, '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'): if scope_filter(spawner, kind='server'):
model['state'] = spawner.get_state() model['state'] = spawner.get_state()
return model return model
@@ -219,9 +219,9 @@ class APIHandler(BaseHandler):
'read:users:name': {'kind', 'name', 'admin'}, 'read:users:name': {'kind', 'name', 'admin'},
'read:users:groups': {'kind', 'name', 'groups'}, 'read:users:groups': {'kind', 'name', 'groups'},
'read:users:activity': {'kind', 'name', 'last_activity'}, 'read:users:activity': {'kind', 'name', 'last_activity'},
'read:users:servers': {'kind', 'name', 'servers'}, 'read:servers': {'kind', 'name', 'servers'},
'read:users:roles': {'kind', 'name', 'roles', 'admin'}, 'read:roles:users': {'kind', 'name', 'roles', 'admin'},
'admin:users:auth_state': {'kind', 'name', 'auth_state'}, 'admin:auth_state': {'kind', 'name', 'auth_state'},
} }
self.log.debug( self.log.debug(
"Asking for user model of %s with scopes [%s]", "Asking for user model of %s with scopes [%s]",
@@ -237,7 +237,7 @@ class APIHandler(BaseHandler):
model['pending'] = user.spawners[''].pending model['pending'] = user.spawners[''].pending
servers = model['servers'] = {} 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(): for name, spawner in user.spawners.items():
# include 'active' servers, not just ready # include 'active' servers, not just ready
# (this includes pending events) # (this includes pending events)
@@ -258,7 +258,7 @@ class APIHandler(BaseHandler):
access_map = { access_map = {
'read:groups': {'kind', 'name', 'users'}, 'read:groups': {'kind', 'name', 'users'},
'read:groups:name': {'kind', 'name'}, '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') model = self._filter_model(model, access_map, group, 'group')
return model return model
@@ -290,7 +290,7 @@ class APIHandler(BaseHandler):
'display', 'display',
}, },
'read:services:name': {'kind', 'name', 'admin'}, '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') model = self._filter_model(model, access_map, service, 'service')
return model return model

View File

@@ -34,7 +34,7 @@ class _GroupAPIHandler(APIHandler):
class GroupListAPIHandler(_GroupAPIHandler): 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): def get(self):
"""List groups""" """List groups"""
query = self.db.query(orm.Group) query = self.db.query(orm.Group)
@@ -77,7 +77,7 @@ class GroupListAPIHandler(_GroupAPIHandler):
class GroupAPIHandler(_GroupAPIHandler): class GroupAPIHandler(_GroupAPIHandler):
"""View and modify groups by name""" """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): def get(self, group_name):
group = self.find_group(group_name) group = self.find_group(group_name)
self.write(json.dumps(self.group_model(group))) self.write(json.dumps(self.group_model(group)))

View File

@@ -11,7 +11,7 @@ from .base import APIHandler
class ServiceListAPIHandler(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): def get(self):
data = {} data = {}
for name, service in self.services.items(): for name, service in self.services.items():
@@ -22,7 +22,7 @@ class ServiceListAPIHandler(APIHandler):
class ServiceAPIHandler(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): def get(self, service_name):
service = self.services[service_name] service = self.services[service_name]
self.write(json.dumps(self.service_model(service))) self.write(json.dumps(self.service_model(service)))

View File

@@ -75,10 +75,10 @@ class UserListAPIHandler(APIHandler):
@needs_scope( @needs_scope(
'read:users', 'read:users',
'read:users:name', 'read:users:name',
'read:users:servers', 'read:servers',
'read:users:groups', 'read:users:groups',
'read:users:activity', 'read:users:activity',
'read:users:roles', 'read:roles:users',
) )
def get(self): def get(self):
state_filter = self.get_argument("state", None) state_filter = self.get_argument("state", None)
@@ -195,10 +195,10 @@ class UserAPIHandler(APIHandler):
@needs_scope( @needs_scope(
'read:users', 'read:users',
'read:users:name', 'read:users:name',
'read:users:servers', 'read:servers',
'read:users:groups', 'read:users:groups',
'read:users:activity', 'read:users:activity',
'read:users:roles', 'read:roles:users',
) )
async def get(self, user_name): async def get(self, user_name):
user = self.find_user(user_name) user = self.find_user(user_name)
@@ -297,7 +297,7 @@ class UserAPIHandler(APIHandler):
class UserTokenListAPIHandler(APIHandler): class UserTokenListAPIHandler(APIHandler):
"""API endpoint for listing/creating tokens""" """API endpoint for listing/creating tokens"""
@needs_scope('read:users:tokens') @needs_scope('read:tokens')
def get(self, user_name): def get(self, user_name):
"""Get tokens for a given user""" """Get tokens for a given user"""
user = self.find_user(user_name) user = self.find_user(user_name)
@@ -352,7 +352,7 @@ class UserTokenListAPIHandler(APIHandler):
self._resolve_roles_and_scopes() self._resolve_roles_and_scopes()
user = self.find_user(user_name) user = self.find_user(user_name)
kind = 'user' if isinstance(requester, User) else 'service' 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): if user is None or not scope_filter(user, kind):
raise web.HTTPError( raise web.HTTPError(
403, 403,
@@ -417,7 +417,7 @@ class UserTokenAPIHandler(APIHandler):
raise web.HTTPError(404, "Token not found %s", orm_token) raise web.HTTPError(404, "Token not found %s", orm_token)
return orm_token return orm_token
@needs_scope('read:users:tokens') @needs_scope('read:tokens')
def get(self, user_name, token_id): def get(self, user_name, token_id):
"""""" """"""
user = self.find_user(user_name) user = self.find_user(user_name)
@@ -426,7 +426,7 @@ class UserTokenAPIHandler(APIHandler):
token = self.find_token_by_id(user, token_id) token = self.find_token_by_id(user, token_id)
self.write(json.dumps(self.token_model(token))) self.write(json.dumps(self.token_model(token)))
@needs_scope('users:tokens') @needs_scope('tokens')
def delete(self, user_name, token_id): def delete(self, user_name, token_id):
"""Delete a token""" """Delete a token"""
user = self.find_user(user_name) user = self.find_user(user_name)
@@ -451,7 +451,7 @@ class UserTokenAPIHandler(APIHandler):
class UserServerAPIHandler(APIHandler): class UserServerAPIHandler(APIHandler):
"""Start and stop single-user servers""" """Start and stop single-user servers"""
@needs_scope('users:servers') @needs_scope('servers')
async def post(self, user_name, server_name=''): async def post(self, user_name, server_name=''):
user = self.find_user(user_name) user = self.find_user(user_name)
if server_name: if server_name:
@@ -496,7 +496,7 @@ class UserServerAPIHandler(APIHandler):
self.set_header('Content-Type', 'text/plain') self.set_header('Content-Type', 'text/plain')
self.set_status(status) self.set_status(status)
@needs_scope('users:servers') @needs_scope('servers')
async def delete(self, user_name, server_name=''): async def delete(self, user_name, server_name=''):
user = self.find_user(user_name) user = self.find_user(user_name)
options = self.get_json_body() 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. 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): def post(self, user_name):
self.log.warning( self.log.warning(
"Deprecated in JupyterHub 0.8." "Deprecated in JupyterHub 0.8."
@@ -625,7 +625,7 @@ class SpawnProgressAPIHandler(APIHandler):
await asyncio.wait([self._finish_future], timeout=self.keepalive_interval) 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=''): async def get(self, user_name, server_name=''):
self.set_header('Cache-Control', 'no-cache') self.set_header('Cache-Control', 'no-cache')
if server_name is None: if server_name is None:

View File

@@ -456,7 +456,7 @@ class AdminHandler(BaseHandler):
@web.authenticated @web.authenticated
@needs_scope('users') @needs_scope('users')
@needs_scope('admin:users') @needs_scope('admin:users')
@needs_scope('admin:users:servers') @needs_scope('admin:servers')
async def get(self): async def get(self):
auth_state = await self.current_user.get_auth_state() auth_state = await self.current_user.get_auth_state()
html = await self.render_template( html = await self.render_template(

View File

@@ -33,16 +33,16 @@ def get_default_roles():
'description': 'Elevated privileges (can do anything)', 'description': 'Elevated privileges (can do anything)',
'scopes': [ 'scopes': [
'admin:users', 'admin:users',
'admin:users:servers', 'admin:servers',
'users:tokens', 'tokens',
'admin:groups', 'admin:groups',
'read:services', 'read:services',
'read:hub', 'read:hub',
'proxy', 'proxy',
'shutdown', 'shutdown',
'access:services', 'access:services',
'access:users:servers', 'access:servers',
'read:services:roles', 'read:roles',
], ],
}, },
{ {
@@ -50,7 +50,7 @@ def get_default_roles():
'description': 'Post activity only', 'description': 'Post activity only',
'scopes': [ 'scopes': [
'users:activity!user', 'users:activity!user',
'access:users:servers!user', 'access:servers!user',
], ],
}, },
{ {
@@ -70,9 +70,10 @@ def expand_self_scope(name):
users:name users:name
users:groups users:groups
users:activity users:activity
users:servers tokens
users:tokens servers
access:users:servers access:servers
Arguments: Arguments:
name (str): user name name (str): user name
@@ -87,11 +88,11 @@ def expand_self_scope(name):
'read:users:groups', 'read:users:groups',
'users:activity', 'users:activity',
'read:users:activity', 'read:users:activity',
'users:servers', 'servers',
'read:users:servers', 'read:servers',
'users:tokens', 'tokens',
'read:users:tokens', 'read:tokens',
'access:users:servers', 'access:servers',
] ]
return {"{}!user={}".format(scope, name) for scope in scope_list} return {"{}!user={}".format(scope, name) for scope in scope_list}

View File

@@ -34,9 +34,9 @@ scope_definitions = {
}, },
'admin:users': { 'admin:users': {
'description': 'Read, write, create and delete users and their authentication state, not including their servers or tokens.', '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 users authentication state.'}, 'admin:auth_state': {'description': 'Read a users authentication state.'},
'users': { 'users': {
'description': 'Read and write permissions to user models (excluding servers, tokens and authentication state).', 'description': 'Read and write permissions to user models (excluding servers, tokens and authentication state).',
'subscopes': ['read:users', 'users:activity'], 'subscopes': ['read:users', 'users:activity'],
@@ -52,32 +52,38 @@ scope_definitions = {
'read:users:name': {'description': 'Read names of users.'}, 'read:users:name': {'description': 'Read names of users.'},
'read:users:groups': {'description': 'Read users group membership.'}, 'read:users:groups': {'description': 'Read users group membership.'},
'read:users:activity': {'description': 'Read time of last user activity.'}, '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': { 'users:activity': {
'description': 'Update time of last user activity.', 'description': 'Update time of last user activity.',
'subscopes': ['read:users:activity'], 'subscopes': ['read:users:activity'],
}, },
'admin:users:servers': { 'admin:servers': {
'description': 'Read, start, stop, create and delete user servers and their state.', '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.'}, 'admin:server_state': {'description': 'Read and write users server state.'},
'users:servers': { 'servers': {
'description': 'Start and stop user 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).', 'description': 'Read users names and their server models (excluding the server state).',
'subscopes': ['read:users:name'], 'subscopes': ['read:users:name'],
}, },
'users:tokens': { 'tokens': {
'description': 'Read, write, create and delete user 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': { 'admin:groups': {
'description': 'Read and write group information, create and delete groups.', 'description': 'Read and write group information, create and delete groups.',
'subscopes': ['groups', 'read:groups:roles'], 'subscopes': ['groups', 'read:roles:groups'],
}, },
'groups': { 'groups': {
'description': 'Read and write group information, including adding/removing users to/from groups.', 'description': 'Read and write group information, including adding/removing users to/from groups.',
@@ -88,15 +94,13 @@ scope_definitions = {
'subscopes': ['read:groups:name'], 'subscopes': ['read:groups:name'],
}, },
'read:groups:name': {'description': 'Read group names.'}, 'read:groups:name': {'description': 'Read group names.'},
'read:groups:roles': {'description': 'Read group role assignments.'},
'read:services': { 'read:services': {
'description': 'Read service models.', 'description': 'Read service models.',
'subscopes': ['read:services:name'], 'subscopes': ['read:services:name'],
}, },
'read:services:name': {'description': 'Read service names.'}, 'read:services:name': {'description': 'Read service names.'},
'read:services:roles': {'description': 'Read service role assignments.'},
'read:hub': {'description': 'Read detailed information about the Hub.'}, 'read:hub': {'description': 'Read detailed information about the Hub.'},
'access:users:servers': { 'access:servers': {
'description': 'Access user servers via API or browser.', 'description': 'Access user servers via API or browser.',
}, },
'access:services': { 'access:services': {
@@ -279,7 +283,7 @@ def get_scopes_for(orm_object):
spawner = orm_object.oauth_client.spawner spawner = orm_object.oauth_client.spawner
if spawner: if spawner:
token_scopes.add( token_scopes.add(
f"access:users:servers!server={spawner.user.name}/{spawner.name}" f"access:servers!server={spawner.user.name}/{spawner.name}"
) )
else: else:
service = orm_object.oauth_client.service 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 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 would lead to scope model
{ {
"users":scope.ALL, "users":scope.ALL,
@@ -397,7 +401,7 @@ def parse_scopes(scope_list):
"alice" "alice"
] ]
}, },
"users:servers":{ "servers":{
"server":[ "server":[
"user/bar", "user/bar",
"user/baz" "user/baz"

View File

@@ -835,8 +835,8 @@ class HubAuthenticated:
which in turn is set by $JUPYTERHUB_OAUTH_SCOPES which in turn is set by $JUPYTERHUB_OAUTH_SCOPES
Default values include: Default values include:
- 'access:services', 'access:services!service={service_name}' for services - 'access:services', 'access:services!service={service_name}' for services
- 'access:users:servers', 'access:users:servers!user={user}', - 'access:servers', 'access:servers!user={user}',
'access:users:servers!server={user}/{server_name}' 'access:servers!server={user}/{server_name}'
for single-user servers for single-user servers
If hub_scopes is not used (e.g. JupyterHub 1.x), If hub_scopes is not used (e.g. JupyterHub 1.x),

View File

@@ -222,8 +222,8 @@ class Spawner(LoggingConfigurable):
@default("oauth_scopes") @default("oauth_scopes")
def _default_oauth_scopes(self): def _default_oauth_scopes(self):
return [ return [
f"access:users:servers!server={self.user.name}/{self.name}", f"access:servers!server={self.user.name}/{self.name}",
f"access:users:servers!user={self.user.name}", f"access:servers!user={self.user.name}",
] ]
handler = Any() handler = Any()

View File

@@ -956,7 +956,7 @@ async def test_oauth_page_scope_appearance(
[ [
'self', 'self',
'read:users!user=gawain', 'read:users!user=gawain',
'read:users:tokens', 'read:tokens',
'read:groups!group=mythos', 'read:groups!group=mythos',
] ]
) )

View File

@@ -180,13 +180,13 @@ def test_orm_roles_delete_cascade(db):
['admin:users'], ['admin:users'],
{ {
'admin:users', 'admin:users',
'admin:users:auth_state', 'admin:auth_state',
'users', 'users',
'read:users', 'read:users',
'users:activity', 'users:activity',
'read:users:name', 'read:users:name',
'read:users:groups', 'read:users:groups',
'read:users:roles', 'read:roles:users',
'read:users:activity', 'read:users:activity',
}, },
), ),
@@ -210,32 +210,32 @@ def test_orm_roles_delete_cascade(db):
'read:users:activity', 'read:users:activity',
}, },
), ),
(['read:users:servers'], {'read:users:servers', 'read:users:name'}), (['read:servers'], {'read:servers', 'read:users:name'}),
( (
['admin:groups'], ['admin:groups'],
{ {
'admin:groups', 'admin:groups',
'groups', 'groups',
'read:groups', 'read:groups',
'read:groups:roles', 'read:roles:groups',
'read:groups:name', 'read:groups:name',
}, },
), ),
( (
['admin:groups', 'read:users:servers'], ['admin:groups', 'read:servers'],
{ {
'admin:groups', 'admin:groups',
'groups', 'groups',
'read:groups', 'read:groups',
'read:groups:roles', 'read:roles:groups',
'read:groups:name', 'read:groups:name',
'read:users:servers', 'read:servers',
'read:users:name', 'read:users:name',
}, },
), ),
( (
['users:tokens!group=hobbits'], ['tokens!group=hobbits'],
{'users:tokens!group=hobbits', 'read:users: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',
'users!user=charlie', 'users!user=charlie',
'admin:groups', 'admin:groups',
'read:users:tokens', 'read:tokens',
], ],
}, },
'existing', 'existing',
@@ -501,8 +501,8 @@ async def test_load_roles_services(tmpdir, request):
'scopes': [ 'scopes': [
'read:users:name', 'read:users:name',
'read:users:activity', 'read:users:activity',
'read:users:servers', 'read:servers',
'users:servers', 'servers',
], ],
'services': ['idle-culler'], 'services': ['idle-culler'],
}, },

View File

@@ -79,9 +79,9 @@ def test_scope_multiple_filters():
def test_scope_parse_server_name(): def test_scope_parse_server_name():
handler = get_handler_with_scopes( 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: class MockAPIHandler:
@@ -99,7 +99,7 @@ class MockAPIHandler:
def user_thing(self, user_name): def user_thing(self, user_name):
return True return True
@needs_scope('users:servers') @needs_scope('servers')
def server_thing(self, user_name, server_name): def server_thing(self, user_name, server_name):
return True return True
@@ -140,18 +140,18 @@ def mock_handler():
(['users!user=george'], 'user_thing', ('fake_user',), False), (['users!user=george'], 'user_thing', ('fake_user',), False),
(['users!user=george'], 'user_thing', ('oscar',), False), (['users!user=george'], 'user_thing', ('oscar',), False),
(['users!user=george', 'users!user=oscar'], 'user_thing', ('oscar',), True), (['users!user=george', 'users!user=oscar'], 'user_thing', ('oscar',), True),
(['users:servers'], 'server_thing', ('user1', 'server_1'), True), (['servers'], 'server_thing', ('user1', 'server_1'), True),
(['users:servers'], 'server_thing', ('user1', ''), True), (['servers'], 'server_thing', ('user1', ''), True),
(['users:servers'], 'server_thing', ('user1', None), True), (['servers'], 'server_thing', ('user1', None), True),
( (
['users:servers!server=maeby/bluth'], ['servers!server=maeby/bluth'],
'server_thing', 'server_thing',
('maeby', 'bluth'), ('maeby', 'bluth'),
True, 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', 'server_thing',
('maybe', 'bluth2'), ('maybe', 'bluth2'),
False, False,
@@ -488,17 +488,17 @@ async def test_metascope_all_expansion(app, create_user_with_scopes):
@mark.parametrize( @mark.parametrize(
"scopes, can_stop ,num_servers, keys_in, keys_out", "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()), (['admin:users', 'read:users'], False, 0, set(), set()),
( (
['read:users:servers!group=nuts', 'users:servers'], ['read:servers!group=nuts', 'servers'],
True, True,
2, 2,
{'name'}, {'name'},
{'state'}, {'state'},
), ),
( (
['admin:users:server_state', 'read:users:servers'], ['admin:server_state', 'read:servers'],
False, False,
2, 2,
{'name', 'state'}, {'name', 'state'},
@@ -506,8 +506,8 @@ async def test_metascope_all_expansion(app, create_user_with_scopes):
), ),
( (
[ [
'read:users:servers!server=almond/bianca', 'read:servers!server=almond/bianca',
'admin:users:server_state!server=almond/bianca', 'admin:server_state!server=almond/bianca',
], ],
False, False,
1, 1,
@@ -634,8 +634,8 @@ async def test_server_state_access(
), ),
( (
'no_intersection_user_server', 'no_intersection_user_server',
['users:servers!user=y'], ['servers!user=y'],
['users:servers!server=x'], ['servers!server=x'],
set(), 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'}, {'name', 'kind', 'roles', 'admin'},
), ),
({'read:services:name'}, {'name', 'kind', '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'}, {'name', 'kind', 'roles'},
), ),
({'read:groups:name'}, {'name', 'kind'}), ({'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): async def test_roles_access(app, create_service_with_scopes, create_user_with_scopes):
user = add_user(app.db, name='miranda') 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( r = await api_request(
app, 'users', user.name, headers=auth_header(app.db, read_user.name) 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 # resolves server under user, without warning
( (
set(["read:users:servers!user=abc"]), set(["read:servers!user=abc"]),
set(["read:users:servers!server=abc/xyz"]), set(["read:servers!server=abc/xyz"]),
set(["read:users:servers!server=abc/xyz"]), set(["read:servers!server=abc/xyz"]),
False, False,
), ),
# user->server, no match # user->server, no match
( (
set(["read:users:servers!user=abc"]), set(["read:servers!user=abc"]),
set(["read:users:servers!server=abcd/xyz"]), set(["read:servers!server=abcd/xyz"]),
set([]), set([]),
False, False,
), ),

View File

@@ -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, False,
), ),

View File

@@ -17,18 +17,18 @@ from .utils import AsyncSession
@pytest.mark.parametrize( @pytest.mark.parametrize(
"access_scopes, server_name, expect_success", "access_scopes, server_name, expect_success",
[ [
(["access:users:servers!group=$group"], "", True), (["access:servers!group=$group"], "", True),
(["access:users:servers!group=other-group"], "", False), (["access:servers!group=other-group"], "", False),
(["access:users:servers"], "", True), (["access:servers"], "", True),
(["access:users:servers"], "named", True), (["access:servers"], "named", True),
(["access:users:servers!user=$user"], "", True), (["access:servers!user=$user"], "", True),
(["access:users:servers!user=$user"], "named", True), (["access:servers!user=$user"], "named", True),
(["access:users:servers!server=$server"], "", True), (["access:servers!server=$server"], "", True),
(["access:users:servers!server=$server"], "named-server", True), (["access:servers!server=$server"], "named-server", True),
(["access:users:servers!server=$user/other"], "", False), (["access:servers!server=$user/other"], "", False),
(["access:users:servers!server=$user/other"], "some-name", False), (["access:servers!server=$user/other"], "some-name", False),
(["access:users:servers!user=$other"], "", False), (["access:servers!user=$other"], "", False),
(["access:users:servers!user=$other"], "named", False), (["access:servers!user=$other"], "named", False),
(["access:services"], "", False), (["access:services"], "", False),
(["self"], "named", False), (["self"], "named", False),
([], "", False), ([], "", False),