diff --git a/.gitignore b/.gitignore index 7683d902..c2a2ef40 100644 --- a/.gitignore +++ b/.gitignore @@ -7,8 +7,6 @@ node_modules dist docs/_build docs/build -docs/source/_static/rest-api -docs/source/rbac/scope-table.md docs/source/reference/metrics.md .ipynb_checkpoints diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 58f2ee14..0fdf7ea9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -33,7 +33,7 @@ repos: rev: v4.0.0-alpha.8 hooks: - id: prettier - exclude: .*/templates/.* + exclude: .*/templates/.*|docs/source/_static/rest-api.yml|docs/source/rbac/scope-table.md # autoformat HTML templates - repo: https://github.com/djlint/djLint @@ -56,3 +56,16 @@ repos: - id: requirements-txt-fixer - id: check-case-conflict - id: check-executables-have-shebangs + + # source docs: rest-api.yml and scope-table.md are autogenerated + - repo: local + hooks: + - id: update-api-and-scope-docs + name: Update rest-api.yml and scope-table.md based on scopes.py + language: python + additional_dependencies: ["pytablewriter", "ruamel.yaml"] + entry: python docs/source/rbac/generate-scope-table.py + args: + - --update + files: jupyterhub/scopes.py + pass_filenames: false diff --git a/docs/Makefile b/docs/Makefile index 6be01454..b824651f 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -35,7 +35,7 @@ help: # - NOTE: If the pre-requisites for the html target is updated, also update the # Read The Docs section in docs/source/conf.py. # -html: metrics scopes +html: metrics $(SPHINXBUILD) -b html "$(SOURCEDIR)" "$(BUILDDIR)/html" $(SPHINXOPTS) @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." @@ -44,10 +44,6 @@ metrics: source/reference/metrics.md source/reference/metrics.md: python3 generate-metrics.py -scopes: source/rbac/scope-table.md -source/rbac/scope-table.md: - python3 source/rbac/generate-scope-table.py - # Manually added targets - related to development # ---------------------------------------------------------------------------- @@ -56,7 +52,7 @@ source/rbac/scope-table.md: # - requires sphinx-autobuild, see # https://sphinxcontrib-spelling.readthedocs.io/en/latest/ # - builds and rebuilds html on changes to source, but does not re-generate -# metrics/scopes files +# metrics files # - starts a livereload enabled webserver and opens up a browser devenv: html sphinx-autobuild -b html --open-browser "$(SOURCEDIR)" "$(BUILDDIR)/html" diff --git a/docs/source/_static/rest-api.yml b/docs/source/_static/rest-api.yml index 191687f5..2aa05d42 100644 --- a/docs/source/_static/rest-api.yml +++ b/docs/source/_static/rest-api.yml @@ -62,8 +62,7 @@ paths: properties: class: type: string - description: - The Python class currently active for JupyterHub + description: The Python class currently active for JupyterHub Authentication version: type: string @@ -73,8 +72,7 @@ paths: properties: class: type: string - description: - The Python class currently active for spawning + description: The Python class currently active for spawning single-user notebook servers version: type: string @@ -258,8 +256,7 @@ paths: parameters: - $ref: "#/components/parameters/userName" requestBody: - description: - Updated user info. At least one key to be updated (name or admin) + description: Updated user info. At least one key to be updated (name or admin) is required. content: application/json: @@ -268,13 +265,11 @@ paths: properties: name: type: string - description: - the new name (optional, if another key is updated i.e. + description: the new name (optional, if another key is updated i.e. admin) admin: type: boolean - description: - update admin (optional, if another key is updated i.e. + description: update admin (optional, if another key is updated i.e. name) required: true responses: @@ -291,8 +286,7 @@ paths: post: operationId: post-user-activity summary: Notify Hub of activity for a given user - description: - Notify the Hub of activity by the user, e.g. accessing a service + description: Notify the Hub of activity by the user, e.g. accessing a service or (more likely) actively using a server. parameters: - $ref: "#/components/parameters/userName" @@ -372,8 +366,7 @@ paths: description: The user's notebook server has started content: {} 202: - description: - The user's notebook server has not yet started, but has been + description: The user's notebook server has not yet started, but has been requested content: {} security: @@ -387,8 +380,7 @@ paths: - $ref: "#/components/parameters/userName" responses: 202: - description: - The user's notebook server has not yet stopped as it is taking + description: The user's notebook server has not yet stopped as it is taking a while to stop content: {} 204: @@ -420,8 +412,7 @@ paths: description: The user's notebook named-server has started content: {} 202: - description: - The user's notebook named-server has not yet started, but has + description: The user's notebook named-server has not yet started, but has been requested content: {} security: @@ -457,8 +448,7 @@ paths: required: false responses: 202: - description: - The user's notebook named-server has not yet stopped as it + description: The user's notebook named-server has not yet stopped as it is taking a while to stop content: {} 204: @@ -472,8 +462,7 @@ paths: get: operationId: get-user-shared summary: List servers shared with user - description: - Returns list of Shares granting the user access to servers owned + description: Returns list of Shares granting the user access to servers owned by others (new in 5.0) parameters: - $ref: "#/components/parameters/userName" @@ -587,8 +576,7 @@ paths: expires_in: type: number example: 3600 - description: - lifetime (in seconds) after which the requested token + description: lifetime (in seconds) after which the requested token will expire. Omit, or specify null or 0 for no expiration. note: type: string @@ -1262,8 +1250,7 @@ paths: get: operationId: get-proxy summary: Get the proxy's routing table - description: - A convenience alias for getting the routing table directly from + description: A convenience alias for getting the routing table directly from the proxy parameters: - $ref: "#/components/parameters/paginationOffset" @@ -1275,8 +1262,7 @@ paths: application/json: schema: type: object - description: - configurable-http-proxy routing table (see configurable-http-proxy + description: configurable-http-proxy routing table (see configurable-http-proxy docs for details) security: - oauth2: @@ -1296,8 +1282,7 @@ paths: summary: Notify the Hub about a new proxy description: Notifies the Hub of a new proxy to use. requestBody: - description: - Any values that have changed for the new proxy. All keys are + description: Any values that have changed for the new proxy. All keys are optional. content: application/json: @@ -1389,8 +1374,7 @@ paths: get: operationId: get-auth-cookie summary: Identify a user from a cookie - description: - Used by single-user notebook servers to hand off cookie authentication + description: Used by single-user notebook servers to hand off cookie authentication to the Hub parameters: - name: cookie_name @@ -1515,13 +1499,11 @@ paths: properties: proxy: type: boolean - description: - Whether the proxy should be shutdown as well (default + description: Whether the proxy should be shutdown as well (default from Hub config) servers: type: boolean - description: - Whether users' notebook servers should be shutdown + description: Whether users' notebook servers should be shutdown as well (default from Hub config) required: false responses: @@ -1666,8 +1648,7 @@ components: type: string server: type: string - description: - The user's notebook server's base URL, if running; null if + description: The user's notebook server's base URL, if running; null if not. pending: type: string @@ -1699,8 +1680,7 @@ components: properties: name: type: string - description: - The server's name. The user's default server has an empty name + description: The server's name. The user's default server has an empty name ('') ready: type: boolean @@ -1763,15 +1743,13 @@ components: state: type: object properties: {} - description: - Arbitrary internal state from this server's spawner. Only available + description: Arbitrary internal state from this server's spawner. Only available on the hub's users list or get-user-by-name method, and only with admin:users:server_state scope. None otherwise. user_options: type: object properties: {} - description: - User specified options for the user's spawned instance of a + description: User specified options for the user's spawned instance of a single-user server. RequestIdentity: description: | @@ -1940,8 +1918,7 @@ components: items: type: string group: - description: - the group being shared with (exactly one of 'user' or 'group' + description: the group being shared with (exactly one of 'user' or 'group' will be non-null, the other will be null) type: - object @@ -1950,8 +1927,7 @@ components: name: type: string user: - description: - the user being shared with (exactly one of 'user' or 'group' + description: the user being shared with (exactly one of 'user' or 'group' will be non-null, the other will be null) type: - object @@ -1965,8 +1941,7 @@ components: format: date-time ShareCode: - description: - A single sharing code. There is at most one of these objects per + description: A single sharing code. There is at most one of these objects per (server, user) or (server, group) combination. type: object properties: @@ -2002,8 +1977,7 @@ components: properties: id: type: string - description: - The id of the API token. Used for modifying or deleting the + description: The id of the API token. Used for modifying or deleting the token. user: type: string @@ -2013,22 +1987,19 @@ components: description: The service that owns the token (undefined of owned by a user) roles: type: array - description: - Deprecated in JupyterHub 3, always an empty list. Tokens have + description: Deprecated in JupyterHub 3, always an empty list. Tokens have 'scopes' starting from JupyterHub 3. items: type: string scopes: type: array - description: - List of scopes this token has been assigned. New in JupyterHub + description: List of scopes this token has been assigned. New in JupyterHub 3. In JupyterHub 2.x, tokens were assigned 'roles' instead of scopes. items: type: string note: type: string - description: - A note about the token, typically describing what it was created + description: A note about the token, typically describing what it was created for. created: type: string @@ -2059,13 +2030,11 @@ components: properties: token: type: string - description: - The token itself. Only present in responses to requests for + description: The token itself. Only present in responses to requests for a new token. id: type: string - description: - The id of the API token. Used for modifying or deleting the + description: The id of the API token. Used for modifying or deleting the token. user: type: string @@ -2075,22 +2044,19 @@ components: description: The service that owns the token (undefined of owned by a user) roles: type: array - description: - Deprecated in JupyterHub 3, always an empty list. Tokens have + description: Deprecated in JupyterHub 3, always an empty list. Tokens have 'scopes' starting from JupyterHub 3. items: type: string scopes: type: array - description: - List of scopes this token has been assigned. New in JupyterHub + description: List of scopes this token has been assigned. New in JupyterHub 3. In JupyterHub 2.x, tokens were assigned 'roles' instead of scopes. items: type: string note: type: string - description: - A note about the token, typically describing what it was created + description: A note about the token, typically describing what it was created for. created: type: string @@ -2128,27 +2094,21 @@ components: tokenUrl: /hub/api/oauth2/token scopes: (no_scope): Identify the owner of the requesting entity. - self: - The user’s own resources _(metascope for users, resolves to (no_scope) + self: The user’s own resources _(metascope for users, resolves to (no_scope) for services)_ - inherit: - Everything that the token-owning entity can access _(metascope + inherit: Everything that the token-owning entity can access _(metascope for tokens)_ - admin-ui: - Access the admin page. Permission to take actions via the admin + admin-ui: Access the admin page. Permission to take actions via the admin page granted separately. - admin:users: - Read, modify, create, and delete users and their authentication + admin:users: Read, modify, create, and delete users and their authentication state, not including their servers or tokens. This is an extremely privileged scope and should be considered tantamount to superuser. admin:auth_state: Read a user’s authentication state. - users: - Read and write permissions to user models (excluding servers, tokens + users: Read and write permissions to user models (excluding servers, tokens and authentication state). delete:users: Delete users. list:users: List users, including at least their names. - read:users: - Read user models (including the URL of the default server + read:users: Read user models (including the URL of the default server if it is running). read:users:name: Read names of users. read:users:groups: Read users’ group membership. @@ -2158,27 +2118,23 @@ components: read:roles:services: Read service role assignments. read:roles:groups: Read group role assignments. users:activity: Update time of last user activity. - admin:servers: - Read, start, stop, create and delete user servers and their + admin:servers: Read, start, stop, create and delete user servers and their state. admin:server_state: Read and write users’ server state. servers: Start and stop user servers. - read:servers: - Read users’ names and their server models (excluding the + read:servers: Read users’ names and their server models (excluding the server state). delete:servers: Stop and delete users' servers. tokens: Read, write, create and delete user tokens. read:tokens: Read user tokens. admin:groups: Read and write group information, create and delete groups. - groups: - "Read and write group information, including adding/removing any - users to/from groups. Note: adding users to groups may affect permissions." + groups: 'Read and write group information, including adding/removing any + users to/from groups. Note: adding users to groups may affect permissions.' list:groups: List groups, including at least their names. read:groups: Read group models. read:groups:name: Read group names. delete:groups: Delete groups. - admin:services: - Create, read, update, delete services, not including services + admin:services: Create, read, update, delete services, not including services defined from config files. list:services: List services, including at least their names. read:services: Read service models. @@ -2192,8 +2148,7 @@ components: read:groups:shares: Read servers shared with a group. read:shares: Read information about shared access to servers. shares: Manage access to shared servers. - proxy: - Read information about the proxy’s routing table, sync the Hub + proxy: Read information about the proxy’s routing table, sync the Hub with the proxy and notify the Hub about a new proxy. shutdown: Shutdown the hub. read:metrics: Read prometheus metrics. diff --git a/docs/source/rbac/generate-scope-table.py b/docs/source/rbac/generate-scope-table.py index 374871a7..f793268e 100644 --- a/docs/source/rbac/generate-scope-table.py +++ b/docs/source/rbac/generate-scope-table.py @@ -14,26 +14,52 @@ The files are: scopes descriptions are updated in it. """ -import os from collections import defaultdict from pathlib import Path -from subprocess import run from pytablewriter import MarkdownTableWriter from ruamel.yaml import YAML -from jupyterhub import __version__ -from jupyterhub.scopes import scope_definitions - -HERE = os.path.abspath(os.path.dirname(__file__)) -DOCS = Path(HERE).parent.parent.absolute() +HERE = Path(__file__).parent.absolute() +DOCS = HERE / ".." / ".." REST_API_YAML = DOCS.joinpath("source", "_static", "rest-api.yml") -SCOPE_TABLE_MD = Path(HERE).joinpath("scope-table.md") +SCOPE_TABLE_MD = HERE.joinpath("scope-table.md") + + +def _load_jupyterhub_info(): + """ + The equivalent of + + from jupyterhub import __version__ + from jupyterhub.scopes import scope_definitions + + but without needing to install JupyterHub and dependencies + so that we can run this pre-commit + """ + root = HERE / ".." / ".." / ".." + g = {} + exec((root / "jupyterhub" / "_version.py").read_text(), g) + + # To avoid parsing the whole of scope_definitions.py just pull out + # the relevant lines + scopes_file = root / "jupyterhub" / "scopes.py" + scopes_lines = [] + for line in scopes_file.read_text().splitlines(): + if not scopes_lines and line == "scope_definitions = {": + scopes_lines.append(line) + elif scopes_lines: + scopes_lines.append(line) + if line == "}": + break + + exec("\n".join(scopes_lines), g) + + return g["__version__"], g["scope_definitions"] class ScopeTableGenerator: def __init__(self): - self.scopes = scope_definitions + self.version, self.scopes = _load_jupyterhub_info() @classmethod def create_writer(cls, table_name, headers, values): @@ -131,7 +157,7 @@ class ScopeTableGenerator: with open(filename) as f: content = yaml.load(f.read()) - content["info"]["version"] = __version__ + content["info"]["version"] = self.version for scope in self.scopes: description = self.scopes[scope]['description'] doc_description = self.scopes[scope].get('doc_description', '') @@ -145,12 +171,6 @@ class ScopeTableGenerator: with open(filename, 'w') as f: yaml.dump(content, f) - run( - ['pre-commit', 'run', 'prettier', '--files', filename], - cwd=HERE, - check=False, - ) - def main(): table_generator = ScopeTableGenerator() diff --git a/docs/source/rbac/scope-table.md b/docs/source/rbac/scope-table.md new file mode 100644 index 00000000..8b247eb4 --- /dev/null +++ b/docs/source/rbac/scope-table.md @@ -0,0 +1,58 @@ +Table 1. Available scopes and their hierarchy +| Scope | Grants permission to: | +| --------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `(no_scope)` | Identify the owner of the requesting entity. | +| `self` | The user’s own resources _(metascope for users, resolves to (no_scope) for services)_ | +| `inherit` | Everything that the token-owning entity can access _(metascope for tokens)_ | +| `admin-ui` | Access the admin page. Permission to take actions via the admin page granted separately. | +| `admin:users` | Read, modify, create, and delete users and their authentication state, not including their servers or tokens. This is an extremely privileged scope and should be considered tantamount to superuser. | +|    `admin:auth_state` | Read a user’s authentication state. | +|    `users` | Read and write permissions to user models (excluding servers, tokens and authentication state). | +|       `read:users` | Read user models (including servers, tokens and authentication state). | +|          `read:users:name` | Read names of users. | +|          `read:users:groups` | Read users’ group membership. | +|          `read:users:activity` | Read time of last user activity. | +|       `list:users` | List users, including at least their names. | +|          `read:users:name` | Read names of users. | +|       `users:activity` | Update time of last user activity. | +|          `read:users:activity` | Read time of last user activity. | +|    `read:roles:users` | Read user role assignments. | +|    `delete:users` | Delete users. | +| `read:roles` | Read role assignments. | +|    `read:roles:users` | Read user role assignments. | +|    `read:roles:services` | Read service role assignments. | +|    `read:roles:groups` | Read group role assignments. | +| `admin:servers` | Read, start, stop, create and delete user servers and their state. | +|    `admin:server_state` | Read and write users’ server state. | +|    `servers` | Start and stop user servers. | +|       `read:servers` | Read users’ names and their server models (excluding the server state). | +|          `read:users:name` | Read names of users. | +|       `delete:servers` | Stop and delete users' servers. | +| `tokens` | Read, write, create and delete user tokens. | +|    `read:tokens` | Read user tokens. | +| `admin:groups` | Read and write group information, create and delete groups. | +|    `groups` | Read and write group information, including adding/removing any users to/from groups. Note: adding users to groups may affect permissions. | +|       `read:groups` | Read group models. | +|          `read:groups:name` | Read group names. | +|       `list:groups` | List groups, including at least their names. | +|          `read:groups:name` | Read group names. | +|    `read:roles:groups` | Read group role assignments. | +|    `delete:groups` | Delete groups. | +| `admin:services` | Create, read, update, delete services, not including services defined from config files. | +|    `list:services` | List services, including at least their names. | +|       `read:services:name` | Read service names. | +|    `read:services` | Read service models. | +|       `read:services:name` | Read service names. | +|    `read:roles:services` | Read service role assignments. | +| `read:hub` | Read detailed information about the Hub. | +| `access:services` | Access services via API or browser. | +| `shares` | Manage access to shared servers. | +|    `access:servers` | Access user servers via API or browser. | +|    `read:shares` | Read information about shared access to servers. | +|    `users:shares` | Read and revoke a user's access to shared servers. | +|       `read:users:shares` | Read servers shared with a user. | +|    `groups:shares` | Read and revoke a group's access to shared servers. | +|       `read:groups:shares` | Read servers shared with a group. | +| `proxy` | Read information about the proxy’s routing table, sync the Hub with the proxy and notify the Hub about a new proxy. | +| `shutdown` | Shutdown the hub. | +| `read:metrics` | Read prometheus metrics. | diff --git a/jupyterhub/scopes.py b/jupyterhub/scopes.py index 76d15149..2c81b1a3 100644 --- a/jupyterhub/scopes.py +++ b/jupyterhub/scopes.py @@ -31,8 +31,11 @@ from tornado.log import app_log from . import orm, roles from ._memoize import DoNotCache, FrozenDict, lru_cache_key -"""when modifying the scope definitions, make sure that `docs/source/rbac/generate-scope-table.py` is run - so that changes are reflected in the documentation and REST API description.""" +"""when modifying the scope definitions + `docs/source/rbac/generate-scope-table.py` must be run + so that changes are reflected in the documentation and REST API description. + `pre-commit run -a` should automatically take care of this. + """ scope_definitions = { '(no_scope)': {'description': 'Identify the owner of the requesting entity.'}, 'self': {