From f836fc9dc264f9db7419e76caad3e4d43c0cecd7 Mon Sep 17 00:00:00 2001 From: Russ Allbery Date: Wed, 14 Apr 2021 15:29:16 -0700 Subject: [PATCH 1/3] Remove safir and aiohttp app We're using this code as a mixin into JupyterHub via its configuration hooks, not as a standalone service, so remove all the web service bits that we're not using. When we've needed a web service, we've written that as a separate package (cachemachine, moneypenny, etc.). --- Makefile | 4 -- pyproject.toml | 5 -- requirements/dev.in | 2 - requirements/dev.txt | 30 +----------- requirements/main.in | 2 - requirements/main.txt | 22 ++------- src/nublado2/app.py | 41 ---------------- src/nublado2/cli.py | 55 ---------------------- src/nublado2/handlers/__init__.py | 39 --------------- src/nublado2/handlers/external/__init__.py | 7 --- src/nublado2/handlers/external/index.py | 22 --------- src/nublado2/handlers/internal/__init__.py | 10 ---- src/nublado2/handlers/internal/index.py | 18 ------- tests/handlers/__init__.py | 0 tests/handlers/external/__init__.py | 0 tests/handlers/external/index_test.py | 27 ----------- tests/handlers/internal/__init__.py | 0 tests/handlers/internal/index_test.py | 26 ---------- 18 files changed, 5 insertions(+), 305 deletions(-) delete mode 100644 src/nublado2/app.py delete mode 100644 src/nublado2/cli.py delete mode 100644 src/nublado2/handlers/__init__.py delete mode 100644 src/nublado2/handlers/external/__init__.py delete mode 100644 src/nublado2/handlers/external/index.py delete mode 100644 src/nublado2/handlers/internal/__init__.py delete mode 100644 src/nublado2/handlers/internal/index.py delete mode 100644 tests/handlers/__init__.py delete mode 100644 tests/handlers/external/__init__.py delete mode 100644 tests/handlers/external/index_test.py delete mode 100644 tests/handlers/internal/__init__.py delete mode 100644 tests/handlers/internal/index_test.py diff --git a/Makefile b/Makefile index de3541c..e6beacb 100644 --- a/Makefile +++ b/Makefile @@ -14,7 +14,3 @@ init: .PHONY: update update: update-deps init - -.PHONY: run -run: - adev runserver --app-factory create_app src/nublado2/app.py diff --git a/pyproject.toml b/pyproject.toml index 85a6a20..3522e0c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,11 +46,6 @@ skip_install = true deps = pre-commit commands = pre-commit run --all-files - -[testenv:run] -description = Run the development server with auto-reload for code changes. -usedevelop = true -commands = adev runserver --app-factory create_app src/nublado2/app.py """ [tool.coverage.run] diff --git a/requirements/dev.in b/requirements/dev.in index 2e49edb..cbfcbc9 100644 --- a/requirements/dev.in +++ b/requirements/dev.in @@ -5,7 +5,6 @@ # After editing, update requirements/dev.txt by running: # make update-deps -aiohttp-devtools aioresponses pre-commit coverage[toml] @@ -13,7 +12,6 @@ flake8 mypy jupyterhub pytest -pytest-aiohttp pytest-asyncio tornado diff --git a/requirements/dev.txt b/requirements/dev.txt index 24250c5..aaa70c2 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -4,10 +4,6 @@ # # pip-compile --generate-hashes --output-file=requirements/dev.txt requirements/dev.in # -aiohttp-devtools==0.13.1 \ - --hash=sha256:70dbab32e2234296ddc995cacc06acf6b7ee2cf2ac1be62d4e12e0d333e63f00 \ - --hash=sha256:da9490c56671ed3920ceb98fc558065dec573c771ca17d500467b245b63f0f9d - # via -r requirements/dev.in aiohttp==3.7.4.post0 \ --hash=sha256:02f46fc0e3c5ac58b80d4d56eb0a7c7d97fcef69ace9326289fb9f1955e65cfe \ --hash=sha256:0563c1b3826945eecd62186f3f5c7d31abb7391fedc893b7e2b26303b5a9f3fe \ @@ -46,10 +42,7 @@ aiohttp==3.7.4.post0 \ --hash=sha256:f705e12750171c0ab4ef2a3c76b9a4024a62c4103e3a55dd6f99265b9bc6fcfc \ --hash=sha256:f881853d2643a29e643609da57b96d5f9c9b93f62429dcc1cbb413c7d07f0e1a \ --hash=sha256:fe60131d21b31fd1a14bd43e6bb88256f69dfc3188b3a89d736d6c71ed43ec95 - # via - # aiohttp-devtools - # aioresponses - # pytest-aiohttp + # via aioresponses aioresponses==0.7.2 \ --hash=sha256:2f8ff624543066eb465b0238de68d29231e8488f41dc4b5a9dae190982cdae50 \ --hash=sha256:82e495d118b74896aa5b4d47e17effb5e2cc783e510ae395ceade5e87cabe89a @@ -134,10 +127,6 @@ chardet==4.0.0 \ # via # aiohttp # requests -click==7.1.2 \ - --hash=sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a \ - --hash=sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc - # via aiohttp-devtools coverage[toml]==5.5 \ --hash=sha256:004d1880bed2d97151facef49f08e255a20ceb6f9432df75f4eef018fdd5a78c \ --hash=sha256:01d84219b5cdbfc8122223b39a954820929497a1cb1422824bb86b07b74594b6 \ @@ -206,10 +195,6 @@ cryptography==3.4.7 \ --hash=sha256:eb8cc2afe8b05acbd84a43905832ec78e7b3873fb124ca190f574dca7389a87d \ --hash=sha256:ee77aa129f481be46f8d92a1a7db57269a2f23052d5f2433b4621bb457081cc9 # via pyopenssl -devtools==0.6.1 \ - --hash=sha256:7334183972a8d04e81d08b7f62126abca9b6f4de51d825c5fdcb9c88f252601a \ - --hash=sha256:a054307594d35d28fae8df7629967363e851ae0ac7b2152640a8a401c39d42d7 - # via aiohttp-devtools distlib==0.3.1 \ --hash=sha256:8c09de2c67b3e7deef7184574fc060ab8a793e7adbb183d942c389c8b13c52fb \ --hash=sha256:edf6116872c863e1aa9d5bb7cb5e05a022c519a4594dc703843343a9ddd9bff1 @@ -482,10 +467,6 @@ pyflakes==2.3.1 \ --hash=sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3 \ --hash=sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db # via flake8 -pygments==2.8.1 \ - --hash=sha256:2656e1a6edcdabf4275f9a3640db59fd5de107d88e8663c5d4e9a0fa62f77f94 \ - --hash=sha256:534ef71d539ae97d4c3a4cf7d6f110f214b0e687e92f9cb9d2a3b0d3101289c8 - # via aiohttp-devtools pyopenssl==20.0.1 \ --hash=sha256:4c231c759543ba02560fcd2480c48dcec4dae34c9da7d3747c508227e0624b51 \ --hash=sha256:818ae18e06922c066f777a33f1fca45786d85edfe71cd043de6379337a7f274b @@ -497,10 +478,6 @@ pyparsing==2.4.7 \ pyrsistent==0.17.3 \ --hash=sha256:2e636185d9eb976a18a8a8e96efce62f2905fea90041958d8cc2a189756ebf3e # via jsonschema -pytest-aiohttp==0.3.0 \ - --hash=sha256:0b9b660b146a65e1313e2083d0d2e1f63047797354af9a28d6b7c9f0726fa33d \ - --hash=sha256:c929854339637977375838703b62fef63528598bc0a9d451639eba95f4aaa44f - # via -r requirements/dev.in pytest-asyncio==0.14.0 \ --hash=sha256:2eae1e34f6c68fc0a9dc12d4bea190483843ff4708d24277c41568d6b6044f1d \ --hash=sha256:9882c0c6b24429449f5f969a5158b528f39bde47dc32e85b9f0403965017e700 @@ -510,7 +487,6 @@ pytest==6.2.3 \ --hash=sha256:6ad9c7bdf517a808242b998ac20063c41532a570d088d77eec1ee12b0b5574bc # via # -r requirements/dev.in - # pytest-aiohttp # pytest-asyncio python-dateutil==2.8.1 \ --hash=sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c \ @@ -751,10 +727,6 @@ virtualenv==20.4.3 \ --hash=sha256:49ec4eb4c224c6f7dd81bb6d0a28a09ecae5894f4e593c89b0db0885f565a107 \ --hash=sha256:83f95875d382c7abafe06bd2a4cdd1b363e1bb77e02f155ebe8ac082a916b37c # via pre-commit -watchgod==0.7 \ - --hash=sha256:48140d62b0ebe9dd9cf8381337f06351e1f2e70b2203fa9c6eff4e572ca84f29 \ - --hash=sha256:d6c1ea21df37847ac0537ca0d6c2f4cdf513562e95f77bb93abbcf05573407b7 - # via aiohttp-devtools yarl==1.6.3 \ --hash=sha256:00d7ad91b6583602eb9c1d085a2cf281ada267e9a197e8b7cae487dadbfa293e \ --hash=sha256:0355a701b3998dcd832d0dc47cc5dedf3874f966ac7f870e0f3a6788d802d434 \ diff --git a/requirements/main.in b/requirements/main.in index 038093a..5eb19d5 100644 --- a/requirements/main.in +++ b/requirements/main.in @@ -10,12 +10,10 @@ # Docker image that we use as a base. However, the plugins that we use # are included, since those are not preinstalled in the Docker image. -safir aiohttp aiodns cchardet importlib_metadata -click jinja2 jupyterhub-idle-culler pyjwt diff --git a/requirements/main.txt b/requirements/main.txt index 6989a0a..e3ce596 100644 --- a/requirements/main.txt +++ b/requirements/main.txt @@ -46,9 +46,7 @@ aiohttp==3.7.4.post0 \ --hash=sha256:f705e12750171c0ab4ef2a3c76b9a4024a62c4103e3a55dd6f99265b9bc6fcfc \ --hash=sha256:f881853d2643a29e643609da57b96d5f9c9b93f62429dcc1cbb413c7d07f0e1a \ --hash=sha256:fe60131d21b31fd1a14bd43e6bb88256f69dfc3188b3a89d736d6c71ed43ec95 - # via - # -r requirements/main.in - # safir + # via -r requirements/main.in alembic==1.5.8 \ --hash=sha256:8a259f0a4c8b350b03579d77ce9e810b19c65bf0af05f84efb69af13ad50801e \ --hash=sha256:e27fd67732c97a1c370c33169ef4578cf96436fa0e7dcfaeeef4a917d0737d56 @@ -161,10 +159,6 @@ chardet==4.0.0 \ # via # aiohttp # requests -click==7.1.2 \ - --hash=sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a \ - --hash=sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc - # via -r requirements/main.in cryptography==3.4.7 \ --hash=sha256:0f1212a66329c80d68aeeb39b8a16d54ef57071bf22ff4e521657b27372e327d \ --hash=sha256:1e056c28420c072c5e3cb36e2b23ee55e260cb04eee08f702e0edfec3fb51959 \ @@ -242,9 +236,9 @@ idna==2.10 \ # via # requests # yarl -importlib-metadata==3.10.0 \ - --hash=sha256:c9db46394197244adf2f0b08ec5bc3cf16757e9590b02af1fca085c16c0d600a \ - --hash=sha256:d2d46ef77ffc85cbf7dac7e81dd663fde71c45326131bea8033b9bad42268ebe +importlib-metadata==3.10.1 \ + --hash=sha256:2ec0faae539743ae6aaa84b49a169670a465f7f5d64e6add98388cc29fd1f2f6 \ + --hash=sha256:c9356b657de65c53744046fa8f7358afe0714a1af7d570c00c3835c2d724a7c1 # via -r requirements/main.in ipython-genutils==0.2.0 \ --hash=sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8 \ @@ -557,10 +551,6 @@ ruamel.yaml==0.17.4 \ # via # -r requirements/main.in # jupyter-telemetry -safir==0.1.1 \ - --hash=sha256:6de9adb068a5f32b2c436179685ac6a956450cce2dca80cc40d2ecd988ade879 \ - --hash=sha256:8a38991e0d334f3eaafc8d0ba6d79dc34ead94585997d0cbaf24e0ef5a07aac3 - # via -r requirements/main.in six==1.15.0 \ --hash=sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259 \ --hash=sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced @@ -609,10 +599,6 @@ sqlalchemy==1.4.7 \ # via # alembic # jupyterhub -structlog==21.1.0 \ - --hash=sha256:62f06fc0ee32fb8580f0715eea66cb87271eb7efb0eaf9af6b639cba8981de47 \ - --hash=sha256:d9d2d890532e8db83c6977a2a676fb1889922ff0c26ad4dc0ecac26f9fafbc57 - # via safir text-unidecode==1.3 \ --hash=sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8 \ --hash=sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93 diff --git a/src/nublado2/app.py b/src/nublado2/app.py deleted file mode 100644 index cfd1d7e..0000000 --- a/src/nublado2/app.py +++ /dev/null @@ -1,41 +0,0 @@ -"""The main application definition for nublado2 service.""" - -__all__ = ["create_app"] - -from aiohttp import web -from safir.http import init_http_session -from safir.logging import configure_logging -from safir.metadata import setup_metadata -from safir.middleware import bind_logger - -from nublado2.config import Configuration -from nublado2.handlers import init_external_routes, init_internal_routes - - -def create_app() -> web.Application: - """Create and configure the aiohttp.web application.""" - config = Configuration() - configure_logging( - profile=config.profile, - log_level=config.log_level, - name=config.logger_name, - ) - - root_app = web.Application() - root_app["safir/config"] = config - setup_metadata(package_name="nublado2", app=root_app) - setup_middleware(root_app) - root_app.add_routes(init_internal_routes()) - root_app.cleanup_ctx.append(init_http_session) - - sub_app = web.Application() - setup_middleware(sub_app) - sub_app.add_routes(init_external_routes()) - root_app.add_subapp(f'/{root_app["safir/config"].name}', sub_app) - - return root_app - - -def setup_middleware(app: web.Application) -> None: - """Add middleware to the application.""" - app.middlewares.append(bind_logger) diff --git a/src/nublado2/cli.py b/src/nublado2/cli.py deleted file mode 100644 index e247068..0000000 --- a/src/nublado2/cli.py +++ /dev/null @@ -1,55 +0,0 @@ -"""Administrative command-line interface.""" - -__all__ = ["main", "help", "run"] - -from typing import Any, Union - -import click -from aiohttp.web import run_app - -from nublado2.app import create_app - -# Add -h as a help shortcut option -CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) - - -@click.group(context_settings=CONTEXT_SETTINGS) -@click.version_option(message="%(version)s") -@click.pass_context -def main(ctx: click.Context) -> None: - """nublado2 - - Administrative command-line interface for nublado2. - """ - # Subcommands should use the click.pass_obj decorator to get this - # ctx object as the first argument. - ctx.obj = {} - - -@main.command() -@click.argument("topic", default=None, required=False, nargs=1) -@click.pass_context -def help(ctx: click.Context, topic: Union[None, str], **kw: Any) -> None: - """Show help for any command.""" - # The help command implementation is taken from - # https://www.burgundywall.com/post/having-click-help-subcommand - if topic: - if topic in main.commands: - ctx.info_name = topic - click.echo(main.commands[topic].get_help(ctx)) - else: - raise click.UsageError(f"Unknown help topic {topic}", ctx) - else: - assert ctx.parent - click.echo(ctx.parent.get_help()) - - -@main.command() -@click.option( - "--port", default=8080, type=int, help="Port to run the application on." -) -@click.pass_context -def run(ctx: click.Context, port: int) -> None: - """Run the application (for production).""" - app = create_app() - run_app(app, port=port) diff --git a/src/nublado2/handlers/__init__.py b/src/nublado2/handlers/__init__.py deleted file mode 100644 index 9282094..0000000 --- a/src/nublado2/handlers/__init__.py +++ /dev/null @@ -1,39 +0,0 @@ -"""HTTP API route tables.""" - -__all__ = ["internal_routes", "routes", "init_internal_routes", "init_routes"] - -from aiohttp import web - -internal_routes = web.RouteTableDef() -"""Routes for the root application that serves from ``/`` - -Application-specific routes don't get attached here. In practice, only routes -for metrics and health checks get attached to this table. Attach public APIs -to `routes` instead since those are accessible from the public API gateway and -are prefixed with the application name. -""" - -routes = web.RouteTableDef() -"""Routes for the public API that serves from ``//``.""" - - -def init_external_routes() -> web.RouteTableDef: - """Initialize the route table and handlers from the application APIs, - served at ``//``. - """ - # Import handlers so that they are registered with the routes table via - # decorators. This isn't a global import to avoid circular dependencies. - import nublado2.handlers.external # noqa: F401 - - return routes - - -def init_internal_routes() -> web.RouteTableDef: - """Initialize the route table and handlers for the root APIs (not the - ones publicly available). - """ - # Import handlers so that they are registered with the routes table via - # decorators. This isn't a global import to avoid circular dependencies. - import nublado2.handlers.internal # noqa: F401 - - return internal_routes diff --git a/src/nublado2/handlers/external/__init__.py b/src/nublado2/handlers/external/__init__.py deleted file mode 100644 index a3c4239..0000000 --- a/src/nublado2/handlers/external/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -"""Externally-accessible endpoint handlers that serve relative to -``//``. -""" - -__all__ = ["get_index"] - -from nublado2.handlers.external.index import get_index diff --git a/src/nublado2/handlers/external/index.py b/src/nublado2/handlers/external/index.py deleted file mode 100644 index 44a5db2..0000000 --- a/src/nublado2/handlers/external/index.py +++ /dev/null @@ -1,22 +0,0 @@ -"""Handlers for the app's external root, ``//``.""" - -__all__ = ["get_index"] - -from aiohttp import web - -from nublado2.handlers import routes - - -@routes.get("/") -async def get_index(request: web.Request) -> web.Response: - """GET // (the app's external root). - - By convention, the root of the external API includes a field called - ``_metadata`` that provides the same metadata as the internal root - endpoint. Here, the metadata is namespace so that you can customize the - root of your API. For example, consider listing key API URLs. - """ - metadata = request.config_dict["safir/metadata"] - data = {"_metadata": metadata} - - return web.json_response(data) diff --git a/src/nublado2/handlers/internal/__init__.py b/src/nublado2/handlers/internal/__init__.py deleted file mode 100644 index b6acd9a..0000000 --- a/src/nublado2/handlers/internal/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -"""Internal HTTP handlers that serve relative to the root path, ``/``. - -These handlers aren't externally visible since the app is available at a path, -``/nublado2``. See `nublado2.handlers.external` for -the external endpoint handlers. -""" - -__all__ = ["get_index"] - -from nublado2.handlers.internal.index import get_index diff --git a/src/nublado2/handlers/internal/index.py b/src/nublado2/handlers/internal/index.py deleted file mode 100644 index 3348bfe..0000000 --- a/src/nublado2/handlers/internal/index.py +++ /dev/null @@ -1,18 +0,0 @@ -"""Handlers for the app's root, ``/``.""" - -__all__ = ["get_index"] - -from aiohttp import web - -from nublado2.handlers import internal_routes - - -@internal_routes.get("/") -async def get_index(request: web.Request) -> web.Response: - """GET / (the app's internal root). - - By convention, this endpoint returns only the application's metadata. - """ - metadata = request.config_dict["safir/metadata"] - - return web.json_response(metadata) diff --git a/tests/handlers/__init__.py b/tests/handlers/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/handlers/external/__init__.py b/tests/handlers/external/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/handlers/external/index_test.py b/tests/handlers/external/index_test.py deleted file mode 100644 index 10659c1..0000000 --- a/tests/handlers/external/index_test.py +++ /dev/null @@ -1,27 +0,0 @@ -"""Tests for the nublado2.handlers.external.index module and routes.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -from nublado2.app import create_app - -if TYPE_CHECKING: - from aiohttp.pytest_plugin.test_utils import TestClient - - -async def test_get_index(aiohttp_client: TestClient) -> None: - """Test GET /app-name/""" - app = create_app() - name = app["safir/config"].name - client = await aiohttp_client(app) - - response = await client.get(f"/{name}/") - assert response.status == 200 - data = await response.json() - metadata = data["_metadata"] - assert metadata["name"] == name - assert isinstance(metadata["version"], str) - assert isinstance(metadata["description"], str) - assert isinstance(metadata["repository_url"], str) - assert isinstance(metadata["documentation_url"], str) diff --git a/tests/handlers/internal/__init__.py b/tests/handlers/internal/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/handlers/internal/index_test.py b/tests/handlers/internal/index_test.py deleted file mode 100644 index a31a10b..0000000 --- a/tests/handlers/internal/index_test.py +++ /dev/null @@ -1,26 +0,0 @@ -"""Tests for the nublado2.handlers.internal.index module and routes. -""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -from nublado2.app import create_app - -if TYPE_CHECKING: - from aiohttp.pytest_plugin.test_utils import TestClient - - -async def test_get_index(aiohttp_client: TestClient) -> None: - """Test GET /""" - app = create_app() - client = await aiohttp_client(app) - - response = await client.get("/") - assert response.status == 200 - data = await response.json() - assert data["name"] == app["safir/config"].name - assert isinstance(data["version"], str) - assert isinstance(data["description"], str) - assert isinstance(data["repository_url"], str) - assert isinstance(data["documentation_url"], str) From 7e7958ed0a361811ce0fb8eb736ae3e75f88c7f1 Mon Sep 17 00:00:00 2001 From: Russ Allbery Date: Thu, 15 Apr 2021 11:01:05 -0700 Subject: [PATCH 2/3] Add support for Gafaelfawr 2.0 Support a Gafaelfawr token mounted inside the pod and use that by preference if it exists as the admin token for calls to moneypenny rather than creating a JWT. Use the existence of that token as a trigger to use the new token API for getting user information for a user. This is a hack, but it's a temporary hack since we'll be deleting the old code once Gafaelfawr 2.0 is deployed everywhere. --- src/nublado2/auth.py | 42 ++++++++++++++-- src/nublado2/nublado_config.py | 11 ++++- src/nublado2/resourcemgr.py | 5 +- tests/auth_test.py | 88 ++++++++++++++++++++++++++++++---- 4 files changed, 130 insertions(+), 16 deletions(-) diff --git a/src/nublado2/auth.py b/src/nublado2/auth.py index ddc7a9c..b679e9a 100644 --- a/src/nublado2/auth.py +++ b/src/nublado2/auth.py @@ -137,8 +137,8 @@ async def get(self) -> None: # they needed to be authenticated. self.redirect(self.get_next_url(user)) - @staticmethod - async def _build_auth_info(headers: HTTPHeaders) -> Dict[str, Any]: + @classmethod + async def _build_auth_info(cls, headers: HTTPHeaders) -> Dict[str, Any]: """Construct the authentication information for a user. Retrieve the token from the headers, use that token to retrieve the @@ -150,11 +150,43 @@ async def _build_auth_info(headers: HTTPHeaders) -> Dict[str, Any]: if not token: raise web.HTTPError(401, "No request token") + config = NubladoConfig() + if not config.gafaelfawr_token: + return await cls._get_legacy_userinfo(config, token) + if not config.base_url: + raise web.HTTPError(500, "base_url not set in configuration") + # Retrieve the token metadata. - base_url = NubladoConfig().base_url - if not base_url: + api_url = url_path_join(config.base_url, "/auth/api/v1/user-info") + session = await get_session() + resp = await session.get( + api_url, headers={"Authorization": f"bearer {token}"} + ) + if resp.status != 200: + raise web.HTTPError(500, "Cannot reach token analysis API") + try: + auth_state = await resp.json() + except Exception: + raise web.HTTPError(500, "Cannot get information for token") + if "username" not in auth_state or "uid" not in auth_state: + raise web.HTTPError(403, "Request token is invalid") + + auth_state["token"] = token + if "groups" not in auth_state: + auth_state["groups"] = [] + return { + "name": auth_state["username"], + "auth_state": auth_state, + } + + @staticmethod + async def _get_legacy_userinfo( + config: NubladoConfig, token: str + ) -> Dict[str, Any]: + """Get user information from a token for Gafaelfawr 1.x.""" + if not config.base_url: raise web.HTTPError(500, "base_url not set in configuration") - api_url = url_path_join(base_url, "/auth/analyze") + api_url = url_path_join(config.base_url, "/auth/analyze") session = await get_session() resp = await session.post(api_url, data={"token": token}) if resp.status != 200: diff --git a/src/nublado2/nublado_config.py b/src/nublado2/nublado_config.py index a74f40d..60151a9 100644 --- a/src/nublado2/nublado_config.py +++ b/src/nublado2/nublado_config.py @@ -2,7 +2,7 @@ __all__ = ["NubladoConfig"] -from typing import Any, Dict, List +from typing import Any, Dict, List, Optional from ruamel import yaml from ruamel.yaml import RoundTripLoader @@ -33,6 +33,15 @@ def base_url(self) -> str: """Base URL for the environment, like https://data.lsst.cloud""" return self._config["base_url"] + @property + def gafaelfawr_token(self) -> Optional[str]: + """Retrieve the Gafaelfawr token for moneypenny calls.""" + try: + with open("/etc/keys/gafaelfawr-token") as f: + return f.read() + except FileNotFoundError: + return None + @property def images_url(self) -> str: """URL to fetch list of images to show in options form. diff --git a/src/nublado2/resourcemgr.py b/src/nublado2/resourcemgr.py index 7fe7ce1..263677c 100644 --- a/src/nublado2/resourcemgr.py +++ b/src/nublado2/resourcemgr.py @@ -97,7 +97,10 @@ async def _request_homedir_provisioning(self, spawner: Spawner) -> None: "uid": int(auth_state["uid"]), "groups": auth_state["groups"], } - token = await self._mint_admin_token() + if nc.gafaelfawr_token: + token = nc.gafaelfawr_token + else: + token = await self._mint_admin_token() endpt = f"{nc.base_url}/moneypenny/commission" auth = {"Authorization": f"Bearer {token}"} self.log.debug(f"Posting dossier {dossier} to {endpt}") diff --git a/tests/auth_test.py b/tests/auth_test.py index d875d19..013e880 100644 --- a/tests/auth_test.py +++ b/tests/auth_test.py @@ -22,12 +22,13 @@ @pytest.fixture(autouse=True) -async def use_config_mock() -> AsyncGenerator: +async def config_mock() -> AsyncGenerator: """Use a mock NubladoConfig object.""" with patch("nublado2.auth.NubladoConfig") as mock: mock.return_value = MagicMock() mock.return_value.base_url = "https://data.example.com/" - yield + mock.return_value.gafaelfawr_token = None + yield mock.return_value def test_authenticator() -> None: @@ -38,7 +39,17 @@ def test_authenticator() -> None: assert authenticator.login_url("/hub") == "/hub/gafaelfawr/login" -def build_handler( +def build_userinfo_handler( + data: Dict[str, Any] +) -> Callable[..., CallbackResult]: + def handler(url: str, **kwargs: Any) -> CallbackResult: + assert kwargs["headers"] == {"Authorization": "bearer user-token"} + return CallbackResult(payload=data, status=200) + + return handler + + +def build_analyze_handler( data: Dict[str, Any], valid: bool = True ) -> Callable[..., CallbackResult]: def handler(url: str, **kwargs: Any) -> CallbackResult: @@ -55,17 +66,76 @@ def handler(url: str, **kwargs: Any) -> CallbackResult: @pytest.mark.asyncio -async def test_login_handler() -> None: +async def test_login_handler(config_mock: MagicMock) -> None: + config_mock.gafaelfawr_token = "admin-token" + headers = HTTPHeaders({"X-Auth-Request-Token": "user-token"}) + url = "https://data.example.com/auth/api/v1/user-info" + # No headers. with aioresponses() as m: with pytest.raises(web.HTTPError): await GafaelfawrLoginHandler._build_auth_info(HTTPHeaders()) + # Invalid token. + with aioresponses() as m: + m.get(url, status=403) + with pytest.raises(web.HTTPError): + await GafaelfawrLoginHandler._build_auth_info(headers) + + # Bad API response payload. + with aioresponses() as m: + m.get(url, payload={}, status=200) + with pytest.raises(web.HTTPError): + await GafaelfawrLoginHandler._build_auth_info(headers) + + # Test minimum data. + with aioresponses() as m: + handler = build_userinfo_handler({"username": "foo", "uid": 1234}) + m.get(url, callback=handler) + assert await GafaelfawrLoginHandler._build_auth_info(headers) == { + "name": "foo", + "auth_state": { + "username": "foo", + "uid": 1234, + "token": "user-token", + "groups": [], + }, + } + + # Test full data. + with aioresponses() as m: + handler = build_userinfo_handler( + { + "username": "bar", + "uid": 4510, + "groups": [ + {"name": "group-one", "id": 1726}, + {"name": "another", "id": 6789}, + ], + } + ) + m.get(url, callback=handler) + assert await GafaelfawrLoginHandler._build_auth_info(headers) == { + "name": "bar", + "auth_state": { + "username": "bar", + "uid": 4510, + "token": "user-token", + "groups": [ + {"name": "group-one", "id": 1726}, + {"name": "another", "id": 6789}, + ], + }, + } + + +@pytest.mark.asyncio +async def test_legacy_login_handler() -> None: headers = HTTPHeaders({"X-Auth-Request-Token": "some-token"}) # Invalid token. with aioresponses() as m: - handler = build_handler({"uid": "foo"}, valid=False) + handler = build_analyze_handler({"uid": "foo"}, valid=False) m.post("https://data.example.com/auth/analyze", callback=handler) with pytest.raises(web.HTTPError): await GafaelfawrLoginHandler._build_auth_info(headers) @@ -84,7 +154,7 @@ async def test_login_handler() -> None: # Test minimum data. with aioresponses() as m: - handler = build_handler({"uid": "foo"}) + handler = build_analyze_handler({"uid": "foo"}) m.post("https://data.example.com/auth/analyze", callback=handler) assert await GafaelfawrLoginHandler._build_auth_info(headers) == { "name": "foo", @@ -97,7 +167,7 @@ async def test_login_handler() -> None: # Test full data. with aioresponses() as m: - handler = build_handler( + handler = build_analyze_handler( { "uid": "bar", "uidNumber": "4510", @@ -124,7 +194,7 @@ async def test_login_handler() -> None: # Check invalid format of isMemberOf. with aioresponses() as m: - handler = build_handler( + handler = build_analyze_handler( {"uid": "bar", "isMemberOf": [{"name": "foo", "id": ["foo"]}]} ) m.post("https://data.example.com/auth/analyze", callback=handler) @@ -133,7 +203,7 @@ async def test_login_handler() -> None: # Test groups without GIDs. with aioresponses() as m: - handler = build_handler( + handler = build_analyze_handler( { "uid": "bar", "uidNumber": "4510", From 0d65ed79d4d6a890385a1eda02811883e66ab6a2 Mon Sep 17 00:00:00 2001 From: Russ Allbery Date: Fri, 16 Apr 2021 17:27:10 -0700 Subject: [PATCH 3/3] Update dependencies --- requirements/dev.txt | 82 +++++++++++++++++++++---------------------- requirements/main.txt | 76 +++++++++++++++++++-------------------- 2 files changed, 79 insertions(+), 79 deletions(-) diff --git a/requirements/dev.txt b/requirements/dev.txt index aaa70c2..e3b4205 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -207,9 +207,9 @@ filelock==3.0.12 \ --hash=sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59 \ --hash=sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836 # via virtualenv -flake8==3.9.0 \ - --hash=sha256:12d05ab02614b6aee8df7c36b97d1a3b2372761222b19b58621355e82acddcff \ - --hash=sha256:78873e372b12b093da7b5e5ed302e8ad9e988b38b063b61ad937f26ca58fc5f0 +flake8==3.9.1 \ + --hash=sha256:1aa8990be1e689d96c745c5682b687ea49f2e05a443aff1f8251092b0014e378 \ + --hash=sha256:3b9f848952dddccf635be78098ca75010f073bfe14d2c6bda867154bea728d2a # via -r requirements/dev.in greenlet==1.0.0 \ --hash=sha256:0a77691f0080c9da8dfc81e23f4e3cffa5accf0f5b56478951016d7cfead9196 \ @@ -443,9 +443,9 @@ pluggy==0.13.1 \ --hash=sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0 \ --hash=sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d # via pytest -pre-commit==2.12.0 \ - --hash=sha256:029d53cb83c241fe7d66eeee1e24db426f42c858f15a38d20bcefd8d8e05c9da \ - --hash=sha256:46b6ffbab37986c47d0a35e40906ae029376deed89a0eb2e446fb6e67b220427 +pre-commit==2.12.1 \ + --hash=sha256:70c5ec1f30406250b706eda35e868b87e3e4ba099af8787e3e8b4b01e84f4712 \ + --hash=sha256:900d3c7e1bf4cf0374bb2893c24c23304952181405b4d88c9c40b72bda1bb8a9 # via -r requirements/dev.in prometheus-client==0.10.1 \ --hash=sha256:030e4f9df5f53db2292eec37c6255957eb76168c6f974e4176c711cf91ed34aa \ @@ -584,41 +584,41 @@ six==1.15.0 \ # pyopenssl # python-dateutil # virtualenv -sqlalchemy==1.4.7 \ - --hash=sha256:069fcda89c7d168382f674b5b566643f1420e4e7704c00cced2579675deb4eed \ - --hash=sha256:1d1172a9e5ead90d9299ccad8c5eecf40372a3721ff82fc4b4ee42835baf4659 \ - --hash=sha256:2bfadb3279f51252565baed9aa071c1bef875fcde60bf4a172136289ac208804 \ - --hash=sha256:3a022a7985a49cacf21e2a73bab083e4852943466d250d932554650d705fcc62 \ - --hash=sha256:50b1cb7c9f6f0bbc68c06453d66d4a34ca75ba60bce61d49bf007edfd2621d0a \ - --hash=sha256:58075eab5e32daf51e637ac88c63057c3a5e84602cfeb30db4258838ef6f7a2b \ - --hash=sha256:5d84442d85491dc473bf99f4d90ad45dd2e5539743f4d1216b15ba26575ba572 \ - --hash=sha256:606ac6a7640cc642fd53c5e693c560ad9fc21ef97aa7e799eb96b6d7f28ad723 \ - --hash=sha256:673cb375deb17e1561340710f428b33c27a11980d991a2ac88d7bf1c623faa0b \ - --hash=sha256:6913ea108e7583f2d7ba4bc9cf4f2b1e0cdacf7e66e4cdc04192f870e64306ff \ - --hash=sha256:6adb07e199781457b75f4773e63577a2898f95141f030b956a2a186055f24e76 \ - --hash=sha256:72152b64508dd807ba2a26d9dfc4da450d0ba1808c9f96ddbc397c435735fac3 \ - --hash=sha256:84115f97d88c8ccf26db81b7997c5f5de9ae360e0785ef859d0987794495f0a9 \ - --hash=sha256:8672ff62c9d48f62aa17bb806a591cdfed801d139eecbcf9224bffb80f6fdc30 \ - --hash=sha256:89860d594cb3256718d74ff7406a405a890eac71bcc044b3ba6868850d934a48 \ - --hash=sha256:8df743c79181ecc6aadaf10569d452ef3eda06764fe0adc4ea981a48c01e1ad5 \ - --hash=sha256:8ecabd4cead9a582e2ffa7a3918bc31155d5c24b1fd16ed617171f913c438da1 \ - --hash=sha256:94fece3fdc777fbf37378513414bcf19ae89e1b598edf33d957a2898991d714f \ - --hash=sha256:9de4c84ea180c07f1d4010db2cfdbf9fe67bf7caafcfb1053644c8c03bfa3fd0 \ - --hash=sha256:a3a40d2a0cb2ca2886f8f2fe768e83aeca489a162c8233974b9b2e429827ed85 \ - --hash=sha256:a7f450cbab9670949e7d9f0eac1dd93eaaffce319608bf4b863f0b751decef42 \ - --hash=sha256:abf18c62c4740d7199e443537066904789052d6d165cb279eb91bea35ea42ec4 \ - --hash=sha256:aea57c7a5a4135abc10f81ce433b23325cbb9648a5dcb0ac1af1cdd413f7d0cb \ - --hash=sha256:bb69a2d93c1a98a8d4ca24a8012ade4b771087dddbe077ad4ef4911d7f17185d \ - --hash=sha256:bea07faab746743c8d82650b51129ff2705d53a0094161cfa6145e7ce77b9644 \ - --hash=sha256:c74310f13e5a113ef658345e2cedf9aa1fcb8b9a588e07d54c083c7fc71edf42 \ - --hash=sha256:d10117c9ce096bd6fb9a13c6fad274982f7889028e22a05719a6d219e2cf977e \ - --hash=sha256:d26d8a3865c9f33d7b3b356a577c7f26c499a9f080ae33e4282a65a8a2170cef \ - --hash=sha256:d5ef5619d421f8a86af874f867d17d823cd970ad0f2ae7c30723beb16922b4d6 \ - --hash=sha256:d81a68df4f3eee490b66ba990648d3c77cbf2475291ef92aa4e05ef541ecfd66 \ - --hash=sha256:e98934855337d76aa7726f444b0fa597a462271a95d01bc050644d88e1ee5aae \ - --hash=sha256:e9c2aaaa9738ba3334262734bd25d9b2d6ea446400f815bbdea17571b9e6d8fb \ - --hash=sha256:ef6d98d5b51eb826516499429e059872b61e272cb44630ca8de87650242d07d8 \ - --hash=sha256:fdd1e4ed5d526aa4c7a01ed2157d01f0234eaecdb04b1c3b5084d0902986be9f +sqlalchemy==1.4.8 \ + --hash=sha256:15480706c51bdf72d726d0efde77eef6b5f09fbef65bc520f2c4e1f3a429fddf \ + --hash=sha256:1dc4f31124cc359065dceef1a9afe4e2d07a05b2e1e184f5fa48cba96c2249a3 \ + --hash=sha256:1e1c2c65f732f7740d184b4133e5207c0f8974663ab1b79ce1b599ecf55bd3e9 \ + --hash=sha256:221ead411c5e455bbe32b8eb2e8521a31a0769684a93b6895e515e9ce3a49906 \ + --hash=sha256:29e135f8c890c16251162dd40022074430fc39668c8666220a73cb500a3697af \ + --hash=sha256:3bc31ad707f8587c9f93d50cd7cee80ba352162d322808ddbb5bcb5fcfd2bb83 \ + --hash=sha256:43b4958d959b1ee00540b963620f6bffacdd8ac0a19d31450828a1cbacf3693c \ + --hash=sha256:46a454a366f6274c18d3204d11b9e4b98a5c99cba99230e24848d82bef069f75 \ + --hash=sha256:49a85c143f90c74b1d000506e125476d4dec3342f8052ad98e007fb5c657c46c \ + --hash=sha256:4b0404b7a18658d5661120ae0f6f57b220c57126d07a067bfdff304d4c50eaa3 \ + --hash=sha256:64a6645ee39f45f153b45addace5727aef7eb517b115c8bdc77e02be8c8c43a3 \ + --hash=sha256:6d18aec6f16b48b2903940776d0f455d82b0c35f4b307ae5393987c600f8fcda \ + --hash=sha256:76354282fbb3e5b33aee8fcfc2394c7b26d08c53025960d41abc57a6222321cc \ + --hash=sha256:767c0bbe5af14c2ebcad7314faea9709b59593daa3250fe6224429a344c21438 \ + --hash=sha256:81ea6270359629b9c56251bdfc12f2a8afb55034a3ff3698b6a764b300bfe605 \ + --hash=sha256:831aa088b0056b8040423c53543b5bf5663d1d5ffbc175387f2216de7780113e \ + --hash=sha256:859cf5aae0ecc1aa526f9cebdf6ae8078527f0f3aa3fb10156ecc1d044c1c545 \ + --hash=sha256:8a067fbf3ea3f53e4223990d92a8e44259571ef182039ddcb4ffcf217e08a901 \ + --hash=sha256:a7641c8a2d008ee8cef21d3b9eaf7f68259b965318055148fbc5ae6961ae287e \ + --hash=sha256:aa470facb52b927a5a5a1a4755b1452f7e77dcf93e822077354075e7a811bec2 \ + --hash=sha256:b6106343fb97771f20cd945ce6b1d07f8247121d1d4baad062c028e5f0a1f034 \ + --hash=sha256:b8280da3f36b9156ff251a14e5a3e82a4bb58958bdddbd0867cd29e6f3f809de \ + --hash=sha256:c1457a86209c56dfdd1a62904445bc727f962226bf8866f3834fffff8bd7282d \ + --hash=sha256:cb08be4a43ce6729b37efb95816258b00bbd3442eef4a740c09384fb9fe99076 \ + --hash=sha256:d37683c5d84f0694f159a2a515a34f81daf7da96e7efb9ba9d5daf4ae8dde47e \ + --hash=sha256:ddc9de7e46e8044099a94b8d8b92b02693df030ca8684aa775908295270a3556 \ + --hash=sha256:de2e700a2e98b4c621976fdb3174e9e5947c38efedcf60e6d7c20cc7dddb3a99 \ + --hash=sha256:dec822c2a9436092798998475b3b8edd0d59be7fecdd5fb8411ac8db1575ed64 \ + --hash=sha256:e7a4bcb79aa64f1bf995dc2f5966fccc7d21b99cdfb63b57c609cfdba2ea5906 \ + --hash=sha256:ed660a67c0d8690078560d62767a69b359409863827b367167f18fa14ca51ff6 \ + --hash=sha256:eff2c66e930030110a6139b0374013fa1f1a397a67a0c8a1a5d387ca2a112b45 \ + --hash=sha256:fc5dea79bd2626ee2ed034144f6c590441e7c8c036c57c1939ec4a18481c0de1 \ + --hash=sha256:fccd2de1b3b47ca62c2fa5d344e16266a6edc8cce8b80f32e40126df60dbd2c4 \ + --hash=sha256:fe2558be48c92a9be69bd9e7c41bfc46714c5eafc4d59f85fdc338d6999c4962 # via # alembic # jupyterhub diff --git a/requirements/main.txt b/requirements/main.txt index e3ce596..fc0706c 100644 --- a/requirements/main.txt +++ b/requirements/main.txt @@ -181,9 +181,9 @@ escapism==1.0.1 \ --hash=sha256:73256bdfb4f22230f0428fc6efecee61cdc4fad531b6f98b849cb9c80711e4ec \ --hash=sha256:d28f19edc3cb1ffc36fa238956ecc068695477e748f57157c6dde00a6b77f229 # via jupyterhub-kubespawner -google-auth==1.28.1 \ - --hash=sha256:186fe2564634d67fbbb64f3daf8bc8c9cecbb2a7f535ed1a8a71795e50db8d87 \ - --hash=sha256:70b39558712826e41f65e5f05a8d879361deaf84df8883e5dd0ec3d0da6ab66e +google-auth==1.29.0 \ + --hash=sha256:010f011c4e27d3d5eb01106fba6aac39d164842dfcd8709955c4638f5b11ccf8 \ + --hash=sha256:f30a672a64d91cc2e3137765d088c5deec26416246f7a9e956eaf69a8d7ed49c # via kubernetes greenlet==1.0.0 \ --hash=sha256:0a77691f0080c9da8dfc81e23f4e3cffa5accf0f5b56478951016d7cfead9196 \ @@ -561,41 +561,41 @@ six==1.15.0 \ # pyopenssl # python-dateutil # websocket-client -sqlalchemy==1.4.7 \ - --hash=sha256:069fcda89c7d168382f674b5b566643f1420e4e7704c00cced2579675deb4eed \ - --hash=sha256:1d1172a9e5ead90d9299ccad8c5eecf40372a3721ff82fc4b4ee42835baf4659 \ - --hash=sha256:2bfadb3279f51252565baed9aa071c1bef875fcde60bf4a172136289ac208804 \ - --hash=sha256:3a022a7985a49cacf21e2a73bab083e4852943466d250d932554650d705fcc62 \ - --hash=sha256:50b1cb7c9f6f0bbc68c06453d66d4a34ca75ba60bce61d49bf007edfd2621d0a \ - --hash=sha256:58075eab5e32daf51e637ac88c63057c3a5e84602cfeb30db4258838ef6f7a2b \ - --hash=sha256:5d84442d85491dc473bf99f4d90ad45dd2e5539743f4d1216b15ba26575ba572 \ - --hash=sha256:606ac6a7640cc642fd53c5e693c560ad9fc21ef97aa7e799eb96b6d7f28ad723 \ - --hash=sha256:673cb375deb17e1561340710f428b33c27a11980d991a2ac88d7bf1c623faa0b \ - --hash=sha256:6913ea108e7583f2d7ba4bc9cf4f2b1e0cdacf7e66e4cdc04192f870e64306ff \ - --hash=sha256:6adb07e199781457b75f4773e63577a2898f95141f030b956a2a186055f24e76 \ - --hash=sha256:72152b64508dd807ba2a26d9dfc4da450d0ba1808c9f96ddbc397c435735fac3 \ - --hash=sha256:84115f97d88c8ccf26db81b7997c5f5de9ae360e0785ef859d0987794495f0a9 \ - --hash=sha256:8672ff62c9d48f62aa17bb806a591cdfed801d139eecbcf9224bffb80f6fdc30 \ - --hash=sha256:89860d594cb3256718d74ff7406a405a890eac71bcc044b3ba6868850d934a48 \ - --hash=sha256:8df743c79181ecc6aadaf10569d452ef3eda06764fe0adc4ea981a48c01e1ad5 \ - --hash=sha256:8ecabd4cead9a582e2ffa7a3918bc31155d5c24b1fd16ed617171f913c438da1 \ - --hash=sha256:94fece3fdc777fbf37378513414bcf19ae89e1b598edf33d957a2898991d714f \ - --hash=sha256:9de4c84ea180c07f1d4010db2cfdbf9fe67bf7caafcfb1053644c8c03bfa3fd0 \ - --hash=sha256:a3a40d2a0cb2ca2886f8f2fe768e83aeca489a162c8233974b9b2e429827ed85 \ - --hash=sha256:a7f450cbab9670949e7d9f0eac1dd93eaaffce319608bf4b863f0b751decef42 \ - --hash=sha256:abf18c62c4740d7199e443537066904789052d6d165cb279eb91bea35ea42ec4 \ - --hash=sha256:aea57c7a5a4135abc10f81ce433b23325cbb9648a5dcb0ac1af1cdd413f7d0cb \ - --hash=sha256:bb69a2d93c1a98a8d4ca24a8012ade4b771087dddbe077ad4ef4911d7f17185d \ - --hash=sha256:bea07faab746743c8d82650b51129ff2705d53a0094161cfa6145e7ce77b9644 \ - --hash=sha256:c74310f13e5a113ef658345e2cedf9aa1fcb8b9a588e07d54c083c7fc71edf42 \ - --hash=sha256:d10117c9ce096bd6fb9a13c6fad274982f7889028e22a05719a6d219e2cf977e \ - --hash=sha256:d26d8a3865c9f33d7b3b356a577c7f26c499a9f080ae33e4282a65a8a2170cef \ - --hash=sha256:d5ef5619d421f8a86af874f867d17d823cd970ad0f2ae7c30723beb16922b4d6 \ - --hash=sha256:d81a68df4f3eee490b66ba990648d3c77cbf2475291ef92aa4e05ef541ecfd66 \ - --hash=sha256:e98934855337d76aa7726f444b0fa597a462271a95d01bc050644d88e1ee5aae \ - --hash=sha256:e9c2aaaa9738ba3334262734bd25d9b2d6ea446400f815bbdea17571b9e6d8fb \ - --hash=sha256:ef6d98d5b51eb826516499429e059872b61e272cb44630ca8de87650242d07d8 \ - --hash=sha256:fdd1e4ed5d526aa4c7a01ed2157d01f0234eaecdb04b1c3b5084d0902986be9f +sqlalchemy==1.4.8 \ + --hash=sha256:15480706c51bdf72d726d0efde77eef6b5f09fbef65bc520f2c4e1f3a429fddf \ + --hash=sha256:1dc4f31124cc359065dceef1a9afe4e2d07a05b2e1e184f5fa48cba96c2249a3 \ + --hash=sha256:1e1c2c65f732f7740d184b4133e5207c0f8974663ab1b79ce1b599ecf55bd3e9 \ + --hash=sha256:221ead411c5e455bbe32b8eb2e8521a31a0769684a93b6895e515e9ce3a49906 \ + --hash=sha256:29e135f8c890c16251162dd40022074430fc39668c8666220a73cb500a3697af \ + --hash=sha256:3bc31ad707f8587c9f93d50cd7cee80ba352162d322808ddbb5bcb5fcfd2bb83 \ + --hash=sha256:43b4958d959b1ee00540b963620f6bffacdd8ac0a19d31450828a1cbacf3693c \ + --hash=sha256:46a454a366f6274c18d3204d11b9e4b98a5c99cba99230e24848d82bef069f75 \ + --hash=sha256:49a85c143f90c74b1d000506e125476d4dec3342f8052ad98e007fb5c657c46c \ + --hash=sha256:4b0404b7a18658d5661120ae0f6f57b220c57126d07a067bfdff304d4c50eaa3 \ + --hash=sha256:64a6645ee39f45f153b45addace5727aef7eb517b115c8bdc77e02be8c8c43a3 \ + --hash=sha256:6d18aec6f16b48b2903940776d0f455d82b0c35f4b307ae5393987c600f8fcda \ + --hash=sha256:76354282fbb3e5b33aee8fcfc2394c7b26d08c53025960d41abc57a6222321cc \ + --hash=sha256:767c0bbe5af14c2ebcad7314faea9709b59593daa3250fe6224429a344c21438 \ + --hash=sha256:81ea6270359629b9c56251bdfc12f2a8afb55034a3ff3698b6a764b300bfe605 \ + --hash=sha256:831aa088b0056b8040423c53543b5bf5663d1d5ffbc175387f2216de7780113e \ + --hash=sha256:859cf5aae0ecc1aa526f9cebdf6ae8078527f0f3aa3fb10156ecc1d044c1c545 \ + --hash=sha256:8a067fbf3ea3f53e4223990d92a8e44259571ef182039ddcb4ffcf217e08a901 \ + --hash=sha256:a7641c8a2d008ee8cef21d3b9eaf7f68259b965318055148fbc5ae6961ae287e \ + --hash=sha256:aa470facb52b927a5a5a1a4755b1452f7e77dcf93e822077354075e7a811bec2 \ + --hash=sha256:b6106343fb97771f20cd945ce6b1d07f8247121d1d4baad062c028e5f0a1f034 \ + --hash=sha256:b8280da3f36b9156ff251a14e5a3e82a4bb58958bdddbd0867cd29e6f3f809de \ + --hash=sha256:c1457a86209c56dfdd1a62904445bc727f962226bf8866f3834fffff8bd7282d \ + --hash=sha256:cb08be4a43ce6729b37efb95816258b00bbd3442eef4a740c09384fb9fe99076 \ + --hash=sha256:d37683c5d84f0694f159a2a515a34f81daf7da96e7efb9ba9d5daf4ae8dde47e \ + --hash=sha256:ddc9de7e46e8044099a94b8d8b92b02693df030ca8684aa775908295270a3556 \ + --hash=sha256:de2e700a2e98b4c621976fdb3174e9e5947c38efedcf60e6d7c20cc7dddb3a99 \ + --hash=sha256:dec822c2a9436092798998475b3b8edd0d59be7fecdd5fb8411ac8db1575ed64 \ + --hash=sha256:e7a4bcb79aa64f1bf995dc2f5966fccc7d21b99cdfb63b57c609cfdba2ea5906 \ + --hash=sha256:ed660a67c0d8690078560d62767a69b359409863827b367167f18fa14ca51ff6 \ + --hash=sha256:eff2c66e930030110a6139b0374013fa1f1a397a67a0c8a1a5d387ca2a112b45 \ + --hash=sha256:fc5dea79bd2626ee2ed034144f6c590441e7c8c036c57c1939ec4a18481c0de1 \ + --hash=sha256:fccd2de1b3b47ca62c2fa5d344e16266a6edc8cce8b80f32e40126df60dbd2c4 \ + --hash=sha256:fe2558be48c92a9be69bd9e7c41bfc46714c5eafc4d59f85fdc338d6999c4962 # via # alembic # jupyterhub