From e6a0cbe5231a81c7ad8137ac6a9848dcf3dbefde Mon Sep 17 00:00:00 2001 From: Fabien Arcellier Date: Tue, 30 Apr 2024 07:21:34 +0200 Subject: [PATCH 01/18] feat: implement authentication workflow on streamsync * feat: implements fastapi application access for external code --- src/streamsync/serve.py | 42 +++++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/src/streamsync/serve.py b/src/streamsync/serve.py index e758ed0e7..1868cf86c 100644 --- a/src/streamsync/serve.py +++ b/src/streamsync/serve.py @@ -6,7 +6,7 @@ import textwrap import typing from contextlib import asynccontextmanager -from typing import Any, Callable, Dict, List, Optional, Set, Union +from typing import Any, Callable, Dict, List, Optional, Set, Union, cast from urllib.parse import urlsplit import uvicorn @@ -14,6 +14,7 @@ from fastapi.routing import Mount from fastapi.staticfiles import StaticFiles from pydantic import ValidationError +from starlette.responses import FileResponse from starlette.websockets import WebSocket, WebSocketDisconnect, WebSocketState from streamsync import VERSION @@ -37,12 +38,15 @@ MAX_WEBSOCKET_MESSAGE_SIZE = 201*1024*1024 logging.getLogger().setLevel(logging.INFO) +app: FastAPI = cast(FastAPI, None) + def get_asgi_app( user_app_path: str, serve_mode: ServeMode, enable_remote_edit: bool = False, on_load: Optional[Callable] = None, on_shutdown: Optional[Callable] = None) -> FastAPI: + global app if serve_mode not in ["run", "edit"]: raise ValueError("""Invalid mode. Must be either "run" or "edit".""") @@ -50,15 +54,15 @@ def get_asgi_app( app_runner = AppRunner(user_app_path, serve_mode) @asynccontextmanager - async def lifespan(app: FastAPI): + async def lifespan(asgi_app: FastAPI): nonlocal app_runner app_runner.hook_to_running_event_loop() app_runner.load() if on_load is not None \ - and hasattr(app.state, 'is_server_static_mounted') \ - and app.state.is_server_static_mounted: + and hasattr(asgi_app.state, 'is_server_static_mounted') \ + and asgi_app.state.is_server_static_mounted: on_load() try: @@ -70,12 +74,12 @@ async def lifespan(app: FastAPI): if on_shutdown is not None: on_shutdown() - asgi_app = FastAPI(lifespan=lifespan) + app = FastAPI(lifespan=lifespan) """ Reuse the same pattern to give variable to FastAPI application - than `asgi_app.state.is_server_static_mounted` already use in streamsync. + than `app.state.is_server_static_mounted` already use in streamsync. """ - asgi_app.state.streamsync_app = True + app.state.streamsync_app = True def _get_extension_paths() -> List[str]: extensions_path = pathlib.Path(user_app_path) / "extensions" @@ -126,7 +130,7 @@ def _get_edit_starter_pack(payload: InitSessionResponsePayload): extensionPaths=cached_extension_paths ) - @asgi_app.post("/api/init") + @app.post("/api/init") async def init(initBody: InitRequestBody, request: Request) -> Union[InitResponseBodyRun, InitResponseBodyEdit]: """ @@ -331,7 +335,7 @@ async def _stream_outgoing_announcements(websocket: WebSocket): except (WebSocketDisconnect): pass - @asgi_app.websocket("/api/stream") + @app.websocket("/api/stream") async def stream(websocket: WebSocket): """ Initialises incoming and outgoing communications on the stream. """ @@ -371,20 +375,21 @@ async def stream(websocket: WebSocket): user_app_static_path = pathlib.Path(user_app_path) / "static" if user_app_static_path.exists(): - asgi_app.mount( + app.mount( "/static", StaticFiles(directory=str(user_app_static_path)), name="user_static") user_app_extensions_path = pathlib.Path(user_app_path) / "extensions" if user_app_extensions_path.exists(): - asgi_app.mount( + app.mount( "/extensions", StaticFiles(directory=str(user_app_extensions_path)), name="extensions") server_path = os.path.dirname(__file__) server_static_path = pathlib.Path(server_path) / "static" if server_static_path.exists(): - asgi_app.mount( - "/", StaticFiles(directory=str(server_static_path), html=True), name="server_static") - asgi_app.state.is_server_static_mounted = True + app.mount("/assets", StaticFiles(directory=os.path.join(server_static_path, 'assets'), html=True), name="server_static") + app.get('/')(lambda: FileResponse(os.path.join(server_static_path, 'index.html'))) + app.get('/favicon.png')(lambda: FileResponse(os.path.join(server_static_path, 'favicon.png'))) + app.state.is_server_static_mounted = True else: logging.error( textwrap.dedent( @@ -401,7 +406,7 @@ async def stream(websocket: WebSocket): # Return - return asgi_app + return app def print_init_message(): @@ -433,12 +438,9 @@ def on_load(): run_name = "Builder" if mode == "edit" else "App" print_route_message(run_name, port, host) - asgi_app = get_asgi_app( - app_path, mode, enable_remote_edit, on_load) + app = get_asgi_app(app_path, mode, enable_remote_edit, on_load) log_level = "warning" - - uvicorn.run(asgi_app, host=host, - port=port, log_level=log_level, ws_max_size=MAX_WEBSOCKET_MESSAGE_SIZE) + uvicorn.run(app, host=host, port=port, log_level=log_level, ws_max_size=MAX_WEBSOCKET_MESSAGE_SIZE) @asynccontextmanager async def lifespan(app: FastAPI): From 81882a2cc9681f7aa1de746b39da0629cf00796c Mon Sep 17 00:00:00 2001 From: Fabien Arcellier Date: Wed, 1 May 2024 15:04:20 +0200 Subject: [PATCH 02/18] feat: implement authentication workflow on streamsync * feat: add specific routes on / to allow user to implement their own route in server_setup module --- src/streamsync/serve.py | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/src/streamsync/serve.py b/src/streamsync/serve.py index 1868cf86c..92d95bcd5 100644 --- a/src/streamsync/serve.py +++ b/src/streamsync/serve.py @@ -383,12 +383,10 @@ async def stream(websocket: WebSocket): app.mount( "/extensions", StaticFiles(directory=str(user_app_extensions_path)), name="extensions") - server_path = os.path.dirname(__file__) - server_static_path = pathlib.Path(server_path) / "static" + server_path = pathlib.Path(__file__) + server_static_path = server_path.parent / "static" if server_static_path.exists(): - app.mount("/assets", StaticFiles(directory=os.path.join(server_static_path, 'assets'), html=True), name="server_static") - app.get('/')(lambda: FileResponse(os.path.join(server_static_path, 'index.html'))) - app.get('/favicon.png')(lambda: FileResponse(os.path.join(server_static_path, 'favicon.png'))) + _mount_server_static_path(app, server_static_path) app.state.is_server_static_mounted = True else: logging.error( @@ -504,3 +502,21 @@ def _fix_mimetype(): js_mimetype = mimetypes.guess_type("myfile.js")[0] if js_mimetype[0] != "text/javascript": mimetypes.add_type("text/javascript", ".js") + +def _mount_server_static_path(app: FastAPI, server_static_path: pathlib.Path) -> None: + """ + Unitarily declares the files and folders present in "/static" directory of source code. + + We avoid the general declaration as below. This declaration limit the ability of a developper to + declare it's own route. + + >>> asgi_app.mount("/", StaticFiles(directory=str(server_static_path), html=True), name="server_static") + + Streamsync routes remain priority. A developer cannot come and overload them. + """ + app.get('/')(lambda: FileResponse(server_static_path.joinpath('index.html'))) + for f in server_static_path.glob('*'): + if f.is_file(): + app.get(f"/{f.name}")(lambda: FileResponse(f)) + if f.is_dir(): + app.mount(f"/{f.name}", StaticFiles(directory=f), name=f"server_static_{f}") From e9683c7c1226cbab9443ca00693877e9ea0c288b Mon Sep 17 00:00:00 2001 From: Fabien Arcellier Date: Wed, 1 May 2024 17:29:25 +0200 Subject: [PATCH 03/18] feat: implement authentication workflow on streamsync * feat: load server_setup.py in run mode if it's present * feat: load server_setup.py in run mode or in edit mode with flag enable-server-setup --- src/streamsync/command_line.py | 22 +++++++++++++++----- src/streamsync/serve.py | 29 ++++++++++++++++++++++++--- tests/backend/test_serve.py | 23 +++++++++++++++++++++ tests/backend/testapp/server_setup.py | 7 +++++++ 4 files changed, 73 insertions(+), 8 deletions(-) create mode 100644 tests/backend/testapp/server_setup.py diff --git a/src/streamsync/command_line.py b/src/streamsync/command_line.py index 1bca9db81..b06424e6e 100644 --- a/src/streamsync/command_line.py +++ b/src/streamsync/command_line.py @@ -21,11 +21,14 @@ def main(): "--host", help="The host on which to run the server. Use 0.0.0.0 to share in your local network.") parser.add_argument( "--enable-remote-edit", help="Set this flag to allow non-local requests in edit mode.", action='store_true') + parser.add_argument( + "--enable-server-setup", help="Set this flag to enable server setup hook in edit mode.", action='store_true') args = parser.parse_args() command = args.command default_port = 3006 if command in ("edit", "hello") else 3005 enable_remote_edit = args.enable_remote_edit + enable_server_setup_hook = args.enable_server_setup port = int(args.port) if args.port else default_port absolute_app_path = _get_absolute_app_path( @@ -33,7 +36,7 @@ def main(): host = args.host if args.host else None _perform_checks(command, absolute_app_path, host, enable_remote_edit) - _route(command, absolute_app_path, port, host, enable_remote_edit) + _route(command, absolute_app_path, port, host, enable_remote_edit, enable_server_setup_hook) def _perform_checks(command: str, absolute_app_path: str, host: Optional[str], enable_remote_edit: Optional[bool]): is_path_folder = absolute_app_path is not None and os.path.isdir(absolute_app_path) @@ -62,19 +65,28 @@ def _perform_checks(command: str, absolute_app_path: str, host: Optional[str], e sys.exit(1) -def _route(command: str, absolute_app_path: str, port: int, host: Optional[str], enable_remote_edit: Optional[bool]): +def _route( + command: str, + absolute_app_path: str, + port: int, + host: Optional[str], + enable_remote_edit: Optional[bool], + enable_server_setup: Optional[bool] +): if host is None: host = "127.0.0.1" if command in ("edit"): streamsync.serve.serve( - absolute_app_path, mode="edit", port=port, host=host, enable_remote_edit=enable_remote_edit) + absolute_app_path, mode="edit", port=port, host=host, + enable_remote_edit=enable_remote_edit, enable_server_setup=enable_server_setup) if command in ("run"): streamsync.serve.serve( - absolute_app_path, mode="run", port=port, host=host) + absolute_app_path, mode="run", port=port, host=host, enable_server_setup=True) elif command in ("hello"): create_app("hello", template_name="hello", overwrite=True) streamsync.serve.serve("hello", mode="edit", - port=port, host=host, enable_remote_edit=enable_remote_edit) + port=port, host=host, enable_remote_edit=enable_remote_edit, + enable_server_setup=False) elif command in ("create"): create_app(absolute_app_path) diff --git a/src/streamsync/serve.py b/src/streamsync/serve.py index 92d95bcd5..51ac03594 100644 --- a/src/streamsync/serve.py +++ b/src/streamsync/serve.py @@ -1,4 +1,5 @@ import asyncio +import importlib.util import logging import mimetypes import os @@ -6,6 +7,7 @@ import textwrap import typing from contextlib import asynccontextmanager +from importlib.machinery import ModuleSpec from typing import Any, Callable, Dict, List, Optional, Set, Union, cast from urllib.parse import urlsplit @@ -44,8 +46,10 @@ def get_asgi_app( user_app_path: str, serve_mode: ServeMode, enable_remote_edit: bool = False, + enable_server_setup: bool = False, on_load: Optional[Callable] = None, - on_shutdown: Optional[Callable] = None) -> FastAPI: + on_shutdown: Optional[Callable] = None, +) -> FastAPI: global app if serve_mode not in ["run", "edit"]: raise ValueError("""Invalid mode. Must be either "run" or "edit".""") @@ -403,6 +407,8 @@ async def stream(websocket: WebSocket): ) # Return + if enable_server_setup is True: + _execute_server_setup_hook(user_app_path) return app @@ -427,7 +433,7 @@ def print_route_message(run_name: str, port: int, host: str): print(f"{run_name} is available at:{END_TOKEN}{GREEN_TOKEN} http://{host}:{port}{END_TOKEN}", flush=True) -def serve(app_path: str, mode: ServeMode, port, host, enable_remote_edit=False): +def serve(app_path: str, mode: ServeMode, port, host, enable_remote_edit=False, enable_server_setup=False): """ Initialises the web server. """ print_init_message() @@ -436,7 +442,12 @@ def on_load(): run_name = "Builder" if mode == "edit" else "App" print_route_message(run_name, port, host) - app = get_asgi_app(app_path, mode, enable_remote_edit, on_load) + """ + Loading of the server_setup.py is active by default + when streamsync is launched with the run command. + """ + enable_server_setup = mode == "run" or enable_server_setup + app = get_asgi_app(app_path, mode, enable_remote_edit, on_load=on_load, enable_server_setup=enable_server_setup) log_level = "warning" uvicorn.run(app, host=host, port=port, log_level=log_level, ws_max_size=MAX_WEBSOCKET_MESSAGE_SIZE) @@ -520,3 +531,15 @@ def _mount_server_static_path(app: FastAPI, server_static_path: pathlib.Path) -> app.get(f"/{f.name}")(lambda: FileResponse(f)) if f.is_dir(): app.mount(f"/{f.name}", StaticFiles(directory=f), name=f"server_static_{f}") + +def _execute_server_setup_hook(user_app_path: str) -> None: + """ + Runs the server_setup.py module if present in the application directory. + + """ + server_setup_path = os.path.join(user_app_path, "server_setup.py") + if os.path.isfile(server_setup_path): + spec = cast(ModuleSpec, importlib.util.spec_from_file_location("server_setup", server_setup_path)) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) # type: ignore + diff --git a/tests/backend/test_serve.py b/tests/backend/test_serve.py index 5d764b5e7..3c07defdd 100644 --- a/tests/backend/test_serve.py +++ b/tests/backend/test_serve.py @@ -146,3 +146,26 @@ def test_multiapp_should_run_the_lifespan_of_all_streamsync_app(self): }) with pytest.raises(fastapi.WebSocketDisconnect): websocket.receive_json() + + def test_server_setup_hook_is_executed_when_its_present_and_enabled(self): + """ + This test verifies that the server_setup.py hook is executed when the application + is started with the enable_server_setup=True option. + + """ + asgi_app: fastapi.FastAPI = streamsync.serve.get_asgi_app(test_app_dir, "run", enable_server_setup=True) + with fastapi.testclient.TestClient(asgi_app) as client: + res = client.get("/probes/healthcheck") + assert res.status_code == 200 + assert res.text == '"1"' + + def test_server_setup_hook_is_ignored_when_its_disabled(self): + """ + This test verifies that the server_setup.py hook is not executed + when the application is started by disabling the server_setup.py hook. + + """ + asgi_app: fastapi.FastAPI = streamsync.serve.get_asgi_app(test_app_dir, "run", enable_server_setup=False) + with fastapi.testclient.TestClient(asgi_app) as client: + res = client.get("/probes/healthcheck") + assert res.status_code == 404 diff --git a/tests/backend/testapp/server_setup.py b/tests/backend/testapp/server_setup.py new file mode 100644 index 000000000..692642687 --- /dev/null +++ b/tests/backend/testapp/server_setup.py @@ -0,0 +1,7 @@ +import streamsync.serve + +asgi_app = streamsync.serve.app + +@asgi_app.get("/probes/healthcheck") +def hello(): + return "1" From 916528ae4b99385b4d4d45b36146b475fe7f4089 Mon Sep 17 00:00:00 2001 From: Fabien Arcellier Date: Wed, 1 May 2024 17:51:00 +0200 Subject: [PATCH 04/18] streamsync.serve.app return fastapi application * docs: document the feature to tune server on server_setup --- docs/docs/custom-server.md | 38 ++++++++++++++++++++++++++- tests/backend/testapp/server_setup.py | 8 +++++- 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/docs/docs/custom-server.md b/docs/docs/custom-server.md index a5f7549fa..b03ce70d3 100644 --- a/docs/docs/custom-server.md +++ b/docs/docs/custom-server.md @@ -2,7 +2,37 @@ Streamsync uses Uvicorn and serves the app in the root path i.e. `/`. If you need to use another ASGI-compatible server or fine-tune Uvicorn, you can easily do so. -## Getting the ASGI app +## Tune server + +You can tune your server at startup by adding a `server_setup.py` file to the root +of your application, next to the `main.py` and `ui.json` files. + +This file is executed during server startup. It allows you to configure [authentication](), +add your own routes and middlewares on FastAPI before the server starts. + +```python +# server_setup.py +import typing + +import streamsync.serve + +if typing.TYPE_CHECKING: + from fastapi import FastAPI + +# Returns the FastAPI application associated with the streamsync server. +asgi_app: FastAPI = streamsync.serve.app + +@asgi_app.get("/probes/healthcheck") +def hello(): + return "1" +``` + +::: warning Using in `edit` mode +If you want to use in `edit` mode, +you can launch `streamsync edit --enable-server-setup `. +::: + +## Implement custom server You can import `streamsync.serve` and use the function `get_asgi_app`. This returns an ASGI app created by FastAPI, which you can choose how to serve. @@ -28,6 +58,12 @@ Note the inclusion of the imported `ws_max_size` setting. This is important for Fine-tuning Uvicorn allows you to set up SSL, configure proxy headers, etc, which can prove vital in complex deployments. +::: tip Use server setup hook +```python +asgi_app = streamsync.serve.get_asgi_app(app_path, mode, enable_server_setup=True) +``` +::: + ## Multiple apps at once Streamsync is built using relative paths, so it can be served from any path. This allows multiple apps to be simultaneously served on different paths. diff --git a/tests/backend/testapp/server_setup.py b/tests/backend/testapp/server_setup.py index 692642687..b1deb0467 100644 --- a/tests/backend/testapp/server_setup.py +++ b/tests/backend/testapp/server_setup.py @@ -1,6 +1,12 @@ +import typing + import streamsync.serve -asgi_app = streamsync.serve.app +if typing.TYPE_CHECKING: + from fastapi import FastAPI + +# Returns the FastAPI application associated with the streamsync server. +asgi_app: FastAPI = streamsync.serve.app @asgi_app.get("/probes/healthcheck") def hello(): From 10e97b79816adf7f28bbfbb0cf1aea88c689557d Mon Sep 17 00:00:00 2001 From: Fabien Arcellier Date: Mon, 6 May 2024 11:43:35 +0200 Subject: [PATCH 05/18] feat: implement authentication workflow on streamsync * feat: implement oidc connector --- alfred/ci.py | 20 -- poetry.lock | 284 +++++++++++++++++++++++++- pyproject.toml | 2 + src/streamsync/app_runner.py | 28 ++- src/streamsync/auth.py | 102 +++++++++ src/streamsync/core.py | 19 +- src/streamsync/serve.py | 46 ++++- src/streamsync/ss_types.py | 4 +- tests/backend/testapp/server_setup.py | 2 +- 9 files changed, 466 insertions(+), 41 deletions(-) create mode 100644 src/streamsync/auth.py diff --git a/alfred/ci.py b/alfred/ci.py index dc06509a9..a6b06bdf4 100644 --- a/alfred/ci.py +++ b/alfred/ci.py @@ -41,24 +41,4 @@ def ci_test(): os.chdir("tests") alfred.run("pytest") -@contextlib.contextmanager -def _preserve_files(path: List[str]): - """ - Preserve files in a temporary directory and restore them after the context - - :param path: list of files to preserve - """ - tmpdir = tempfile.mkdtemp() - try: - for p in path: - os.makedirs(os.path.dirname(os.path.join(tmpdir, p)), exist_ok=True) - shutil.copy(p, os.path.join(tmpdir, p)) - - yield - finally: - for p in path: - shutil.copy(os.path.join(tmpdir, p), p) - - shutil.rmtree(tmpdir) - diff --git a/poetry.lock b/poetry.lock index e18bd42bb..6cb102cc8 100644 --- a/poetry.lock +++ b/poetry.lock @@ -94,6 +94,20 @@ tests = ["attrs[tests-no-zope]", "zope-interface"] tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"] tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] +[[package]] +name = "authlib" +version = "1.3.0" +description = "The ultimate Python library in building OAuth and OpenID Connect servers and clients." +optional = false +python-versions = ">=3.8" +files = [ + {file = "Authlib-1.3.0-py2.py3-none-any.whl", hash = "sha256:9637e4de1fb498310a56900b3e2043a206b03cb11c05422014b0302cbc814be3"}, + {file = "Authlib-1.3.0.tar.gz", hash = "sha256:959ea62a5b7b5123c5059758296122b57cd2585ae2ed1c0622c21b371ffdae06"}, +] + +[package.dependencies] +cryptography = "*" + [[package]] name = "certifi" version = "2024.2.2" @@ -105,6 +119,169 @@ files = [ {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, ] +[[package]] +name = "cffi" +version = "1.16.0" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"}, + {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"}, + {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"}, + {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"}, + {file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"}, + {file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"}, + {file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"}, + {file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"}, + {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"}, + {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"}, + {file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"}, + {file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"}, + {file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"}, + {file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"}, + {file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"}, + {file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"}, + {file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"}, + {file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"}, + {file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"}, + {file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"}, + {file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"}, + {file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"}, + {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"}, + {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"}, + {file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"}, + {file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"}, + {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"}, +] + +[package.dependencies] +pycparser = "*" + +[[package]] +name = "charset-normalizer" +version = "3.3.2" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, + {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, +] + [[package]] name = "click" version = "8.1.7" @@ -130,6 +307,60 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[[package]] +name = "cryptography" +version = "42.0.7" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +optional = false +python-versions = ">=3.7" +files = [ + {file = "cryptography-42.0.7-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:a987f840718078212fdf4504d0fd4c6effe34a7e4740378e59d47696e8dfb477"}, + {file = "cryptography-42.0.7-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:bd13b5e9b543532453de08bcdc3cc7cebec6f9883e886fd20a92f26940fd3e7a"}, + {file = "cryptography-42.0.7-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a79165431551042cc9d1d90e6145d5d0d3ab0f2d66326c201d9b0e7f5bf43604"}, + {file = "cryptography-42.0.7-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a47787a5e3649008a1102d3df55424e86606c9bae6fb77ac59afe06d234605f8"}, + {file = "cryptography-42.0.7-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:02c0eee2d7133bdbbc5e24441258d5d2244beb31da5ed19fbb80315f4bbbff55"}, + {file = "cryptography-42.0.7-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:5e44507bf8d14b36b8389b226665d597bc0f18ea035d75b4e53c7b1ea84583cc"}, + {file = "cryptography-42.0.7-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:7f8b25fa616d8b846aef64b15c606bb0828dbc35faf90566eb139aa9cff67af2"}, + {file = "cryptography-42.0.7-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:93a3209f6bb2b33e725ed08ee0991b92976dfdcf4e8b38646540674fc7508e13"}, + {file = "cryptography-42.0.7-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e6b8f1881dac458c34778d0a424ae5769de30544fc678eac51c1c8bb2183e9da"}, + {file = "cryptography-42.0.7-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3de9a45d3b2b7d8088c3fbf1ed4395dfeff79d07842217b38df14ef09ce1d8d7"}, + {file = "cryptography-42.0.7-cp37-abi3-win32.whl", hash = "sha256:789caea816c6704f63f6241a519bfa347f72fbd67ba28d04636b7c6b7da94b0b"}, + {file = "cryptography-42.0.7-cp37-abi3-win_amd64.whl", hash = "sha256:8cb8ce7c3347fcf9446f201dc30e2d5a3c898d009126010cbd1f443f28b52678"}, + {file = "cryptography-42.0.7-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:a3a5ac8b56fe37f3125e5b72b61dcde43283e5370827f5233893d461b7360cd4"}, + {file = "cryptography-42.0.7-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:779245e13b9a6638df14641d029add5dc17edbef6ec915688f3acb9e720a5858"}, + {file = "cryptography-42.0.7-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d563795db98b4cd57742a78a288cdbdc9daedac29f2239793071fe114f13785"}, + {file = "cryptography-42.0.7-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:31adb7d06fe4383226c3e963471f6837742889b3c4caa55aac20ad951bc8ffda"}, + {file = "cryptography-42.0.7-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:efd0bf5205240182e0f13bcaea41be4fdf5c22c5129fc7ced4a0282ac86998c9"}, + {file = "cryptography-42.0.7-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a9bc127cdc4ecf87a5ea22a2556cab6c7eda2923f84e4f3cc588e8470ce4e42e"}, + {file = "cryptography-42.0.7-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:3577d029bc3f4827dd5bf8bf7710cac13527b470bbf1820a3f394adb38ed7d5f"}, + {file = "cryptography-42.0.7-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2e47577f9b18723fa294b0ea9a17d5e53a227867a0a4904a1a076d1646d45ca1"}, + {file = "cryptography-42.0.7-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1a58839984d9cb34c855197043eaae2c187d930ca6d644612843b4fe8513c886"}, + {file = "cryptography-42.0.7-cp39-abi3-win32.whl", hash = "sha256:e6b79d0adb01aae87e8a44c2b64bc3f3fe59515280e00fb6d57a7267a2583cda"}, + {file = "cryptography-42.0.7-cp39-abi3-win_amd64.whl", hash = "sha256:16268d46086bb8ad5bf0a2b5544d8a9ed87a0e33f5e77dd3c3301e63d941a83b"}, + {file = "cryptography-42.0.7-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2954fccea107026512b15afb4aa664a5640cd0af630e2ee3962f2602693f0c82"}, + {file = "cryptography-42.0.7-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:362e7197754c231797ec45ee081f3088a27a47c6c01eff2ac83f60f85a50fe60"}, + {file = "cryptography-42.0.7-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:4f698edacf9c9e0371112792558d2f705b5645076cc0aaae02f816a0171770fd"}, + {file = "cryptography-42.0.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5482e789294854c28237bba77c4c83be698be740e31a3ae5e879ee5444166582"}, + {file = "cryptography-42.0.7-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e9b2a6309f14c0497f348d08a065d52f3020656f675819fc405fb63bbcd26562"}, + {file = "cryptography-42.0.7-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d8e3098721b84392ee45af2dd554c947c32cc52f862b6a3ae982dbb90f577f14"}, + {file = "cryptography-42.0.7-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c65f96dad14f8528a447414125e1fc8feb2ad5a272b8f68477abbcc1ea7d94b9"}, + {file = "cryptography-42.0.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:36017400817987670037fbb0324d71489b6ead6231c9604f8fc1f7d008087c68"}, + {file = "cryptography-42.0.7.tar.gz", hash = "sha256:ecbfbc00bf55888edda9868a4cf927205de8499e7fabe6c050322298382953f2"}, +] + +[package.dependencies] +cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} + +[package.extras] +docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] +docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"] +nox = ["nox"] +pep8test = ["check-sdist", "click", "mypy", "ruff"] +sdist = ["build"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["certifi", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] +test-randomorder = ["pytest-randomly"] + [[package]] name = "distro" version = "1.9.0" @@ -692,7 +923,6 @@ optional = false python-versions = ">=3.9" files = [ {file = "pandas-2.2.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:90c6fca2acf139569e74e8781709dccb6fe25940488755716d1d354d6bc58bce"}, - {file = "pandas-2.2.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c7adfc142dac335d8c1e0dcbd37eb8617eac386596eb9e1a1b77791cf2498238"}, {file = "pandas-2.2.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4abfe0be0d7221be4f12552995e58723c7422c80a659da13ca382697de830c08"}, {file = "pandas-2.2.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8635c16bf3d99040fdf3ca3db669a7250ddf49c55dc4aa8fe0ae0fa8d6dcc1f0"}, {file = "pandas-2.2.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:40ae1dffb3967a52203105a077415a86044a2bea011b5f321c6aa64b379a3f51"}, @@ -713,7 +943,6 @@ files = [ {file = "pandas-2.2.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:43498c0bdb43d55cb162cdc8c06fac328ccb5d2eabe3cadeb3529ae6f0517c32"}, {file = "pandas-2.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:d187d355ecec3629624fccb01d104da7d7f391db0311145817525281e2804d23"}, {file = "pandas-2.2.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0ca6377b8fca51815f382bd0b697a0814c8bda55115678cbc94c30aacbb6eff2"}, - {file = "pandas-2.2.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9057e6aa78a584bc93a13f0a9bf7e753a5e9770a30b4d758b8d5f2a62a9433cd"}, {file = "pandas-2.2.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:001910ad31abc7bf06f49dcc903755d2f7f3a9186c0c040b827e522e9cef0863"}, {file = "pandas-2.2.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66b479b0bd07204e37583c191535505410daa8df638fd8e75ae1b383851fe921"}, {file = "pandas-2.2.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a77e9d1c386196879aa5eb712e77461aaee433e54c68cf253053a73b7e49c33a"}, @@ -904,6 +1133,17 @@ files = [ [package.dependencies] numpy = ">=1.16.6,<2" +[[package]] +name = "pycparser" +version = "2.22" +description = "C parser in Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, + {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, +] + [[package]] name = "pydantic" version = "2.7.1" @@ -1196,6 +1436,27 @@ files = [ attrs = ">=22.2.0" rpds-py = ">=0.7.0" +[[package]] +name = "requests" +version = "2.31.0" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.7" +files = [ + {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, + {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + [[package]] name = "rich" version = "13.7.1" @@ -1584,6 +1845,23 @@ files = [ {file = "ujson-5.10.0.tar.gz", hash = "sha256:b3cd8f3c5d8c7738257f1018880444f7b7d9b66232c64649f562d7ba86ad4bc1"}, ] +[[package]] +name = "urllib3" +version = "2.2.1" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.8" +files = [ + {file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"}, + {file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + [[package]] name = "uvicorn" version = "0.29.0" @@ -1897,4 +2175,4 @@ ds = ["pandas", "plotly", "pyarrow"] [metadata] lock-version = "2.0" python-versions = ">=3.9.2, <4.0" -content-hash = "7e07bc698f2f4f308882092e1af307007d0948c3cdd502d698493481353a3184" +content-hash = "3ee7a7551fff7da5d312f98fd7035e6a10589b7c6ba57013b87128c13a4a55c8" diff --git a/pyproject.toml b/pyproject.toml index 4f6d67bbd..4807c0682 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,6 +40,8 @@ writer-sdk = "0.1.0a1" pandas = {version = ">= 2.2.0, < 3", optional = true} pyarrow = {version = ">= 15.0.0, < 16.0.0",optional = true} plotly = {version = ">= 5.18.0, < 6", optional = true} +authlib = "^1.3.0" +requests = "^2.31.0" [tool.poetry.group.build] diff --git a/src/streamsync/app_runner.py b/src/streamsync/app_runner.py index 33d60df30..b72bddd98 100644 --- a/src/streamsync/app_runner.py +++ b/src/streamsync/app_runner.py @@ -129,8 +129,9 @@ def _handle_session_init(self, payload: InitSessionRequestPayload) -> InitSessio import streamsync - session = streamsync.session_manager.get_new_session( - payload.cookies, payload.headers, payload.proposedSessionId) + session = streamsync.session_manager.get_session(payload.proposedSessionId) + if session is None: + session = streamsync.session_manager.get_new_session(payload.cookies, payload.headers, payload.proposedSessionId) if session is None: raise MessageHandlingException("Session rejected.") @@ -254,6 +255,14 @@ def _handle_message(self, session_id: str, request: AppProcessServerRequest) -> payload=self._handle_state_enquiry(session) ) + if type == "setUserinfo": + session.userinfo = request.payload + return AppProcessServerResponse( + status="ok", + status_message=None, + payload=None + ) + if self.mode == "edit" and type == "componentUpdate": cu_req_payload = ComponentUpdateRequestPayload.parse_obj( request.payload) @@ -808,3 +817,18 @@ async def notify_of_code_update(self): self.code_update_condition.notify_all() finally: self.code_update_condition.release() + + def set_userinfo(self, session_id: str, userinfo: dict) -> None: + def run_async_in_thread(): + message = AppProcessServerRequest( + type="setUserinfo", + payload=userinfo + ) + + asyncio.run(self.dispatch_message(session_id, message)) + + thread = threading.Thread(target=run_async_in_thread) + thread.start() + thread.join() + return + diff --git a/src/streamsync/auth.py b/src/streamsync/auth.py new file mode 100644 index 000000000..59005cde3 --- /dev/null +++ b/src/streamsync/auth.py @@ -0,0 +1,102 @@ +import dataclasses +from abc import ABCMeta, abstractmethod +from typing import Callable, Optional + +from authlib.integrations.requests_client.oauth2_session import OAuth2Session # type: ignore +from fastapi import Request +from starlette.responses import RedirectResponse + +import streamsync.serve +from streamsync.core import session_manager +from streamsync.serve import StreamsyncFastAPI +from streamsync.ss_types import InitSessionRequestPayload + + +class Auth: + """ + Interface to implement authentication in streamsync. + """ + __metaclass__ = ABCMeta + + @abstractmethod + def register(self, app: StreamsyncFastAPI, callback: Optional[Callable[[Request, str], None]] = None): + raise NotImplementedError + + +@dataclasses.dataclass +class Oidc(Auth): + """ + Configure streamsync to use OpenID Connect. If this is set, streamsync will + redirect anonymous users to OpenID Connect issuer. + + The issuer will then + authenticate the user and redirect back to the streamsync application with + an authorization code. The streamsync application will then exchange the + authorization code for an access token and use the access token to + authenticate the user and fetch user information. + + >>> oidc = Oidc( + ... client_id="xxxxxxx", + ... client_secret="xxxxxxxxxxxxx.apps.googleusercontent.com", + ... url_authorize="https://accounts.google.com/o/oauth2/auth", + ... url_oauthtoken="https://oauth2.googleapis.com/token", + ... url_userinfo="https://www.googleapis.com/oauth2/v1/userinfo?alt=json", + ... ) + >>> streamsync.server.register_auth(oidc) + + """ + client_id: str + client_secret: str + host_url: str + url_authorize: str + url_oauthtoken: str + scope: str = "openid email profile" + callback_route: str = "/callback" + url_userinfo: Optional[str] = None + + authlib: OAuth2Session = None + callback_func: Optional[Callable[[Request, str], None]] = None + + + def register(self, asgi_app: StreamsyncFastAPI, callback: Optional[Callable[[Request, str], None]] = None): + self.authlib = OAuth2Session( + client_id=self.client_id, + client_secret=self.client_secret, + scope=self.scope.split(" "), + redirect_uri=f"{self.host_url}{self.callback_route}", + authorization_endpoint=self.url_authorize, + token_endpoint=self.url_oauthtoken, + ) + + self.callback_func = callback + + @asgi_app.middleware("http") + async def oidc_middleware(request: Request, call_next): + session = request.cookies.get('session') + if session is not None or request.url.path in [self.callback_route]: + return await call_next(request) + else: + url = self.authlib.create_authorization_url(self.url_authorize) + response = RedirectResponse(url=url[0]) + return response + + @asgi_app.get(self.callback_route) + async def route_callback(request: Request): + self.authlib.fetch_token(url=self.url_oauthtoken, authorization_response=str(request.url)) + response = RedirectResponse(url='/') + session_id = session_manager.generate_session_id() + + app_runner = streamsync.serve.app_runner(asgi_app) + await app_runner.init_session(InitSessionRequestPayload( + cookies=request.cookies, headers=request.headers, proposedSessionId=session_id)) + + if self.url_userinfo: + userinfo = self.authlib.get(self.url_userinfo).json() + app_runner.set_userinfo(session_id=session_id, userinfo=userinfo) + + if self.callback_func: + self.callback_func(request, session_id) + + # At this part, we should + response.set_cookie(key="session", value=session_id, httponly=True, expires=0) + return response diff --git a/src/streamsync/core.py b/src/streamsync/core.py index 905b525b9..3c21b3bcd 100644 --- a/src/streamsync/core.py +++ b/src/streamsync/core.py @@ -753,7 +753,7 @@ class HandlerEntry(TypedDict): meta: 'EventHandlerRegistry.HandlerMeta' def __init__(self): - self.handler_map: Dict[str, 'EventHandlerRegistry.HandlerEntry'] = {} + self.handler_map: Dict[str, 'EventHandlerRegistry.HandlerEntry'] = {} # type: ignore def __iter__(self): return iter(self.handler_map.keys()) @@ -1181,6 +1181,7 @@ def __init__(self, session_id: str, cookies: Optional[Dict[str, str]], headers: self.session_state = new_state self.session_component_tree = core_ui.build_session_component_tree(base_component_tree) self.event_handler = EventHandler(self) + self.userinfo: Optional[dict] = None def update_last_active_timestamp(self) -> None: self.last_active_timestamp = int(time.time()) @@ -1245,7 +1246,10 @@ def get_new_session(self, cookies: Optional[Dict] = None, headers: Optional[Dict self.sessions[new_id] = new_session return new_session - def get_session(self, session_id: str) -> Optional[StreamsyncSession]: + def get_session(self, session_id: Optional[str]) -> Optional[StreamsyncSession]: + if session_id is None: + return None + return self.sessions.get(session_id) def _generate_session_id(self) -> str: @@ -1269,6 +1273,14 @@ def prune_sessions(self) -> None: for session_id in prune_sessions: self.close_session(session_id) + @staticmethod + def generate_session_id() -> str: + """ + Generates a random session identifier which can be used to propose a session number before starting + the app process in the apiinit route. + """ + return secrets.token_hex(SessionManager.TOKEN_SIZE_BYTES) + class EventHandler: @@ -1342,7 +1354,8 @@ def _call_handler_callable(self, event_type, target_component, instance_path, pa session_info = { "id": self.session.session_id, "cookies": self.session.cookies, - "headers": self.session.headers + "headers": self.session.headers, + "userinfo": self.session.userinfo } arg_values.append(session_info) elif arg == "ui": diff --git a/src/streamsync/serve.py b/src/streamsync/serve.py index 51ac03594..d17b0d1b6 100644 --- a/src/streamsync/serve.py +++ b/src/streamsync/serve.py @@ -12,7 +12,7 @@ from urllib.parse import urlsplit import uvicorn -from fastapi import FastAPI, HTTPException, Request +from fastapi import FastAPI, HTTPException, Request, Response from fastapi.routing import Mount from fastapi.staticfiles import StaticFiles from pydantic import ValidationError @@ -37,10 +37,25 @@ StreamsyncWebsocketOutgoing, ) +if typing.TYPE_CHECKING: + from .auth import Auth + MAX_WEBSOCKET_MESSAGE_SIZE = 201*1024*1024 logging.getLogger().setLevel(logging.INFO) -app: FastAPI = cast(FastAPI, None) + +class StreamsyncState(typing.Protocol): + app_runner: AppRunner + streamsync_app: bool + is_server_static_mounted: bool + +class StreamsyncAsgi(typing.Protocol): + state: StreamsyncState + +class StreamsyncFastAPI(FastAPI, StreamsyncAsgi): # type: ignore + pass + +app: StreamsyncFastAPI = cast(StreamsyncFastAPI, None) def get_asgi_app( user_app_path: str, @@ -49,7 +64,7 @@ def get_asgi_app( enable_server_setup: bool = False, on_load: Optional[Callable] = None, on_shutdown: Optional[Callable] = None, -) -> FastAPI: +) -> StreamsyncFastAPI: global app if serve_mode not in ["run", "edit"]: raise ValueError("""Invalid mode. Must be either "run" or "edit".""") @@ -78,12 +93,13 @@ async def lifespan(asgi_app: FastAPI): if on_shutdown is not None: on_shutdown() - app = FastAPI(lifespan=lifespan) + app = cast(StreamsyncFastAPI, FastAPI(lifespan=lifespan)) """ Reuse the same pattern to give variable to FastAPI application than `app.state.is_server_static_mounted` already use in streamsync. """ app.state.streamsync_app = True + app.state.app_runner = app_runner def _get_extension_paths() -> List[str]: extensions_path = pathlib.Path(user_app_path) / "extensions" @@ -135,7 +151,7 @@ def _get_edit_starter_pack(payload: InitSessionResponsePayload): ) @app.post("/api/init") - async def init(initBody: InitRequestBody, request: Request) -> Union[InitResponseBodyRun, InitResponseBodyEdit]: + async def init(initBody: InitRequestBody, request: Request, response: Response) -> Union[InitResponseBodyRun, InitResponseBodyEdit]: """ Handles session init and provides a "starter pack" to the frontend. @@ -149,21 +165,26 @@ async def init(initBody: InitRequestBody, request: Request) -> Union[InitRespons raise HTTPException( status_code=403, detail="Incorrect origin. Only local origins are allowed.") - response = await app_runner.init_session(InitSessionRequestPayload( + session_id = request.cookies.get("session") + if session_id is not None: + initBody.proposedSessionId = session_id + + app_response = await app_runner.init_session(InitSessionRequestPayload( cookies=dict(request.cookies), headers=dict(request.headers), proposedSessionId=initBody.proposedSessionId )) - status = response.status - if status == "error" or response.payload is None: + status = app_response.status + + if status == "error" or app_response.payload is None: raise HTTPException(status_code=403, detail="Session rejected.") if serve_mode == "run": - return _get_run_starter_pack(response.payload) + return _get_run_starter_pack(app_response.payload) if serve_mode == "edit": - return _get_edit_starter_pack(response.payload) + return _get_edit_starter_pack(app_response.payload) # Streaming @@ -432,6 +453,8 @@ def print_route_message(run_name: str, port: int, host: str): print(f"{run_name} is available at:{END_TOKEN}{GREEN_TOKEN} http://{host}:{port}{END_TOKEN}", flush=True) +def register_auth(auth: 'Auth', callback: Optional[Callable[[Request, str], None]] = None): + auth.register(app, callback=callback) def serve(app_path: str, mode: ServeMode, port, host, enable_remote_edit=False, enable_server_setup=False): """ Initialises the web server. """ @@ -543,3 +566,6 @@ def _execute_server_setup_hook(user_app_path: str) -> None: module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) # type: ignore + +def app_runner(asgi_app: StreamsyncFastAPI) -> AppRunner: + return asgi_app.state.app_runner diff --git a/src/streamsync/ss_types.py b/src/streamsync/ss_types.py index bae9c7236..94e0b7e77 100644 --- a/src/streamsync/ss_types.py +++ b/src/streamsync/ss_types.py @@ -25,7 +25,8 @@ def read(self) -> Any: ServeMode = Literal["run", "edit"] MessageType = Literal["sessionInit", "componentUpdate", - "event", "codeUpdate", "codeSave", "checkSession", "keepAlive", "stateEnquiry"] + "event", "codeUpdate", "codeSave", "checkSession", + "keepAlive", "stateEnquiry", "setUserinfo"] # Web server models @@ -77,7 +78,6 @@ class InitSessionRequestPayload(BaseModel): headers: Optional[Dict[str, str]] = None proposedSessionId: Optional[str] = None - class InitSessionRequest(AppProcessServerRequest): type: Literal["sessionInit"] payload: InitSessionRequestPayload diff --git a/tests/backend/testapp/server_setup.py b/tests/backend/testapp/server_setup.py index b1deb0467..b209aa210 100644 --- a/tests/backend/testapp/server_setup.py +++ b/tests/backend/testapp/server_setup.py @@ -6,7 +6,7 @@ from fastapi import FastAPI # Returns the FastAPI application associated with the streamsync server. -asgi_app: FastAPI = streamsync.serve.app +asgi_app: 'FastAPI' = streamsync.serve.app @asgi_app.get("/probes/healthcheck") def hello(): From 8d58f42c5c5c9b9be7176ded8860901d375feca3 Mon Sep 17 00:00:00 2001 From: Fabien Arcellier Date: Wed, 8 May 2024 15:34:11 +0200 Subject: [PATCH 06/18] feat: implement authentication workflow on streamsync * doc: write documentation for authentication --- docs/docs/.vitepress/config.ts | 1 + docs/docs/authentication.md | 106 ++++++++++++++++++ docs/docs/images/auth_auth0_fill.png | Bin 0 -> 7930 bytes docs/docs/images/auth_github_fill.png | Bin 0 -> 425 bytes docs/docs/images/auth_google_fill.png | Bin 0 -> 378 bytes docs/docs/images/auth_microsoft_fill.png | Bin 0 -> 156 bytes docs/docs/images/authentication_oidc.png | Bin 0 -> 69824 bytes .../images/authentication_oidc_principle.png | Bin 0 -> 24949 bytes docs/docs/images/quickstart/img.png | Bin 0 -> 28892 bytes src/streamsync/auth.py | 39 +++++++ src/streamsync/core.py | 2 +- 11 files changed, 147 insertions(+), 1 deletion(-) create mode 100644 docs/docs/authentication.md create mode 100644 docs/docs/images/auth_auth0_fill.png create mode 100644 docs/docs/images/auth_github_fill.png create mode 100644 docs/docs/images/auth_google_fill.png create mode 100644 docs/docs/images/auth_microsoft_fill.png create mode 100644 docs/docs/images/authentication_oidc.png create mode 100644 docs/docs/images/authentication_oidc_principle.png create mode 100644 docs/docs/images/quickstart/img.png diff --git a/docs/docs/.vitepress/config.ts b/docs/docs/.vitepress/config.ts index 2e3b2eca8..09ae35aa8 100644 --- a/docs/docs/.vitepress/config.ts +++ b/docs/docs/.vitepress/config.ts @@ -74,6 +74,7 @@ export default { {text: "Custom server", link: "/custom-server"}, {text: "State schema", link: "/state-schema"}, {text: "Backend-driven UI", link: "/backend-driven-ui"}, + {text: "Authentication", link: "/authentication"}, ], }, { diff --git a/docs/docs/authentication.md b/docs/docs/authentication.md new file mode 100644 index 000000000..dd690b438 --- /dev/null +++ b/docs/docs/authentication.md @@ -0,0 +1,106 @@ +# Authentication + +The StreamSync authentication module allows you to restrict access to your application. + +Streamsync will be able to authenticate a user through an identity provider such as Google, Microsoft, Facebook, Github, Auth0, etc. + +::: warning Authentication is done before accessing the application +Authentication is done before accessing the application. It is not possible to trigger authentication for certain pages exclusively. +::: + +## Use OIDC provider + +Authentication configuration is done in [the `server_setup.py` module](custom-server.md). The configuration depends on your identity provider. +Here is an example configuration for Google. + + + +*server_setup.py* +```python +import os +import streamsync.serve +import streamsync.auth + +oidc = streamsync.auth.Oidc( + client_id="1xxxxxxxxx-qxxxxxxxxxxxxxxx.apps.googleusercontent.com", + client_secret="GOxxxx-xxxxxxxxxxxxxxxxxxxxx", + host_url=os.getenv('HOST_URL', "http://localhost:5000"), + url_authorize="https://accounts.google.com/o/oauth2/auth", + url_oauthtoken="https://oauth2.googleapis.com/token", + url_userinfo='https://www.googleapis.com/oauth2/v1/userinfo?alt=json' +) + +streamsync.serve.register_auth(oidc) +``` +### Use pre-configured OIDC + +StreamSync provides pre-configured OIDC providers. You can use them directly in your application. + +| | Provider | Function | Description | +|------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------|----------|-------------------------------------------------------------------------------------------------| +| | Google | `streamsync.auth.Google` | Allow your users to login with their Google Account | +| | Microsoft | `streamsync.auth.Microsoft` | Allow your users to login with their Microsoft Account | +| | Github | `streamsync.auth.Github` | Allow your users to login with their Github Account | +| | Auth0 | `streamsync.auth.Auth0` | Allow your users to login with different providers or with login password through Auth0 | + + +#### Google + +You have to register your application into [Google Cloud Console](https://console.cloud.google.com/). + +*server_setup.py* +```python +import os +import streamsync.serve +import streamsync.auth + +oidc = streamsync.auth.Google( + client_id="1xxxxxxxxx-qxxxxxxxxxxxxxxx.apps.googleusercontent.com", + client_secret="GOxxxx-xxxxxxxxxxxxxxxxxxxxx", + host_url=os.getenv('HOST_URL', "http://localhost:5000") +) + +streamsync.serve.register_auth(oidc) +``` + +#### Auth0 + +You have to register your application into [Auth0](https://auth0.com/). + +*server_setup.py* +```python +import os +import streamsync.serve +import streamsync.auth + +oidc = streamsync.auth.Auth0( + client_id="xxxxxxx", + client_secret="xxxxxxxxxxxxx", + domain="xxx-xxxxx.eu.auth0.com", + host_url=os.getenv('HOST_URL', "http://localhost:5000") +) + +streamsync.serve.register_auth(oidc) +``` + +[//]: # (### Callback to refuse access to the application) + +[//]: # () +[//]: # (### Callback to modify user information) + + +### Authentication workflow + + + +## User information in event handler + +When the `user_info` route is configured, user information will be accessible +in the event handler through the `session` argument. + +```python +def on_page_load(state, session): + email = session['userinfo'].get('email', None) + state['email'] = email +``` + diff --git a/docs/docs/images/auth_auth0_fill.png b/docs/docs/images/auth_auth0_fill.png new file mode 100644 index 0000000000000000000000000000000000000000..a99088db5e9302e785e1309c61a15dc70bfaec18 GIT binary patch literal 7930 zcmeHrc{o&k*#FoiWlhq|q~xn|}O3ae(S>DlYY2X23PS>Sb3 zuJ-JWQ1mqNMgD{U`sdP}U3uIQw#~!0TSE4I@~(p225WohNlIBek9M}d>jIBmj5y|j zi@G~;EICRexc#hp4Oq-G%ET(R>ALOOJzdWSqs^tmnM>cp?iZBJOSUQNc0W2A_}<^- ze$bI#Q8i_iatYKcEq(3*@N~BD=nl`h)^}%I5KmpN;!?Gqiz5cpiHIGlk=j{`d9D*z zGm{501{*QU%-Qo7a}I|-R}8O9nz(u6Jk&I2uvNtkf`3g5szM1<7H-!ZdEX>-Z=k{} z#pZl<;rRxC|MISa>9A+KX%|{jrFvMiOFwSKge-V@f$3*Rdm5o4s0-4f=%e+wQaSpw zp(j@*0Al!e0CLI+zDX1*G08Nufg~t+ak7weG(*3!n@fp%qUiF{eUopz7ZXZ9^Gmjj z@+RLE7~`NU7zc?+=1dlGdPm3$>&=J}%3+)*g$ae}hC3Bo-x5#59Y}U+Gq=G4;z`Q( zW#mJZ=Q(0~6Ox}gKzoAZihW+}ICt?7g5%Ar>Qovw)@z;7 z+zQIUl!x|c^97%&7${ya=l61^DCDQazZ{`N4h1cA!&I-dNx)E!@jm@a)C-eRwF9UKZ`OxX zD37xAO`S=foRx_tJ6{(F?C1V8QoT;TI@gDq9Q6yyvK%zs+FXq5y{D# zW%*`t+MGj`;@#0z(cbU#9WL%z@#_vY0_)QR$-pnfIq~#$iPmrNdj8;Q7can!xSN;FLQ!Ds;bKt`zNxnRtmFr7b>0H)a2l z`eKS=rO={(RYRhAdUUEvh`iIw9;elP&mRrc{?t9R$CRofMAQ#+W~Pcn7x+rp2{v{M z`+R{k7;PUH|mDj7XqSk_U@jI;H;^WJo0y>S+Oq7{fnXa?n{fj6~i!^ zio_wSCqFyBSPKn0yho-;s|dP8rIaBql{ZXh==hh$#TK3P7k=8Yul7uiwl5B?yVtR8 zvU%=k;Y+ZXt~tso{|aAQ{8xaSaC8 w5{1EJArA>TF4Pt+)q3bhOpgpQhjg6icDL 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 literal 0 HcmV?d00001 diff --git a/docs/docs/images/auth_github_fill.png b/docs/docs/images/auth_github_fill.png new file mode 100644 index 0000000000000000000000000000000000000000..7b7a6fe164fa64d6794977d128da09ba47046486 GIT binary patch literal 425 zcmV;a0apHrP)Px$V@X6oR7gv;m4QveKnz9QNq|jY5?~Tw66hqrB(MqeN%^OLt}nS%s-;L#>+`d} z?aLvqY=H%}}BkmYA_ARC+?7n%4EBSXD^EIuSfudq9>A_G26bI*o=xsm=G zy%M17i36Ar05Y;e*BQ&QJ}mv{#&qa3L7*CvlXN6Tf*ru+K@POsV}8C>?-Ko8D36YL zA6kMAV0u&{jY7vgRx02CcQ_lj36d+&5_EuljQ)PnRXx%6s&C?U6PPzyRp8{1YU#410<+G@h literal 0 HcmV?d00001 diff --git a/docs/docs/images/auth_google_fill.png b/docs/docs/images/auth_google_fill.png new file mode 100644 index 0000000000000000000000000000000000000000..e1d897aa08dee2ad839665b80b1073382ae4ba12 GIT binary patch literal 378 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`GjjKx9jP7LeL$-D%zO*~y3Lp;2X zPQA!`$biS?u5W@60^VTx6%e#@<>VcUM5le4e&^`((@Q7LTs>1V-iC!+@MhNut2NU* zj(ioE=>2k%DigzbT?Uq=O*z6I?7xdx+RL67w$x0RvnEfcY2$-CkL7l)C~MF$fBdYZ z`5uF!WVYz8GfmBt%RZQxxW_xJY2v8dw3M?faYa=z=N(fe--cv`&ep%*^hDCxSK2g0 z%caM!wP;^mW$eK@O|tgB=@)}PjE+?X%lEf0m>hndp*rbu#D0O2Pt!QM{`sYN>0dv& znsbf&vyCfD*B!pfVk4Ee=&maThr)4KNLwOj?qb*sC$K6Dmn3sk?Gt@UQp zu^C$H6b*a>e;0J*<;K45RaD$&!hG>_>Lh;`W9C+!y6J)+ekT<_RV+|BEYE2-O~6gE Uy=ku(FfbWBUHx3vIVCg!0ERf6=l}o! literal 0 HcmV?d00001 diff --git a/docs/docs/images/auth_microsoft_fill.png b/docs/docs/images/auth_microsoft_fill.png new file mode 100644 index 0000000000000000000000000000000000000000..ed9750efd2fbe6fd50163c85d0635c9f644b9582 GIT binary patch literal 156 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ℜUn>~i?h)z~ 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 v-`$1qLhtVw0MF6yP6xbY^!GQzqc#PV literal 0 HcmV?d00001 diff --git a/docs/docs/images/quickstart/img.png b/docs/docs/images/quickstart/img.png new file mode 100644 index 0000000000000000000000000000000000000000..e565d03db86a6f1f81beebe5dcf74312d6fd0091 GIT binary patch literal 28892 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

