""" This script updates two files with the RBAC scope descriptions found in `scopes.py`. The files are: 1. scope-table.md This file is git ignored and referenced by the documentation. 2. rest-api.yml This file is JupyterHub's REST API schema. Both a version and the RBAC scopes descriptions are updated in it. """ from collections import defaultdict from pathlib import Path from pytablewriter import MarkdownTableWriter from ruamel.yaml import YAML HERE = Path(__file__).parent.absolute() DOCS = HERE / ".." / ".." REST_API_YAML = DOCS.joinpath("source", "_static", "rest-api.yml") 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.version, self.scopes = _load_jupyterhub_info() @classmethod def create_writer(cls, table_name, headers, values): writer = MarkdownTableWriter() writer.table_name = table_name writer.headers = headers writer.value_matrix = values writer.margin = 1 return writer def _get_scope_relationships(self): """Returns a tuple of dictionary of all scope-subscope pairs and a list of just subscopes: ({scope: subscope}, [subscopes]) used for creating hierarchical scope table in _parse_scopes() """ pairs = [] for scope, data in self.scopes.items(): subscopes = data.get('subscopes') if subscopes is not None: for subscope in subscopes: pairs.append((scope, subscope)) else: pairs.append((scope, None)) subscopes = [pair[1] for pair in pairs] pairs_dict = defaultdict(list) for scope, subscope in pairs: pairs_dict[scope].append(subscope) return pairs_dict, subscopes def _get_top_scopes(self, subscopes): """Returns a list of highest level scopes (not a subscope of any other scopes)""" top_scopes = [] for scope in self.scopes.keys(): if scope not in subscopes: top_scopes.append(scope) return top_scopes def _parse_scopes(self): """Returns a list of table rows where row: [indented scopename string, scope description string]""" scope_pairs, subscopes = self._get_scope_relationships() top_scopes = self._get_top_scopes(subscopes) table_rows = [] md_indent = "   " def _add_subscopes(table_rows, scopename, depth=0): description = self.scopes[scopename]['description'] doc_description = self.scopes[scopename].get('doc_description', '') if doc_description: description = doc_description table_row = [f"{md_indent * depth}`{scopename}`", description] table_rows.append(table_row) for subscope in scope_pairs[scopename]: if subscope: _add_subscopes(table_rows, subscope, depth + 1) for scope in top_scopes: _add_subscopes(table_rows, scope) return table_rows def write_table(self): """Generates the RBAC scopes reference documentation as a markdown table and writes it to the .gitignored `scope-table.md`.""" filename = SCOPE_TABLE_MD table_name = "" headers = ["Scope", "Grants permission to:"] values = self._parse_scopes() writer = self.create_writer(table_name, headers, values) title = "Table 1. Available scopes and their hierarchy" content = f"{title}\n{writer.dumps()}" with open(filename, 'w') as f: f.write(content) print(f"Generated {filename}.") print( "Run 'make clean' before 'make html' to ensure the built scopes.html contains latest scope table changes." ) def write_api(self): """Loads `rest-api.yml` and writes it back with a dynamically set JupyterHub version field and list of RBAC scopes descriptions from `scopes.py`.""" filename = REST_API_YAML yaml = YAML(typ="rt") yaml.preserve_quotes = True yaml.indent(mapping=2, offset=2, sequence=4) scope_dict = {} with open(filename) as f: content = yaml.load(f.read()) content["info"]["version"] = self.version for scope in self.scopes: description = self.scopes[scope]['description'] doc_description = self.scopes[scope].get('doc_description', '') if doc_description: description = doc_description scope_dict[scope] = description content['components']['securitySchemes']['oauth2']['flows'][ 'authorizationCode' ]['scopes'] = scope_dict with open(filename, 'w') as f: yaml.dump(content, f) def main(): table_generator = ScopeTableGenerator() table_generator.write_table() table_generator.write_api() if __name__ == "__main__": main()