^-;4@sE%!M(rA!7Zt!E#cv5XoWt* zvc%BAO2R4uMzik|E0g*t>j2vXa~1+O0TiC@ntE2;=yR_I=KH!EsB|6oUyk1 z8sXFb>aiuM6V{}zB}quwZu^pFSpKhV1y@l3zyh48Pmy&tOjj*YvF~_+o}h zrtyh&e?}Wriy&ZI-IW(Eo~<(lrY97_E&}Ww@nJ5P%i3d2@2_`d!l@zONK=t(}-G3E?*q*Ohe^DHQnJ*iak>7D` zgtL##>kfnE0%XfYwZADYPd*LO@6|OjLMHw!P~5w~tfM$p_YbR4w{LxDg=>Uwe}69V zC%{|0uG131OA1mWl2Nk)9w4D{TRuwQr`^HEy~kkIUfmi>p+gmN2RDteTIFcDapdM#1|0DS9(yPonY&s${%re1YOnE*~4c$ ze@ah4>UE70xSVSZ37^0D-B3V8dS}X>H@TRD8{gDH71u!WoHsZ18InxEr7$LDY|OzG z>l=zb6s|qSVpOKl8GRfB(>_YbB9nBnd5Rwx>A)FXG@`>*ry|Ozl%-^hrX&swUD)q_ zC645`HKn>lID)Vw*ZDdj=U=noi=LZp^dvui-l~cTjo?Pk z4#csyI5^x{-`Kqo6>H-9hpvj12PRCqd-Jb=+2sk%q@ITDK83h)nM%l^ZvD0Jj z@Mm4Q26h`H(N<5`lPt`+BH_gS@P#1zdLvH=HtzMm!vA+zf2lz;Ad~X3FMf5$ah%&@ z2Y~`S-~)4Ua{1h<{lXj(C)crYh~U?^0BOd#2fbR=Lvc7PO4-`3&~u~=g1&uq(9s6b zsl)Fx{cC^B%VGkAzU>!(vC>N%&}?_uISTYD807#p^=lEO?_guIGS&B%8*M~>ur&78 zb`y3?%Cb&$o#%I8eZi#76ymd`&hpzwBBS?&3~ zaqEtf4NPH*~|B4 zO#z(|PkH{i=;)pu1bVjK)wU>v7WC&4{`sff`Y`=f=2`r269Ej9(Nf~lUSvgoee<%* zxL)Y0eQ-claW>pv`gWOZhG8Ot*I4#*unz%Igh2&OYirxj*EzM8HNPV7xEIVZ4JR86%$sr_(8l!Pk(5LUBu$Xb6Kx_{C>pm zyvgU}k@!op>;XEq5`BKK`0-=S>3DAJO;YO$lh=|R%Dj4Ra$bh2P7Nc+JFjR3^dL@h zUJhPXhg0LP6EXy2$cT=(S?D3R2o@F$9}{G2a6}M0neQ`j`-`*xWlxO#!_Fb4RzzWZ|{j1RNpj%?)xp!9PZ^ct1n;Wd19mKkD(%=r;&;Qmz{qv3%h^U zkR+(I97|?5mbpfsMXKVC$wiVoGA$w;v6+v%=Z#KOv8_8c&ukAfAlN+yi$O$rC!u!| zs(FqF^k-;6R1ub5iXR4>b;iw(A?Ck`<&~2 zyQkl3@l-Nwg5w%Qc|bocxBPnJ(N6nZ5eJWsqDZ|z6uwO;iDVm&)kRG`)ht0G@dueN#7+K(vEf#KOyEz zPBq=vOPx#gRZG1~A*|!j_IGKcDvT_En7PhohX3l0eiRTR0oRbd#m>RO2+5}|frB1+ zcU1FrAPM`L3=%zt8zA`uy01y#oXyS6B5M(p2=>2inu*SKl!Yuu>B@8=Ph>d2MlcAk zf)Uaqv@5vrGsIOQrH)e`@=a^r=c4!Y7wgsfdu`&?@{}SxnBzIu1UHX+ZIG&W@#8mo8eE0l^i)3l9u>;56{k*uB1JhOLn`BWipB~>krSSZ zg(=Jj5<&tz7czQk{gGgXIS#GwX%fNdIhvKXN?slJ3Z2`{mOkrc3z^!0Coh~D6*EH- ze4aer6s*iSBhi2!#rsY|PnB_3G>}7_ zfbRjYt8-r6*@2zz8hGl7&;uy^+rI5$B@pfb$))t*lNp6+#`;JX)TR5CpuPyG?_dLB z%@L<+J*y&0P*s4VmeiPX(95)_`W0Mg@oY+)t~$xx&hRP`1{eZ|kLRu$ zx)7b_Tjery>x99K6`z%q>_MD|<4KUfni@oOh}hvmAD{cOupl)nXiel?$!fiWnbB69 zE+2O?t7j-Hwn*VCLj5nK944#mEx`7ez$^nCP*PHwgqBA)U2F@)oHYq=O;%n2$;wd? z3p+bI_4`@SvI~YeT#!o=bXTsv2XwF8roD*^ZJ}K&uSK{7r+~ncm__FyF>yA<9?jpI z59Wrx%FO|kHfjrjLT;qy(O z)laf8$=0k1>$qRl#mNXKF8ZPK=ZDFYKKjDAc=u{AIT`5Lmd_I8JWb9wJ`*O)5TmIX zCTwIIy%%KKM_>;It!-08!mrdkEV?sz>t~|9mIjwpn~8WSbVWT=x^w+Y7F6^F-D1ru zb6Ofb4!M~*n14-1eMO+%pBt43IS~J;OT2%!a54Dj)ld7AYIsbwGDqRsUI~kSo!gb( z$ABk#HnX6h*LPoR0y>365g1Bx+JAm+CV+SM=4h{ArK)6!pvIWARI?x`2iOb5)&UabUDbIB+XJ9Qs zU0u{y5G4t?DkNxk^VO=*!{d zQZ0cbG3IcJuQIZ-0`~JCwc0^R?bcl~DReQUl>D)fhbqxEpaW(>do-@FoI0-$4=BvB z?=^8P-;>rp*__}2QbK7(V*^7&!$TF`Udk$)iE{D%Nn9d^b|_E*IKT>-V4x(Yqpj0n z5i^q2n6SS$bd{XluvVZ}gpr{FRTFS;-L_6TAcCsfuj-_8!H7>UhVxX=Ne5TZ+dw3u zmn}!?UJq26B0!Te7Led|ifnTBGNMwvnsG&r6lxxTc#m>5@gf826V;UWes>>rdS2a! z`R;%Hd&`h8oAhUVa^4r9eSAXFvi5y8XgM#xNaipNnUse$g3c#mbfYh^^Gcs`)T2xH zi(2vu38DIt28RhQSb^j2km<4YAfw&J#>e6A$WyTb36g@L7PlCCEIxL=*~=Q-$(ZmJ zB6wflV&lv2drORFK#s(yn)?+>2WSjfvHg=15r@UkK9nk85uzr5B>UH7Y-FTrj>2Pz zRlat0vCKQ5Id(f*hZud4(X6zHYB3D!$XCr%uCX&9eaJz}sH9QrkR@m}+i(UnJRrUR zWih|)Yy(h%b*dny+644KpE{5n=jR+B8~_Owq|C5I6+PK=VH^*tPx*uThJiUcad!-g`J#X|jO=4s_gcOLN~poZs)=j}R;& z^ZBc~tW{-oHuwF;n7Q2OIF9r^hR6BF(N++TRlstc;3(+Aw{I`Gf+-93fab|2q(~{F z_1(1_!=(*t7&#J(A6f6JnAOf@Il2j|pI;u(K*ybD!NSHcYpanpHZ~epcKzHBF@-ML z-70fhTDmk_lWe%iEbr5(u}cH$g_sJzDe$xa>)iQ}Jw@8p`CqayQoH?Y8^d`K9^&P0 zI~O&SCFGz2o?zS5f%=or>wCS-qrW)fe{>gq`?RS{0}Boa@YtLGL~Y^$ zX{}WjBQhR9WaGEN1cVo~?tZZ%u%^yxYH<~Ptxl%!!6Ri28 zM~SE!SWF%VXdVowE{FhpC}t(R6`+qhUx5j`+P*kxG7v3M!}NLw6R2GV9kt@?;{^FEM%MIak8aKJK{; znv@_5(UWZnAW}@^O+$^ItPQnwy~%=5Qc^-O;^0H7S#M?OjgjR{h;{Y^h0J-MmgCYW?QgjG`%NZq0{MZ;~)Q(AyW7TnAN zoM}hFQo=#l*4B0grw+(-2m3(vdbk-JH(-$y*Z#anH_RywRP@;`PIrI#-+RpOd0d!p zDD@+U_UmC8&^Z-X)EG7U-OVS^m6j!LWxS{pk;ZLIoG7Zh8*ynI2367;7QFx#YOL{K z6bKGG*mou?Wt04lfeW^e59KI&@+pRg!~E}%0>Sp8nZBQKOPz4i7ZBh&55d0RFvxJ0 zn7^uWfu%2pNEZS)eWqD1SE$f^Hb=(6^Y$B7oExw=EsWl`AyG#*(erpyDHwBx1@pd+ zUa!n5PTOP>HvQJT%vn8C2GCW%?bEXjkGITm?ReL~a%8GI%fzSfwOYHLEgIe>`l*`| z2%FbL4uKFcLTR$YrdTKC5U9TNdMg&89%JT$^4PL)ns{Pas?C zhA*8t*sZStJ0&3_%cp$~;#Nt?4&rgGq%Kun?+3@M;wEw-4<}FxmN9Xa5Tk^rgy1&oqcU&~v~>^8NSCAF@N+ zBj^;NH2@*Xt;(@50!f-$q|b#sZ8Dc-^h_|9$t%6eLb)-4$WI?XnyJgdt*6v1(MvJ< zsu`BSC75$3WMfcs8qd1WU|P*H7AxU4Gn2_zP2I620JG9H8Upy$;T(@;% z%pX%yQDrq^M9YDNlv~$dZb<`y_C}P7xd!UbF0$wrwUzQOK^xsxVy1g(6ypuy$o9#FD|6`Uv9{1IhyvvG((QO}gZ!gc==N)WjYPl{e*F3Yh`I(x_*GR1;h~N_v&W@Y+=Q z)s1OG<;sB>ovG$HCTlG%tq|9)z31g0_0>Bbq_oErrsfe8-XrII+T52Sq8{_+Xmvnr z@G0rn<}t7txW3ZyrS6AZ{|6kzK>qPYLP?A5JBv8bcMUZlj=rI=7rDXGuhko4DI$YHNUiwGbd5h91- zL)uKyphk6hwvxgg$dr@0Up!P0lhFyK1d`(HF`K|_p}fN9q`7*HbsIUWK;|jrd3@-g z_tIl+kmkri2al7zesN<`%u8WEEqsA(th`Br3~=&{lxM8}Ovl_i&N&$h(6JGGB#tFQ zUKzJMLiL5Jl#g5`QH-bwk&Y_41!)|Jc5j6?heJcwVe?c;f?VU2MO{XAiPLoSk>v7p z#EDn1dDkVnE;i|7{Z(b-etLTwkI32D$9JuNb*`XpI4iy*2gf0w6D1xWs-o zk>m`F8&D)*YR!N}g57yOgC8G(h4;mANDgvKD`3zGL^<7hm&bs@4ElkCS#*E`8K~F| z3=Dq#`W4>WQ7nJ#lni6aq+&JLU+h?K%8kbyo-tyYNQx;&?Pp?4RDc(uT}vyHS%X3i zey?rO3K>ONXz+pzInj}nV2SqkZJ;4QS7iJ&4a2w4`KP={CTNqcwT zPJ&s*BVt(G+R_EGZD-s;u!7vgGab*O=(Fg7y4C=WQd+m1RnC&pZStDf zF2u?ghy)!&G7dFB)lI~zcUiyFbl@->hB=#Hz1R*!<_dul4lYv0;b-sFnj+VXe~sY0 z1t1#YI_%!54ZIE1S1OS&qN=r*_Z@r^he>@MUZ*wLVGi{mw~|@_&Y%-|*J8Ag-ja%+ zUrX)tx+cGBhE9rVbbr?wq)LuTl4`$6)|QyEEN(f3bccCJwU$wM3yjvMBl7Aq4~qk7t=Yj3Sb`1=Bgi3p)~udx&V z%*xH33z`?~JGja4A+?)>o#5%S30E3(`a+1kB-Ju?$;!f&*@D7;heG?TXAyqFB8XBz z00T(Aw5)7bcXxMZXJ=0j(azny!j=Tl%cnoX>eJFh1&!A>WYA(?DE!@6ed>m?wmYg+_n*L*&9OnT+??F_Mn%a zX7dOboA_CIR8=BI!9R0;7SxvE&6+d?E0*!BZ=0h^ARbQ0M zI-lu>N*`-SM5!8}Ha-S*;0--uLb&u(UfTX&?RxT&t#IBdFP$?ecos0_sF#sr&MN98qggdE98BhJ6B8s7fbuamLuJr1tezl zRrc>+9<{>&5K4rX7lm6i4}`e7 zysX?8G;tR-zNRIpd!N5mw@%m!jb*Q#VX~2%@Q~iKj%`O?O-d9gY`4%kBpuagJWZDlvg;=u&K=r&85Q_>t<5;JR zZwSw|*^(_NI5=$?;Bbhkn(%^NxRAN&gaCMkR&GNzbparc9mTP7$@_zP9UFY29RnGB zbVW$-?WZW1yywKNx;mHHP8;9!1H>2<91P#jBh+-7@$h=KIYoQi0-2*C_1A?8Q9834md`^Siv^$EC?9@pS-3##f0#uOwZVvJsLIz>V6rxQXRm4}p2*tO;KxMHcDACBki~ zSxTD~7$&PitD4Zn(wktf-nD~2*qXP|iiOiI(2}~?%+9dJnv>F~?UD`He!Y>$+Z$8s zLC+FpEAMw94m<3demR8x=U6R?CqRnCW zFR<{Mu7yCuBi*raaU7hSm&1fGsM)Dk=WppONcz_@xm}u!6j(_!Qa!e>mBRs_TT#{} zSdj@Fl5RV5#SA7Mo8w6uyd!kWg;eH5%U#wxyAIEcqVyz_Aujy!<$}ML`0)3`K^wQ4nN7H<-`puSx!NKrV5|r z{;u=TkXPLLPrZ-plKHSHfJ*$f@g_@ttJVr)-QC~*bl_sE;y|po>E6R7oxdru`)LS| zUksI;cq@8(SF7Z$TX+Q(ApuVnId|?&az>a(S1b4LGBREOSDW~(sQ?6R)s=X0@{Z}1 zGnt750>P-pb$cLn|3JHJZNPKdVUxDn)f=(oa6>B27*ca-|ZRhgYg>KOXLXE$>6R{5WI z?7NTX6`rvx%5|Q#4b)a6wvHvQSTl&lS#`8FL-ekBd9-Y)NSoUXQ{rDlkpj&5Vzh~A&n%rG)+T_ zm)qwJ^MO`dM)ll$obDR!r9O;iGp@+();Ri&mg-!YB&H|y9vg>Qx>#+1KD6Am z_hy9dOM%Ivk*{f=ZM3TNijK{ zxx!FUhp2@~zf;6PO;P^z8Mc@Jeu2HF!93`2j6>g zufd#Rumm7J(mD==@FbBRSMj0^g@Am$u>*w5T9a-_NlE?wdn3@W1B#?(Tw*}C4R}2x z5U&H$TqZy`4$!Fp(u^DkFUfh2_S{^YsIR5oD?bRLcJI*qqp~Y?<_fGBPyrAgLf3F&0c>wy+VY9OgYQf9seO8GF8FXSpx;S; z?7d27&00(ksNHp*v1plShxRB`FWn6$*Iv3fC3+HyKkD+ia|eZ`nUuuiwm$*&dzIvu zzgq}`cVOQEW9Op+*VoqvQbYhCZLY5;0{KKNwRyj&%wLYAd+){!$YQGBm*U8%fG=0# zN4EUb03A)uM>KKq5++OVfVV?)azMFG?CDu4>ybyu(Juo1Ne^ZD?X&liRTe>XdIG)w zW8}y0q#CU5EEexlD6bCPGkvo;s2!fpJOfvvXL3~6cnv?~;q2O*lGWeeKWE^}#(x1Q ztDP+FY3>e3&pW$FpxzB3Hjils|s!S!)lJGqHXC~k27vbL#)*& zQW}9jwDQ828qf3b8OZYtiRCG8$KELQcPB4MobtTB?a08JJ7@!24>ID%z7De4}1_LCqHpK{30&z{*RK!yOCy={A zVnuI_qaJ+eoEJz|{Sl%*;YwYc(Pw2I$2M#(@(h1?e|MzPwb$%0cn?JSy0fdUH@#ig z#byB$viDrGd}|dm6Su6r9wk9B6-TgowF%Ytr}yLEUu5%H{eqg9-+l)ri@xUVCy}1k z*Ki6Yu3RR4JDQrlu}GWyvf`{!NZNVWgD;Ucf6pWf?)hwQeUqU%;1(+cvX*OmCicIP zMv|B~$8kG5p+)t$C9-kMeeaD8T&fSny_wq5U5xVkv&Qe^g_SimRyd}R{)cbSb*KGs z1fXV|Ur+!|_a`&*0AaOz_>A=QY3QQ?u(>%*eD42nMc!s0)dcRw2uO542YXCDG^+c; zUrwNP05;gJ!B^IsF`TF%%0kV@1e!Y!vfw_qlc2g7r&-SgHo|C#I%!lM$>7PR-aIlkT%PzmJ@F8( zc8?y-x9qm*9za!;3;mI}9gl(>{o7T}lW+Ww()}S@$Wu-a93T(u#Who7mZ+p8Z|aVl z=e)yc1AFfZkjw+pmw>FYAD8e3H|ptGchTRr zMEF9Qr|?p`7p9VbF&9!>U#difCQ>+d_e+8Lr-ItE8(Lpf3@TJ=3nS3$l>Pj$1M>jP) zKoRqqtO@0oS%WM|=Xmk&jazs6Dm1sC%rEB{9i4Uaqbt!ja)t8Jz&J$y_@BeycNfq4 zsKA6NE$z1!??8c}Y9-vuURYR&6rlVn?mb;&*9YuE|FcfGyjd~Wy3*VwMN?Hzw3ChT zeN_!bCwH`|j`2I{*rbc7K0}MmolCq*iCxV_x|`GF8<#B^-9rD~CXa zzGN#$uyp>ro$Nv0%d1`zEEW2Jx`wg3+JcdAJ*8vko@1SsKFB(KVtuLL zbn^7eCO(0Rp1Z(0PgwK58>Nva;dA~pBu43<)9Bilw^ed)l3CIi#sn>Z--?q`u9(dxD!#|=u9rm{$S?&|#f@TV6agK+5yK#)2N1V>_$&Wxlr<-nH zb};qViaZ^xphadg-~Sz>H1c3plSjvpHS=xZM8DY@%c%Rt`5)1>gM->`60~rr=!tP5 zQCq}v9cG=6M`KJlhC|v6++15v*Lfys62T#Tr_SD!nBAcUiTjThX!%l;_jEOD;KLEV zRts88t)IN6x38b;(o#DaQ8giyD_UP6<(h@{$;qEa`_HmrN5E(niL-M^-W;3I_7}rj z?LjJPXy!?G^7L@6fTZ5i7V-{8(q=*{hyS&-r8t7vY(C^#M4YjL+x)`wRLdZ49|aqA&*5z< zbS?q6(T;7M+tQnacjdLd!7CXz3B zmhG+g`{&gSZoT(ZkAw!c6U?oIwxu0gEBvpbVD=}sN*mnJ#&w_9KU0i2w8AgUPZuzQQz0^?E}-J9!<~qHt0Dd{^kuW_#L@Ocomf=_8~<7x`4LOMiR~y_BAAZ za6pnlE3)(ij%wp{NAv6E_-7T)<5Qz=&8vJrgp|r@4D78wZ-yRL+ZZU0O&I2>XQwp( zipit6-ko^1$Pr1`N1BR&?KRj$k0>GOzUP$t z1okWU+ML*4EHbfj_EV7?w1wnC?iB1)g%*mz5pffRW!4R2eSEPh%q(MAi}st|^q86p z1LWS(OAXuV+X7+?Y8(zY?AC+P)vNd=yxWiB;d>7H=>pNoMDrN^E^3RB-)##OPsQll zv5$5udgs=7e~9X*x?AG$sh!WdtY%brEC^>2#iaJUzmwN`tUC2>a>1MO*fkSUcPFJn%dOfQqTuMo!e?vgl2kRBQ zd9xdGD}(CN+foBh%_i*h?VDM&jJI~ghlV!Df30{(E|riNnhLv5U-W9GEBn?WYn41h z*&AH<PludV6E%DzVUosW>B_h#+lNv(;&0wVjw-j9zOUZVd-C_hg`*bM0m zUx_a5F+I!Sa@Dft-&iHBvs5?Ph3zMCCsXImeRPIm_(|hc#k3F#p>`RfS8u)lsV~t| z2qqhiMNLL?;`z)_Y!CH`_ke||Zz+bWiT<9oRCCL)QxanNjQWXl&yGnBpATfI`pZL+Bv zvqfrPC90PzKWJ-7VGvn)nj5h)JAM)&WjDLGO07~4y*OM^Q)xS~cZ8G8U@GN#J6=P0 z)crm}M78#QFKe*9>7PNXM=$JrbfU+;h2Z#V1W z=YkK#qj;00hFV5THnvcT8T{_#RITi7&zXa%NPjgAw zb$;cX+bXP(f13GlWMJhsM2)kdOlrf&@cr_UBhofGrHgZR()C^oZJDgxOyIu2+B;kU zYWEv&y=%?Z&^TTKPET5`=rnyAxRD+ne^(>Na!!Q@I{*kb{`Zobu0CaiYd*btUHy(y z(oke`-(`6hnEikBZbKG@&j z^yiQbC60>RWE7gatI8)a`x&ZE>|(DzNjIW7@<{uD0Ae_FP~`t4`{Wf#cD$g& z9(D5{;sigSqZ{M;2a@G1docj0EG>jbCu?s$fWUNg9%uehI4C#^)AeX)l<1FZY~B_D z`kZuCxBkM>JSVKu4~eAX`ZDd`Fsl@S+U%dcQ&+E#5M`%JdBZU6` zn5522%ARv-@G6T%1`+c`E=;pTqL!R3w1(vtxrL~s8r!1^9U=2Bv)j9O7}OAqN7$IA znqE_6F>xOO z5}2H+dTk)*mM|ho#?P0S3wGc&0$5a4?>hJAz6Q0*FwP{5ZyR9Yhy!D<`+zo{yxMkhEoNOqi zgSGlwr3}CL&$)Y2L+xZy^e;N`9Z*2K?k(5T--X%^-tJ3)DXd&yeF+(yX%QB4QfkTE z$8k3`-4a!Z^SLJIVQ)R-ueQ)-FsJcUaZl1~Vz0wMdA$`AQ{kC4G(02Fy z((b3Uha5gW&*Xx-04&nQ5DLYJha_@w_)?7BRmV5;*Mnb#I>06hbnI2lTKI@B0-kfn z95NwOZPxMv%XFB7y5-NX%DDJg-)Vq^+Zk??2IHT3w=d2e)tx52N|tCu+t0SGiq$HA zc*XEkE6#o`={2uxw$OHO-s-tS#^wSV#t(FcH1)=@;(}NA44pp3OZVLtbBE2#Sls>A zbX;*k=Ekl6%?F}&hlrxTzUbVo+vQ+Tj_X`c6>}c6p48u0Jhx%8H>W_gq;DyljE8Fq z+DD;9=O}qDQ%&_t&lDAymj|j$Mqu))cg7$m&s=jVMd`JybK=ld53UW~K6g{~%TuC? z^yOX2=P$bA#pOKK|2)6g^d;k$=%k8vsTFDbTu+UsM{}4uUQ5)jI3Ies1l8NWAxV-J zm)+O+nrL^*s)J=tyo(TV!*qVx^No|*2x&>usjS|=QvCB$U5pb$xPm5>meOAvQ9|^I z<4KS}&1ll(zUZ1HT|;cIzSGLy7x$Q@^nP7C9A;<8$t@-495K1m+nXOrC9-S8uW(;l zJ|kuGesD#egCll3jGHjGIF_-|Ukr}qHa6kcWTvL~)C1)|2WI#W9|=nklYKM~YmU3O z70kk|plkf}Lr@(GIOY?0#$WtE_gxirWp{K*&E7&Nd_n25m8jwUpKZDS{vh!Gd;CWp z_^+}ASa>?RG&#Wge1D#h4~WR`&!{rU9N!;n7La_tKmVzQdyx7@yw)$*9p4@NhpLLk K%_5~o&;AVxjN;4y literal 0 HcmV?d00001 diff --git a/src/streamsync/auth.py b/src/streamsync/auth.py index 59005cde3..2fa15ef67 100644 --- a/src/streamsync/auth.py +++ b/src/streamsync/auth.py @@ -100,3 +100,42 @@ async def route_callback(request: Request): # At this part, we should response.set_cookie(key="session", value=session_id, httponly=True, expires=0) return response + +def Google(client_id: str, client_secret: str, host_url: str) -> Oidc: + """ + Configure Google Social login configured through Client Id for Web application in Google Cloud Console. + + >>> import streamsync.auth + >>> oidc = streamsync.auth.Google(client_id="xxxxxxx", client_secret="xxxxxxxxxxxxx.apps.googleusercontent.com", host_url="http://localhost:5000") + + :param client_id: client id of Web application + :param client_secret: client secret of Web application + :param host_url: The URL of the streamsync application (for callback) + """ + return Oidc( + client_id=client_id, + client_secret=client_secret, + host_url=host_url, + url_authorize="https://accounts.google.com/o/oauth2/auth", + url_oauthtoken="https://oauth2.googleapis.com/token", + url_userinfo="https://www.googleapis.com/oauth2/v1/userinfo?alt=json") + +def Auth0(client_id: str, client_secret: str, domain: str, host_url: str) -> Oidc: + """ + Configure Auth0 application for authentication. + + >>> import streamsync.auth + >>> oidc = streamsync.auth.Auth0(client_id="xxxxxxx", client_secret="xxxxxxxxxxxxx", domain="xxx-xxxxx.eu.auth0.com", host_url="http://localhost:5000") + + :param client_id: client id + :param client_secret: client secret + :param domain: Domain of the Auth0 application + :param host_url: The URL of the streamsync application (for callback) + """ + return Oidc( + client_id=client_id, + client_secret=client_secret, + host_url=host_url, + url_authorize=f"https://{domain}/authorize", + url_oauthtoken=f"https://{domain}/oauth/token", + url_userinfo=f"https://{domain}/userinfo") diff --git a/src/streamsync/core.py b/src/streamsync/core.py index 3c21b3bcd..fc76b5b6a 100644 --- a/src/streamsync/core.py +++ b/src/streamsync/core.py @@ -1355,7 +1355,7 @@ def _call_handler_callable(self, event_type, target_component, instance_path, pa "id": self.session.session_id, "cookies": self.session.cookies, "headers": self.session.headers, - "userinfo": self.session.userinfo + "userinfo": self.session.userinfo or {} } arg_values.append(session_info) elif arg == "ui": From 07119f5bd072c258e61effc6cb7f6b0e07fa0ab2 Mon Sep 17 00:00:00 2001 From: Fabien Arcellier Date: Thu, 9 May 2024 12:54:27 +0200 Subject: [PATCH 07/18] feat: implement authentication workflow on streamsync * feat: configure unauthorized page * docs: customize unauthorized page --- docs/docs/authentication.md | 69 +++++++++++++++-- .../docs/images/auth_unauthorized_default.png | Bin 0 -> 22785 bytes src/streamsync/auth.py | 73 +++++++++++++----- src/streamsync/serve.py | 11 ++- .../templates/auth_unauthorized.html | 35 +++++++++ 5 files changed, 160 insertions(+), 28 deletions(-) create mode 100644 docs/docs/images/auth_unauthorized_default.png create mode 100644 src/streamsync/templates/auth_unauthorized.html diff --git a/docs/docs/authentication.md b/docs/docs/authentication.md index dd690b438..f8c1a9531 100644 --- a/docs/docs/authentication.md +++ b/docs/docs/authentication.md @@ -83,12 +83,6 @@ oidc = streamsync.auth.Auth0( streamsync.serve.register_auth(oidc) ``` -[//]: # (### Callback to refuse access to the application) - -[//]: # () -[//]: # (### Callback to modify user information) - - ### Authentication workflow @@ -104,3 +98,66 @@ def on_page_load(state, session): state['email'] = email ``` + +## Unauthorize access + +It is possible to reject a user who, for example, does not have the correct email address. + +::: tip you can also use userinfo inside app +You can restrict access to certain pages inside the application by using the `session` object. +See [User information in event handler](#user-information-in-event-handler) +::: + +```python +from fastapi import Request + +import streamsync.serve +import streamsync.auth + +oidc = ... + +def callback(request: Request, session_id: str, userinfo: dict): + if userinfo['email'] not in ['nom.prenom123@example.com']: + raise streamsync.auth.Unauthorized(more_info="You can contact the administrator at support.example.com") + +streamsync.serve.register_auth(oidc, callback=callback) +``` + +The default authentication error page look like this: + + + +*streamsync.auth.Unauthorized* + +| Parameter | Description | +|-----------|-------------| +| status_code | HTTP status code | +| message | Error message | +| more_info | Additional information | + +## Custom unauthorized page + +You can customize the access denial page using your own template. + +```python +import os + +from fastapi import Request, Response +from fastapi.templating import Jinja2Templates + +import streamsync.serve +import streamsync.auth + +oidc = ... + +def unauthorized(request: Request, exc: streamsync.auth.Unauthorized) -> Response: + templates = Jinja2Templates(directory=os.path.join(os.path.dirname(__file__), "templates")) + return templates.TemplateResponse(request=request, name="unauthorized.html", status_code=exc.status_code, context={ + "status_code": exc.status_code, + "message": exc.message, + "more_info": exc.more_info + }) + +streamsync.serve.register_auth(oidc, unauthorized_action=unauthorized) +``` + diff --git a/docs/docs/images/auth_unauthorized_default.png b/docs/docs/images/auth_unauthorized_default.png new file mode 100644 index 0000000000000000000000000000000000000000..280ebeb6c402364b23cb27e1e98f769957e7b84c GIT binary patch literal 22785 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 + """ + def __init__(self, status_code = 401, message = "Unauthorized", more_info = ""): + self.status_code = status_code + self.message = message + self.more_info = more_info + + class Auth: """ Interface to implement authentication in streamsync. @@ -19,7 +33,11 @@ class Auth: __metaclass__ = ABCMeta @abstractmethod - def register(self, app: StreamsyncFastAPI, callback: Optional[Callable[[Request, str], None]] = None): + def register(self, + app: StreamsyncFastAPI, + callback: Optional[Callable[[Request, str, dict], None]] = None, + unauthorized_action: Optional[Callable[[Request, Unauthorized], Response]] = None + ): raise NotImplementedError @@ -55,10 +73,15 @@ class Oidc(Auth): url_userinfo: Optional[str] = None authlib: OAuth2Session = None - callback_func: Optional[Callable[[Request, str], None]] = None + callback_func: Optional[Callable[[Request, str, dict], None]] = None # Callback to validate user authentication + unauthorized_action: Optional[Callable[[Request, Unauthorized], Response]] = None # Callback to build its own page when a user is not allowed - def register(self, asgi_app: StreamsyncFastAPI, callback: Optional[Callable[[Request, str], None]] = None): + def register(self, + asgi_app: StreamsyncFastAPI, + callback: Optional[Callable[[Request, str, dict], None]] = None, + unauthorized_action: Optional[Callable[[Request, Unauthorized], Response]] = None + ): self.authlib = OAuth2Session( client_id=self.client_id, client_secret=self.client_secret, @@ -73,7 +96,7 @@ def register(self, asgi_app: StreamsyncFastAPI, callback: Optional[Callable[[Req @asgi_app.middleware("http") async def oidc_middleware(request: Request, call_next): session = request.cookies.get('session') - if session is not None or request.url.path in [self.callback_route]: + if session is not None or request.url.path in [self.callback_route] or request.url.path.startswith('/assets'): return await call_next(request) else: url = self.authlib.create_authorization_url(self.url_authorize) @@ -83,23 +106,35 @@ async def oidc_middleware(request: Request, call_next): @asgi_app.get(self.callback_route) async def route_callback(request: Request): self.authlib.fetch_token(url=self.url_oauthtoken, authorization_response=str(request.url)) - response = RedirectResponse(url='/') - session_id = session_manager.generate_session_id() + try: + response = RedirectResponse(url='/') + session_id = session_manager.generate_session_id() + + app_runner = streamsync.serve.app_runner(asgi_app) + await app_runner.init_session(InitSessionRequestPayload( + cookies=request.cookies, headers=request.headers, proposedSessionId=session_id)) - app_runner = streamsync.serve.app_runner(asgi_app) - await app_runner.init_session(InitSessionRequestPayload( - cookies=request.cookies, headers=request.headers, proposedSessionId=session_id)) + userinfo = {} + if self.url_userinfo: + userinfo = self.authlib.get(self.url_userinfo).json() + app_runner.set_userinfo(session_id=session_id, userinfo=userinfo) - if self.url_userinfo: - userinfo = self.authlib.get(self.url_userinfo).json() - app_runner.set_userinfo(session_id=session_id, userinfo=userinfo) + if self.callback_func: + self.callback_func(request, session_id, userinfo) - if self.callback_func: - self.callback_func(request, session_id) + response.set_cookie(key="session", value=session_id, httponly=True, expires=0) + return response + except Unauthorized as exc: + if self.unauthorized_action: + return self.unauthorized_action(request, exc) + else: + templates = Jinja2Templates(directory=os.path.join(os.path.dirname(__file__), "templates")) + return templates.TemplateResponse(request=request, name="auth_unauthorized.html", status_code=exc.status_code, context={ + "status_code": exc.status_code, + "message": exc.message, + "more_info": exc.more_info + }) - # At this part, we should - response.set_cookie(key="session", value=session_id, httponly=True, expires=0) - return response def Google(client_id: str, client_secret: str, host_url: str) -> Oidc: """ diff --git a/src/streamsync/serve.py b/src/streamsync/serve.py index d17b0d1b6..b7f2e1b32 100644 --- a/src/streamsync/serve.py +++ b/src/streamsync/serve.py @@ -3,6 +3,7 @@ import logging import mimetypes import os +import os.path import pathlib import textwrap import typing @@ -38,7 +39,7 @@ ) if typing.TYPE_CHECKING: - from .auth import Auth + from .auth import Auth, Unauthorized MAX_WEBSOCKET_MESSAGE_SIZE = 201*1024*1024 logging.getLogger().setLevel(logging.INFO) @@ -453,8 +454,12 @@ def print_route_message(run_name: str, port: int, host: str): print(f"{run_name} is available at:{END_TOKEN}{GREEN_TOKEN} http://{host}:{port}{END_TOKEN}", flush=True) -def register_auth(auth: 'Auth', callback: Optional[Callable[[Request, str], None]] = None): - auth.register(app, callback=callback) +def register_auth( + auth: 'Auth', + callback: Optional[Callable[[Request, str, dict], None]] = None, + unauthorized_action: Optional[Callable[[Request, 'Unauthorized'], Response]] = None +): + auth.register(app, callback=callback, unauthorized_action=unauthorized_action) def serve(app_path: str, mode: ServeMode, port, host, enable_remote_edit=False, enable_server_setup=False): """ Initialises the web server. """ diff --git a/src/streamsync/templates/auth_unauthorized.html b/src/streamsync/templates/auth_unauthorized.html new file mode 100644 index 000000000..460e57a8d --- /dev/null +++ b/src/streamsync/templates/auth_unauthorized.html @@ -0,0 +1,35 @@ + + + +

+ + From 90dd21b6d6869f466f78b1f62174d6ecc75d97c5 Mon Sep 17 00:00:00 2001 From: Fabien Arcellier Date: Thu, 9 May 2024 13:49:19 +0200 Subject: [PATCH 08/18] feat: implement authentication workflow on streamsync * configure github provider --- docs/docs/authentication.md | 19 +++++++++++++++++++ src/streamsync/auth.py | 19 +++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/docs/docs/authentication.md b/docs/docs/authentication.md index f8c1a9531..c8fb09c97 100644 --- a/docs/docs/authentication.md +++ b/docs/docs/authentication.md @@ -63,6 +63,25 @@ oidc = streamsync.auth.Google( streamsync.serve.register_auth(oidc) ``` +#### Github + +You have to register your application into [Github](https://docs.github.com/en/apps/creating-github-apps/registering-a-github-app/registering-a-github-app#registering-a-github-app) + +*server_setup.py* +```python +import os +import streamsync.serve +import streamsync.auth + +oidc = streamsync.auth.Github( + client_id="xxxxxxx", + client_secret="xxxxxxxxxxxxx", + host_url=os.getenv('HOST_URL', "http://localhost:5000") +) + +streamsync.serve.register_auth(oidc) +``` + #### Auth0 You have to register your application into [Auth0](https://auth0.com/). diff --git a/src/streamsync/auth.py b/src/streamsync/auth.py index 2d391c5a5..a04721ff0 100644 --- a/src/streamsync/auth.py +++ b/src/streamsync/auth.py @@ -155,6 +155,25 @@ def Google(client_id: str, client_secret: str, host_url: str) -> Oidc: url_oauthtoken="https://oauth2.googleapis.com/token", url_userinfo="https://www.googleapis.com/oauth2/v1/userinfo?alt=json") +def Github(client_id: str, client_secret: str, host_url: str) -> Oidc: + """ + Configure Github authentication. + + >>> import streamsync.auth + >>> oidc = streamsync.auth.Github(client_id="xxxxxxx", client_secret="xxxxxxxxxxxxx", host_url="http://localhost:5000") + + :param client_id: client id + :param client_secret: client secret + :param host_url: The URL of the streamsync application (for callback) + """ + return Oidc( + client_id=client_id, + client_secret=client_secret, + host_url=host_url, + url_authorize=f"https://github.com/login/oauth/authorize", + url_oauthtoken=f"https://github.com/login/oauth/access_token", + url_userinfo=f"https://api.github.com/user") + def Auth0(client_id: str, client_secret: str, domain: str, host_url: str) -> Oidc: """ Configure Auth0 application for authentication. From 95f9a8c5de64322aba48b3a1e0a7e65cf8c289b7 Mon Sep 17 00:00:00 2001 From: Fabien Arcellier Date: Thu, 9 May 2024 14:29:51 +0200 Subject: [PATCH 09/18] feat: implement authentication workflow on streamsync * configure microsoft provider --- docs/docs/authentication.md | 20 ++++++++++++++++++++ src/streamsync/auth.py | 26 +++++++++++++++++++++++--- 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/docs/docs/authentication.md b/docs/docs/authentication.md index c8fb09c97..16562afb5 100644 --- a/docs/docs/authentication.md +++ b/docs/docs/authentication.md @@ -63,6 +63,26 @@ oidc = streamsync.auth.Google( streamsync.serve.register_auth(oidc) ``` +#### Microsoft + +You have to register your application into [Azure AD](https://docs.snowflake.com/en/user-guide/oauth-azure) + +*server_setup.py* +```python +import os +import streamsync.serve +import streamsync.auth + +oidc = streamsync.auth.Microsoft( + client_id="xxxxxxx", + client_secret="xxxxxxxxxxxxx", + tenant_id="xxxxxxxxxxxxx", + host_url=os.getenv('HOST_URL', "http://localhost:5000") +) + +streamsync.serve.register_auth(oidc) +``` + #### Github You have to register your application into [Github](https://docs.github.com/en/apps/creating-github-apps/registering-a-github-app/registering-a-github-app#registering-a-github-app) diff --git a/src/streamsync/auth.py b/src/streamsync/auth.py index a04721ff0..8741bf8b0 100644 --- a/src/streamsync/auth.py +++ b/src/streamsync/auth.py @@ -155,6 +155,26 @@ def Google(client_id: str, client_secret: str, host_url: str) -> Oidc: url_oauthtoken="https://oauth2.googleapis.com/token", url_userinfo="https://www.googleapis.com/oauth2/v1/userinfo?alt=json") +def Microsoft(client_id: str, client_secret: str, tenant_id: str, host_url: str) -> Oidc: + """ + Configure Office 365 login configured through Client Id for Web application in Google Cloud Console. + + >>> import streamsync.auth + >>> oidc = streamsync.auth.Microsoft(client_id="xxxxxxx", client_secret="xxxxxxxxxxxxxxx", tenant_id='xxxxxxxxxxxxxxxxx', host_url="http://localhost:5000") + + :param client_id: client id of Web application + :param client_secret: client secret of Web application + :param tenant_id: tenant id of the application + :param host_url: The URL of the streamsync application (for callback) + """ + return Oidc( + client_id=client_id, + client_secret=client_secret, + host_url=host_url, + url_authorize=f"https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/authorize", + url_oauthtoken=f"https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token", + url_userinfo="https://graph.microsoft.com/v1.0/me") + def Github(client_id: str, client_secret: str, host_url: str) -> Oidc: """ Configure Github authentication. @@ -170,9 +190,9 @@ def Github(client_id: str, client_secret: str, host_url: str) -> Oidc: client_id=client_id, client_secret=client_secret, host_url=host_url, - url_authorize=f"https://github.com/login/oauth/authorize", - url_oauthtoken=f"https://github.com/login/oauth/access_token", - url_userinfo=f"https://api.github.com/user") + url_authorize="https://github.com/login/oauth/authorize", + url_oauthtoken="https://github.com/login/oauth/access_token", + url_userinfo="https://api.github.com/user") def Auth0(client_id: str, client_secret: str, domain: str, host_url: str) -> Oidc: """ From 1b1028df5907ca3da1273b4e75bdf78f674a3815 Mon Sep 17 00:00:00 2001 From: Fabien Arcellier Date: Thu, 9 May 2024 17:19:18 +0200 Subject: [PATCH 10/18] feat: implement authentication workflow on streamsync * use the endpoint authorize as redirect callback --- docs/docs/authentication.md | 37 ++++++++++++++++++++ poetry.lock | 2 +- pyproject.toml | 2 ++ src/streamsync/auth.py | 68 ++++++++++++++++++++++++++++++++----- src/streamsync/serve.py | 9 +++++ 5 files changed, 108 insertions(+), 10 deletions(-) diff --git a/docs/docs/authentication.md b/docs/docs/authentication.md index 16562afb5..6750a8a45 100644 --- a/docs/docs/authentication.md +++ b/docs/docs/authentication.md @@ -174,6 +174,30 @@ The default authentication error page look like this: | message | Error message | | more_info | Additional information | +## Modify user info + +User info can be modified in the callback. + +```python +from fastapi import Request + +import streamsync.serve +import streamsync.auth + +oidc = ... + +def callback(request: Request, session_id: str, userinfo: dict): + userinfo['group'] = [] + if userinfo['email'] in ['fabien@example.com']: + userinfo['group'].append('admin') + userinfo['group'].append('user') + else: + userinfo['group'].append('user') + +streamsync.serve.register_auth(oidc, callback=callback) +``` +from fastapi import Request + ## Custom unauthorized page You can customize the access denial page using your own template. @@ -200,3 +224,16 @@ def unauthorized(request: Request, exc: streamsync.auth.Unauthorized) -> Respons streamsync.serve.register_auth(oidc, unauthorized_action=unauthorized) ``` +## Enable in edit mode + +Authentication is disabled in edit mode. To activate it, +you must trigger the loading of the server_setup module in edition mode. + +```bash +streamsync edit --enable-server-setup +``` + +``` + + +```python diff --git a/poetry.lock b/poetry.lock index 6cb102cc8..07f0a7817 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2175,4 +2175,4 @@ ds = ["pandas", "plotly", "pyarrow"] [metadata] lock-version = "2.0" python-versions = ">=3.9.2, <4.0" -content-hash = "3ee7a7551fff7da5d312f98fd7035e6a10589b7c6ba57013b87128c13a4a55c8" +content-hash = "14d24fe303bef4e05f04602ee213ecc2bb533e72915ec481d39b11fe06398f4e" diff --git a/pyproject.toml b/pyproject.toml index 4807c0682..d19ced263 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,6 +26,7 @@ packages = [ include = [ "src/streamsync/*.py", "src/streamsync/static/**/*", + "src/streamsync/templates/**/*", "src/streamsync/app_templates/**/*" ] @@ -42,6 +43,7 @@ pyarrow = {version = ">= 15.0.0, < 16.0.0",optional = true} plotly = {version = ">= 5.18.0, < 6", optional = true} authlib = "^1.3.0" requests = "^2.31.0" +jinja2 = "^3.1.4" [tool.poetry.group.build] diff --git a/src/streamsync/auth.py b/src/streamsync/auth.py index 8741bf8b0..cd10eb000 100644 --- a/src/streamsync/auth.py +++ b/src/streamsync/auth.py @@ -2,6 +2,7 @@ import os.path from abc import ABCMeta, abstractmethod from typing import Callable, Optional +from urllib.parse import urlparse from authlib.integrations.requests_client.oauth2_session import OAuth2Session # type: ignore from fastapi import Request, Response @@ -69,7 +70,7 @@ class Oidc(Auth): url_authorize: str url_oauthtoken: str scope: str = "openid email profile" - callback_route: str = "/callback" + callback_authorize: str = "authorize" url_userinfo: Optional[str] = None authlib: OAuth2Session = None @@ -86,7 +87,7 @@ def register(self, client_id=self.client_id, client_secret=self.client_secret, scope=self.scope.split(" "), - redirect_uri=f"{self.host_url}{self.callback_route}", + redirect_uri=_urljoin(self.host_url, self.callback_authorize), authorization_endpoint=self.url_authorize, token_endpoint=self.url_oauthtoken, ) @@ -96,18 +97,23 @@ def register(self, @asgi_app.middleware("http") async def oidc_middleware(request: Request, call_next): session = request.cookies.get('session') - if session is not None or request.url.path in [self.callback_route] or request.url.path.startswith('/assets'): - return await call_next(request) + host_url_path = _urlpath(self.host_url) + full_callback_authorize = '/' + _urljoin(host_url_path, self.callback_authorize) + full_assets = '/' + _urljoin(host_url_path, '/assets') + if session is not None or request.url.path in [full_callback_authorize] or request.url.path.startswith(full_assets): + response: Response = await call_next(request) + return response else: url = self.authlib.create_authorization_url(self.url_authorize) response = RedirectResponse(url=url[0]) return response - @asgi_app.get(self.callback_route) + @asgi_app.get('/' + _urlstrip(self.callback_authorize)) async def route_callback(request: Request): self.authlib.fetch_token(url=self.url_oauthtoken, authorization_response=str(request.url)) try: - response = RedirectResponse(url='/') + host_url_path = _urlpath(self.host_url) + response = RedirectResponse(url=host_url_path) session_id = session_manager.generate_session_id() app_runner = streamsync.serve.app_runner(asgi_app) @@ -117,15 +123,17 @@ async def route_callback(request: Request): userinfo = {} if self.url_userinfo: userinfo = self.authlib.get(self.url_userinfo).json() - app_runner.set_userinfo(session_id=session_id, userinfo=userinfo) if self.callback_func: self.callback_func(request, session_id, userinfo) - response.set_cookie(key="session", value=session_id, httponly=True, expires=0) + if self.url_userinfo: + app_runner.set_userinfo(session_id=session_id, userinfo=userinfo) + + response.set_cookie(key="session", value=session_id, httponly=True) return response except Unauthorized as exc: - if self.unauthorized_action: + if self.unauthorized_action is not None: return self.unauthorized_action(request, exc) else: templates = Jinja2Templates(directory=os.path.join(os.path.dirname(__file__), "templates")) @@ -213,3 +221,45 @@ def Auth0(client_id: str, client_secret: str, domain: str, host_url: str) -> Oid url_authorize=f"https://{domain}/authorize", url_oauthtoken=f"https://{domain}/oauth/token", url_userinfo=f"https://{domain}/userinfo") + +def _urlpath(url: str): + """ + >>> _urlpath("http://localhost/app1") + >>> "/app1" + """ + return urlparse(url).path + +def _urljoin(*args): + """ + >>> _urljoin("http://localhost/app1", "edit") + >>> "http://localhost/app1/edit" + + >>> _urljoin("app1/", "edit") + >>> "app1/edit" + + >>> _urljoin("app1", "edit") + >>> "app1/edit" + + >>> _urljoin("/app1/", "/edit") + >>> "app1/edit" + """ + url_strip_parts = [] + for part in args: + if part: + url_strip_parts.append(_urlstrip(part)) + + return '/'.join(url_strip_parts) + +def _urlstrip(url_path: str): + """ + + >>> _urlstrip("/app1/") + >>> "app1" + + >>> _urlstrip("http://localhost/app1") + >>> "http://localhost/app1" + + >>> _urlstrip("http://localhost/app1/") + >>> "http://localhost/app1" + """ + return url_path.strip('/') diff --git a/src/streamsync/serve.py b/src/streamsync/serve.py index b7f2e1b32..d1c0f59b5 100644 --- a/src/streamsync/serve.py +++ b/src/streamsync/serve.py @@ -170,6 +170,7 @@ async def init(initBody: InitRequestBody, request: Request, response: Response) if session_id is not None: initBody.proposedSessionId = session_id + app_response = await app_runner.init_session(InitSessionRequestPayload( cookies=dict(request.cookies), headers=dict(request.headers), @@ -178,6 +179,13 @@ async def init(initBody: InitRequestBody, request: Request, response: Response) status = app_response.status + """ + Deletes the session cookie that was set by + authentication when it is present. + """ + if session_id is not None: + response.delete_cookie("session") + if status == "error" or app_response.payload is None: raise HTTPException(status_code=403, detail="Session rejected.") @@ -187,6 +195,7 @@ async def init(initBody: InitRequestBody, request: Request, response: Response) if serve_mode == "edit": return _get_edit_starter_pack(app_response.payload) + # Streaming async def _stream_session_init(websocket: WebSocket): From 6f75c16bb41c2794941b1d0470f93ee5d1a5c0bf Mon Sep 17 00:00:00 2001 From: Fabien Arcellier Date: Thu, 16 May 2024 14:39:34 +0200 Subject: [PATCH 11/18] feat: implement authentication workflow on streamsync * remove microsoft implementation (not tested) * improve documentation --- docs/docs/authentication.md | 25 ----------------------- docs/docs/custom-server.md | 10 ++++----- docs/docs/images/authentication_oidc.png | Bin 69824 -> 73195 bytes src/streamsync/ai.py | 3 +-- src/streamsync/auth.py | 20 ------------------ src/streamsync/serve.py | 2 +- 6 files changed, 7 insertions(+), 53 deletions(-) diff --git a/docs/docs/authentication.md b/docs/docs/authentication.md index 6750a8a45..3e543c23e 100644 --- a/docs/docs/authentication.md +++ b/docs/docs/authentication.md @@ -39,7 +39,6 @@ StreamSync provides pre-configured OIDC providers. You can use them directly in | | Provider | Function | Description | |------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------|----------|-------------------------------------------------------------------------------------------------| | | Google | `streamsync.auth.Google` | Allow your users to login with their Google Account | -| | Microsoft | `streamsync.auth.Microsoft` | Allow your users to login with their Microsoft Account | | | Github | `streamsync.auth.Github` | Allow your users to login with their Github Account | | | Auth0 | `streamsync.auth.Auth0` | Allow your users to login with different providers or with login password through Auth0 | @@ -63,26 +62,6 @@ oidc = streamsync.auth.Google( streamsync.serve.register_auth(oidc) ``` -#### Microsoft - -You have to register your application into [Azure AD](https://docs.snowflake.com/en/user-guide/oauth-azure) - -*server_setup.py* -```python -import os -import streamsync.serve -import streamsync.auth - -oidc = streamsync.auth.Microsoft( - client_id="xxxxxxx", - client_secret="xxxxxxxxxxxxx", - tenant_id="xxxxxxxxxxxxx", - host_url=os.getenv('HOST_URL', "http://localhost:5000") -) - -streamsync.serve.register_auth(oidc) -``` - #### Github You have to register your application into [Github](https://docs.github.com/en/apps/creating-github-apps/registering-a-github-app/registering-a-github-app#registering-a-github-app) @@ -233,7 +212,3 @@ you must trigger the loading of the server_setup module in edition mode. streamsync edit --enable-server-setup ``` -``` - - -```python diff --git a/docs/docs/custom-server.md b/docs/docs/custom-server.md index b03ce70d3..d6c4bd102 100644 --- a/docs/docs/custom-server.md +++ b/docs/docs/custom-server.md @@ -2,13 +2,13 @@ Streamsync uses Uvicorn and serves the app in the root path i.e. `/`. If you need to use another ASGI-compatible server or fine-tune Uvicorn, you can easily do so. -## Tune server +## Configure webserver -You can tune your server at startup by adding a `server_setup.py` file to the root +You can tune your server by adding a `server_setup.py` file to the root of your application, next to the `main.py` and `ui.json` files. -This file is executed during server startup. It allows you to configure [authentication](), -add your own routes and middlewares on FastAPI before the server starts. +This file is executed before starting streamsync. It allows you to configure [authentication](./authentication.md), +add your own routes and middlewares on FastAPI. ```python # server_setup.py @@ -27,7 +27,7 @@ def hello(): return "1" ``` -::: warning Using in `edit` mode +::: warning Use `server_setup.py` in `edit` mode If you want to use in `edit` mode, you can launch `streamsync edit --enable-server-setup `. ::: diff --git a/docs/docs/images/authentication_oidc.png b/docs/docs/images/authentication_oidc.png index c2c0c9fad4cecfb391c1de2fedbab88b529f6cce..ad3982978ca569be4ce5a89993f83b2951e52335 100644 GIT binary patch literal 73195 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�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