From b35983837f45c8f9d291b07d9ae19823e796c6a2 Mon Sep 17 00:00:00 2001 From: Eugene Date: Sun, 24 Nov 2024 19:35:31 +0100 Subject: [PATCH] Separate DB models for credentials (#1143) --- .github/workflows/test.yml | 7 +- Cargo.lock | 16 + Cargo.toml | 8 + tests/.gitignore | 1 + tests/api_client.py | 99 +- tests/api_sdk/.gitkeep | 0 tests/api_sdk/__init__.py | 0 tests/conftest.py | 36 +- tests/poetry.lock | 1197 +++++++++-------- tests/pyproject.toml | 5 +- tests/run.sh | 4 +- tests/test_http_basic.py | 70 +- tests/test_http_conntest.py | 62 +- tests/test_http_cookies.py | 62 +- tests/test_http_redirects.py | 62 +- tests/test_http_user_auth_logout.py | 64 +- tests/test_http_user_auth_otp.py | 161 +-- tests/test_http_user_auth_password.py | 121 +- tests/test_http_user_auth_ticket.py | 104 +- tests/test_http_websocket.py | 62 +- tests/test_postgres_user_auth_password.py | 68 +- tests/test_ssh_conntest.py | 74 +- tests/test_ssh_proto.py | 91 +- tests/test_ssh_target_selection.py | 62 +- tests/test_ssh_user_auth_otp.py | 93 +- tests/test_ssh_user_auth_password.py | 68 +- tests/test_ssh_user_auth_pubkey.py | 129 +- tests/test_ssh_user_auth_ticket.py | 73 +- warpgate-admin/Cargo.toml | 8 +- warpgate-admin/src/api/known_hosts_detail.rs | 3 + warpgate-admin/src/api/known_hosts_list.rs | 3 + warpgate-admin/src/api/logs.rs | 3 + warpgate-admin/src/api/mod.rs | 38 +- warpgate-admin/src/api/otp_credentials.rs | 148 ++ .../src/api/password_credentials.rs | 145 ++ .../src/api/public_key_credentials.rs | 195 +++ warpgate-admin/src/api/recordings_detail.rs | 3 + warpgate-admin/src/api/roles.rs | 7 + warpgate-admin/src/api/sessions_detail.rs | 5 + warpgate-admin/src/api/sessions_list.rs | 3 + warpgate-admin/src/api/ssh_keys.rs | 3 + warpgate-admin/src/api/sso_credentials.rs | 195 +++ warpgate-admin/src/api/targets.rs | 10 + warpgate-admin/src/api/tickets_detail.rs | 3 + warpgate-admin/src/api/tickets_list.rs | 4 + warpgate-admin/src/api/users.rs | 52 +- warpgate-common/Cargo.toml | 8 +- warpgate-common/src/config/mod.rs | 41 +- warpgate-common/src/tls/rustls_helpers.rs | 7 +- warpgate-core/Cargo.toml | 8 +- warpgate-core/src/config_providers/db.rs | 181 +-- warpgate-core/src/config_providers/file.rs | 308 ----- warpgate-core/src/config_providers/mod.rs | 2 - warpgate-core/src/db/mod.rs | 147 +- warpgate-core/src/services.rs | 8 +- warpgate-database-protocols/Cargo.toml | 2 +- warpgate-db-entities/Cargo.toml | 5 +- warpgate-db-entities/src/OtpCredential.rs | 61 + .../src/PasswordCredential.rs | 75 ++ .../src/PublicKeyCredential.rs | 61 + warpgate-db-entities/src/SsoCredential.rs | 64 + warpgate-db-entities/src/User.rs | 122 +- warpgate-db-entities/src/lib.rs | 4 + warpgate-db-migrations/Cargo.toml | 4 +- warpgate-db-migrations/src/lib.rs | 2 + warpgate-db-migrations/src/m00008_users.rs | 2 +- .../src/m00009_credential_models.rs | 307 +++++ warpgate-protocol-http/Cargo.toml | 6 +- warpgate-protocol-http/src/api/auth.rs | 13 +- warpgate-protocol-http/src/api/mod.rs | 19 +- .../src/api/targets_list.rs | 8 +- warpgate-protocol-http/src/catchall.rs | 18 +- warpgate-protocol-http/src/common.rs | 51 +- warpgate-protocol-http/src/error.rs | 2 + warpgate-protocol-http/src/lib.rs | 2 +- warpgate-protocol-mysql/Cargo.toml | 3 +- warpgate-protocol-mysql/src/lib.rs | 15 +- warpgate-protocol-postgres/Cargo.toml | 2 +- warpgate-protocol-postgres/src/lib.rs | 15 +- warpgate-protocol-ssh/Cargo.toml | 5 +- warpgate-sso/Cargo.toml | 8 +- warpgate-sso/src/sso.rs | 8 +- warpgate-web/Cargo.toml | 4 +- warpgate-web/package.json | 1 + .../src/admin/AuthPolicyEditor.svelte | 19 +- warpgate-web/src/admin/CreateOtpModal.svelte | 175 +++ .../src/admin/CreatePasswordModal.svelte | 74 + warpgate-web/src/admin/CreateUser.svelte | 4 +- .../src/admin/CredentialEditor.svelte | 375 ++++++ .../src/admin/PublicKeyCredentialModal.svelte | 87 ++ .../src/admin/SsoCredentialModal.svelte | 101 ++ warpgate-web/src/admin/User.svelte | 192 +-- .../src/admin/UserCredentialModal.svelte | 290 ---- .../src/admin/lib/openapi-schema.json | 1156 +++++++++++++--- warpgate-web/src/common/protocols.ts | 8 + .../src/gateway/lib/openapi-schema.json | 24 +- warpgate/Cargo.toml | 6 +- warpgate/src/commands/recover_access.rs | 37 +- warpgate/src/commands/run.rs | 4 +- warpgate/src/commands/setup.rs | 52 +- warpgate/src/commands/test_target.rs | 2 +- warpgate/src/main.rs | 10 +- 102 files changed, 4893 insertions(+), 2949 deletions(-) create mode 100644 tests/.gitignore create mode 100644 tests/api_sdk/.gitkeep create mode 100644 tests/api_sdk/__init__.py create mode 100644 warpgate-admin/src/api/otp_credentials.rs create mode 100644 warpgate-admin/src/api/password_credentials.rs create mode 100644 warpgate-admin/src/api/public_key_credentials.rs create mode 100644 warpgate-admin/src/api/sso_credentials.rs delete mode 100644 warpgate-core/src/config_providers/file.rs create mode 100644 warpgate-db-entities/src/OtpCredential.rs create mode 100644 warpgate-db-entities/src/PasswordCredential.rs create mode 100644 warpgate-db-entities/src/PublicKeyCredential.rs create mode 100644 warpgate-db-entities/src/SsoCredential.rs create mode 100644 warpgate-db-migrations/src/m00009_credential_models.rs create mode 100644 warpgate-web/src/admin/CreateOtpModal.svelte create mode 100644 warpgate-web/src/admin/CreatePasswordModal.svelte create mode 100644 warpgate-web/src/admin/CredentialEditor.svelte create mode 100644 warpgate-web/src/admin/PublicKeyCredentialModal.svelte create mode 100644 warpgate-web/src/admin/SsoCredentialModal.svelte delete mode 100644 warpgate-web/src/admin/UserCredentialModal.svelte diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a605a3af4..c19150afb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -22,10 +22,11 @@ jobs: cargo clean rustup component add llvm-tools-preview - - name: Build admin UI + - name: Build UI run: | just yarn --network-timeout 1000000000 just openapi + just yarn openapi:tests-sdk just yarn build - name: Build images @@ -37,14 +38,14 @@ jobs: working-directory: tests run: | sudo apt install -y gnome-keyring - pip3 install keyring==23.8.2 poetry==1.1.14 + pip3 install keyring==24 poetry==1.8.3 poetry install - name: Run working-directory: tests run: | TIMEOUT=120 poetry run ./run.sh - cargo llvm-cov --no-run --hide-instantiations --lcov > coverage.lcov + cargo llvm-cov report --lcov > coverage.lcov - name: SonarCloud Scan uses: SonarSource/sonarcloud-github-action@master diff --git a/Cargo.lock b/Cargo.lock index a5837c56c..3e1458119 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1346,6 +1346,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "324a1be68054ef05ad64b861cc9eaf1d623d2d8cb25b4bf2cb9cdd902b4bf253" dependencies = [ "crc32fast", + "libz-sys", "miniz_oxide", ] @@ -2275,6 +2276,17 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "libz-sys" +version = "1.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2d16453e800a8cf6dd2fc3eb4bc99b786a9b90c663b8559a5b1a041bf89e472" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + [[package]] name = "linked-hash-map" version = "0.5.6" @@ -5745,6 +5757,7 @@ dependencies = [ name = "warpgate-db-entities" version = "0.11.0" dependencies = [ + "bytes", "chrono", "poem-openapi", "sea-orm", @@ -5759,8 +5772,10 @@ name = "warpgate-db-migrations" version = "0.11.0" dependencies = [ "chrono", + "data-encoding", "sea-orm", "sea-orm-migration", + "serde", "serde_json", "tokio", "uuid", @@ -5806,6 +5821,7 @@ dependencies = [ "anyhow", "async-trait", "bytes", + "flate2", "mysql_common", "once_cell", "password-hash 0.2.1", diff --git a/Cargo.toml b/Cargo.toml index 56c6eaffe..92358424e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,14 @@ members = [ default-members = ["warpgate"] resolver = "2" +[workspace.dependencies] +bytes = "1.4" +data-encoding = "2.3" +serde = "1.0" +serde_json = "1.0" +russh = { version = "0.46.0", features = ["legacy-ed25519-pkcs8-parser"] } +russh-keys = { version = "0.46.0", features = ["legacy-ed25519-pkcs8-parser"] } + [profile.release] lto = true panic = "abort" diff --git a/tests/.gitignore b/tests/.gitignore new file mode 100644 index 000000000..7b6a510ee --- /dev/null +++ b/tests/.gitignore @@ -0,0 +1 @@ +api_sdk diff --git a/tests/api_client.py b/tests/api_client.py index 28d9dd9fa..700840a89 100644 --- a/tests/api_client.py +++ b/tests/api_client.py @@ -1,92 +1,21 @@ -import requests from contextlib import contextmanager +try: + # in-IDE + import api_sdk.openapi_client as sdk +except ImportError: + import openapi_client as sdk -@contextmanager -def api_admin_session(url): - session = requests.Session() - session.verify = False - response = session.post( - f"{url}/@warpgate/api/auth/login", - json={ - "username": "admin", - "password": "123", - }, - ) - assert response.status_code // 100 == 2 - yield session - - -def assert_response(response, code): - if response.status_code != code: - print(response.text) - assert response.status_code == code - - -def api_list_users(url, session): - response = session.get( - f"{url}/@warpgate/admin/api/users", - ) - assert_response(response, 200) - return response.json() - - -def api_create_target(url, session, config): - response = session.post( - f"{url}/@warpgate/admin/api/targets", - json=config, - ) - assert_response(response, 201) - return response.json() - - -def api_create_role(url, session, config): - response = session.post( - f"{url}/@warpgate/admin/api/roles", - json=config, - ) - assert_response(response, 201) - return response.json() - - -def api_create_user(url, session, config): - response = session.post( - f"{url}/@warpgate/admin/api/users", - json=config, - ) - assert_response(response, 201) - return response.json() - - -def api_add_role_to_target(url, session, target_id, role_id): - response = session.post( - f"{url}/@warpgate/admin/api/targets/{target_id}/roles/{role_id}", - ) - assert_response(response, 201) -def api_add_role_to_user(url, session, user_id, role_id): - response = session.post( - f"{url}/@warpgate/admin/api/users/{user_id}/roles/{role_id}", - ) - assert_response(response, 201) - - -def api_create_ticket(url, session, username, target_name): - response = session.post( - f"{url}/@warpgate/api/auth/login", - json={ - "username": "admin", - "password": "123", - }, - ) - assert response.status_code // 100 == 2 - response = session.post( - f"{url}/@warpgate/admin/api/tickets", - json={ - "username": username, - "target_name": target_name, +@contextmanager +def admin_client(host): + config = sdk.Configuration( + host=f"{host}/@warpgate/admin/api", + api_key={ + "TokenSecurityScheme": "token-value", }, ) - assert response.status_code == 201 - return response.json()["secret"] + config.verify_ssl = False + with sdk.ApiClient(config) as api_client: + yield sdk.DefaultApi(api_client) diff --git a/tests/api_sdk/.gitkeep b/tests/api_sdk/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/tests/api_sdk/__init__.py b/tests/api_sdk/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/conftest.py b/tests/conftest.py index d1a1a5200..876e64354 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -20,6 +20,12 @@ cargo_root = Path(os.getcwd()).parent +enable_coverage = os.getenv("ENABLE_COVERAGE", "0") == "1" +binary_path = ( + "target/llvm-cov-target/debug/warpgate" + if enable_coverage + else "target/debug/warpgate" +) @dataclass @@ -135,7 +141,16 @@ def start_postgres_server(self): port = alloc_port() container_name = f"warpgate-e2e-postgres-server-{uuid.uuid4()}" self.start( - ["docker", "run", "--rm", '--name', container_name, "-p", f"{port}:5432", "warpgate-e2e-postgres-server"] + [ + "docker", + "run", + "--rm", + "--name", + container_name, + "-p", + f"{port}:5432", + "warpgate-e2e-postgres-server", + ] ) def wait_postgres(): @@ -169,7 +184,7 @@ def start_wg( stderr=None, stdout=None, ) -> WarpgateProcess: - args = args or ["run"] + args = args or ["run", "--admin-token", "token-value"] if share_with: config_path = share_with.config_path @@ -206,7 +221,7 @@ def start_wg( def run(args, env={}): return self.start( [ - f"{cargo_root}/target/llvm-cov-target/debug/warpgate", + os.path.join(cargo_root, binary_path), "--config", str(config_path), *args, @@ -315,9 +330,22 @@ def processes(ctx, timeout, report_generation): @pytest.fixture(scope="session", autouse=True) def report_generation(): + if not enable_coverage: + yield None + return # subprocess.call(['cargo', 'llvm-cov', 'clean', '--workspace']) subprocess.check_call( - ["cargo", "llvm-cov", "run", "--no-cfg-coverage-nightly", "--all-features", "--no-report", "--", "--version"], cwd=cargo_root + [ + "cargo", + "llvm-cov", + "run", + "--no-cfg-coverage-nightly", + "--all-features", + "--no-report", + "--", + "--version", + ], + cwd=cargo_root, ) yield # subprocess.check_call(['cargo', 'llvm-cov', '--no-run', '--hide-instantiations', '--html'], cwd=cargo_root) diff --git a/tests/poetry.lock b/tests/poetry.lock index ab5a7f53e..f1944a51b 100644 --- a/tests/poetry.lock +++ b/tests/poetry.lock @@ -1,18 +1,36 @@ +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. + +[[package]] +name = "annotated-types" +version = "0.5.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.7" +files = [ + {file = "annotated_types-0.5.0-py3-none-any.whl", hash = "sha256:58da39888f92c276ad970249761ebea80ba544b77acddaa1a4d6cf78287d45fd"}, + {file = "annotated_types-0.5.0.tar.gz", hash = "sha256:47cdc3490d9ac1506ce92c7aaa76c579dc3509ff11e098fc867e5130ab7be802"}, +] + [[package]] name = "atomicwrites" version = "1.4.1" description = "Atomic file writes." -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "atomicwrites-1.4.1.tar.gz", hash = "sha256:81b2c9071a49367a7f770170e5eec8cb66567cfbbc8c73d20ce5ca4a8d71cf11"}, +] [[package]] name = "attrs" version = "21.4.0" description = "Classes Without Boilerplate" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, + {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, +] [package.extras] dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "six", "sphinx", "sphinx-notfound-page", "zope.interface"] @@ -24,9 +42,21 @@ tests-no-zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy" name = "bcrypt" version = "3.2.2" description = "Modern password hashing for your software and your servers" -category = "main" optional = false python-versions = ">=3.6" +files = [ + {file = "bcrypt-3.2.2-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:7180d98a96f00b1050e93f5b0f556e658605dd9f524d0b0e68ae7944673f525e"}, + {file = "bcrypt-3.2.2-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:61bae49580dce88095d669226d5076d0b9d927754cedbdf76c6c9f5099ad6f26"}, + {file = "bcrypt-3.2.2-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88273d806ab3a50d06bc6a2fc7c87d737dd669b76ad955f449c43095389bc8fb"}, + {file = "bcrypt-3.2.2-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:6d2cb9d969bfca5bc08e45864137276e4c3d3d7de2b162171def3d188bf9d34a"}, + {file = "bcrypt-3.2.2-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b02d6bfc6336d1094276f3f588aa1225a598e27f8e3388f4db9948cb707b521"}, + {file = "bcrypt-3.2.2-cp36-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a2c46100e315c3a5b90fdc53e429c006c5f962529bc27e1dfd656292c20ccc40"}, + {file = "bcrypt-3.2.2-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:7d9ba2e41e330d2af4af6b1b6ec9e6128e91343d0b4afb9282e54e5508f31baa"}, + {file = "bcrypt-3.2.2-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:cd43303d6b8a165c29ec6756afd169faba9396a9472cdff753fe9f19b96ce2fa"}, + {file = "bcrypt-3.2.2-cp36-abi3-win32.whl", hash = "sha256:4e029cef560967fb0cf4a802bcf4d562d3d6b4b1bf81de5ec1abbe0f1adb027e"}, + {file = "bcrypt-3.2.2-cp36-abi3-win_amd64.whl", hash = "sha256:7ff2069240c6bbe49109fe84ca80508773a904f5a8cb960e02a977f7f519b129"}, + {file = "bcrypt-3.2.2.tar.gz", hash = "sha256:433c410c2177057705da2a9f2cd01dd157493b2a7ac14c8593a16b3dab6b6bfb"}, +] [package.dependencies] cffi = ">=1.1" @@ -37,24 +67,47 @@ typecheck = ["mypy"] [[package]] name = "black" -version = "22.10.0" +version = "24.10.0" description = "The uncompromising code formatter." -category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" +files = [ + {file = "black-24.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6668650ea4b685440857138e5fe40cde4d652633b1bdffc62933d0db4ed9812"}, + {file = "black-24.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1c536fcf674217e87b8cc3657b81809d3c085d7bf3ef262ead700da345bfa6ea"}, + {file = "black-24.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:649fff99a20bd06c6f727d2a27f401331dc0cc861fb69cde910fe95b01b5928f"}, + {file = "black-24.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:fe4d6476887de70546212c99ac9bd803d90b42fc4767f058a0baa895013fbb3e"}, + {file = "black-24.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5a2221696a8224e335c28816a9d331a6c2ae15a2ee34ec857dcf3e45dbfa99ad"}, + {file = "black-24.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f9da3333530dbcecc1be13e69c250ed8dfa67f43c4005fb537bb426e19200d50"}, + {file = "black-24.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4007b1393d902b48b36958a216c20c4482f601569d19ed1df294a496eb366392"}, + {file = "black-24.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:394d4ddc64782e51153eadcaaca95144ac4c35e27ef9b0a42e121ae7e57a9175"}, + {file = "black-24.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e39e0fae001df40f95bd8cc36b9165c5e2ea88900167bddf258bacef9bbdc3"}, + {file = "black-24.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d37d422772111794b26757c5b55a3eade028aa3fde43121ab7b673d050949d65"}, + {file = "black-24.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14b3502784f09ce2443830e3133dacf2c0110d45191ed470ecb04d0f5f6fcb0f"}, + {file = "black-24.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:30d2c30dc5139211dda799758559d1b049f7f14c580c409d6ad925b74a4208a8"}, + {file = "black-24.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cbacacb19e922a1d75ef2b6ccaefcd6e93a2c05ede32f06a21386a04cedb981"}, + {file = "black-24.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1f93102e0c5bb3907451063e08b9876dbeac810e7da5a8bfb7aeb5a9ef89066b"}, + {file = "black-24.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ddacb691cdcdf77b96f549cf9591701d8db36b2f19519373d60d31746068dbf2"}, + {file = "black-24.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:680359d932801c76d2e9c9068d05c6b107f2584b2a5b88831c83962eb9984c1b"}, + {file = "black-24.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:17374989640fbca88b6a448129cd1745c5eb8d9547b464f281b251dd00155ccd"}, + {file = "black-24.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:63f626344343083322233f175aaf372d326de8436f5928c042639a4afbbf1d3f"}, + {file = "black-24.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfa1d0cb6200857f1923b602f978386a3a2758a65b52e0950299ea014be6800"}, + {file = "black-24.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:2cd9c95431d94adc56600710f8813ee27eea544dd118d45896bb734e9d7a0dc7"}, + {file = "black-24.10.0-py3-none-any.whl", hash = "sha256:3bb2b7a1f7b685f85b11fed1ef10f8a9148bceb49853e47a294a3dd963c1dd7d"}, + {file = "black-24.10.0.tar.gz", hash = "sha256:846ea64c97afe3bc677b761787993be4991810ecc7a4a937816dd6bddedc4875"}, +] [package.dependencies] click = ">=8.0.0" mypy-extensions = ">=0.4.3" +packaging = ">=22.0" pathspec = ">=0.9.0" platformdirs = ">=2" -tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""} -typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implementation_name == \"cpython\""} -typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} [package.extras] colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.7.4)"] +d = ["aiohttp (>=3.10)"] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] uvloop = ["uvloop (>=0.15.2)"] @@ -62,17 +115,85 @@ uvloop = ["uvloop (>=0.15.2)"] name = "certifi" version = "2022.12.7" description = "Python package for providing Mozilla's CA Bundle." -category = "main" optional = false python-versions = ">=3.6" +files = [ + {file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"}, + {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"}, +] [[package]] name = "cffi" version = "1.15.1" description = "Foreign Function Interface for Python calling C code." -category = "main" optional = false python-versions = "*" +files = [ + {file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"}, + {file = "cffi-1.15.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2"}, + {file = "cffi-1.15.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914"}, + {file = "cffi-1.15.1-cp27-cp27m-win32.whl", hash = "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3"}, + {file = "cffi-1.15.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e"}, + {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162"}, + {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b"}, + {file = "cffi-1.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21"}, + {file = "cffi-1.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4"}, + {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01"}, + {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e"}, + {file = "cffi-1.15.1-cp310-cp310-win32.whl", hash = "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2"}, + {file = "cffi-1.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d"}, + {file = "cffi-1.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac"}, + {file = "cffi-1.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c"}, + {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef"}, + {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8"}, + {file = "cffi-1.15.1-cp311-cp311-win32.whl", hash = "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d"}, + {file = "cffi-1.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104"}, + {file = "cffi-1.15.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e"}, + {file = "cffi-1.15.1-cp36-cp36m-win32.whl", hash = "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf"}, + {file = "cffi-1.15.1-cp36-cp36m-win_amd64.whl", hash = "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497"}, + {file = "cffi-1.15.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426"}, + {file = "cffi-1.15.1-cp37-cp37m-win32.whl", hash = "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9"}, + {file = "cffi-1.15.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045"}, + {file = "cffi-1.15.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192"}, + {file = "cffi-1.15.1-cp38-cp38-win32.whl", hash = "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314"}, + {file = "cffi-1.15.1-cp38-cp38-win_amd64.whl", hash = "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5"}, + {file = "cffi-1.15.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585"}, + {file = "cffi-1.15.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27"}, + {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76"}, + {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3"}, + {file = "cffi-1.15.1-cp39-cp39-win32.whl", hash = "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee"}, + {file = "cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c"}, + {file = "cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9"}, +] [package.dependencies] pycparser = "*" @@ -81,9 +202,12 @@ pycparser = "*" name = "charset-normalizer" version = "2.1.0" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -category = "main" optional = false python-versions = ">=3.6.0" +files = [ + {file = "charset-normalizer-2.1.0.tar.gz", hash = "sha256:575e708016ff3a5e3681541cb9d79312c416835686d054a23accb873b254f413"}, + {file = "charset_normalizer-2.1.0-py3-none-any.whl", hash = "sha256:5189b6f22b01957427f35b6a08d9a0bc45b46d3788ef5a92e978433c7a35f8a5"}, +] [package.extras] unicode-backport = ["unicodedata2"] @@ -92,29 +216,61 @@ unicode-backport = ["unicodedata2"] name = "click" version = "8.1.3" description = "Composable command line interface toolkit" -category = "main" optional = false python-versions = ">=3.7" +files = [ + {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, + {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, +] [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} -importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} [[package]] name = "colorama" version = "0.4.5" description = "Cross-platform colored terminal text." -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"}, + {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, +] [[package]] name = "cryptography" version = "38.0.3" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." -category = "main" optional = false python-versions = ">=3.6" +files = [ + {file = "cryptography-38.0.3-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:984fe150f350a3c91e84de405fe49e688aa6092b3525f407a18b9646f6612320"}, + {file = "cryptography-38.0.3-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:ed7b00096790213e09eb11c97cc6e2b757f15f3d2f85833cd2d3ec3fe37c1722"}, + {file = "cryptography-38.0.3-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:bbf203f1a814007ce24bd4d51362991d5cb90ba0c177a9c08825f2cc304d871f"}, + {file = "cryptography-38.0.3-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:554bec92ee7d1e9d10ded2f7e92a5d70c1f74ba9524947c0ba0c850c7b011828"}, + {file = "cryptography-38.0.3-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1b52c9e5f8aa2b802d48bd693190341fae201ea51c7a167d69fc48b60e8a959"}, + {file = "cryptography-38.0.3-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:728f2694fa743a996d7784a6194da430f197d5c58e2f4e278612b359f455e4a2"}, + {file = "cryptography-38.0.3-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:dfb4f4dd568de1b6af9f4cda334adf7d72cf5bc052516e1b2608b683375dd95c"}, + {file = "cryptography-38.0.3-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:5419a127426084933076132d317911e3c6eb77568a1ce23c3ac1e12d111e61e0"}, + {file = "cryptography-38.0.3-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:9b24bcff7853ed18a63cfb0c2b008936a9554af24af2fb146e16d8e1aed75748"}, + {file = "cryptography-38.0.3-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:25c1d1f19729fb09d42e06b4bf9895212292cb27bb50229f5aa64d039ab29146"}, + {file = "cryptography-38.0.3-cp36-abi3-win32.whl", hash = "sha256:7f836217000342d448e1c9a342e9163149e45d5b5eca76a30e84503a5a96cab0"}, + {file = "cryptography-38.0.3-cp36-abi3-win_amd64.whl", hash = "sha256:c46837ea467ed1efea562bbeb543994c2d1f6e800785bd5a2c98bc096f5cb220"}, + {file = "cryptography-38.0.3-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06fc3cc7b6f6cca87bd56ec80a580c88f1da5306f505876a71c8cfa7050257dd"}, + {file = "cryptography-38.0.3-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:65535bc550b70bd6271984d9863a37741352b4aad6fb1b3344a54e6950249b55"}, + {file = "cryptography-38.0.3-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:5e89468fbd2fcd733b5899333bc54d0d06c80e04cd23d8c6f3e0542358c6060b"}, + {file = "cryptography-38.0.3-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:6ab9516b85bebe7aa83f309bacc5f44a61eeb90d0b4ec125d2d003ce41932d36"}, + {file = "cryptography-38.0.3-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:068147f32fa662c81aebab95c74679b401b12b57494872886eb5c1139250ec5d"}, + {file = "cryptography-38.0.3-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:402852a0aea73833d982cabb6d0c3bb582c15483d29fb7085ef2c42bfa7e38d7"}, + {file = "cryptography-38.0.3-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b1b35d9d3a65542ed2e9d90115dfd16bbc027b3f07ee3304fc83580f26e43249"}, + {file = "cryptography-38.0.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:6addc3b6d593cd980989261dc1cce38263c76954d758c3c94de51f1e010c9a50"}, + {file = "cryptography-38.0.3-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:be243c7e2bfcf6cc4cb350c0d5cdf15ca6383bbcb2a8ef51d3c9411a9d4386f0"}, + {file = "cryptography-38.0.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78cf5eefac2b52c10398a42765bfa981ce2372cbc0457e6bf9658f41ec3c41d8"}, + {file = "cryptography-38.0.3-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:4e269dcd9b102c5a3d72be3c45d8ce20377b8076a43cbed6f660a1afe365e436"}, + {file = "cryptography-38.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:8d41a46251bf0634e21fac50ffd643216ccecfaf3701a063257fe0b2be1b6548"}, + {file = "cryptography-38.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:785e4056b5a8b28f05a533fab69febf5004458e20dad7e2e13a3120d8ecec75a"}, + {file = "cryptography-38.0.3.tar.gz", hash = "sha256:bfbe6ee19615b07a98b1d2287d6a6073f734735b49ee45b11324d85efc4d5cbd"}, +] [package.dependencies] cffi = ">=1.12" @@ -131,12 +287,14 @@ test = ["hypothesis (>=1.11.4,!=3.79.2)", "iso8601", "pretend", "pytest (>=6.2.0 name = "flake8" version = "5.0.2" description = "the modular source code checker: pep8 pyflakes and co" -category = "dev" optional = false python-versions = ">=3.6.1" +files = [ + {file = "flake8-5.0.2-py2.py3-none-any.whl", hash = "sha256:a7926e0b6d23c0991245b60279e774d2596dfecd9b158525d1f8c050a61eae5a"}, + {file = "flake8-5.0.2.tar.gz", hash = "sha256:9cc32bc0c5d16eacc014c7ec6f0e9565fd81df66c2092c3c9df06e3c1ac95e5d"}, +] [package.dependencies] -importlib-metadata = {version = "<4.3", markers = "python_version < \"3.8\""} mccabe = ">=0.7.0,<0.8.0" pycodestyle = ">=2.9.0,<2.10.0" pyflakes = ">=2.5.0,<2.6.0" @@ -145,13 +303,15 @@ pyflakes = ">=2.5.0,<2.6.0" name = "flask" version = "2.2.1" description = "A simple framework for building complex web applications." -category = "main" optional = false python-versions = ">=3.7" +files = [ + {file = "Flask-2.2.1-py3-none-any.whl", hash = "sha256:3c604c48c3d5b4c63e72134044c0b4fe90ff01ef65280b9fe2d38c8860d99fe5"}, + {file = "Flask-2.2.1.tar.gz", hash = "sha256:9c2b81b9b1edcc835af72d600f1955e713a065e7cb41d7e51ee762b449d9c65d"}, +] [package.dependencies] click = ">=8.0" -importlib-metadata = {version = ">=3.6.0", markers = "python_version < \"3.10\""} itsdangerous = ">=2.0" Jinja2 = ">=3.0" Werkzeug = ">=2.2.0" @@ -164,9 +324,12 @@ dotenv = ["python-dotenv"] name = "flask-sock" version = "0.5.2" description = "WebSocket support for Flask" -category = "main" optional = false python-versions = ">=3.6" +files = [ + {file = "flask-sock-0.5.2.tar.gz", hash = "sha256:c36e92813e897a325a48caee640509f88c465b8df642e40126c1b25fc38a2c30"}, + {file = "flask_sock-0.5.2-py3-none-any.whl", hash = "sha256:bdd60520d031eb92e6fa2dbd3deffbb6e71d3662e9d39bfe53d2eccf971d0fd0"}, +] [package.dependencies] flask = ">=2" @@ -176,60 +339,56 @@ simple-websocket = ">=0.5.1" name = "h11" version = "0.13.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" -category = "main" optional = false python-versions = ">=3.6" - -[package.dependencies] -typing-extensions = {version = "*", markers = "python_version < \"3.8\""} +files = [ + {file = "h11-0.13.0-py3-none-any.whl", hash = "sha256:8ddd78563b633ca55346c8cd41ec0af27d3c79931828beffb46ce70a379e7442"}, + {file = "h11-0.13.0.tar.gz", hash = "sha256:70813c1135087a248a4d38cc0e1a0181ffab2188141a93eaf567940c3957ff06"}, +] [[package]] name = "idna" version = "3.3" description = "Internationalized Domain Names in Applications (IDNA)" -category = "main" optional = false python-versions = ">=3.5" - -[[package]] -name = "importlib-metadata" -version = "4.2.0" -description = "Read metadata from Python packages" -category = "main" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} -zipp = ">=0.5" - -[package.extras] -docs = ["jaraco.packaging (>=8.2)", "rst.linker (>=1.9)", "sphinx"] -testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pep517", "pyfakefs", "pytest (>=4.6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-flake8", "pytest-mypy"] +files = [ + {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, + {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, +] [[package]] name = "iniconfig" version = "1.1.1" description = "iniconfig: brain-dead simple config-ini parsing" -category = "main" optional = false python-versions = "*" +files = [ + {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, + {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, +] [[package]] name = "itsdangerous" version = "2.1.2" description = "Safely pass data to untrusted environments and back." -category = "main" optional = false python-versions = ">=3.7" +files = [ + {file = "itsdangerous-2.1.2-py3-none-any.whl", hash = "sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44"}, + {file = "itsdangerous-2.1.2.tar.gz", hash = "sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a"}, +] [[package]] name = "jinja2" version = "3.1.2" description = "A very fast and expressive template engine." -category = "main" optional = false python-versions = ">=3.7" +files = [ + {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, + {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, +] [package.dependencies] MarkupSafe = ">=2.0" @@ -241,44 +400,113 @@ i18n = ["Babel (>=2.7)"] name = "markupsafe" version = "2.1.1" description = "Safely add untrusted strings to HTML/XML markup." -category = "main" optional = false python-versions = ">=3.7" +files = [ + {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-win32.whl", hash = "sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-win32.whl", hash = "sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-win32.whl", hash = "sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-win32.whl", hash = "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247"}, + {file = "MarkupSafe-2.1.1.tar.gz", hash = "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b"}, +] [[package]] name = "mccabe" version = "0.7.0" description = "McCabe checker, plugin for flake8" -category = "dev" optional = false python-versions = ">=3.6" +files = [ + {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, + {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, +] [[package]] name = "mypy-extensions" version = "0.4.3" description = "Experimental type system extensions for programs checked with the mypy typechecker." -category = "dev" optional = false python-versions = "*" +files = [ + {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, + {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, +] [[package]] -name = "packaging" -version = "21.3" -description = "Core utilities for Python packages" -category = "main" +name = "openapi-client" +version = "1.0.0" +description = "Warpgate Web Admin" optional = false -python-versions = ">=3.6" +python-versions = "^3.7" +files = [] +develop = true [package.dependencies] -pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" +pydantic = ">=2" +python-dateutil = ">=2.8.2" +typing-extensions = ">=4.7.1" +urllib3 = ">= 1.25.3" + +[package.source] +type = "directory" +url = "api_sdk" + +[[package]] +name = "packaging" +version = "24.0" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, + {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, +] [[package]] name = "paramiko" version = "2.11.0" description = "SSH2 protocol library" -category = "main" optional = false python-versions = "*" +files = [ + {file = "paramiko-2.11.0-py2.py3-none-any.whl", hash = "sha256:655f25dc8baf763277b933dfcea101d636581df8d6b9774d1fb653426b72c270"}, + {file = "paramiko-2.11.0.tar.gz", hash = "sha256:003e6bee7c034c21fbb051bf83dc0a9ee4106204dd3c53054c71452cc4ec3938"}, +] [package.dependencies] bcrypt = ">=3.1.3" @@ -296,17 +524,23 @@ invoke = ["invoke (>=1.3)"] name = "pathspec" version = "0.10.1" description = "Utility library for gitignore style pattern matching of file paths." -category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "pathspec-0.10.1-py3-none-any.whl", hash = "sha256:46846318467efc4556ccfd27816e004270a9eeeeb4d062ce5e6fc7a87c573f93"}, + {file = "pathspec-0.10.1.tar.gz", hash = "sha256:7ace6161b621d31e7902eb6b5ae148d12cfd23f4a249b9ffb6b9fee12084323d"}, +] [[package]] name = "platformdirs" version = "2.5.3" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "platformdirs-2.5.3-py3-none-any.whl", hash = "sha256:0cb405749187a194f444c25c82ef7225232f11564721eabffc6ec70df83b11cb"}, + {file = "platformdirs-2.5.3.tar.gz", hash = "sha256:6e52c21afff35cb659c6e52d8b4d61b9bd544557180440538f255d9382c8cbe0"}, +] [package.extras] docs = ["furo (>=2022.9.29)", "proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.4)"] @@ -316,12 +550,12 @@ test = ["appdirs (==1.4.4)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-mock name = "pluggy" version = "1.0.0" description = "plugin and hook calling mechanisms for python" -category = "main" optional = false python-versions = ">=3.6" - -[package.dependencies] -importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} +files = [ + {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, + {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, +] [package.extras] dev = ["pre-commit", "tox"] @@ -331,92 +565,278 @@ testing = ["pytest", "pytest-benchmark"] name = "psutil" version = "5.9.1" description = "Cross-platform lib for process and system monitoring in Python." -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[package.extras] -test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"] - -[[package]] -name = "py" -version = "1.11.0" -description = "library with cross-python path, ini-parsing, io, code, log facilities" -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" - -[[package]] -name = "pycodestyle" -version = "2.9.0" -description = "Python style guide checker" -category = "dev" -optional = false -python-versions = ">=3.6" - -[[package]] +files = [ + {file = "psutil-5.9.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:799759d809c31aab5fe4579e50addf84565e71c1dc9f1c31258f159ff70d3f87"}, + {file = "psutil-5.9.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:9272167b5f5fbfe16945be3db475b3ce8d792386907e673a209da686176552af"}, + {file = "psutil-5.9.1-cp27-cp27m-win32.whl", hash = "sha256:0904727e0b0a038830b019551cf3204dd48ef5c6868adc776e06e93d615fc5fc"}, + {file = "psutil-5.9.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e7e10454cb1ab62cc6ce776e1c135a64045a11ec4c6d254d3f7689c16eb3efd2"}, + {file = "psutil-5.9.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:56960b9e8edcca1456f8c86a196f0c3d8e3e361320071c93378d41445ffd28b0"}, + {file = "psutil-5.9.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:44d1826150d49ffd62035785a9e2c56afcea66e55b43b8b630d7706276e87f22"}, + {file = "psutil-5.9.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c7be9d7f5b0d206f0bbc3794b8e16fb7dbc53ec9e40bbe8787c6f2d38efcf6c9"}, + {file = "psutil-5.9.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd9246e4cdd5b554a2ddd97c157e292ac11ef3e7af25ac56b08b455c829dca8"}, + {file = "psutil-5.9.1-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:29a442e25fab1f4d05e2655bb1b8ab6887981838d22effa2396d584b740194de"}, + {file = "psutil-5.9.1-cp310-cp310-win32.whl", hash = "sha256:20b27771b077dcaa0de1de3ad52d22538fe101f9946d6dc7869e6f694f079329"}, + {file = "psutil-5.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:58678bbadae12e0db55186dc58f2888839228ac9f41cc7848853539b70490021"}, + {file = "psutil-5.9.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:3a76ad658641172d9c6e593de6fe248ddde825b5866464c3b2ee26c35da9d237"}, + {file = "psutil-5.9.1-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a6a11e48cb93a5fa606306493f439b4aa7c56cb03fc9ace7f6bfa21aaf07c453"}, + {file = "psutil-5.9.1-cp36-cp36m-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:068935df39055bf27a29824b95c801c7a5130f118b806eee663cad28dca97685"}, + {file = "psutil-5.9.1-cp36-cp36m-win32.whl", hash = "sha256:0f15a19a05f39a09327345bc279c1ba4a8cfb0172cc0d3c7f7d16c813b2e7d36"}, + {file = "psutil-5.9.1-cp36-cp36m-win_amd64.whl", hash = "sha256:db417f0865f90bdc07fa30e1aadc69b6f4cad7f86324b02aa842034efe8d8c4d"}, + {file = "psutil-5.9.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:91c7ff2a40c373d0cc9121d54bc5f31c4fa09c346528e6a08d1845bce5771ffc"}, + {file = "psutil-5.9.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fea896b54f3a4ae6f790ac1d017101252c93f6fe075d0e7571543510f11d2676"}, + {file = "psutil-5.9.1-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3054e923204b8e9c23a55b23b6df73a8089ae1d075cb0bf711d3e9da1724ded4"}, + {file = "psutil-5.9.1-cp37-cp37m-win32.whl", hash = "sha256:d2d006286fbcb60f0b391741f520862e9b69f4019b4d738a2a45728c7e952f1b"}, + {file = "psutil-5.9.1-cp37-cp37m-win_amd64.whl", hash = "sha256:b14ee12da9338f5e5b3a3ef7ca58b3cba30f5b66f7662159762932e6d0b8f680"}, + {file = "psutil-5.9.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:19f36c16012ba9cfc742604df189f2f28d2720e23ff7d1e81602dbe066be9fd1"}, + {file = "psutil-5.9.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:944c4b4b82dc4a1b805329c980f270f170fdc9945464223f2ec8e57563139cf4"}, + {file = "psutil-5.9.1-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b6750a73a9c4a4e689490ccb862d53c7b976a2a35c4e1846d049dcc3f17d83b"}, + {file = "psutil-5.9.1-cp38-cp38-win32.whl", hash = "sha256:a8746bfe4e8f659528c5c7e9af5090c5a7d252f32b2e859c584ef7d8efb1e689"}, + {file = "psutil-5.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:79c9108d9aa7fa6fba6e668b61b82facc067a6b81517cab34d07a84aa89f3df0"}, + {file = "psutil-5.9.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:28976df6c64ddd6320d281128817f32c29b539a52bdae5e192537bc338a9ec81"}, + {file = "psutil-5.9.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b88f75005586131276634027f4219d06e0561292be8bd6bc7f2f00bdabd63c4e"}, + {file = "psutil-5.9.1-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:645bd4f7bb5b8633803e0b6746ff1628724668681a434482546887d22c7a9537"}, + {file = "psutil-5.9.1-cp39-cp39-win32.whl", hash = "sha256:32c52611756096ae91f5d1499fe6c53b86f4a9ada147ee42db4991ba1520e574"}, + {file = "psutil-5.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:f65f9a46d984b8cd9b3750c2bdb419b2996895b005aefa6cbaba9a143b1ce2c5"}, + {file = "psutil-5.9.1.tar.gz", hash = "sha256:57f1819b5d9e95cdfb0c881a8a5b7d542ed0b7c522d575706a80bedc848c8954"}, +] + +[package.extras] +test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"] + +[[package]] +name = "py" +version = "1.11.0" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, + {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, +] + +[[package]] +name = "pycodestyle" +version = "2.9.0" +description = "Python style guide checker" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pycodestyle-2.9.0-py2.py3-none-any.whl", hash = "sha256:289cdc0969d589d90752582bef6dff57c5fbc6949ee8b013ad6d6449a8ae9437"}, + {file = "pycodestyle-2.9.0.tar.gz", hash = "sha256:beaba44501f89d785be791c9462553f06958a221d166c64e1f107320f839acc2"}, +] + +[[package]] name = "pycparser" version = "2.21" description = "C parser in Python" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, + {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, +] + +[[package]] +name = "pydantic" +version = "2.5.3" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pydantic-2.5.3-py3-none-any.whl", hash = "sha256:d0caf5954bee831b6bfe7e338c32b9e30c85dfe080c843680783ac2b631673b4"}, + {file = "pydantic-2.5.3.tar.gz", hash = "sha256:b3ef57c62535b0941697cce638c08900d87fcb67e29cfa99e8a68f747f393f7a"}, +] + +[package.dependencies] +annotated-types = ">=0.4.0" +pydantic-core = "2.14.6" +typing-extensions = ">=4.6.1" + +[package.extras] +email = ["email-validator (>=2.0.0)"] + +[[package]] +name = "pydantic-core" +version = "2.14.6" +description = "" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pydantic_core-2.14.6-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:72f9a942d739f09cd42fffe5dc759928217649f070056f03c70df14f5770acf9"}, + {file = "pydantic_core-2.14.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6a31d98c0d69776c2576dda4b77b8e0c69ad08e8b539c25c7d0ca0dc19a50d6c"}, + {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5aa90562bc079c6c290f0512b21768967f9968e4cfea84ea4ff5af5d917016e4"}, + {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:370ffecb5316ed23b667d99ce4debe53ea664b99cc37bfa2af47bc769056d534"}, + {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f85f3843bdb1fe80e8c206fe6eed7a1caeae897e496542cee499c374a85c6e08"}, + {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9862bf828112e19685b76ca499b379338fd4c5c269d897e218b2ae8fcb80139d"}, + {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:036137b5ad0cb0004c75b579445a1efccd072387a36c7f217bb8efd1afbe5245"}, + {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:92879bce89f91f4b2416eba4429c7b5ca22c45ef4a499c39f0c5c69257522c7c"}, + {file = "pydantic_core-2.14.6-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0c08de15d50fa190d577e8591f0329a643eeaed696d7771760295998aca6bc66"}, + {file = "pydantic_core-2.14.6-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:36099c69f6b14fc2c49d7996cbf4f87ec4f0e66d1c74aa05228583225a07b590"}, + {file = "pydantic_core-2.14.6-cp310-none-win32.whl", hash = "sha256:7be719e4d2ae6c314f72844ba9d69e38dff342bc360379f7c8537c48e23034b7"}, + {file = "pydantic_core-2.14.6-cp310-none-win_amd64.whl", hash = "sha256:36fa402dcdc8ea7f1b0ddcf0df4254cc6b2e08f8cd80e7010d4c4ae6e86b2a87"}, + {file = "pydantic_core-2.14.6-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:dea7fcd62915fb150cdc373212141a30037e11b761fbced340e9db3379b892d4"}, + {file = "pydantic_core-2.14.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ffff855100bc066ff2cd3aa4a60bc9534661816b110f0243e59503ec2df38421"}, + {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b027c86c66b8627eb90e57aee1f526df77dc6d8b354ec498be9a757d513b92b"}, + {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:00b1087dabcee0b0ffd104f9f53d7d3eaddfaa314cdd6726143af6bc713aa27e"}, + {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:75ec284328b60a4e91010c1acade0c30584f28a1f345bc8f72fe8b9e46ec6a96"}, + {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7e1f4744eea1501404b20b0ac059ff7e3f96a97d3e3f48ce27a139e053bb370b"}, + {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2602177668f89b38b9f84b7b3435d0a72511ddef45dc14446811759b82235a1"}, + {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6c8edaea3089bf908dd27da8f5d9e395c5b4dc092dbcce9b65e7156099b4b937"}, + {file = "pydantic_core-2.14.6-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:478e9e7b360dfec451daafe286998d4a1eeaecf6d69c427b834ae771cad4b622"}, + {file = "pydantic_core-2.14.6-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b6ca36c12a5120bad343eef193cc0122928c5c7466121da7c20f41160ba00ba2"}, + {file = "pydantic_core-2.14.6-cp311-none-win32.whl", hash = "sha256:2b8719037e570639e6b665a4050add43134d80b687288ba3ade18b22bbb29dd2"}, + {file = "pydantic_core-2.14.6-cp311-none-win_amd64.whl", hash = "sha256:78ee52ecc088c61cce32b2d30a826f929e1708f7b9247dc3b921aec367dc1b23"}, + {file = "pydantic_core-2.14.6-cp311-none-win_arm64.whl", hash = "sha256:a19b794f8fe6569472ff77602437ec4430f9b2b9ec7a1105cfd2232f9ba355e6"}, + {file = "pydantic_core-2.14.6-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:667aa2eac9cd0700af1ddb38b7b1ef246d8cf94c85637cbb03d7757ca4c3fdec"}, + {file = "pydantic_core-2.14.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cdee837710ef6b56ebd20245b83799fce40b265b3b406e51e8ccc5b85b9099b7"}, + {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c5bcf3414367e29f83fd66f7de64509a8fd2368b1edf4351e862910727d3e51"}, + {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:26a92ae76f75d1915806b77cf459811e772d8f71fd1e4339c99750f0e7f6324f"}, + {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a983cca5ed1dd9a35e9e42ebf9f278d344603bfcb174ff99a5815f953925140a"}, + {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cb92f9061657287eded380d7dc455bbf115430b3aa4741bdc662d02977e7d0af"}, + {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4ace1e220b078c8e48e82c081e35002038657e4b37d403ce940fa679e57113b"}, + {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ef633add81832f4b56d3b4c9408b43d530dfca29e68fb1b797dcb861a2c734cd"}, + {file = "pydantic_core-2.14.6-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7e90d6cc4aad2cc1f5e16ed56e46cebf4877c62403a311af20459c15da76fd91"}, + {file = "pydantic_core-2.14.6-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e8a5ac97ea521d7bde7621d86c30e86b798cdecd985723c4ed737a2aa9e77d0c"}, + {file = "pydantic_core-2.14.6-cp312-none-win32.whl", hash = "sha256:f27207e8ca3e5e021e2402ba942e5b4c629718e665c81b8b306f3c8b1ddbb786"}, + {file = "pydantic_core-2.14.6-cp312-none-win_amd64.whl", hash = "sha256:b3e5fe4538001bb82e2295b8d2a39356a84694c97cb73a566dc36328b9f83b40"}, + {file = "pydantic_core-2.14.6-cp312-none-win_arm64.whl", hash = "sha256:64634ccf9d671c6be242a664a33c4acf12882670b09b3f163cd00a24cffbd74e"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:24368e31be2c88bd69340fbfe741b405302993242ccb476c5c3ff48aeee1afe0"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:e33b0834f1cf779aa839975f9d8755a7c2420510c0fa1e9fa0497de77cd35d2c"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6af4b3f52cc65f8a0bc8b1cd9676f8c21ef3e9132f21fed250f6958bd7223bed"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d15687d7d7f40333bd8266f3814c591c2e2cd263fa2116e314f60d82086e353a"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:095b707bb287bfd534044166ab767bec70a9bba3175dcdc3371782175c14e43c"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94fc0e6621e07d1e91c44e016cc0b189b48db053061cc22d6298a611de8071bb"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ce830e480f6774608dedfd4a90c42aac4a7af0a711f1b52f807130c2e434c06"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a306cdd2ad3a7d795d8e617a58c3a2ed0f76c8496fb7621b6cd514eb1532cae8"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:2f5fa187bde8524b1e37ba894db13aadd64faa884657473b03a019f625cee9a8"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:438027a975cc213a47c5d70672e0d29776082155cfae540c4e225716586be75e"}, + {file = "pydantic_core-2.14.6-cp37-none-win32.whl", hash = "sha256:f96ae96a060a8072ceff4cfde89d261837b4294a4f28b84a28765470d502ccc6"}, + {file = "pydantic_core-2.14.6-cp37-none-win_amd64.whl", hash = "sha256:e646c0e282e960345314f42f2cea5e0b5f56938c093541ea6dbf11aec2862391"}, + {file = "pydantic_core-2.14.6-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:db453f2da3f59a348f514cfbfeb042393b68720787bbef2b4c6068ea362c8149"}, + {file = "pydantic_core-2.14.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3860c62057acd95cc84044e758e47b18dcd8871a328ebc8ccdefd18b0d26a21b"}, + {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36026d8f99c58d7044413e1b819a67ca0e0b8ebe0f25e775e6c3d1fabb3c38fb"}, + {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8ed1af8692bd8d2a29d702f1a2e6065416d76897d726e45a1775b1444f5928a7"}, + {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:314ccc4264ce7d854941231cf71b592e30d8d368a71e50197c905874feacc8a8"}, + {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:982487f8931067a32e72d40ab6b47b1628a9c5d344be7f1a4e668fb462d2da42"}, + {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dbe357bc4ddda078f79d2a36fc1dd0494a7f2fad83a0a684465b6f24b46fe80"}, + {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2f6ffc6701a0eb28648c845f4945a194dc7ab3c651f535b81793251e1185ac3d"}, + {file = "pydantic_core-2.14.6-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7f5025db12fc6de7bc1104d826d5aee1d172f9ba6ca936bf6474c2148ac336c1"}, + {file = "pydantic_core-2.14.6-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:dab03ed811ed1c71d700ed08bde8431cf429bbe59e423394f0f4055f1ca0ea60"}, + {file = "pydantic_core-2.14.6-cp38-none-win32.whl", hash = "sha256:dfcbebdb3c4b6f739a91769aea5ed615023f3c88cb70df812849aef634c25fbe"}, + {file = "pydantic_core-2.14.6-cp38-none-win_amd64.whl", hash = "sha256:99b14dbea2fdb563d8b5a57c9badfcd72083f6006caf8e126b491519c7d64ca8"}, + {file = "pydantic_core-2.14.6-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:4ce8299b481bcb68e5c82002b96e411796b844d72b3e92a3fbedfe8e19813eab"}, + {file = "pydantic_core-2.14.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b9a9d92f10772d2a181b5ca339dee066ab7d1c9a34ae2421b2a52556e719756f"}, + {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd9e98b408384989ea4ab60206b8e100d8687da18b5c813c11e92fd8212a98e0"}, + {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4f86f1f318e56f5cbb282fe61eb84767aee743ebe32c7c0834690ebea50c0a6b"}, + {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86ce5fcfc3accf3a07a729779d0b86c5d0309a4764c897d86c11089be61da160"}, + {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dcf1978be02153c6a31692d4fbcc2a3f1db9da36039ead23173bc256ee3b91b"}, + {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eedf97be7bc3dbc8addcef4142f4b4164066df0c6f36397ae4aaed3eb187d8ab"}, + {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d5f916acf8afbcab6bacbb376ba7dc61f845367901ecd5e328fc4d4aef2fcab0"}, + {file = "pydantic_core-2.14.6-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:8a14c192c1d724c3acbfb3f10a958c55a2638391319ce8078cb36c02283959b9"}, + {file = "pydantic_core-2.14.6-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0348b1dc6b76041516e8a854ff95b21c55f5a411c3297d2ca52f5528e49d8411"}, + {file = "pydantic_core-2.14.6-cp39-none-win32.whl", hash = "sha256:de2a0645a923ba57c5527497daf8ec5df69c6eadf869e9cd46e86349146e5975"}, + {file = "pydantic_core-2.14.6-cp39-none-win_amd64.whl", hash = "sha256:aca48506a9c20f68ee61c87f2008f81f8ee99f8d7f0104bff3c47e2d148f89d9"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:d5c28525c19f5bb1e09511669bb57353d22b94cf8b65f3a8d141c389a55dec95"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:78d0768ee59baa3de0f4adac9e3748b4b1fffc52143caebddfd5ea2961595277"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b93785eadaef932e4fe9c6e12ba67beb1b3f1e5495631419c784ab87e975670"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a874f21f87c485310944b2b2734cd6d318765bcbb7515eead33af9641816506e"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b89f4477d915ea43b4ceea6756f63f0288941b6443a2b28c69004fe07fde0d0d"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:172de779e2a153d36ee690dbc49c6db568d7b33b18dc56b69a7514aecbcf380d"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:dfcebb950aa7e667ec226a442722134539e77c575f6cfaa423f24371bb8d2e94"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:55a23dcd98c858c0db44fc5c04fc7ed81c4b4d33c653a7c45ddaebf6563a2f66"}, + {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-macosx_10_7_x86_64.whl", hash = "sha256:4241204e4b36ab5ae466ecec5c4c16527a054c69f99bba20f6f75232a6a534e2"}, + {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e574de99d735b3fc8364cba9912c2bec2da78775eba95cbb225ef7dda6acea24"}, + {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1302a54f87b5cd8528e4d6d1bf2133b6aa7c6122ff8e9dc5220fbc1e07bffebd"}, + {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8e81e4b55930e5ffab4a68db1af431629cf2e4066dbdbfef65348b8ab804ea8"}, + {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c99462ffc538717b3e60151dfaf91125f637e801f5ab008f81c402f1dff0cd0f"}, + {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e4cf2d5829f6963a5483ec01578ee76d329eb5caf330ecd05b3edd697e7d768a"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:cf10b7d58ae4a1f07fccbf4a0a956d705356fea05fb4c70608bb6fa81d103cda"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:399ac0891c284fa8eb998bcfa323f2234858f5d2efca3950ae58c8f88830f145"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c6a5c79b28003543db3ba67d1df336f253a87d3112dac3a51b94f7d48e4c0e1"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:599c87d79cab2a6a2a9df4aefe0455e61e7d2aeede2f8577c1b7c0aec643ee8e"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:43e166ad47ba900f2542a80d83f9fc65fe99eb63ceec4debec160ae729824052"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3a0b5db001b98e1c649dd55afa928e75aa4087e587b9524a4992316fa23c9fba"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:747265448cb57a9f37572a488a57d873fd96bf51e5bb7edb52cfb37124516da4"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:7ebe3416785f65c28f4f9441e916bfc8a54179c8dea73c23023f7086fa601c5d"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:86c963186ca5e50d5c8287b1d1c9d3f8f024cbe343d048c5bd282aec2d8641f2"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:e0641b506486f0b4cd1500a2a65740243e8670a2549bb02bc4556a83af84ae03"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71d72ca5eaaa8d38c8df16b7deb1a2da4f650c41b58bb142f3fb75d5ad4a611f"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27e524624eace5c59af499cd97dc18bb201dc6a7a2da24bfc66ef151c69a5f2a"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a3dde6cac75e0b0902778978d3b1646ca9f438654395a362cb21d9ad34b24acf"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:00646784f6cd993b1e1c0e7b0fdcbccc375d539db95555477771c27555e3c556"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:23598acb8ccaa3d1d875ef3b35cb6376535095e9405d91a3d57a8c7db5d29341"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7f41533d7e3cf9520065f610b41ac1c76bc2161415955fbcead4981b22c7611e"}, + {file = "pydantic_core-2.14.6.tar.gz", hash = "sha256:1fd0c1d395372843fba13a51c28e3bb9d59bd7aebfeb17358ffaaa1e4dbbe948"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" [[package]] name = "pyflakes" version = "2.5.0" description = "passive checker of Python programs" -category = "dev" optional = false python-versions = ">=3.6" +files = [ + {file = "pyflakes-2.5.0-py2.py3-none-any.whl", hash = "sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2"}, + {file = "pyflakes-2.5.0.tar.gz", hash = "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3"}, +] [[package]] name = "pynacl" version = "1.5.0" description = "Python binding to the Networking and Cryptography (NaCl) library" -category = "main" optional = false python-versions = ">=3.6" +files = [ + {file = "PyNaCl-1.5.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1"}, + {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92"}, + {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a36d4a9dda1f19ce6e03c9a784a2921a4b726b02e1c736600ca9c22029474394"}, + {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0c84947a22519e013607c9be43706dd42513f9e6ae5d39d3613ca1e142fba44d"}, + {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06b8f6fa7f5de8d5d2f7573fe8c863c051225a27b61e6860fd047b1775807858"}, + {file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a422368fc821589c228f4c49438a368831cb5bbc0eab5ebe1d7fac9dded6567b"}, + {file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:61f642bf2378713e2c2e1de73444a3778e5f0a38be6fee0fe532fe30060282ff"}, + {file = "PyNaCl-1.5.0-cp36-abi3-win32.whl", hash = "sha256:e46dae94e34b085175f8abb3b0aaa7da40767865ac82c928eeb9e57e1ea8a543"}, + {file = "PyNaCl-1.5.0-cp36-abi3-win_amd64.whl", hash = "sha256:20f42270d27e1b6a29f54032090b972d97f0a1b0948cc52392041ef7831fee93"}, + {file = "PyNaCl-1.5.0.tar.gz", hash = "sha256:8ac7448f09ab85811607bdd21ec2464495ac8b7c66d146bf545b0f08fb9220ba"}, +] [package.dependencies] cffi = ">=1.4.1" [package.extras] -docs = ["sphinx (>=1.6.5)", "sphinx_rtd_theme"] +docs = ["sphinx (>=1.6.5)", "sphinx-rtd-theme"] tests = ["hypothesis (>=3.27.0)", "pytest (>=3.2.1,!=3.3.0)"] [[package]] name = "pyotp" version = "2.6.0" description = "Python One Time Password Library" -category = "main" optional = false python-versions = "*" - -[[package]] -name = "pyparsing" -version = "3.0.9" -description = "pyparsing module - Classes and methods to define and execute parsing grammars" -category = "main" -optional = false -python-versions = ">=3.6.8" - -[package.extras] -diagrams = ["jinja2", "railroad-diagrams"] +files = [ + {file = "pyotp-2.6.0-py2.py3-none-any.whl", hash = "sha256:9d144de0f8a601d6869abe1409f4a3f75f097c37b50a36a3bf165810a6e23f28"}, + {file = "pyotp-2.6.0.tar.gz", hash = "sha256:d28ddfd40e0c1b6a6b9da961c7d47a10261fb58f378cb00f05ce88b26df9c432"}, +] [[package]] name = "pytest" version = "7.1.2" description = "pytest: simple powerful testing with Python" -category = "main" optional = false python-versions = ">=3.7" +files = [ + {file = "pytest-7.1.2-py3-none-any.whl", hash = "sha256:13d0e3ccfc2b6e26be000cb6568c832ba67ba32e719443bfe725814d3c42433c"}, + {file = "pytest-7.1.2.tar.gz", hash = "sha256:a06a0425453864a270bc45e71f783330a7428defb4230fb5e6a731fde06ecd45"}, +] [package.dependencies] atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} attrs = ">=19.2.0" colorama = {version = "*", markers = "sys_platform == \"win32\""} -importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} iniconfig = "*" packaging = "*" pluggy = ">=0.12,<2.0" @@ -426,21 +846,79 @@ tomli = ">=1.0.0" [package.extras] testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[package.dependencies] +six = ">=1.5" + [[package]] name = "pyyaml" version = "6.0" description = "YAML parser and emitter for Python" -category = "main" optional = false python-versions = ">=3.6" +files = [ + {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, + {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, + {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, + {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, + {file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"}, + {file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"}, + {file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"}, + {file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"}, + {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, + {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, + {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, + {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, + {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, + {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, + {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, + {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, + {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, + {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, + {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, + {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, +] [[package]] name = "requests" version = "2.28.1" description = "Python HTTP for Humans." -category = "main" optional = false python-versions = ">=3.7, <4" +files = [ + {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"}, + {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"}, +] [package.dependencies] certifi = ">=2017.4.17" @@ -456,9 +934,12 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] name = "simple-websocket" version = "0.8.0" description = "Simple WebSocket server and client for Python" -category = "main" optional = false python-versions = ">=3.6" +files = [ + {file = "simple-websocket-0.8.0.tar.gz", hash = "sha256:b25387c08a6455f8fac087117ad40dd56b9d1df28e18bd1b0b0a374c49a4f993"}, + {file = "simple_websocket-0.8.0-py3-none-any.whl", hash = "sha256:fe812eb97f3748b6bdf89470f3cc60f4c6f92cc8935e43df8634a1aed56813a6"}, +] [package.dependencies] wsproto = "*" @@ -467,41 +948,45 @@ wsproto = "*" name = "six" version = "1.16.0" description = "Python 2 and 3 compatibility utilities" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] [[package]] name = "tomli" version = "2.0.1" description = "A lil' TOML parser" -category = "main" optional = false python-versions = ">=3.7" - -[[package]] -name = "typed-ast" -version = "1.5.4" -description = "a fork of Python 2 and 3 ast modules with type comment support" -category = "dev" -optional = false -python-versions = ">=3.6" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] [[package]] name = "typing-extensions" -version = "4.3.0" +version = "4.7.1" description = "Backported and Experimental Type Hints for Python 3.7+" -category = "main" optional = false python-versions = ">=3.7" +files = [ + {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"}, + {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"}, +] [[package]] name = "urllib3" version = "1.26.11" description = "HTTP library with thread-safe connection pooling, file post, and more." -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4" +files = [ + {file = "urllib3-1.26.11-py2.py3-none-any.whl", hash = "sha256:c33ccba33c819596124764c23a97d25f32b28433ba0dedeb77d873a38722c9bc"}, + {file = "urllib3-1.26.11.tar.gz", hash = "sha256:ea6e8fb210b19d950fab93b60c9009226c63a28808bc8386e05301e25883ac0a"}, +] [package.extras] brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] @@ -512,9 +997,12 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] name = "websocket-client" version = "1.3.3" description = "WebSocket client for Python with low level API options" -category = "main" optional = false python-versions = ">=3.7" +files = [ + {file = "websocket-client-1.3.3.tar.gz", hash = "sha256:d58c5f284d6a9bf8379dab423259fe8f85b70d5fa5d2916d5791a84594b122b1"}, + {file = "websocket_client-1.3.3-py3-none-any.whl", hash = "sha256:5d55652dc1d0b3c734f044337d929aaf83f4f9138816ec680c1aefefb4dc4877"}, +] [package.extras] docs = ["Sphinx (>=3.4)", "sphinx-rtd-theme (>=0.5)"] @@ -525,9 +1013,12 @@ test = ["websockets"] name = "werkzeug" version = "2.2.1" description = "The comprehensive WSGI web application library." -category = "main" optional = false python-versions = ">=3.7" +files = [ + {file = "Werkzeug-2.2.1-py3-none-any.whl", hash = "sha256:7e1db6a5ba6b9a8be061e47e900456355b8714c0f238b0313f53afce1a55a79a"}, + {file = "Werkzeug-2.2.1.tar.gz", hash = "sha256:4d7013ef96fd197d1cdeb03e066c6c5a491ccb44758a5b2b91137319383e5a5a"}, +] [package.dependencies] MarkupSafe = ">=2.1.1" @@ -539,469 +1030,17 @@ watchdog = ["watchdog"] name = "wsproto" version = "1.1.0" description = "WebSockets state-machine based protocol implementation" -category = "main" optional = false python-versions = ">=3.7.0" +files = [ + {file = "wsproto-1.1.0-py3-none-any.whl", hash = "sha256:2218cb57952d90b9fca325c0dcfb08c3bda93e8fd8070b0a17f048e2e47a521b"}, + {file = "wsproto-1.1.0.tar.gz", hash = "sha256:a2e56bfd5c7cd83c1369d83b5feccd6d37798b74872866e62616e0ecf111bda8"}, +] [package.dependencies] h11 = ">=0.9.0,<1" -[[package]] -name = "zipp" -version = "3.8.1" -description = "Backport of pathlib-compatible object wrapper for zip files" -category = "main" -optional = false -python-versions = ">=3.7" - -[package.extras] -docs = ["jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx"] -testing = ["func-timeout", "jaraco.itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] - [metadata] -lock-version = "1.1" -python-versions = "^3.7" -content-hash = "3a5e8c2efbb8a9dbd12ebfbcd4216085d17f66692f64e81f61f3feb4d3214680" - -[metadata.files] -atomicwrites = [ - {file = "atomicwrites-1.4.1.tar.gz", hash = "sha256:81b2c9071a49367a7f770170e5eec8cb66567cfbbc8c73d20ce5ca4a8d71cf11"}, -] -attrs = [ - {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, - {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, -] -bcrypt = [ - {file = "bcrypt-3.2.2-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:7180d98a96f00b1050e93f5b0f556e658605dd9f524d0b0e68ae7944673f525e"}, - {file = "bcrypt-3.2.2-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:61bae49580dce88095d669226d5076d0b9d927754cedbdf76c6c9f5099ad6f26"}, - {file = "bcrypt-3.2.2-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88273d806ab3a50d06bc6a2fc7c87d737dd669b76ad955f449c43095389bc8fb"}, - {file = "bcrypt-3.2.2-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:6d2cb9d969bfca5bc08e45864137276e4c3d3d7de2b162171def3d188bf9d34a"}, - {file = "bcrypt-3.2.2-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b02d6bfc6336d1094276f3f588aa1225a598e27f8e3388f4db9948cb707b521"}, - {file = "bcrypt-3.2.2-cp36-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a2c46100e315c3a5b90fdc53e429c006c5f962529bc27e1dfd656292c20ccc40"}, - {file = "bcrypt-3.2.2-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:7d9ba2e41e330d2af4af6b1b6ec9e6128e91343d0b4afb9282e54e5508f31baa"}, - {file = "bcrypt-3.2.2-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:cd43303d6b8a165c29ec6756afd169faba9396a9472cdff753fe9f19b96ce2fa"}, - {file = "bcrypt-3.2.2-cp36-abi3-win32.whl", hash = "sha256:4e029cef560967fb0cf4a802bcf4d562d3d6b4b1bf81de5ec1abbe0f1adb027e"}, - {file = "bcrypt-3.2.2-cp36-abi3-win_amd64.whl", hash = "sha256:7ff2069240c6bbe49109fe84ca80508773a904f5a8cb960e02a977f7f519b129"}, - {file = "bcrypt-3.2.2.tar.gz", hash = "sha256:433c410c2177057705da2a9f2cd01dd157493b2a7ac14c8593a16b3dab6b6bfb"}, -] -black = [ - {file = "black-22.10.0-1fixedarch-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:5cc42ca67989e9c3cf859e84c2bf014f6633db63d1cbdf8fdb666dcd9e77e3fa"}, - {file = "black-22.10.0-1fixedarch-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:5d8f74030e67087b219b032aa33a919fae8806d49c867846bfacde57f43972ef"}, - {file = "black-22.10.0-1fixedarch-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:197df8509263b0b8614e1df1756b1dd41be6738eed2ba9e9769f3880c2b9d7b6"}, - {file = "black-22.10.0-1fixedarch-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:2644b5d63633702bc2c5f3754b1b475378fbbfb481f62319388235d0cd104c2d"}, - {file = "black-22.10.0-1fixedarch-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:e41a86c6c650bcecc6633ee3180d80a025db041a8e2398dcc059b3afa8382cd4"}, - {file = "black-22.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2039230db3c6c639bd84efe3292ec7b06e9214a2992cd9beb293d639c6402edb"}, - {file = "black-22.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14ff67aec0a47c424bc99b71005202045dc09270da44a27848d534600ac64fc7"}, - {file = "black-22.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:819dc789f4498ecc91438a7de64427c73b45035e2e3680c92e18795a839ebb66"}, - {file = "black-22.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5b9b29da4f564ba8787c119f37d174f2b69cdfdf9015b7d8c5c16121ddc054ae"}, - {file = "black-22.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8b49776299fece66bffaafe357d929ca9451450f5466e997a7285ab0fe28e3b"}, - {file = "black-22.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:21199526696b8f09c3997e2b4db8d0b108d801a348414264d2eb8eb2532e540d"}, - {file = "black-22.10.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e464456d24e23d11fced2bc8c47ef66d471f845c7b7a42f3bd77bf3d1789650"}, - {file = "black-22.10.0-cp37-cp37m-win_amd64.whl", hash = "sha256:9311e99228ae10023300ecac05be5a296f60d2fd10fff31cf5c1fa4ca4b1988d"}, - {file = "black-22.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:fba8a281e570adafb79f7755ac8721b6cf1bbf691186a287e990c7929c7692ff"}, - {file = "black-22.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:915ace4ff03fdfff953962fa672d44be269deb2eaf88499a0f8805221bc68c87"}, - {file = "black-22.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:444ebfb4e441254e87bad00c661fe32df9969b2bf224373a448d8aca2132b395"}, - {file = "black-22.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:974308c58d057a651d182208a484ce80a26dac0caef2895836a92dd6ebd725e0"}, - {file = "black-22.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72ef3925f30e12a184889aac03d77d031056860ccae8a1e519f6cbb742736383"}, - {file = "black-22.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:432247333090c8c5366e69627ccb363bc58514ae3e63f7fc75c54b1ea80fa7de"}, - {file = "black-22.10.0-py3-none-any.whl", hash = "sha256:c957b2b4ea88587b46cf49d1dc17681c1e672864fd7af32fc1e9664d572b3458"}, - {file = "black-22.10.0.tar.gz", hash = "sha256:f513588da599943e0cde4e32cc9879e825d58720d6557062d1098c5ad80080e1"}, -] -certifi = [ - {file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"}, - {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"}, -] -cffi = [ - {file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"}, - {file = "cffi-1.15.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2"}, - {file = "cffi-1.15.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914"}, - {file = "cffi-1.15.1-cp27-cp27m-win32.whl", hash = "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3"}, - {file = "cffi-1.15.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e"}, - {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162"}, - {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b"}, - {file = "cffi-1.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21"}, - {file = "cffi-1.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4"}, - {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01"}, - {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e"}, - {file = "cffi-1.15.1-cp310-cp310-win32.whl", hash = "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2"}, - {file = "cffi-1.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d"}, - {file = "cffi-1.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac"}, - {file = "cffi-1.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c"}, - {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef"}, - {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8"}, - {file = "cffi-1.15.1-cp311-cp311-win32.whl", hash = "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d"}, - {file = "cffi-1.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104"}, - {file = "cffi-1.15.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e"}, - {file = "cffi-1.15.1-cp36-cp36m-win32.whl", hash = "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf"}, - {file = "cffi-1.15.1-cp36-cp36m-win_amd64.whl", hash = "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497"}, - {file = "cffi-1.15.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426"}, - {file = "cffi-1.15.1-cp37-cp37m-win32.whl", hash = "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9"}, - {file = "cffi-1.15.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045"}, - {file = "cffi-1.15.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192"}, - {file = "cffi-1.15.1-cp38-cp38-win32.whl", hash = "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314"}, - {file = "cffi-1.15.1-cp38-cp38-win_amd64.whl", hash = "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5"}, - {file = "cffi-1.15.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585"}, - {file = "cffi-1.15.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27"}, - {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76"}, - {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3"}, - {file = "cffi-1.15.1-cp39-cp39-win32.whl", hash = "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee"}, - {file = "cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c"}, - {file = "cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9"}, -] -charset-normalizer = [ - {file = "charset-normalizer-2.1.0.tar.gz", hash = "sha256:575e708016ff3a5e3681541cb9d79312c416835686d054a23accb873b254f413"}, - {file = "charset_normalizer-2.1.0-py3-none-any.whl", hash = "sha256:5189b6f22b01957427f35b6a08d9a0bc45b46d3788ef5a92e978433c7a35f8a5"}, -] -click = [ - {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, - {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, -] -colorama = [ - {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"}, - {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, -] -cryptography = [ - {file = "cryptography-38.0.3-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:984fe150f350a3c91e84de405fe49e688aa6092b3525f407a18b9646f6612320"}, - {file = "cryptography-38.0.3-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:ed7b00096790213e09eb11c97cc6e2b757f15f3d2f85833cd2d3ec3fe37c1722"}, - {file = "cryptography-38.0.3-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:bbf203f1a814007ce24bd4d51362991d5cb90ba0c177a9c08825f2cc304d871f"}, - {file = "cryptography-38.0.3-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:554bec92ee7d1e9d10ded2f7e92a5d70c1f74ba9524947c0ba0c850c7b011828"}, - {file = "cryptography-38.0.3-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1b52c9e5f8aa2b802d48bd693190341fae201ea51c7a167d69fc48b60e8a959"}, - {file = "cryptography-38.0.3-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:728f2694fa743a996d7784a6194da430f197d5c58e2f4e278612b359f455e4a2"}, - {file = "cryptography-38.0.3-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:dfb4f4dd568de1b6af9f4cda334adf7d72cf5bc052516e1b2608b683375dd95c"}, - {file = "cryptography-38.0.3-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:5419a127426084933076132d317911e3c6eb77568a1ce23c3ac1e12d111e61e0"}, - {file = "cryptography-38.0.3-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:9b24bcff7853ed18a63cfb0c2b008936a9554af24af2fb146e16d8e1aed75748"}, - {file = "cryptography-38.0.3-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:25c1d1f19729fb09d42e06b4bf9895212292cb27bb50229f5aa64d039ab29146"}, - {file = "cryptography-38.0.3-cp36-abi3-win32.whl", hash = "sha256:7f836217000342d448e1c9a342e9163149e45d5b5eca76a30e84503a5a96cab0"}, - {file = "cryptography-38.0.3-cp36-abi3-win_amd64.whl", hash = "sha256:c46837ea467ed1efea562bbeb543994c2d1f6e800785bd5a2c98bc096f5cb220"}, - {file = "cryptography-38.0.3-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06fc3cc7b6f6cca87bd56ec80a580c88f1da5306f505876a71c8cfa7050257dd"}, - {file = "cryptography-38.0.3-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:65535bc550b70bd6271984d9863a37741352b4aad6fb1b3344a54e6950249b55"}, - {file = "cryptography-38.0.3-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:5e89468fbd2fcd733b5899333bc54d0d06c80e04cd23d8c6f3e0542358c6060b"}, - {file = "cryptography-38.0.3-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:6ab9516b85bebe7aa83f309bacc5f44a61eeb90d0b4ec125d2d003ce41932d36"}, - {file = "cryptography-38.0.3-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:068147f32fa662c81aebab95c74679b401b12b57494872886eb5c1139250ec5d"}, - {file = "cryptography-38.0.3-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:402852a0aea73833d982cabb6d0c3bb582c15483d29fb7085ef2c42bfa7e38d7"}, - {file = "cryptography-38.0.3-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b1b35d9d3a65542ed2e9d90115dfd16bbc027b3f07ee3304fc83580f26e43249"}, - {file = "cryptography-38.0.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:6addc3b6d593cd980989261dc1cce38263c76954d758c3c94de51f1e010c9a50"}, - {file = "cryptography-38.0.3-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:be243c7e2bfcf6cc4cb350c0d5cdf15ca6383bbcb2a8ef51d3c9411a9d4386f0"}, - {file = "cryptography-38.0.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78cf5eefac2b52c10398a42765bfa981ce2372cbc0457e6bf9658f41ec3c41d8"}, - {file = "cryptography-38.0.3-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:4e269dcd9b102c5a3d72be3c45d8ce20377b8076a43cbed6f660a1afe365e436"}, - {file = "cryptography-38.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:8d41a46251bf0634e21fac50ffd643216ccecfaf3701a063257fe0b2be1b6548"}, - {file = "cryptography-38.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:785e4056b5a8b28f05a533fab69febf5004458e20dad7e2e13a3120d8ecec75a"}, - {file = "cryptography-38.0.3.tar.gz", hash = "sha256:bfbe6ee19615b07a98b1d2287d6a6073f734735b49ee45b11324d85efc4d5cbd"}, -] -flake8 = [ - {file = "flake8-5.0.2-py2.py3-none-any.whl", hash = "sha256:a7926e0b6d23c0991245b60279e774d2596dfecd9b158525d1f8c050a61eae5a"}, - {file = "flake8-5.0.2.tar.gz", hash = "sha256:9cc32bc0c5d16eacc014c7ec6f0e9565fd81df66c2092c3c9df06e3c1ac95e5d"}, -] -flask = [ - {file = "Flask-2.2.1-py3-none-any.whl", hash = "sha256:3c604c48c3d5b4c63e72134044c0b4fe90ff01ef65280b9fe2d38c8860d99fe5"}, - {file = "Flask-2.2.1.tar.gz", hash = "sha256:9c2b81b9b1edcc835af72d600f1955e713a065e7cb41d7e51ee762b449d9c65d"}, -] -flask-sock = [ - {file = "flask-sock-0.5.2.tar.gz", hash = "sha256:c36e92813e897a325a48caee640509f88c465b8df642e40126c1b25fc38a2c30"}, - {file = "flask_sock-0.5.2-py3-none-any.whl", hash = "sha256:bdd60520d031eb92e6fa2dbd3deffbb6e71d3662e9d39bfe53d2eccf971d0fd0"}, -] -h11 = [ - {file = "h11-0.13.0-py3-none-any.whl", hash = "sha256:8ddd78563b633ca55346c8cd41ec0af27d3c79931828beffb46ce70a379e7442"}, - {file = "h11-0.13.0.tar.gz", hash = "sha256:70813c1135087a248a4d38cc0e1a0181ffab2188141a93eaf567940c3957ff06"}, -] -idna = [ - {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, - {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, -] -importlib-metadata = [ - {file = "importlib_metadata-4.2.0-py3-none-any.whl", hash = "sha256:057e92c15bc8d9e8109738a48db0ccb31b4d9d5cfbee5a8670879a30be66304b"}, - {file = "importlib_metadata-4.2.0.tar.gz", hash = "sha256:b7e52a1f8dec14a75ea73e0891f3060099ca1d8e6a462a4dff11c3e119ea1b31"}, -] -iniconfig = [ - {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, - {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, -] -itsdangerous = [ - {file = "itsdangerous-2.1.2-py3-none-any.whl", hash = "sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44"}, - {file = "itsdangerous-2.1.2.tar.gz", hash = "sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a"}, -] -jinja2 = [ - {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, - {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, -] -markupsafe = [ - {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-win32.whl", hash = "sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-win32.whl", hash = "sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-win32.whl", hash = "sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-win32.whl", hash = "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247"}, - {file = "MarkupSafe-2.1.1.tar.gz", hash = "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b"}, -] -mccabe = [ - {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, - {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, -] -mypy-extensions = [ - {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, - {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, -] -packaging = [ - {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, - {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, -] -paramiko = [ - {file = "paramiko-2.11.0-py2.py3-none-any.whl", hash = "sha256:655f25dc8baf763277b933dfcea101d636581df8d6b9774d1fb653426b72c270"}, - {file = "paramiko-2.11.0.tar.gz", hash = "sha256:003e6bee7c034c21fbb051bf83dc0a9ee4106204dd3c53054c71452cc4ec3938"}, -] -pathspec = [ - {file = "pathspec-0.10.1-py3-none-any.whl", hash = "sha256:46846318467efc4556ccfd27816e004270a9eeeeb4d062ce5e6fc7a87c573f93"}, - {file = "pathspec-0.10.1.tar.gz", hash = "sha256:7ace6161b621d31e7902eb6b5ae148d12cfd23f4a249b9ffb6b9fee12084323d"}, -] -platformdirs = [ - {file = "platformdirs-2.5.3-py3-none-any.whl", hash = "sha256:0cb405749187a194f444c25c82ef7225232f11564721eabffc6ec70df83b11cb"}, - {file = "platformdirs-2.5.3.tar.gz", hash = "sha256:6e52c21afff35cb659c6e52d8b4d61b9bd544557180440538f255d9382c8cbe0"}, -] -pluggy = [ - {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, - {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, -] -psutil = [ - {file = "psutil-5.9.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:799759d809c31aab5fe4579e50addf84565e71c1dc9f1c31258f159ff70d3f87"}, - {file = "psutil-5.9.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:9272167b5f5fbfe16945be3db475b3ce8d792386907e673a209da686176552af"}, - {file = "psutil-5.9.1-cp27-cp27m-win32.whl", hash = "sha256:0904727e0b0a038830b019551cf3204dd48ef5c6868adc776e06e93d615fc5fc"}, - {file = "psutil-5.9.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e7e10454cb1ab62cc6ce776e1c135a64045a11ec4c6d254d3f7689c16eb3efd2"}, - {file = "psutil-5.9.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:56960b9e8edcca1456f8c86a196f0c3d8e3e361320071c93378d41445ffd28b0"}, - {file = "psutil-5.9.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:44d1826150d49ffd62035785a9e2c56afcea66e55b43b8b630d7706276e87f22"}, - {file = "psutil-5.9.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c7be9d7f5b0d206f0bbc3794b8e16fb7dbc53ec9e40bbe8787c6f2d38efcf6c9"}, - {file = "psutil-5.9.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd9246e4cdd5b554a2ddd97c157e292ac11ef3e7af25ac56b08b455c829dca8"}, - {file = "psutil-5.9.1-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:29a442e25fab1f4d05e2655bb1b8ab6887981838d22effa2396d584b740194de"}, - {file = "psutil-5.9.1-cp310-cp310-win32.whl", hash = "sha256:20b27771b077dcaa0de1de3ad52d22538fe101f9946d6dc7869e6f694f079329"}, - {file = "psutil-5.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:58678bbadae12e0db55186dc58f2888839228ac9f41cc7848853539b70490021"}, - {file = "psutil-5.9.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:3a76ad658641172d9c6e593de6fe248ddde825b5866464c3b2ee26c35da9d237"}, - {file = "psutil-5.9.1-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a6a11e48cb93a5fa606306493f439b4aa7c56cb03fc9ace7f6bfa21aaf07c453"}, - {file = "psutil-5.9.1-cp36-cp36m-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:068935df39055bf27a29824b95c801c7a5130f118b806eee663cad28dca97685"}, - {file = "psutil-5.9.1-cp36-cp36m-win32.whl", hash = "sha256:0f15a19a05f39a09327345bc279c1ba4a8cfb0172cc0d3c7f7d16c813b2e7d36"}, - {file = "psutil-5.9.1-cp36-cp36m-win_amd64.whl", hash = "sha256:db417f0865f90bdc07fa30e1aadc69b6f4cad7f86324b02aa842034efe8d8c4d"}, - {file = "psutil-5.9.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:91c7ff2a40c373d0cc9121d54bc5f31c4fa09c346528e6a08d1845bce5771ffc"}, - {file = "psutil-5.9.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fea896b54f3a4ae6f790ac1d017101252c93f6fe075d0e7571543510f11d2676"}, - {file = "psutil-5.9.1-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3054e923204b8e9c23a55b23b6df73a8089ae1d075cb0bf711d3e9da1724ded4"}, - {file = "psutil-5.9.1-cp37-cp37m-win32.whl", hash = "sha256:d2d006286fbcb60f0b391741f520862e9b69f4019b4d738a2a45728c7e952f1b"}, - {file = "psutil-5.9.1-cp37-cp37m-win_amd64.whl", hash = "sha256:b14ee12da9338f5e5b3a3ef7ca58b3cba30f5b66f7662159762932e6d0b8f680"}, - {file = "psutil-5.9.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:19f36c16012ba9cfc742604df189f2f28d2720e23ff7d1e81602dbe066be9fd1"}, - {file = "psutil-5.9.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:944c4b4b82dc4a1b805329c980f270f170fdc9945464223f2ec8e57563139cf4"}, - {file = "psutil-5.9.1-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b6750a73a9c4a4e689490ccb862d53c7b976a2a35c4e1846d049dcc3f17d83b"}, - {file = "psutil-5.9.1-cp38-cp38-win32.whl", hash = "sha256:a8746bfe4e8f659528c5c7e9af5090c5a7d252f32b2e859c584ef7d8efb1e689"}, - {file = "psutil-5.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:79c9108d9aa7fa6fba6e668b61b82facc067a6b81517cab34d07a84aa89f3df0"}, - {file = "psutil-5.9.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:28976df6c64ddd6320d281128817f32c29b539a52bdae5e192537bc338a9ec81"}, - {file = "psutil-5.9.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b88f75005586131276634027f4219d06e0561292be8bd6bc7f2f00bdabd63c4e"}, - {file = "psutil-5.9.1-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:645bd4f7bb5b8633803e0b6746ff1628724668681a434482546887d22c7a9537"}, - {file = "psutil-5.9.1-cp39-cp39-win32.whl", hash = "sha256:32c52611756096ae91f5d1499fe6c53b86f4a9ada147ee42db4991ba1520e574"}, - {file = "psutil-5.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:f65f9a46d984b8cd9b3750c2bdb419b2996895b005aefa6cbaba9a143b1ce2c5"}, - {file = "psutil-5.9.1.tar.gz", hash = "sha256:57f1819b5d9e95cdfb0c881a8a5b7d542ed0b7c522d575706a80bedc848c8954"}, -] -py = [ - {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, - {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, -] -pycodestyle = [ - {file = "pycodestyle-2.9.0-py2.py3-none-any.whl", hash = "sha256:289cdc0969d589d90752582bef6dff57c5fbc6949ee8b013ad6d6449a8ae9437"}, - {file = "pycodestyle-2.9.0.tar.gz", hash = "sha256:beaba44501f89d785be791c9462553f06958a221d166c64e1f107320f839acc2"}, -] -pycparser = [ - {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, - {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, -] -pyflakes = [ - {file = "pyflakes-2.5.0-py2.py3-none-any.whl", hash = "sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2"}, - {file = "pyflakes-2.5.0.tar.gz", hash = "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3"}, -] -pynacl = [ - {file = "PyNaCl-1.5.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1"}, - {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92"}, - {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a36d4a9dda1f19ce6e03c9a784a2921a4b726b02e1c736600ca9c22029474394"}, - {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0c84947a22519e013607c9be43706dd42513f9e6ae5d39d3613ca1e142fba44d"}, - {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06b8f6fa7f5de8d5d2f7573fe8c863c051225a27b61e6860fd047b1775807858"}, - {file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a422368fc821589c228f4c49438a368831cb5bbc0eab5ebe1d7fac9dded6567b"}, - {file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:61f642bf2378713e2c2e1de73444a3778e5f0a38be6fee0fe532fe30060282ff"}, - {file = "PyNaCl-1.5.0-cp36-abi3-win32.whl", hash = "sha256:e46dae94e34b085175f8abb3b0aaa7da40767865ac82c928eeb9e57e1ea8a543"}, - {file = "PyNaCl-1.5.0-cp36-abi3-win_amd64.whl", hash = "sha256:20f42270d27e1b6a29f54032090b972d97f0a1b0948cc52392041ef7831fee93"}, - {file = "PyNaCl-1.5.0.tar.gz", hash = "sha256:8ac7448f09ab85811607bdd21ec2464495ac8b7c66d146bf545b0f08fb9220ba"}, -] -pyotp = [ - {file = "pyotp-2.6.0-py2.py3-none-any.whl", hash = "sha256:9d144de0f8a601d6869abe1409f4a3f75f097c37b50a36a3bf165810a6e23f28"}, - {file = "pyotp-2.6.0.tar.gz", hash = "sha256:d28ddfd40e0c1b6a6b9da961c7d47a10261fb58f378cb00f05ce88b26df9c432"}, -] -pyparsing = [ - {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, - {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, -] -pytest = [ - {file = "pytest-7.1.2-py3-none-any.whl", hash = "sha256:13d0e3ccfc2b6e26be000cb6568c832ba67ba32e719443bfe725814d3c42433c"}, - {file = "pytest-7.1.2.tar.gz", hash = "sha256:a06a0425453864a270bc45e71f783330a7428defb4230fb5e6a731fde06ecd45"}, -] -pyyaml = [ - {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, - {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, - {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, - {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, - {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, - {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, - {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, - {file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"}, - {file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"}, - {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"}, - {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"}, - {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"}, - {file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"}, - {file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"}, - {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, - {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, - {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, - {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, - {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, - {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, - {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, - {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, - {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, - {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, - {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, - {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, - {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, - {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, - {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, - {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, - {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, - {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, - {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, - {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, - {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, - {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, - {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, - {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, - {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, - {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, -] -requests = [ - {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"}, - {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"}, -] -simple-websocket = [ - {file = "simple-websocket-0.8.0.tar.gz", hash = "sha256:b25387c08a6455f8fac087117ad40dd56b9d1df28e18bd1b0b0a374c49a4f993"}, - {file = "simple_websocket-0.8.0-py3-none-any.whl", hash = "sha256:fe812eb97f3748b6bdf89470f3cc60f4c6f92cc8935e43df8634a1aed56813a6"}, -] -six = [ - {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, - {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, -] -tomli = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, -] -typed-ast = [ - {file = "typed_ast-1.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4"}, - {file = "typed_ast-1.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:211260621ab1cd7324e0798d6be953d00b74e0428382991adfddb352252f1d62"}, - {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:267e3f78697a6c00c689c03db4876dd1efdfea2f251a5ad6555e82a26847b4ac"}, - {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c542eeda69212fa10a7ada75e668876fdec5f856cd3d06829e6aa64ad17c8dfe"}, - {file = "typed_ast-1.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:a9916d2bb8865f973824fb47436fa45e1ebf2efd920f2b9f99342cb7fab93f72"}, - {file = "typed_ast-1.5.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:79b1e0869db7c830ba6a981d58711c88b6677506e648496b1f64ac7d15633aec"}, - {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a94d55d142c9265f4ea46fab70977a1944ecae359ae867397757d836ea5a3f47"}, - {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:183afdf0ec5b1b211724dfef3d2cad2d767cbefac291f24d69b00546c1837fb6"}, - {file = "typed_ast-1.5.4-cp36-cp36m-win_amd64.whl", hash = "sha256:639c5f0b21776605dd6c9dbe592d5228f021404dafd377e2b7ac046b0349b1a1"}, - {file = "typed_ast-1.5.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf4afcfac006ece570e32d6fa90ab74a17245b83dfd6655a6f68568098345ff6"}, - {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed855bbe3eb3715fca349c80174cfcfd699c2f9de574d40527b8429acae23a66"}, - {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6778e1b2f81dfc7bc58e4b259363b83d2e509a65198e85d5700dfae4c6c8ff1c"}, - {file = "typed_ast-1.5.4-cp37-cp37m-win_amd64.whl", hash = "sha256:0261195c2062caf107831e92a76764c81227dae162c4f75192c0d489faf751a2"}, - {file = "typed_ast-1.5.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2efae9db7a8c05ad5547d522e7dbe62c83d838d3906a3716d1478b6c1d61388d"}, - {file = "typed_ast-1.5.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7d5d014b7daa8b0bf2eaef684295acae12b036d79f54178b92a2b6a56f92278f"}, - {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:370788a63915e82fd6f212865a596a0fefcbb7d408bbbb13dea723d971ed8bdc"}, - {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4e964b4ff86550a7a7d56345c7864b18f403f5bd7380edf44a3c1fb4ee7ac6c6"}, - {file = "typed_ast-1.5.4-cp38-cp38-win_amd64.whl", hash = "sha256:683407d92dc953c8a7347119596f0b0e6c55eb98ebebd9b23437501b28dcbb8e"}, - {file = "typed_ast-1.5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4879da6c9b73443f97e731b617184a596ac1235fe91f98d279a7af36c796da35"}, - {file = "typed_ast-1.5.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3e123d878ba170397916557d31c8f589951e353cc95fb7f24f6bb69adc1a8a97"}, - {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebd9d7f80ccf7a82ac5f88c521115cc55d84e35bf8b446fcd7836eb6b98929a3"}, - {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98f80dee3c03455e92796b58b98ff6ca0b2a6f652120c263efdba4d6c5e58f72"}, - {file = "typed_ast-1.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:0fdbcf2fef0ca421a3f5912555804296f0b0960f0418c440f5d6d3abb549f3e1"}, - {file = "typed_ast-1.5.4.tar.gz", hash = "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2"}, -] -typing-extensions = [ - {file = "typing_extensions-4.3.0-py3-none-any.whl", hash = "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02"}, - {file = "typing_extensions-4.3.0.tar.gz", hash = "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6"}, -] -urllib3 = [ - {file = "urllib3-1.26.11-py2.py3-none-any.whl", hash = "sha256:c33ccba33c819596124764c23a97d25f32b28433ba0dedeb77d873a38722c9bc"}, - {file = "urllib3-1.26.11.tar.gz", hash = "sha256:ea6e8fb210b19d950fab93b60c9009226c63a28808bc8386e05301e25883ac0a"}, -] -websocket-client = [ - {file = "websocket-client-1.3.3.tar.gz", hash = "sha256:d58c5f284d6a9bf8379dab423259fe8f85b70d5fa5d2916d5791a84594b122b1"}, - {file = "websocket_client-1.3.3-py3-none-any.whl", hash = "sha256:5d55652dc1d0b3c734f044337d929aaf83f4f9138816ec680c1aefefb4dc4877"}, -] -werkzeug = [ - {file = "Werkzeug-2.2.1-py3-none-any.whl", hash = "sha256:7e1db6a5ba6b9a8be061e47e900456355b8714c0f238b0313f53afce1a55a79a"}, - {file = "Werkzeug-2.2.1.tar.gz", hash = "sha256:4d7013ef96fd197d1cdeb03e066c6c5a491ccb44758a5b2b91137319383e5a5a"}, -] -wsproto = [ - {file = "wsproto-1.1.0-py3-none-any.whl", hash = "sha256:2218cb57952d90b9fca325c0dcfb08c3bda93e8fd8070b0a17f048e2e47a521b"}, - {file = "wsproto-1.1.0.tar.gz", hash = "sha256:a2e56bfd5c7cd83c1369d83b5feccd6d37798b74872866e62616e0ecf111bda8"}, -] -zipp = [ - {file = "zipp-3.8.1-py3-none-any.whl", hash = "sha256:47c40d7fe183a6f21403a199b3e4192cca5774656965b0a4988ad2f8feb5f009"}, - {file = "zipp-3.8.1.tar.gz", hash = "sha256:05b45f1ee8f807d0cc928485ca40a07cb491cf092ff587c0df9cb1fd154848d2"}, -] +lock-version = "2.0" +python-versions = "^3.10" +content-hash = "2b8fb0021ceeefb5f8d4584dde993ba67d6bc4f5def65fc5ef5835a7255b213f" diff --git a/tests/pyproject.toml b/tests/pyproject.toml index 9cda864ce..f644d9e0a 100644 --- a/tests/pyproject.toml +++ b/tests/pyproject.toml @@ -5,7 +5,7 @@ description = "" authors = ["Your Name "] [tool.poetry.dependencies] -python = "^3.7" +python = "^3.10" pytest = "^7.1.2" psutil = "^5.9.1" pyotp = "^2.6.0" @@ -15,11 +15,12 @@ requests = "^2.28.1" flask-sock = "^0.5.2" websocket-client = "^1.3.3" PyYAML = "^6.0" +openapi-client = { path = "./api_sdk", develop = true } [tool.poetry.dev-dependencies] flake8 = "^5.0.2" pytest = "^7.1.2" -black = "^22.10.0" +black = "^24" [tool.pytest.ini_options] minversion = "6.0" diff --git a/tests/run.sh b/tests/run.sh index b925565c3..d42957565 100755 --- a/tests/run.sh +++ b/tests/run.sh @@ -5,5 +5,5 @@ rm target/llvm-cov-target/* || true cargo llvm-cov clean --workspace cargo llvm-cov --no-cfg-coverage-nightly --no-report --workspace --all-features -- --skip agent cd tests -RUST_BACKTRACE=1 poetry run pytest $@ -cargo llvm-cov --no-run --hide-instantiations --html +RUST_BACKTRACE=1 ENABLE_COVERAGE=1 poetry run pytest $@ +cargo llvm-cov report --html diff --git a/tests/test_http_basic.py b/tests/test_http_basic.py index 578b847ce..9196fe5b7 100644 --- a/tests/test_http_basic.py +++ b/tests/test_http_basic.py @@ -4,14 +4,7 @@ from tests.conftest import WarpgateProcess -from .api_client import ( - api_admin_session, - api_create_target, - api_create_user, - api_create_role, - api_add_role_to_user, - api_add_role_to_target, -) +from .api_client import admin_client, sdk from .test_http_common import * # noqa @@ -23,51 +16,42 @@ def test_basic( ): url = f"https://localhost:{shared_wg.http_port}" - with api_admin_session(url) as session: - role = api_create_role(url, session, {"name": f"role-{uuid4()}"}) - user = api_create_user( - url, - session, - { - "username": "user", - "credentials": [ - { - "kind": "Password", - "hash": "123", - } - ], - }, + with admin_client(url) as api: + role = api.create_role(sdk.RoleDataRequest(name=f"role-{uuid4()}")) + user = api.create_user(sdk.CreateUserRequest(username="user")) + api.create_password_credential( + user.id, sdk.NewPasswordCredential(password="123") ) - api_add_role_to_user(url, session, user["id"], role["id"]) - echo_target = api_create_target( - url, - session, - { - "name": f"echo-{uuid4()}", - "options": { - "kind": "Http", - "url": f"http://localhost:{echo_server_port}", - "tls": { - "mode": "Disabled", - "verify": False, - }, - }, - }, + api.add_user_role(user.id, role.id) + target = api.create_target( + sdk.TargetDataRequest( + name=f"echo-{uuid4()}", + options=sdk.TargetOptions( + sdk.TargetOptionsTargetHTTPOptions( + kind="Http", + url=f"http://localhost:{echo_server_port}", + tls=sdk.Tls( + mode=sdk.TlsMode.DISABLED, + verify=False, + ), + ) + ), + ) ) - api_add_role_to_target(url, session, echo_target["id"], role["id"]) + api.add_target_role(target.id, role.id) session = requests.Session() session.verify = False response = session.get( - f"{url}/?warpgate-target={echo_target['name']}", allow_redirects=False + f"{url}/?warpgate-target={target.name}", allow_redirects=False ) assert response.status_code == 307 redirect = response.headers["location"] print(unquote(redirect)) assert ( unquote(redirect) - == f"/@warpgate#/login?next=/?warpgate-target={echo_target['name']}" + == f"/@warpgate#/login?next=/?warpgate-target={target.name}" ) response = session.get(f"{url}/@warpgate/api/info").json() @@ -76,17 +60,17 @@ def test_basic( response = session.post( f"{url}/@warpgate/api/auth/login", json={ - "username": user["username"], + "username": user.username, "password": "123", }, ) assert response.status_code == 201 response = session.get(f"{url}/@warpgate/api/info").json() - assert response["username"] == user["username"] + assert response["username"] == user.username response = session.get( - f"{url}/some/path?a=b&warpgate-target={echo_target['name']}&c=d", + f"{url}/some/path?a=b&warpgate-target={target.name}&c=d", allow_redirects=False, ) assert response.status_code == 200 diff --git a/tests/test_http_conntest.py b/tests/test_http_conntest.py index fdf3f349e..069494e58 100644 --- a/tests/test_http_conntest.py +++ b/tests/test_http_conntest.py @@ -1,6 +1,6 @@ from uuid import uuid4 -from .api_client import api_admin_session, api_create_target +from .api_client import admin_client, sdk from .conftest import ProcessManager, WarpgateProcess @@ -13,26 +13,22 @@ def test_success( shared_wg: WarpgateProcess, ): url = f"https://localhost:{shared_wg.http_port}" - with api_admin_session(url) as session: - echo_target = api_create_target( - url, - session, - { - "name": f"echo-{uuid4()}", - "options": { - "kind": "Http", - "url": f"http://localhost:{echo_server_port}", - "tls": { - "mode": "Disabled", - "verify": False, - }, - }, - }, - ) + with admin_client(url) as api: + echo_target = api.create_target(sdk.TargetDataRequest( + name=f"echo-{uuid4()}", + options=sdk.TargetOptions(sdk.TargetOptionsTargetHTTPOptions( + kind="Http", + url=f"http://localhost:{echo_server_port}", + tls=sdk.Tls( + mode=sdk.TlsMode.DISABLED, + verify=False, + ), + )), + )) proc = processes.start_wg( share_with=shared_wg, - args=["test-target", echo_target["name"]], + args=["test-target", echo_target.name], ).process proc.wait(timeout=timeout) assert proc.returncode == 0 @@ -41,26 +37,22 @@ def test_fail_no_connection( self, processes: ProcessManager, timeout, shared_wg: WarpgateProcess ): url = f"https://localhost:{shared_wg.http_port}" - with api_admin_session(url) as session: - echo_target = api_create_target( - url, - session, - { - "name": f"echo-{uuid4()}", - "options": { - "kind": "Http", - "url": "http://localhostbaddomain", - "tls": { - "mode": "Disabled", - "verify": False, - }, - }, - }, - ) + with admin_client(url) as api: + echo_target = api.create_target(sdk.TargetDataRequest( + name=f"echo-{uuid4()}", + options=sdk.TargetOptions(sdk.TargetOptionsTargetHTTPOptions( + kind="Http", + url="http://localhostbaddomain", + tls=sdk.Tls( + mode=sdk.TlsMode.DISABLED, + verify=False, + ), + )), + )) proc = processes.start_wg( share_with=shared_wg, - args=["test-target", echo_target["name"]], + args=["test-target", echo_target.name], ).process proc.wait(timeout=timeout) assert proc.returncode != 0 diff --git a/tests/test_http_cookies.py b/tests/test_http_cookies.py index a98f1ca7d..b7e45d17d 100644 --- a/tests/test_http_cookies.py +++ b/tests/test_http_cookies.py @@ -1,14 +1,7 @@ import requests from uuid import uuid4 -from .api_client import ( - api_admin_session, - api_create_target, - api_create_user, - api_create_role, - api_add_role_to_user, - api_add_role_to_target, -) +from .api_client import admin_client, sdk from .conftest import WarpgateProcess from .test_http_common import * # noqa @@ -21,38 +14,25 @@ def test( ): url = f"https://localhost:{shared_wg.http_port}" - with api_admin_session(url) as session: - role = api_create_role(url, session, {"name": f"role-{uuid4()}"}) - user = api_create_user( - url, - session, - { - "username": f"user-{uuid4()}", - "credentials": [ - { - "kind": "Password", - "hash": "123", - } - ], - }, + with admin_client(url) as api: + role = api.create_role(sdk.RoleDataRequest(name=f"role-{uuid4()}")) + user = api.create_user(sdk.CreateUserRequest(username=f"user-{uuid4()}")) + api.create_password_credential( + user.id, sdk.NewPasswordCredential(password="123") ) - api_add_role_to_user(url, session, user["id"], role["id"]) - echo_target = api_create_target( - url, - session, - { - "name": f"echo-{uuid4()}", - "options": { - "kind": "Http", - "url": f"http://localhost:{echo_server_port}", - "tls": { - "mode": "Disabled", - "verify": False, - }, - }, - }, - ) - api_add_role_to_target(url, session, echo_target["id"], role["id"]) + api.add_user_role(user.id, role.id) + echo_target = api.create_target(sdk.TargetDataRequest( + name=f"echo-{uuid4()}", + options=sdk.TargetOptions(sdk.TargetOptionsTargetHTTPOptions( + kind="Http", + url=f"http://localhost:{echo_server_port}", + tls=sdk.Tls( + mode=sdk.TlsMode.DISABLED, + verify=False, + ), + )), + )) + api.add_target_role(echo_target.id, role.id) session = requests.Session() session.verify = False @@ -61,14 +41,14 @@ def test( session.post( f"{url}/@warpgate/api/auth/login", json={ - "username": user["username"], + "username": user.username, "password": "123", }, headers=headers, ) session.get( - f"{url}/set-cookie?warpgate-target={echo_target['name']}", headers=headers + f"{url}/set-cookie?warpgate-target={echo_target.name}", headers=headers ) cookies = session.cookies.get_dict() diff --git a/tests/test_http_redirects.py b/tests/test_http_redirects.py index d8ce59b6c..2704ba280 100644 --- a/tests/test_http_redirects.py +++ b/tests/test_http_redirects.py @@ -1,14 +1,7 @@ import requests from uuid import uuid4 -from .api_client import ( - api_admin_session, - api_create_target, - api_create_user, - api_create_role, - api_add_role_to_user, - api_add_role_to_target, -) +from .api_client import admin_client, sdk from .conftest import WarpgateProcess from .test_http_common import * # noqa @@ -20,38 +13,25 @@ def test( echo_server_port, ): url = f"https://localhost:{shared_wg.http_port}" - with api_admin_session(url) as session: - role = api_create_role(url, session, {"name": f"role-{uuid4()}"}) - user = api_create_user( - url, - session, - { - "username": f"user-{uuid4()}", - "credentials": [ - { - "kind": "Password", - "hash": "123", - } - ], - }, + with admin_client(url) as api: + role = api.create_role(sdk.RoleDataRequest(name=f"role-{uuid4()}")) + user = api.create_user(sdk.CreateUserRequest(username=f"user-{uuid4()}")) + api.create_password_credential( + user.id, sdk.NewPasswordCredential(password="123") ) - api_add_role_to_user(url, session, user["id"], role["id"]) - echo_target = api_create_target( - url, - session, - { - "name": f"echo-{uuid4()}", - "options": { - "kind": "Http", - "url": f"http://localhost:{echo_server_port}", - "tls": { - "mode": "Disabled", - "verify": False, - }, - }, - }, - ) - api_add_role_to_target(url, session, echo_target["id"], role["id"]) + api.add_user_role(user.id, role.id) + echo_target = api.create_target(sdk.TargetDataRequest( + name=f"echo-{uuid4()}", + options=sdk.TargetOptions(sdk.TargetOptionsTargetHTTPOptions( + kind="Http", + url=f"http://localhost:{echo_server_port}", + tls=sdk.Tls( + mode=sdk.TlsMode.DISABLED, + verify=False, + ), + )), + )) + api.add_target_role(echo_target.id, role.id) session = requests.Session() session.verify = False @@ -60,14 +40,14 @@ def test( session.post( f"{url}/@warpgate/api/auth/login", json={ - "username": user["username"], + "username": user.username, "password": "123", }, headers=headers, ) response = session.get( - f"{url}/redirect/http://localhost:{echo_server_port}/test?warpgate-target={echo_target['name']}", + f"{url}/redirect/http://localhost:{echo_server_port}/test?warpgate-target={echo_target.name}", headers=headers, allow_redirects=False, ) diff --git a/tests/test_http_user_auth_logout.py b/tests/test_http_user_auth_logout.py index 66decc1f6..72ac96d46 100644 --- a/tests/test_http_user_auth_logout.py +++ b/tests/test_http_user_auth_logout.py @@ -1,14 +1,7 @@ import requests from uuid import uuid4 -from .api_client import ( - api_admin_session, - api_create_target, - api_create_user, - api_create_role, - api_add_role_to_user, - api_add_role_to_target, -) +from .api_client import admin_client, sdk from .conftest import WarpgateProcess from .test_http_common import * # noqa @@ -20,38 +13,25 @@ def test( shared_wg: WarpgateProcess, ): url = f"https://localhost:{shared_wg.http_port}" - with api_admin_session(url) as session: - role = api_create_role(url, session, {"name": f"role-{uuid4()}"}) - user = api_create_user( - url, - session, - { - "username": f"user-{uuid4()}", - "credentials": [ - { - "kind": "Password", - "hash": "123", - } - ], - }, + with admin_client(url) as api: + role = api.create_role(sdk.RoleDataRequest(name=f"role-{uuid4()}")) + user = api.create_user(sdk.CreateUserRequest(username=f"user-{uuid4()}")) + api.create_password_credential( + user.id, sdk.NewPasswordCredential(password="123") ) - api_add_role_to_user(url, session, user["id"], role["id"]) - echo_target = api_create_target( - url, - session, - { - "name": f"echo-{uuid4()}", - "options": { - "kind": "Http", - "url": f"http://localhost:{echo_server_port}", - "tls": { - "mode": "Disabled", - "verify": False, - }, - }, - }, - ) - api_add_role_to_target(url, session, echo_target["id"], role["id"]) + api.add_user_role(user.id, role.id) + echo_target = api.create_target(sdk.TargetDataRequest( + name=f"echo-{uuid4()}", + options=sdk.TargetOptions(sdk.TargetOptionsTargetHTTPOptions( + kind="Http", + url=f"http://localhost:{echo_server_port}", + tls=sdk.Tls( + mode=sdk.TlsMode.DISABLED, + verify=False, + ), + )), + )) + api.add_target_role(echo_target.id, role.id) session = requests.Session() session.verify = False @@ -59,14 +39,14 @@ def test( response = session.post( f"{url}/@warpgate/api/auth/login", json={ - "username": user["username"], + "username": user.username, "password": "123", }, ) assert response.status_code // 100 == 2 response = session.get( - f"{url}/some/path?a=b&warpgate-target={echo_target['name']}&c=d", + f"{url}/some/path?a=b&warpgate-target={echo_target.name}&c=d", allow_redirects=False, ) assert response.status_code // 100 == 2 @@ -75,6 +55,6 @@ def test( response = session.post(f"{url}/@warpgate/api/auth/logout") response = session.get( - f"{url}/?warpgate-target={echo_target['name']}", allow_redirects=False + f"{url}/?warpgate-target={echo_target.name}", allow_redirects=False ) assert response.status_code // 100 != 2 diff --git a/tests/test_http_user_auth_otp.py b/tests/test_http_user_auth_otp.py index 18395533f..c5cfc2711 100644 --- a/tests/test_http_user_auth_otp.py +++ b/tests/test_http_user_auth_otp.py @@ -3,14 +3,7 @@ from base64 import b64decode from uuid import uuid4 -from .api_client import ( - api_admin_session, - api_create_target, - api_create_user, - api_create_role, - api_add_role_to_user, - api_add_role_to_target, -) +from .api_client import admin_client, sdk from .conftest import WarpgateProcess from .test_http_common import * # noqa @@ -24,45 +17,42 @@ def test_auth_otp_success( shared_wg: WarpgateProcess, ): url = f"https://localhost:{shared_wg.http_port}" - with api_admin_session(url) as session: - role = api_create_role(url, session, {"name": f"role-{uuid4()}"}) - user = api_create_user( - url, - session, - { - "username": f"user-{uuid4()}", - "credentials": [ - { - "kind": "Password", - "hash": "123", - }, - { - "kind": "Totp", - "key": list(b64decode(otp_key_base64)), - }, - ], - "credential_policy": { - "http": ["Password", "Totp"], - }, - }, + with admin_client(url) as api: + role = api.create_role(sdk.RoleDataRequest(name=f"role-{uuid4()}")) + user = api.create_user(sdk.CreateUserRequest(username=f"user-{uuid4()}")) + api.create_password_credential( + user.id, sdk.NewPasswordCredential(password="123") ) - api_add_role_to_user(url, session, user["id"], role["id"]) - echo_target = api_create_target( - url, - session, - { - "name": f"echo-{uuid4()}", - "options": { - "kind": "Http", - "url": f"http://localhost:{echo_server_port}", - "tls": { - "mode": "Disabled", - "verify": False, - }, - }, - }, + api.create_otp_credential( + user.id, + sdk.NewOtpCredential(secret_key=list(b64decode(otp_key_base64))), ) - api_add_role_to_target(url, session, echo_target["id"], role["id"]) + api.update_user( + user.id, + sdk.UserDataRequest( + username=user.username, + credential_policy=sdk.UserRequireCredentialsPolicy( + http=["Password", "Totp"] + ), + ), + ) + api.add_user_role(user.id, role.id) + echo_target = api.create_target( + sdk.TargetDataRequest( + name=f"echo-{uuid4()}", + options=sdk.TargetOptions( + sdk.TargetOptionsTargetHTTPOptions( + kind="Http", + url=f"http://localhost:{echo_server_port}", + tls=sdk.Tls( + mode=sdk.TlsMode.DISABLED, + verify=False, + ), + ) + ), + ) + ) + api.add_target_role(echo_target.id, role.id) session = requests.Session() session.verify = False @@ -72,14 +62,14 @@ def test_auth_otp_success( response = session.post( f"{url}/@warpgate/api/auth/login", json={ - "username": user["username"], + "username": user.username, "password": "123", }, ) assert response.status_code // 100 != 2 response = session.get( - f"{url}/some/path?a=b&warpgate-target={echo_target['name']}&c=d", + f"{url}/some/path?a=b&warpgate-target={echo_target.name}&c=d", allow_redirects=False, ) assert response.status_code // 100 != 2 @@ -93,7 +83,7 @@ def test_auth_otp_success( assert response.status_code // 100 == 2 response = session.get( - f"{url}/some/path?a=b&warpgate-target={echo_target['name']}&c=d", + f"{url}/some/path?a=b&warpgate-target={echo_target.name}&c=d", allow_redirects=False, ) assert response.status_code // 100 == 2 @@ -106,45 +96,42 @@ def test_auth_otp_fail( shared_wg: WarpgateProcess, ): url = f"https://localhost:{shared_wg.http_port}" - with api_admin_session(url) as session: - role = api_create_role(url, session, {"name": f"role-{uuid4()}"}) - user = api_create_user( - url, - session, - { - "username": f"user-{uuid4()}", - "credentials": [ - { - "kind": "Password", - "hash": "123", - }, - { - "kind": "Totp", - "key": list(b64decode(otp_key_base64)), - }, - ], - "credential_policy": { - "http": ["PublicKey", "Totp"], - }, - }, + with admin_client(url) as api: + role = api.create_role(sdk.RoleDataRequest(name=f"role-{uuid4()}")) + user = api.create_user(sdk.CreateUserRequest(username=f"user-{uuid4()}")) + api.create_password_credential( + user.id, sdk.NewPasswordCredential(password="123") + ) + api.create_otp_credential( + user.id, + sdk.NewOtpCredential(secret_key=list(b64decode(otp_key_base64))), + ) + api.update_user( + user.id, + sdk.UserDataRequest( + username=user.username, + credential_policy=sdk.UserRequireCredentialsPolicy( + http=["Password", "Totp"] + ), + ), ) - api_add_role_to_user(url, session, user["id"], role["id"]) - echo_target = api_create_target( - url, - session, - { - "name": f"echo-{uuid4()}", - "options": { - "kind": "Http", - "url": f"http://localhost:{echo_server_port}", - "tls": { - "mode": "Disabled", - "verify": False, - }, - }, - }, + api.add_user_role(user.id, role.id) + echo_target = api.create_target( + sdk.TargetDataRequest( + name=f"echo-{uuid4()}", + options=sdk.TargetOptions( + sdk.TargetOptionsTargetHTTPOptions( + kind="Http", + url=f"http://localhost:{echo_server_port}", + tls=sdk.Tls( + mode=sdk.TlsMode.DISABLED, + verify=False, + ), + ) + ), + ) ) - api_add_role_to_target(url, session, echo_target["id"], role["id"]) + api.add_target_role(echo_target.id, role.id) session = requests.Session() session.verify = False @@ -152,7 +139,7 @@ def test_auth_otp_fail( response = session.post( f"{url}/@warpgate/api/auth/login", json={ - "username": user["username"], + "username": user.username, "password": "123", }, ) @@ -167,7 +154,7 @@ def test_auth_otp_fail( assert response.status_code // 100 != 2 response = session.get( - f"{url}/some/path?a=b&warpgate-target={echo_target['name']}&c=d", + f"{url}/some/path?a=b&warpgate-target={echo_target.name}&c=d", allow_redirects=False, ) assert response.status_code // 100 != 2 diff --git a/tests/test_http_user_auth_password.py b/tests/test_http_user_auth_password.py index e5578c8ba..01000c8ab 100644 --- a/tests/test_http_user_auth_password.py +++ b/tests/test_http_user_auth_password.py @@ -1,14 +1,7 @@ import requests from uuid import uuid4 -from .api_client import ( - api_admin_session, - api_create_target, - api_create_user, - api_create_role, - api_add_role_to_user, - api_add_role_to_target, -) +from .api_client import admin_client, sdk from .conftest import WarpgateProcess from .test_http_common import * # noqa @@ -20,58 +13,49 @@ def test_auth_password_success( shared_wg: WarpgateProcess, ): url = f"https://localhost:{shared_wg.http_port}" - with api_admin_session(url) as session: - role = api_create_role(url, session, {"name": f"role-{uuid4()}"}) - user = api_create_user( - url, - session, - { - "username": f"user-{uuid4()}", - "credentials": [ - { - "kind": "Password", - "hash": "123", - }, - ], - }, + with admin_client(url) as api: + role = api.create_role(sdk.RoleDataRequest(name=f"role-{uuid4()}")) + user = api.create_user(sdk.CreateUserRequest(username=f"user-{uuid4()}")) + api.create_password_credential( + user.id, sdk.NewPasswordCredential(password="123") ) - api_add_role_to_user(url, session, user["id"], role["id"]) - echo_target = api_create_target( - url, - session, - { - "name": f"echo-{uuid4()}", - "options": { - "kind": "Http", - "url": f"http://localhost:{echo_server_port}", - "tls": { - "mode": "Disabled", - "verify": False, - }, - }, - }, + api.add_user_role(user.id, role.id) + echo_target = api.create_target( + sdk.TargetDataRequest( + name=f"echo-{uuid4()}", + options=sdk.TargetOptions( + sdk.TargetOptionsTargetHTTPOptions( + kind="Http", + url=f"http://localhost:{echo_server_port}", + tls=sdk.Tls( + mode=sdk.TlsMode.DISABLED, + verify=False, + ), + ) + ), + ) ) - api_add_role_to_target(url, session, echo_target["id"], role["id"]) + api.add_target_role(echo_target.id, role.id) session = requests.Session() session.verify = False response = session.get( - f"{url}/?warpgate-target={echo_target['name']}", allow_redirects=False + f"{url}/?warpgate-target={echo_target.name}", allow_redirects=False ) assert response.status_code // 100 != 2 response = session.post( f"{url}/@warpgate/api/auth/login", json={ - "username": user["username"], + "username": user.username, "password": "123", }, ) assert response.status_code // 100 == 2 response = session.get( - f"{url}/some/path?a=b&warpgate-target={echo_target['name']}&c=d", + f"{url}/some/path?a=b&warpgate-target={echo_target.name}&c=d", allow_redirects=False, ) assert response.status_code // 100 == 2 @@ -83,38 +67,29 @@ def test_auth_password_fail( shared_wg: WarpgateProcess, ): url = f"https://localhost:{shared_wg.http_port}" - with api_admin_session(url) as session: - role = api_create_role(url, session, {"name": f"role-{uuid4()}"}) - user = api_create_user( - url, - session, - { - "username": f"user-{uuid4()}", - "credentials": [ - { - "kind": "Password", - "hash": "123", - }, - ], - }, + with admin_client(url) as api: + role = api.create_role(sdk.RoleDataRequest(name=f"role-{uuid4()}")) + user = api.create_user(sdk.CreateUserRequest(username=f"user-{uuid4()}")) + api.create_password_credential( + user.id, sdk.NewPasswordCredential(password="123") ) - api_add_role_to_user(url, session, user["id"], role["id"]) - echo_target = api_create_target( - url, - session, - { - "name": f"echo-{uuid4()}", - "options": { - "kind": "Http", - "url": f"http://localhost:{echo_server_port}", - "tls": { - "mode": "Disabled", - "verify": False, - }, - }, - }, + api.add_user_role(user.id, role.id) + echo_target = api.create_target( + sdk.TargetDataRequest( + name=f"echo-{uuid4()}", + options=sdk.TargetOptions( + sdk.TargetOptionsTargetHTTPOptions( + kind="Http", + url=f"http://localhost:{echo_server_port}", + tls=sdk.Tls( + mode=sdk.TlsMode.DISABLED, + verify=False, + ), + ) + ), + ) ) - api_add_role_to_target(url, session, echo_target["id"], role["id"]) + api.add_target_role(echo_target.id, role.id) session = requests.Session() session.verify = False @@ -122,14 +97,14 @@ def test_auth_password_fail( response = session.post( f"{url}/@warpgate/api/auth/login", json={ - "username": user["username"], + "username": user.username, "password": "321321", }, ) assert response.status_code // 100 != 2 response = session.get( - f"{url}/some/path?a=b&warpgate-target={echo_target['name']}&c=d", + f"{url}/some/path?a=b&warpgate-target={echo_target.name}&c=d", allow_redirects=False, ) assert response.status_code // 100 != 2 diff --git a/tests/test_http_user_auth_ticket.py b/tests/test_http_user_auth_ticket.py index be74b1464..05f758cda 100644 --- a/tests/test_http_user_auth_ticket.py +++ b/tests/test_http_user_auth_ticket.py @@ -1,15 +1,7 @@ import requests from uuid import uuid4 -from .api_client import ( - api_admin_session, - api_create_target, - api_create_user, - api_create_role, - api_add_role_to_user, - api_create_ticket, - api_add_role_to_target, -) +from .api_client import admin_client, sdk from .conftest import WarpgateProcess from .test_http_common import * # noqa @@ -21,56 +13,46 @@ def test_auth_password_success( shared_wg: WarpgateProcess, ): url = f"https://localhost:{shared_wg.http_port}" - with api_admin_session(url) as session: - role = api_create_role(url, session, {"name": f"role-{uuid4()}"}) - user = api_create_user( - url, - session, - { - "username": f"user-{uuid4()}", - "credentials": [ - { - "kind": "Password", - "hash": "123", - }, - ], - }, + with admin_client(url) as api: + role = api.create_role(sdk.RoleDataRequest(name=f"role-{uuid4()}")) + user = api.create_user(sdk.CreateUserRequest(username=f"user-{uuid4()}")) + api.create_password_credential( + user.id, sdk.NewPasswordCredential(password="123") ) - api_add_role_to_user(url, session, user["id"], role["id"]) - echo_target = api_create_target( - url, - session, - { - "name": f"echo-{uuid4()}", - "options": { - "kind": "Http", - "url": f"http://localhost:{echo_server_port}", - "tls": { - "mode": "Disabled", - "verify": False, - }, - }, - }, - ) - other_target = api_create_target( - url, - session, - { - "name": f"other-{uuid4()}", - "options": { - "kind": "Http", - "url": "http://badhost", - "tls": { - "mode": "Disabled", - "verify": False, - }, - }, - }, - ) - api_add_role_to_target(url, session, echo_target["id"], role["id"]) - secret = api_create_ticket( - url, session, user["username"], echo_target["name"] + api.add_user_role(user.id, role.id) + echo_target = api.create_target(sdk.TargetDataRequest( + name=f"echo-{uuid4()}", + options=sdk.TargetOptions(sdk.TargetOptionsTargetHTTPOptions( + kind="Http", + url=f"http://localhost:{echo_server_port}", + tls=sdk.Tls( + mode=sdk.TlsMode.DISABLED, + verify=False, + ), + )), + )) + api.add_target_role(echo_target.id, role.id) + + other_target = api.create_target( + sdk.TargetDataRequest( + name=f"other-{uuid4()}", + options=sdk.TargetOptions( + sdk.TargetOptionsTargetHTTPOptions( + kind="Http", + url="http://badhost", + tls=sdk.Tls( + mode=sdk.TlsMode.DISABLED, + verify=False, + ), + ) + ), + ) ) + api.add_target_role(other_target.id, role.id) + secret = api.create_ticket(sdk.CreateTicketRequest( + target_name=echo_target.name, + username=user.username, + )).secret # --- @@ -78,14 +60,14 @@ def test_auth_password_success( session.verify = False response = session.get( - f"{url}/some/path?warpgate-target={echo_target['name']}", + f"{url}/some/path?warpgate-target={echo_target.name}", allow_redirects=False, ) assert response.status_code // 100 != 2 # Ticket as a header response = session.get( - f"{url}/some/path?warpgate-target={echo_target['name']}", + f"{url}/some/path?warpgate-target={echo_target.name}", allow_redirects=False, headers={ "Authorization": f"Warpgate {secret}", @@ -96,7 +78,7 @@ def test_auth_password_success( # Bad ticket response = session.get( - f"{url}/some/path?warpgate-target={echo_target['name']}", + f"{url}/some/path?warpgate-target={echo_target.name}", allow_redirects=False, headers={ "Authorization": f"Warpgate bad{secret}", @@ -125,7 +107,7 @@ def test_auth_password_success( assert response.json()["path"] == "/some/path" response = session.get( - f"{url}/some/path?warpgate-ticket={secret}&warpgate-target={other_target['name']}", + f"{url}/some/path?warpgate-ticket={secret}&warpgate-target={other_target.name}", allow_redirects=False, ) assert response.status_code // 100 == 2 diff --git a/tests/test_http_websocket.py b/tests/test_http_websocket.py index 5ca74e99e..dcdf9794e 100644 --- a/tests/test_http_websocket.py +++ b/tests/test_http_websocket.py @@ -3,14 +3,7 @@ from websocket import create_connection from uuid import uuid4 -from .api_client import ( - api_admin_session, - api_create_target, - api_create_user, - api_create_role, - api_add_role_to_user, - api_add_role_to_target, -) +from .api_client import admin_client, sdk from .conftest import WarpgateProcess from .test_http_common import * # noqa @@ -22,38 +15,25 @@ def test_basic( shared_wg: WarpgateProcess, ): url = f"https://localhost:{shared_wg.http_port}" - with api_admin_session(url) as session: - role = api_create_role(url, session, {"name": f"role-{uuid4()}"}) - user = api_create_user( - url, - session, - { - "username": f"user-{uuid4()}", - "credentials": [ - { - "kind": "Password", - "hash": "123", - }, - ], - }, + with admin_client(url) as api: + role = api.create_role(sdk.RoleDataRequest(name=f"role-{uuid4()}")) + user = api.create_user(sdk.CreateUserRequest(username=f"user-{uuid4()}")) + api.create_password_credential( + user.id, sdk.NewPasswordCredential(password="123") ) - api_add_role_to_user(url, session, user["id"], role["id"]) - echo_target = api_create_target( - url, - session, - { - "name": f"echo-{uuid4()}", - "options": { - "kind": "Http", - "url": f"http://localhost:{echo_server_port}", - "tls": { - "mode": "Disabled", - "verify": False, - }, - }, - }, - ) - api_add_role_to_target(url, session, echo_target["id"], role["id"]) + api.add_user_role(user.id, role.id) + echo_target = api.create_target(sdk.TargetDataRequest( + name=f"echo-{uuid4()}", + options=sdk.TargetOptions(sdk.TargetOptionsTargetHTTPOptions( + kind="Http", + url=f"http://localhost:{echo_server_port}", + tls=sdk.Tls( + mode=sdk.TlsMode.DISABLED, + verify=False, + ), + )), + )) + api.add_target_role(echo_target.id, role.id) session = requests.Session() session.verify = False @@ -61,7 +41,7 @@ def test_basic( session.post( f"{url}/@warpgate/api/auth/login", json={ - "username": user["username"], + "username": user.username, "password": "123", }, ) @@ -69,7 +49,7 @@ def test_basic( cookies = session.cookies.get_dict() cookie = "; ".join([f"{k}={v}" for k, v in cookies.items()]) ws = create_connection( - f"wss://localhost:{shared_wg.http_port}/socket?warpgate-target={echo_target['name']}", + f"wss://localhost:{shared_wg.http_port}/socket?warpgate-target={echo_target.name}", cookie=cookie, sslopt={"cert_reqs": ssl.CERT_NONE}, ) diff --git a/tests/test_postgres_user_auth_password.py b/tests/test_postgres_user_auth_password.py index 84f46c3f5..497e4a298 100644 --- a/tests/test_postgres_user_auth_password.py +++ b/tests/test_postgres_user_auth_password.py @@ -2,14 +2,7 @@ import subprocess from uuid import uuid4 -from .api_client import ( - api_admin_session, - api_create_target, - api_create_user, - api_create_role, - api_add_role_to_user, - api_add_role_to_target, -) +from .api_client import admin_client, sdk from .conftest import WarpgateProcess, ProcessManager from .util import wait_port @@ -23,41 +16,28 @@ def test( ): db_port = processes.start_postgres_server() url = f"https://localhost:{shared_wg.http_port}" - with api_admin_session(url) as session: - role = api_create_role(url, session, {"name": f"role-{uuid4()}"}) - user = api_create_user( - url, - session, - { - "username": f"user-{uuid4()}", - "credentials": [ - { - "kind": "Password", - "hash": "123", - }, - ], - }, + with admin_client(url) as api: + role = api.create_role(sdk.RoleDataRequest(name=f"role-{uuid4()}")) + user = api.create_user(sdk.CreateUserRequest(username=f"user-{uuid4()}")) + api.create_password_credential( + user.id, sdk.NewPasswordCredential(password="123") ) - api_add_role_to_user(url, session, user["id"], role["id"]) - target = api_create_target( - url, - session, - { - "name": f"posgresq-{uuid4()}", - "options": { - "kind": "Postgres", - "host": "localhost", - "port": db_port, - "username": "user", - "password": "123", - "tls": { - "mode": "Preferred", - "verify": False, - }, - }, - }, - ) - api_add_role_to_target(url, session, target["id"], role["id"]) + api.add_user_role(user.id, role.id) + target = api.create_target(sdk.TargetDataRequest( + name=f"postgres-{uuid4()}", + options=sdk.TargetOptions(sdk.TargetOptionsTargetPostgresOptions( + kind="Postgres", + host="localhost", + port=db_port, + username="user", + password="123", + tls=sdk.Tls( + mode=sdk.TlsMode.PREFERRED, + verify=False, + ), + )), + )) + api.add_target_role(target.id, role.id) wait_port(db_port, recv=False) wait_port(shared_wg.postgres_port, recv=False) @@ -66,7 +46,7 @@ def test( [ "psql", "--user", - f"{user['username']}#{target['name']}", + f"{user.username}#{target.name}", "--host", "127.0.0.1", "--port", @@ -84,7 +64,7 @@ def test( [ "psql", "--user", - f"{user['username']}#{target['name']}", + f"{user.username}#{target.name}", "--host", "127.0.0.1", "--port", diff --git a/tests/test_ssh_conntest.py b/tests/test_ssh_conntest.py index 69430045e..bc80e05f9 100644 --- a/tests/test_ssh_conntest.py +++ b/tests/test_ssh_conntest.py @@ -1,12 +1,8 @@ from pathlib import Path -import subprocess from textwrap import dedent from uuid import uuid4 -from .api_client import ( - api_admin_session, - api_create_target, -) +from .api_client import admin_client, sdk from .conftest import ProcessManager, WarpgateProcess from .util import alloc_port, wait_port @@ -26,25 +22,29 @@ def test_success( wait_port(ssh_port) url = f"https://localhost:{shared_wg.http_port}" - with api_admin_session(url) as session: - ssh_target = api_create_target( - url, - session, - { - "name": f"ssh-{uuid4()}", - "options": { - "kind": "Ssh", - "host": "localhost", - "port": ssh_port, - "username": "root", - "auth": {"kind": "PublicKey"}, - }, - }, + with admin_client(url) as api: + ssh_target = api.create_target( + sdk.TargetDataRequest( + name=f"ssh-{uuid4()}", + options=sdk.TargetOptions( + sdk.TargetOptionsTargetSSHOptions( + kind="Ssh", + host="localhost", + port=ssh_port, + username="root", + auth=sdk.SSHTargetAuth( + sdk.SSHTargetAuthSshTargetPublicKeyAuth( + kind="PublicKey" + ) + ), + ) + ), + ) ) wg = processes.start_wg( share_with=shared_wg, - args=["test-target", ssh_target["name"]], + args=["test-target", ssh_target.name], ) wg.process.wait(timeout=timeout) assert wg.process.returncode == 0 @@ -58,23 +58,27 @@ def test_fail( ssh_port = alloc_port() url = f"https://localhost:{shared_wg.http_port}" - with api_admin_session(url) as session: - ssh_target = api_create_target( - url, - session, - { - "name": f"ssh-{uuid4()}", - "options": { - "kind": "Ssh", - "host": "localhost", - "port": ssh_port, - "username": "root", - "auth": {"kind": "PublicKey"}, - }, - }, + with admin_client(url) as api: + ssh_target = api.create_target( + sdk.TargetDataRequest( + name=f"ssh-{uuid4()}", + options=sdk.TargetOptions( + sdk.TargetOptionsTargetSSHOptions( + kind="Ssh", + host="localhost", + port=ssh_port, + username="root", + auth=sdk.SSHTargetAuth( + sdk.SSHTargetAuthSshTargetPublicKeyAuth( + kind="PublicKey" + ) + ), + ) + ), + ) ) wg = processes.start_wg( - args=["test-target", ssh_target["name"]], + args=["test-target", ssh_target.name], share_with=shared_wg, ) wg.process.wait(timeout=timeout) diff --git a/tests/test_ssh_proto.py b/tests/test_ssh_proto.py index bad3c8f56..1b3a10125 100644 --- a/tests/test_ssh_proto.py +++ b/tests/test_ssh_proto.py @@ -6,15 +6,7 @@ import pytest from textwrap import dedent -from tests.api_client import ( - api_add_role_to_target, - api_add_role_to_user, - api_admin_session, - api_create_role, - api_create_target, - api_create_user, -) - +from .api_client import admin_client, sdk from .conftest import ProcessManager, WarpgateProcess from .util import wait_port, alloc_port @@ -41,41 +33,38 @@ def setup_user_and_target( wait_port(ssh_port) url = f"https://localhost:{wg.http_port}" - with api_admin_session(url) as session: - role = api_create_role(url, session, {"name": f"role-{uuid4()}"}) - user = api_create_user( - url, - session, - { - "username": f"user-{uuid4()}", - "credentials": [ - { - "kind": "Password", - "hash": "123", - }, - { - "kind": "PublicKey", - "key": open("ssh-keys/id_ed25519.pub").read().strip(), - }, - ], - }, + with admin_client(url) as api: + role = api.create_role( + sdk.RoleDataRequest(name=f"role-{uuid4()}"), + ) + user = api.create_user(sdk.CreateUserRequest(username=f"user-{uuid4()}")) + api.create_password_credential( + user.id, sdk.NewPasswordCredential(password="123") ) - api_add_role_to_user(url, session, user["id"], role["id"]) - ssh_target = api_create_target( - url, - session, - { - "name": f"ssh-{uuid4()}", - "options": { - "kind": "Ssh", - "host": "localhost", - "port": ssh_port, - "username": "root", - "auth": {"kind": "PublicKey"}, - }, - }, + api.create_public_key_credential( + user.id, + sdk.NewPublicKeyCredential( + openssh_public_key=open("ssh-keys/id_ed25519.pub").read().strip(), + ), + ) + api.add_user_role(user.id, role.id) + ssh_target = api.create_target( + sdk.TargetDataRequest( + name=f"ssh-{uuid4()}", + options=sdk.TargetOptions( + sdk.TargetOptionsTargetSSHOptions( + kind="Ssh", + host="localhost", + port=ssh_port, + username="root", + auth=sdk.SSHTargetAuth( + sdk.SSHTargetAuthSshTargetPublicKeyAuth(kind="PublicKey") + ), + ) + ), + ) ) - api_add_role_to_target(url, session, ssh_target["id"], role["id"]) + api.add_target_role(ssh_target.id, role.id) return user, ssh_target @@ -91,7 +80,7 @@ def test_stdout_stderr( processes, shared_wg, wg_c_ed25519_pubkey ) ssh_client = processes.start_ssh_client( - f"{user['username']}:{ssh_target['name']}@localhost", + f"{user.username}:{ssh_target.name}@localhost", "-p", str(shared_wg.ssh_port), *common_args, @@ -117,7 +106,7 @@ def test_pty( processes, shared_wg, wg_c_ed25519_pubkey ) ssh_client = processes.start_ssh_client( - f"{user['username']}:{ssh_target['name']}@localhost", + f"{user.username}:{ssh_target.name}@localhost", "-p", str(shared_wg.ssh_port), "-tt", @@ -142,7 +131,7 @@ def test_signals( processes, shared_wg, wg_c_ed25519_pubkey ) ssh_client = processes.start_ssh_client( - f"{user['username']}:{ssh_target['name']}@localhost", + f"{user.username}:{ssh_target.name}@localhost", "-p", str(shared_wg.ssh_port), "-v", @@ -167,7 +156,7 @@ def test_direct_tcpip( ) local_port = alloc_port() ssh_client = processes.start_ssh_client( - f"{user['username']}:{ssh_target['name']}@localhost", + f"{user.username}:{ssh_target.name}@localhost", "-p", str(shared_wg.ssh_port), "-v", @@ -200,7 +189,7 @@ def test_tcpip_forward( processes, shared_wg, wg_c_ed25519_pubkey ) pf_client = processes.start_ssh_client( - f"{user['username']}:{ssh_target['name']}@localhost", + f"{user.username}:{ssh_target.name}@localhost", "-p", str(shared_wg.ssh_port), "-v", @@ -212,7 +201,7 @@ def test_tcpip_forward( ) time.sleep(5) ssh_client = processes.start_ssh_client( - f"{user['username']}:{ssh_target['name']}@localhost", + f"{user.username}:{ssh_target.name}@localhost", "-p", str(shared_wg.ssh_port), "-v", @@ -241,7 +230,7 @@ def test_shell( f""" set timeout {timeout - 5} - spawn ssh -tt {user['username']}:{ssh_target['name']}@localhost -p {shared_wg.ssh_port} -o StrictHostKeychecking=no -o UserKnownHostsFile=/dev/null -o PreferredAuthentications=password + spawn ssh -tt {user.username}:{ssh_target.name}@localhost -p {shared_wg.ssh_port} -o StrictHostKeychecking=no -o UserKnownHostsFile=/dev/null -o PreferredAuthentications=password expect "password:" sleep 0.5 @@ -278,7 +267,7 @@ def test_connection_error( processes, shared_wg, wg_c_ed25519_pubkey ) ssh_client = processes.start_ssh_client( - f"{user['username']}:{ssh_target['name']}@localhost", + f"{user.username}:{ssh_target.name}@localhost", "-p", str(shared_wg.ssh_port), "-tt", @@ -308,7 +297,7 @@ def test_sftp( "-P", str(shared_wg.ssh_port), "-o", - f"User={user['username']}:{ssh_target['name']}", + f"User={user.username}:{ssh_target.name}", "-o", "IdentitiesOnly=yes", "-o", diff --git a/tests/test_ssh_target_selection.py b/tests/test_ssh_target_selection.py index e702a0382..36dc50647 100644 --- a/tests/test_ssh_target_selection.py +++ b/tests/test_ssh_target_selection.py @@ -2,14 +2,7 @@ import subprocess from uuid import uuid4 -from .api_client import ( - api_add_role_to_target, - api_add_role_to_user, - api_admin_session, - api_create_role, - api_create_target, - api_create_user, -) +from .api_client import admin_client, sdk from .conftest import ProcessManager, WarpgateProcess from .util import wait_port @@ -28,41 +21,32 @@ def test_bad_target( wait_port(ssh_port) url = f"https://localhost:{shared_wg.http_port}" - with api_admin_session(url) as session: - role = api_create_role(url, session, {"name": f"role-{uuid4()}"}) - user = api_create_user( - url, - session, - { - "username": f"user-{uuid4()}", - "credentials": [ - { - "kind": "Password", - "hash": "123", - }, - ], - }, + with admin_client(url) as api: + role = api.create_role( + sdk.RoleDataRequest(name=f"role-{uuid4()}"), ) - api_add_role_to_user(url, session, user["id"], role["id"]) - ssh_target = api_create_target( - url, - session, - { - "name": f"ssh-{uuid4()}", - "options": { - "kind": "Ssh", - "host": "localhost", - "port": ssh_port, - "username": "root", - "auth": {"kind": "PublicKey"}, - }, - }, - ) - api_add_role_to_target(url, session, ssh_target["id"], role["id"]) + user = api.create_user(sdk.CreateUserRequest(username=f"user-{uuid4()}")) + api.create_password_credential(user.id, sdk.NewPasswordCredential(password="123")) + api.add_user_role(user.id, role.id) + ssh_target = api.create_target(sdk.TargetDataRequest( + name=f"ssh-{uuid4()}", + options=sdk.TargetOptions( + sdk.TargetOptionsTargetSSHOptions( + kind="Ssh", + host="localhost", + port=ssh_port, + username="root", + auth=sdk.SSHTargetAuth( + sdk.SSHTargetAuthSshTargetPublicKeyAuth(kind="PublicKey") + ), + ) + ), + )) + api.add_target_role(ssh_target.id, role.id) ssh_client = processes.start_ssh_client( "-t", - f"{user['username']}:badtarget@localhost", + f"{user.username}:badtarget@localhost", "-p", str(shared_wg.ssh_port), "-i", diff --git a/tests/test_ssh_user_auth_otp.py b/tests/test_ssh_user_auth_otp.py index 17a760455..4e8192364 100644 --- a/tests/test_ssh_user_auth_otp.py +++ b/tests/test_ssh_user_auth_otp.py @@ -5,14 +5,7 @@ from pathlib import Path from textwrap import dedent -from .api_client import ( - api_add_role_to_target, - api_add_role_to_user, - api_admin_session, - api_create_role, - api_create_target, - api_create_user, -) +from .api_client import admin_client, sdk from .conftest import ProcessManager, WarpgateProcess from .util import wait_port @@ -34,44 +27,52 @@ def test_otp( wait_port(ssh_port) url = f"https://localhost:{shared_wg.http_port}" - with api_admin_session(url) as session: - role = api_create_role(url, session, {"name": f"role-{uuid4()}"}) - user = api_create_user( - url, - session, - { - "username": f"user-{uuid4()}", - "credentials": [ - { - "kind": "PublicKey", - "key": open("ssh-keys/id_ed25519.pub").read().strip(), - }, - { - "kind": "Totp", - "key": list(b64decode(otp_key_base64)), - }, - ], - "credential_policy": { - "ssh": ["PublicKey", "Totp"], - }, - }, + with admin_client(url) as api: + role = api.create_role( + sdk.RoleDataRequest(name=f"role-{uuid4()}"), ) - api_add_role_to_user(url, session, user["id"], role["id"]) - ssh_target = api_create_target( - url, - session, - { - "name": f"ssh-{uuid4()}", - "options": { - "kind": "Ssh", - "host": "localhost", - "port": ssh_port, - "username": "root", - "auth": {"kind": "PublicKey"}, - }, - }, + user = api.create_user(sdk.CreateUserRequest(username=f"user-{uuid4()}")) + api.create_public_key_credential( + user.id, + sdk.NewPublicKeyCredential( + openssh_public_key=open("ssh-keys/id_ed25519.pub").read().strip() + ), ) - api_add_role_to_target(url, session, ssh_target["id"], role["id"]) + api.create_otp_credential( + user.id, + sdk.NewOtpCredential( + secret_key=list(b64decode(otp_key_base64)), + ), + ) + api.update_user( + user.id, + sdk.UserDataRequest( + username=user.username, + credential_policy=sdk.UserRequireCredentialsPolicy( + ssh=["PublicKey", "Totp"], + ), + ), + ) + api.add_user_role(user.id, role.id) + ssh_target = api.create_target( + sdk.TargetDataRequest( + name=f"ssh-{uuid4()}", + options=sdk.TargetOptions( + sdk.TargetOptionsTargetSSHOptions( + kind="Ssh", + host="localhost", + port=ssh_port, + username="root", + auth=sdk.SSHTargetAuth( + sdk.SSHTargetAuthSshTargetPublicKeyAuth( + kind="PublicKey" + ) + ), + ) + ), + ) + ) + api.add_target_role(ssh_target.id, role.id) totp = pyotp.TOTP(otp_key_base32) @@ -79,7 +80,7 @@ def test_otp( f""" set timeout {timeout - 5} - spawn ssh {user["username"]}:{ssh_target["name"]}@localhost -p {shared_wg.ssh_port} -o StrictHostKeychecking=no -o UserKnownHostsFile=/dev/null -o IdentitiesOnly=yes -o IdentityFile=ssh-keys/id_ed25519 -o PreferredAuthentications=publickey,keyboard-interactive ls /bin/sh + spawn ssh {user.username}:{ssh_target.name}@localhost -p {shared_wg.ssh_port} -o StrictHostKeychecking=no -o UserKnownHostsFile=/dev/null -o IdentitiesOnly=yes -o IdentityFile=ssh-keys/id_ed25519 -o PreferredAuthentications=publickey,keyboard-interactive ls /bin/sh expect "Two-factor authentication" sleep 0.5 @@ -106,7 +107,7 @@ def test_otp( f""" set timeout {timeout - 5} - spawn ssh {user["username"]}:{ssh_target["name"]}@localhost -p {[shared_wg.ssh_port]} -o StrictHostKeychecking=no -o UserKnownHostsFile=/dev/null -o IdentitiesOnly=yes -o IdentityFile=ssh-keys/id_ed25519 -o PreferredAuthentications=publickey,keyboard-interactive ls /bin/sh + spawn ssh {user.username}:{ssh_target.name}@localhost -p {[shared_wg.ssh_port]} -o StrictHostKeychecking=no -o UserKnownHostsFile=/dev/null -o IdentitiesOnly=yes -o IdentityFile=ssh-keys/id_ed25519 -o PreferredAuthentications=publickey,keyboard-interactive ls /bin/sh expect "Two-factor authentication" sleep 0.5 diff --git a/tests/test_ssh_user_auth_password.py b/tests/test_ssh_user_auth_password.py index 007371b99..8d51e97a1 100644 --- a/tests/test_ssh_user_auth_password.py +++ b/tests/test_ssh_user_auth_password.py @@ -1,14 +1,7 @@ from pathlib import Path from uuid import uuid4 -from .api_client import ( - api_add_role_to_target, - api_add_role_to_user, - api_admin_session, - api_create_role, - api_create_target, - api_create_user, -) +from .api_client import admin_client, sdk from .conftest import ProcessManager, WarpgateProcess from .util import wait_port @@ -28,40 +21,37 @@ def test( wait_port(ssh_port) url = f"https://localhost:{shared_wg.http_port}" - with api_admin_session(url) as session: - role = api_create_role(url, session, {"name": f"role-{uuid4()}"}) - user = api_create_user( - url, - session, - { - "username": f"user-{uuid4()}", - "credentials": [ - { - "kind": "Password", - "hash": "123", - }, - ], - }, + with admin_client(url) as api: + role = api.create_role( + sdk.RoleDataRequest(name=f"role-{uuid4()}"), ) - api_add_role_to_user(url, session, user["id"], role["id"]) - ssh_target = api_create_target( - url, - session, - { - "name": f"ssh-{uuid4()}", - "options": { - "kind": "Ssh", - "host": "localhost", - "port": ssh_port, - "username": "root", - "auth": {"kind": "PublicKey"}, - }, - }, + user = api.create_user(sdk.CreateUserRequest(username=f"user-{uuid4()}")) + api.create_password_credential( + user.id, sdk.NewPasswordCredential(password="123") ) - api_add_role_to_target(url, session, ssh_target["id"], role["id"]) + api.add_user_role(user.id, role.id) + ssh_target = api.create_target( + sdk.TargetDataRequest( + name=f"ssh-{uuid4()}", + options=sdk.TargetOptions( + sdk.TargetOptionsTargetSSHOptions( + kind="Ssh", + host="localhost", + port=ssh_port, + username="root", + auth=sdk.SSHTargetAuth( + sdk.SSHTargetAuthSshTargetPublicKeyAuth( + kind="PublicKey" + ) + ), + ) + ), + ) + ) + api.add_target_role(ssh_target.id, role.id) ssh_client = processes.start_ssh_client( - f'{user["username"]}:{ssh_target["name"]}@localhost', + f"{user.username}:{ssh_target.name}@localhost", "-v", "-p", str(shared_wg.ssh_port), @@ -77,7 +67,7 @@ def test( assert ssh_client.returncode == 0 ssh_client = processes.start_ssh_client( - f'{user["username"]}:{ssh_target["name"]}@localhost', + f"{user.username}:{ssh_target.name}@localhost", "-p", str(shared_wg.ssh_port), "-i", diff --git a/tests/test_ssh_user_auth_pubkey.py b/tests/test_ssh_user_auth_pubkey.py index dfb661c16..bb5b533ae 100644 --- a/tests/test_ssh_user_auth_pubkey.py +++ b/tests/test_ssh_user_auth_pubkey.py @@ -1,14 +1,7 @@ from pathlib import Path from uuid import uuid4 -from .api_client import ( - api_add_role_to_target, - api_add_role_to_user, - api_admin_session, - api_create_role, - api_create_target, - api_create_user, -) +from .api_client import admin_client, sdk from .conftest import ProcessManager, WarpgateProcess from .util import wait_port @@ -28,40 +21,40 @@ def test_ed25519( wait_port(ssh_port) url = f"https://localhost:{shared_wg.http_port}" - with api_admin_session(url) as session: - role = api_create_role(url, session, {"name": f"role-{uuid4()}"}) - user = api_create_user( - url, - session, - { - "username": f"user-{uuid4()}", - "credentials": [ - { - "kind": "PublicKey", - "key": open("ssh-keys/id_ed25519.pub").read().strip(), - }, - ], - }, + with admin_client(url) as api: + role = api.create_role( + sdk.RoleDataRequest(name=f"role-{uuid4()}"), ) - api_add_role_to_user(url, session, user["id"], role["id"]) - ssh_target = api_create_target( - url, - session, - { - "name": f"ssh-{uuid4()}", - "options": { - "kind": "Ssh", - "host": "localhost", - "port": ssh_port, - "username": "root", - "auth": {"kind": "PublicKey"}, - }, - }, + user = api.create_user(sdk.CreateUserRequest(username=f"user-{uuid4()}")) + api.create_public_key_credential( + user.id, + sdk.NewPublicKeyCredential( + openssh_public_key=open("ssh-keys/id_ed25519.pub").read().strip() + ), ) - api_add_role_to_target(url, session, ssh_target["id"], role["id"]) + api.add_user_role(user.id, role.id) + ssh_target = api.create_target( + sdk.TargetDataRequest( + name=f"ssh-{uuid4()}", + options=sdk.TargetOptions( + sdk.TargetOptionsTargetSSHOptions( + kind="Ssh", + host="localhost", + port=ssh_port, + username="root", + auth=sdk.SSHTargetAuth( + sdk.SSHTargetAuthSshTargetPublicKeyAuth( + kind="PublicKey" + ) + ), + ) + ), + ) + ) + api.add_target_role(ssh_target.id, role.id) ssh_client = processes.start_ssh_client( - f'{user["username"]}:{ssh_target["name"]}@localhost', + f"{user.username}:{ssh_target.name}@localhost", "-p", str(shared_wg.ssh_port), "-o", @@ -76,7 +69,7 @@ def test_ed25519( assert ssh_client.returncode == 0 ssh_client = processes.start_ssh_client( - f'{user["username"]}:{ssh_target["name"]}@localhost', + f"{user.username}:{ssh_target.name}@localhost", "-p", str(shared_wg.ssh_port), "-o", @@ -103,40 +96,36 @@ def test_rsa( wait_port(ssh_port) url = f"https://localhost:{shared_wg.http_port}" - with api_admin_session(url) as session: - role = api_create_role(url, session, {"name": f"role-{uuid4()}"}) - user = api_create_user( - url, - session, - { - "username": f"user-{uuid4()}", - "credentials": [ - { - "kind": "PublicKey", - "key": open("ssh-keys/id_rsa.pub").read().strip(), - }, - ], - }, + with admin_client(url) as api: + role = api.create_role( + sdk.RoleDataRequest(name=f"role-{uuid4()}"), ) - api_add_role_to_user(url, session, user["id"], role["id"]) - ssh_target = api_create_target( - url, - session, - { - "name": f"ssh-{uuid4()}", - "options": { - "kind": "Ssh", - "host": "localhost", - "port": ssh_port, - "username": "root", - "auth": {"kind": "PublicKey"}, - }, - }, + user = api.create_user(sdk.CreateUserRequest(username=f"user-{uuid4()}")) + api.create_public_key_credential( + user.id, + sdk.NewPublicKeyCredential( + openssh_public_key=open("ssh-keys/id_rsa.pub").read().strip() + ), ) - api_add_role_to_target(url, session, ssh_target["id"], role["id"]) + api.add_user_role(user.id, role.id) + ssh_target = api.create_target(sdk.TargetDataRequest( + name=f"ssh-{uuid4()}", + options=sdk.TargetOptions( + sdk.TargetOptionsTargetSSHOptions( + kind="Ssh", + host="localhost", + port=ssh_port, + username="root", + auth=sdk.SSHTargetAuth( + sdk.SSHTargetAuthSshTargetPublicKeyAuth(kind="PublicKey") + ), + ) + ), + )) + api.add_target_role(ssh_target.id, role.id) ssh_client = processes.start_ssh_client( - f'{user["username"]}:{ssh_target["name"]}@localhost', + f"{user.username}:{ssh_target.name}@localhost", "-v", "-p", str(shared_wg.ssh_port), @@ -153,7 +142,7 @@ def test_rsa( assert ssh_client.returncode == 0 ssh_client = processes.start_ssh_client( - f'{user["username"]}:{ssh_target["name"]}@localhost', + f"{user.username}:{ssh_target.name}@localhost", "-p", str(shared_wg.ssh_port), "-o", diff --git a/tests/test_ssh_user_auth_ticket.py b/tests/test_ssh_user_auth_ticket.py index 53c2727fb..052323b03 100644 --- a/tests/test_ssh_user_auth_ticket.py +++ b/tests/test_ssh_user_auth_ticket.py @@ -1,15 +1,7 @@ from pathlib import Path from uuid import uuid4 -from .api_client import ( - api_add_role_to_target, - api_add_role_to_user, - api_admin_session, - api_create_role, - api_create_target, - api_create_ticket, - api_create_user, -) +from .api_client import admin_client, sdk from .conftest import ProcessManager, WarpgateProcess from .util import wait_port @@ -29,40 +21,41 @@ def test( wait_port(ssh_port) url = f"https://localhost:{shared_wg.http_port}" - with api_admin_session(url) as session: - role = api_create_role(url, session, {"name": f"role-{uuid4()}"}) - user = api_create_user( - url, - session, - { - "username": f"user-{uuid4()}", - "credentials": [ - { - "kind": "Password", - "hash": "123", - }, - ], - }, + with admin_client(url) as api: + role = api.create_role( + sdk.RoleDataRequest(name=f"role-{uuid4()}"), ) - api_add_role_to_user(url, session, user["id"], role["id"]) - ssh_target = api_create_target( - url, - session, - { - "name": f"ssh-{uuid4()}", - "options": { - "kind": "Ssh", - "host": "localhost", - "port": ssh_port, - "username": "root", - "auth": {"kind": "PublicKey"}, - }, - }, + user = api.create_user(sdk.CreateUserRequest(username=f"user-{uuid4()}")) + api.create_password_credential( + user.id, sdk.NewPasswordCredential(password="123") ) - api_add_role_to_target(url, session, ssh_target["id"], role["id"]) - secret = api_create_ticket( - url, session, user["username"], ssh_target["name"] + api.add_user_role(user.id, role.id) + ssh_target = api.create_target( + sdk.TargetDataRequest( + name=f"ssh-{uuid4()}", + options=sdk.TargetOptions( + sdk.TargetOptionsTargetSSHOptions( + kind="Ssh", + host="localhost", + port=ssh_port, + username="root", + auth=sdk.SSHTargetAuth( + sdk.SSHTargetAuthSshTargetPublicKeyAuth( + kind="PublicKey" + ) + ), + ) + ), + ) ) + api.add_target_role(ssh_target.id, role.id) + + secret = api.create_ticket( + sdk.CreateTicketRequest( + target_name=ssh_target.name, + username=user.username, + ) + ).secret ssh_client = processes.start_ssh_client( f"ticket-{secret}@localhost", diff --git a/warpgate-admin/Cargo.toml b/warpgate-admin/Cargo.toml index c16e95961..30e94d9c3 100644 --- a/warpgate-admin/Cargo.toml +++ b/warpgate-admin/Cargo.toml @@ -7,7 +7,7 @@ version = "0.11.0" [dependencies] anyhow = { version = "1.0", features = ["std"] } async-trait = "0.1" -bytes = "1.4" +bytes.workspace = true chrono = { version = "0.4", default-features = false } futures = "0.3" hex = "0.4" @@ -24,14 +24,14 @@ poem-openapi = { version = "5.1", features = [ "uuid", "static-files", ] } -russh = { version = "0.46.0", features = ["legacy-ed25519-pkcs8-parser"] } +russh.workspace = true rust-embed = "8.3" sea-orm = { version = "0.12", features = [ "runtime-tokio-rustls", "macros", ], default-features = false } -serde = "1.0" -serde_json = "1.0" +serde.workspace = true +serde_json.workspace = true thiserror = "1.0" tokio = { version = "1.20", features = ["tracing"] } tracing = "0.1" diff --git a/warpgate-admin/src/api/known_hosts_detail.rs b/warpgate-admin/src/api/known_hosts_detail.rs index 05a06cc5d..76d5b3b53 100644 --- a/warpgate-admin/src/api/known_hosts_detail.rs +++ b/warpgate-admin/src/api/known_hosts_detail.rs @@ -6,6 +6,8 @@ use poem_openapi::{ApiResponse, OpenApi}; use sea_orm::{DatabaseConnection, EntityTrait, ModelTrait}; use tokio::sync::Mutex; use uuid::Uuid; + +use super::TokenSecurityScheme; pub struct Api; #[derive(ApiResponse)] @@ -28,6 +30,7 @@ impl Api { &self, db: Data<&Arc>>, id: Path, + _auth: TokenSecurityScheme, ) -> poem::Result { use warpgate_db_entities::KnownHost; let db = db.lock().await; diff --git a/warpgate-admin/src/api/known_hosts_list.rs b/warpgate-admin/src/api/known_hosts_list.rs index b0c1ba149..5afce0dc1 100644 --- a/warpgate-admin/src/api/known_hosts_list.rs +++ b/warpgate-admin/src/api/known_hosts_list.rs @@ -7,6 +7,8 @@ use sea_orm::{DatabaseConnection, EntityTrait}; use tokio::sync::Mutex; use warpgate_db_entities::KnownHost; +use super::TokenSecurityScheme; + pub struct Api; #[derive(ApiResponse)] @@ -25,6 +27,7 @@ impl Api { async fn api_ssh_get_all_known_hosts( &self, db: Data<&Arc>>, + _auth: TokenSecurityScheme, ) -> poem::Result { use warpgate_db_entities::KnownHost; diff --git a/warpgate-admin/src/api/logs.rs b/warpgate-admin/src/api/logs.rs index 91c176f7a..5689a5cf8 100644 --- a/warpgate-admin/src/api/logs.rs +++ b/warpgate-admin/src/api/logs.rs @@ -9,6 +9,8 @@ use tokio::sync::Mutex; use uuid::Uuid; use warpgate_db_entities::LogEntry; +use super::TokenSecurityScheme; + pub struct Api; #[derive(ApiResponse)] @@ -34,6 +36,7 @@ impl Api { &self, db: Data<&Arc>>, body: Json, + _auth: TokenSecurityScheme, ) -> poem::Result { use warpgate_db_entities::LogEntry; diff --git a/warpgate-admin/src/api/mod.rs b/warpgate-admin/src/api/mod.rs index 75c43a0ce..0e09da36c 100644 --- a/warpgate-admin/src/api/mod.rs +++ b/warpgate-admin/src/api/mod.rs @@ -1,33 +1,49 @@ -use poem_openapi::OpenApi; +use poem_openapi::auth::ApiKey; +use poem_openapi::{OpenApi, SecurityScheme}; mod known_hosts_detail; mod known_hosts_list; mod logs; +mod otp_credentials; mod pagination; +mod password_credentials; +mod public_key_credentials; pub mod recordings_detail; mod roles; mod sessions_detail; pub mod sessions_list; mod ssh_keys; +mod sso_credentials; mod targets; mod tickets_detail; mod tickets_list; mod users; +#[derive(SecurityScheme)] +#[oai(ty = "api_key", key_name = "X-Warpgate-Token", key_in = "header")] +#[allow(dead_code)] +pub struct TokenSecurityScheme(ApiKey); + pub fn get() -> impl OpenApi { ( - sessions_list::Api, - sessions_detail::Api, + (sessions_list::Api, sessions_detail::Api), recordings_detail::Api, - roles::ListApi, - roles::DetailApi, - (targets::ListApi, targets::DetailApi, targets::RolesApi), - (users::ListApi, users::DetailApi, users::RolesApi), - tickets_list::Api, - tickets_detail::Api, - known_hosts_list::Api, - known_hosts_detail::Api, + (roles::ListApi, roles::DetailApi), + (tickets_list::Api, tickets_detail::Api), + (known_hosts_list::Api, known_hosts_detail::Api), ssh_keys::Api, logs::Api, + (targets::ListApi, targets::DetailApi, targets::RolesApi), + (users::ListApi, users::DetailApi, users::RolesApi), + ( + password_credentials::ListApi, + password_credentials::DetailApi, + ), + (sso_credentials::ListApi, sso_credentials::DetailApi), + ( + public_key_credentials::ListApi, + public_key_credentials::DetailApi, + ), + (otp_credentials::ListApi, otp_credentials::DetailApi), ) } diff --git a/warpgate-admin/src/api/otp_credentials.rs b/warpgate-admin/src/api/otp_credentials.rs new file mode 100644 index 000000000..b441fe1ca --- /dev/null +++ b/warpgate-admin/src/api/otp_credentials.rs @@ -0,0 +1,148 @@ +use std::sync::Arc; + +use poem::web::Data; +use poem_openapi::param::Path; +use poem_openapi::payload::Json; +use poem_openapi::{ApiResponse, Object, OpenApi}; +use sea_orm::{ + ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, ModelTrait, QueryFilter, Set, +}; +use tokio::sync::Mutex; +use uuid::Uuid; +use warpgate_common::{UserTotpCredential, WarpgateError}; +use warpgate_db_entities::OtpCredential; + +use super::TokenSecurityScheme; + +#[derive(Object)] +struct ExistingOtpCredential { + id: Uuid, +} + +#[derive(Object)] +struct NewOtpCredential { + secret_key: Vec, +} + +impl From for ExistingOtpCredential { + fn from(credential: OtpCredential::Model) -> Self { + Self { id: credential.id } + } +} + +impl From<&NewOtpCredential> for UserTotpCredential { + fn from(credential: &NewOtpCredential) -> Self { + Self { + key: credential.secret_key.clone().into(), + } + } +} + +#[derive(ApiResponse)] +enum GetOtpCredentialsResponse { + #[oai(status = 200)] + Ok(Json>), +} + +#[derive(ApiResponse)] +enum CreateOtpCredentialResponse { + #[oai(status = 201)] + Created(Json), +} + +pub struct ListApi; + +#[OpenApi] +impl ListApi { + #[oai( + path = "/users/:user_id/credentials/otp", + method = "get", + operation_id = "get_otp_credentials" + )] + async fn api_get_all( + &self, + db: Data<&Arc>>, + user_id: Path, + _auth: TokenSecurityScheme, + ) -> poem::Result { + let db = db.lock().await; + + let objects = OtpCredential::Entity::find() + .filter(OtpCredential::Column::UserId.eq(*user_id)) + .all(&*db) + .await + .map_err(poem::error::InternalServerError)?; + + Ok(GetOtpCredentialsResponse::Ok(Json( + objects.into_iter().map(Into::into).collect(), + ))) + } + + #[oai( + path = "/users/:user_id/credentials/otp", + method = "post", + operation_id = "create_otp_credential" + )] + async fn api_create( + &self, + db: Data<&Arc>>, + body: Json, + user_id: Path, + _auth: TokenSecurityScheme, + ) -> poem::Result { + let db = db.lock().await; + + let object = OtpCredential::ActiveModel { + id: Set(Uuid::new_v4()), + user_id: Set(*user_id), + ..OtpCredential::ActiveModel::from(UserTotpCredential::from(&*body)) + } + .insert(&*db) + .await + .map_err(WarpgateError::from)?; + + Ok(CreateOtpCredentialResponse::Created(Json(object.into()))) + } +} + +#[derive(ApiResponse)] +enum DeleteCredentialResponse { + #[oai(status = 204)] + Deleted, + #[oai(status = 404)] + NotFound, +} + +pub struct DetailApi; + +#[OpenApi] +impl DetailApi { + #[oai( + path = "/users/:user_id/credentials/otp/:id", + method = "delete", + operation_id = "delete_otp_credential" + )] + async fn api_delete( + &self, + db: Data<&Arc>>, + user_id: Path, + id: Path, + _auth: TokenSecurityScheme, + ) -> poem::Result { + let db = db.lock().await; + + let Some(role) = OtpCredential::Entity::find_by_id(id.0) + .filter(OtpCredential::Column::UserId.eq(*user_id)) + .one(&*db) + .await + .map_err(poem::error::InternalServerError)? + else { + return Ok(DeleteCredentialResponse::NotFound); + }; + + role.delete(&*db) + .await + .map_err(poem::error::InternalServerError)?; + Ok(DeleteCredentialResponse::Deleted) + } +} diff --git a/warpgate-admin/src/api/password_credentials.rs b/warpgate-admin/src/api/password_credentials.rs new file mode 100644 index 000000000..5475a6de0 --- /dev/null +++ b/warpgate-admin/src/api/password_credentials.rs @@ -0,0 +1,145 @@ +use std::sync::Arc; + +use poem::web::Data; +use poem_openapi::param::Path; +use poem_openapi::payload::Json; +use poem_openapi::{ApiResponse, Object, OpenApi}; +use sea_orm::{ + ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, ModelTrait, QueryFilter, Set, +}; +use tokio::sync::Mutex; +use uuid::Uuid; +use warpgate_common::{Secret, UserPasswordCredential, WarpgateError}; +use warpgate_db_entities::PasswordCredential; + +use super::TokenSecurityScheme; + +#[derive(Object)] +struct ExistingPasswordCredential { + id: Uuid, +} + +#[derive(Object)] +struct NewPasswordCredential { + password: Secret, +} + +impl From for ExistingPasswordCredential { + fn from(credential: PasswordCredential::Model) -> Self { + Self { id: credential.id } + } +} + +#[derive(ApiResponse)] +enum GetPasswordCredentialsResponse { + #[oai(status = 200)] + Ok(Json>), +} + +#[derive(ApiResponse)] +enum CreatePasswordCredentialResponse { + #[oai(status = 201)] + Created(Json), +} + +pub struct ListApi; + +#[OpenApi] +impl ListApi { + #[oai( + path = "/users/:user_id/credentials/passwords", + method = "get", + operation_id = "get_password_credentials" + )] + async fn api_get_all( + &self, + db: Data<&Arc>>, + user_id: Path, + _auth: TokenSecurityScheme, + ) -> poem::Result { + let db = db.lock().await; + + let objects = PasswordCredential::Entity::find() + .filter(PasswordCredential::Column::UserId.eq(*user_id)) + .all(&*db) + .await + .map_err(poem::error::InternalServerError)?; + + Ok(GetPasswordCredentialsResponse::Ok(Json( + objects.into_iter().map(Into::into).collect(), + ))) + } + + #[oai( + path = "/users/:user_id/credentials/passwords", + method = "post", + operation_id = "create_password_credential" + )] + async fn api_create( + &self, + db: Data<&Arc>>, + body: Json, + user_id: Path, + _auth: TokenSecurityScheme, + ) -> poem::Result { + let db = db.lock().await; + + let object = PasswordCredential::ActiveModel { + id: Set(Uuid::new_v4()), + user_id: Set(*user_id), + ..PasswordCredential::ActiveModel::from(UserPasswordCredential::from_password( + &body.password, + )) + } + .insert(&*db) + .await + .map_err(WarpgateError::from)?; + + Ok(CreatePasswordCredentialResponse::Created(Json( + object.into(), + ))) + } +} + +#[derive(ApiResponse)] +enum DeleteCredentialResponse { + #[oai(status = 204)] + Deleted, + #[oai(status = 404)] + NotFound, +} + +pub struct DetailApi; + +#[OpenApi] +impl DetailApi { + #[oai( + path = "/users/:user_id/credentials/passwords/:id", + method = "delete", + operation_id = "delete_password_credential" + )] + async fn api_delete( + &self, + db: Data<&Arc>>, + user_id: Path, + id: Path, + _auth: TokenSecurityScheme, + ) -> poem::Result { + let db = db.lock().await; + + let Some(model) = PasswordCredential::Entity::find_by_id(id.0) + .filter(PasswordCredential::Column::UserId.eq(*user_id)) + .one(&*db) + .await + .map_err(poem::error::InternalServerError)? + else { + return Ok(DeleteCredentialResponse::NotFound); + }; + + model + .delete(&*db) + .await + .map_err(poem::error::InternalServerError)?; + Ok(DeleteCredentialResponse::Deleted) + } +} diff --git a/warpgate-admin/src/api/public_key_credentials.rs b/warpgate-admin/src/api/public_key_credentials.rs new file mode 100644 index 000000000..1b29bb1dd --- /dev/null +++ b/warpgate-admin/src/api/public_key_credentials.rs @@ -0,0 +1,195 @@ +use std::sync::Arc; + +use poem::web::Data; +use poem_openapi::param::Path; +use poem_openapi::payload::Json; +use poem_openapi::{ApiResponse, Object, OpenApi}; +use sea_orm::{ + ActiveModelTrait, ColumnTrait, DatabaseConnection, DbErr, EntityTrait, ModelTrait, QueryFilter, + Set, +}; +use tokio::sync::Mutex; +use uuid::Uuid; +use warpgate_common::{UserPublicKeyCredential, WarpgateError}; +use warpgate_db_entities::PublicKeyCredential; + +use super::TokenSecurityScheme; + +#[derive(Object)] +struct ExistingPublicKeyCredential { + id: Uuid, + openssh_public_key: String, +} + +#[derive(Object)] +struct NewPublicKeyCredential { + openssh_public_key: String, +} + +impl From for ExistingPublicKeyCredential { + fn from(credential: PublicKeyCredential::Model) -> Self { + Self { + id: credential.id, + openssh_public_key: credential.openssh_public_key, + } + } +} + +impl From<&NewPublicKeyCredential> for UserPublicKeyCredential { + fn from(credential: &NewPublicKeyCredential) -> Self { + Self { + key: credential.openssh_public_key.clone().into(), + } + } +} + +#[derive(ApiResponse)] +enum GetPublicKeyCredentialsResponse { + #[oai(status = 200)] + Ok(Json>), +} + +#[derive(ApiResponse)] +enum CreatePublicKeyCredentialResponse { + #[oai(status = 201)] + Created(Json), +} + +#[derive(ApiResponse)] +enum UpdatePublicKeyCredentialResponse { + #[oai(status = 200)] + Updated(Json), + #[oai(status = 404)] + NotFound, +} + +pub struct ListApi; + +#[OpenApi] +impl ListApi { + #[oai( + path = "/users/:user_id/credentials/public-keys", + method = "get", + operation_id = "get_public_key_credentials" + )] + async fn api_get_all( + &self, + db: Data<&Arc>>, + user_id: Path, + _auth: TokenSecurityScheme, + ) -> poem::Result { + let db = db.lock().await; + + let objects = PublicKeyCredential::Entity::find() + .filter(PublicKeyCredential::Column::UserId.eq(*user_id)) + .all(&*db) + .await + .map_err(poem::error::InternalServerError)?; + + Ok(GetPublicKeyCredentialsResponse::Ok(Json( + objects.into_iter().map(Into::into).collect(), + ))) + } + + #[oai( + path = "/users/:user_id/credentials/public-keys", + method = "post", + operation_id = "create_public_key_credential" + )] + async fn api_create( + &self, + db: Data<&Arc>>, + body: Json, + user_id: Path, + _auth: TokenSecurityScheme, + ) -> poem::Result { + let db = db.lock().await; + + let object = PublicKeyCredential::ActiveModel { + id: Set(Uuid::new_v4()), + user_id: Set(*user_id), + ..PublicKeyCredential::ActiveModel::from(UserPublicKeyCredential::from(&*body)) + } + .insert(&*db) + .await + .map_err(WarpgateError::from)?; + + Ok(CreatePublicKeyCredentialResponse::Created(Json( + object.into(), + ))) + } +} + +#[derive(ApiResponse)] +enum DeleteCredentialResponse { + #[oai(status = 204)] + Deleted, + #[oai(status = 404)] + NotFound, +} + +pub struct DetailApi; + +#[OpenApi] +impl DetailApi { + #[oai( + path = "/users/:user_id/credentials/public-keys/:id", + method = "put", + operation_id = "update_public_key_credential" + )] + async fn api_update( + &self, + db: Data<&Arc>>, + body: Json, + user_id: Path, + id: Path, + _auth: TokenSecurityScheme, + ) -> poem::Result { + let db = db.lock().await; + + let model = PublicKeyCredential::ActiveModel { + id: Set(id.0), + user_id: Set(*user_id), + ..<_>::from(UserPublicKeyCredential::from(&*body)) + } + .update(&*db) + .await; + + match model { + Ok(model) => Ok(UpdatePublicKeyCredentialResponse::Updated(Json( + model.into(), + ))), + Err(DbErr::RecordNotFound(_)) => Ok(UpdatePublicKeyCredentialResponse::NotFound), + Err(e) => Err(poem::error::InternalServerError(e)), + } + } + + #[oai( + path = "/users/:user_id/credentials/public-keys/:id", + method = "delete", + operation_id = "delete_public_key_credential" + )] + async fn api_delete( + &self, + db: Data<&Arc>>, + user_id: Path, + id: Path, + _auth: TokenSecurityScheme, + ) -> poem::Result { + let db = db.lock().await; + + let Some(role) = PublicKeyCredential::Entity::find_by_id(id.0) + .filter(PublicKeyCredential::Column::UserId.eq(*user_id)) + .one(&*db) + .await + .map_err(poem::error::InternalServerError)? + else { + return Ok(DeleteCredentialResponse::NotFound); + }; + + role.delete(&*db) + .await + .map_err(poem::error::InternalServerError)?; + Ok(DeleteCredentialResponse::Deleted) + } +} diff --git a/warpgate-admin/src/api/recordings_detail.rs b/warpgate-admin/src/api/recordings_detail.rs index 3dac4f8e3..23d87ed63 100644 --- a/warpgate-admin/src/api/recordings_detail.rs +++ b/warpgate-admin/src/api/recordings_detail.rs @@ -19,6 +19,8 @@ use uuid::Uuid; use warpgate_core::recordings::{AsciiCast, SessionRecordings, TerminalRecordingItem}; use warpgate_db_entities::Recording::{self, RecordingKind}; +use super::TokenSecurityScheme; + pub struct Api; #[derive(ApiResponse)] @@ -40,6 +42,7 @@ impl Api { &self, db: Data<&Arc>>, id: Path, + _auth: TokenSecurityScheme, ) -> poem::Result { let db = db.lock().await; diff --git a/warpgate-admin/src/api/roles.rs b/warpgate-admin/src/api/roles.rs index 54a05977f..e94a145a1 100644 --- a/warpgate-admin/src/api/roles.rs +++ b/warpgate-admin/src/api/roles.rs @@ -14,6 +14,8 @@ use warpgate_common::{Role as RoleConfig, WarpgateError}; use warpgate_core::consts::BUILTIN_ADMIN_ROLE_NAME; use warpgate_db_entities::Role; +use super::TokenSecurityScheme; + #[derive(Object)] struct RoleDataRequest { name: String, @@ -42,6 +44,7 @@ impl ListApi { &self, db: Data<&Arc>>, search: Query>, + _auth: TokenSecurityScheme, ) -> poem::Result { let db = db.lock().await; @@ -67,6 +70,7 @@ impl ListApi { &self, db: Data<&Arc>>, body: Json, + _auth: TokenSecurityScheme, ) -> poem::Result { use warpgate_db_entities::Role; @@ -124,6 +128,7 @@ impl DetailApi { &self, db: Data<&Arc>>, id: Path, + _auth: TokenSecurityScheme, ) -> poem::Result { let db = db.lock().await; @@ -144,6 +149,7 @@ impl DetailApi { db: Data<&Arc>>, body: Json, id: Path, + _auth: TokenSecurityScheme, ) -> poem::Result { let db = db.lock().await; @@ -174,6 +180,7 @@ impl DetailApi { &self, db: Data<&Arc>>, id: Path, + _auth: TokenSecurityScheme, ) -> poem::Result { let db = db.lock().await; diff --git a/warpgate-admin/src/api/sessions_detail.rs b/warpgate-admin/src/api/sessions_detail.rs index cc84fc58b..e42318ff1 100644 --- a/warpgate-admin/src/api/sessions_detail.rs +++ b/warpgate-admin/src/api/sessions_detail.rs @@ -10,6 +10,8 @@ use uuid::Uuid; use warpgate_core::{SessionSnapshot, State}; use warpgate_db_entities::{Recording, Session}; +use super::TokenSecurityScheme; + pub struct Api; #[allow(clippy::large_enum_variant)] @@ -42,6 +44,7 @@ impl Api { &self, db: Data<&Arc>>, id: Path, + _auth: TokenSecurityScheme, ) -> poem::Result { let db = db.lock().await; @@ -65,6 +68,7 @@ impl Api { &self, db: Data<&Arc>>, id: Path, + _auth: TokenSecurityScheme, ) -> poem::Result { let db = db.lock().await; let recordings: Vec = Recording::Entity::find() @@ -85,6 +89,7 @@ impl Api { &self, state: Data<&Arc>>, id: Path, + _auth: TokenSecurityScheme, ) -> poem::Result { let state = state.lock().await; diff --git a/warpgate-admin/src/api/sessions_list.rs b/warpgate-admin/src/api/sessions_list.rs index 6af56ad47..dddc700a3 100644 --- a/warpgate-admin/src/api/sessions_list.rs +++ b/warpgate-admin/src/api/sessions_list.rs @@ -13,6 +13,7 @@ use tokio::sync::Mutex; use warpgate_core::{SessionSnapshot, State}; use super::pagination::{PaginatedResponse, PaginationParams}; +use super::TokenSecurityScheme; pub struct Api; @@ -38,6 +39,7 @@ impl Api { limit: Query>, active_only: Query>, logged_in_only: Query>, + _auth: TokenSecurityScheme, ) -> poem::Result { use warpgate_db_entities::Session; @@ -74,6 +76,7 @@ impl Api { &self, state: Data<&Arc>>, session: &Session, + _auth: TokenSecurityScheme, ) -> poem::Result { let state = state.lock().await; diff --git a/warpgate-admin/src/api/ssh_keys.rs b/warpgate-admin/src/api/ssh_keys.rs index 2639817ab..217659f09 100644 --- a/warpgate-admin/src/api/ssh_keys.rs +++ b/warpgate-admin/src/api/ssh_keys.rs @@ -8,6 +8,8 @@ use serde::Serialize; use tokio::sync::Mutex; use warpgate_common::WarpgateConfig; +use super::TokenSecurityScheme; + pub struct Api; #[derive(Serialize, Object)] @@ -32,6 +34,7 @@ impl Api { async fn api_ssh_get_own_keys( &self, config: Data<&Arc>>, + _auth: TokenSecurityScheme, ) -> poem::Result { let config = config.lock().await; let keys = warpgate_protocol_ssh::load_client_keys(&config) diff --git a/warpgate-admin/src/api/sso_credentials.rs b/warpgate-admin/src/api/sso_credentials.rs new file mode 100644 index 000000000..40c4034c4 --- /dev/null +++ b/warpgate-admin/src/api/sso_credentials.rs @@ -0,0 +1,195 @@ +use std::sync::Arc; + +use poem::web::Data; +use poem_openapi::param::Path; +use poem_openapi::payload::Json; +use poem_openapi::{ApiResponse, Object, OpenApi}; +use sea_orm::{ + ActiveModelTrait, ColumnTrait, DatabaseConnection, DbErr, EntityTrait, ModelTrait, QueryFilter, + Set, +}; +use tokio::sync::Mutex; +use uuid::Uuid; +use warpgate_common::{UserSsoCredential, WarpgateError}; +use warpgate_db_entities::SsoCredential; + +use super::TokenSecurityScheme; + +#[derive(Object)] +struct ExistingSsoCredential { + id: Uuid, + provider: Option, + email: String, +} + +#[derive(Object)] +struct NewSsoCredential { + provider: Option, + email: String, +} + +impl From for ExistingSsoCredential { + fn from(credential: SsoCredential::Model) -> Self { + Self { + id: credential.id, + email: credential.email, + provider: credential.provider, + } + } +} + +impl From<&NewSsoCredential> for UserSsoCredential { + fn from(credential: &NewSsoCredential) -> Self { + Self { + email: credential.email.clone(), + provider: credential.provider.clone(), + } + } +} + +#[derive(ApiResponse)] +enum GetSsoCredentialsResponse { + #[oai(status = 200)] + Ok(Json>), +} + +#[derive(ApiResponse)] +enum CreateSsoCredentialResponse { + #[oai(status = 201)] + Created(Json), +} + +#[derive(ApiResponse)] +enum UpdateSsoCredentialResponse { + #[oai(status = 200)] + Updated(Json), + #[oai(status = 404)] + NotFound, +} + +pub struct ListApi; + +#[OpenApi] +impl ListApi { + #[oai( + path = "/users/:user_id/credentials/sso", + method = "get", + operation_id = "get_sso_credentials" + )] + async fn api_get_all( + &self, + db: Data<&Arc>>, + user_id: Path, + _auth: TokenSecurityScheme, + ) -> poem::Result { + let db = db.lock().await; + + let objects = SsoCredential::Entity::find() + .filter(SsoCredential::Column::UserId.eq(*user_id)) + .all(&*db) + .await + .map_err(poem::error::InternalServerError)?; + + Ok(GetSsoCredentialsResponse::Ok(Json( + objects.into_iter().map(Into::into).collect(), + ))) + } + + #[oai( + path = "/users/:user_id/credentials/sso", + method = "post", + operation_id = "create_sso_credential" + )] + async fn api_create( + &self, + db: Data<&Arc>>, + body: Json, + user_id: Path, + _auth: TokenSecurityScheme, + ) -> poem::Result { + let db = db.lock().await; + + let object = SsoCredential::ActiveModel { + id: Set(Uuid::new_v4()), + user_id: Set(*user_id), + ..SsoCredential::ActiveModel::from(UserSsoCredential::from(&*body)) + } + .insert(&*db) + .await + .map_err(WarpgateError::from)?; + + Ok(CreateSsoCredentialResponse::Created(Json(object.into()))) + } +} + +#[derive(ApiResponse)] +enum DeleteCredentialResponse { + #[oai(status = 204)] + Deleted, + #[oai(status = 404)] + NotFound, +} + +pub struct DetailApi; + +#[OpenApi] +impl DetailApi { + #[oai( + path = "/users/:user_id/credentials/sso/:id", + method = "put", + operation_id = "update_sso_credential" + )] + async fn api_update( + &self, + db: Data<&Arc>>, + body: Json, + user_id: Path, + id: Path, + _auth: TokenSecurityScheme, + ) -> poem::Result { + let db = db.lock().await; + + let model = SsoCredential::ActiveModel { + id: Set(id.0), + user_id: Set(*user_id), + ..<_>::from(UserSsoCredential::from(&*body)) + } + .update(&*db) + .await; + + match model { + Ok(model) => Ok(UpdateSsoCredentialResponse::Updated(Json(model.into()))), + Err(DbErr::RecordNotFound(_)) => Ok(UpdateSsoCredentialResponse::NotFound), + Err(e) => Err(poem::error::InternalServerError(e)), + } + } + + #[oai( + path = "/users/:user_id/credentials/sso/:id", + method = "delete", + operation_id = "delete_sso_credential" + )] + async fn api_delete( + &self, + db: Data<&Arc>>, + user_id: Path, + id: Path, + _auth: TokenSecurityScheme, + ) -> poem::Result { + let db = db.lock().await; + + let Some(role) = SsoCredential::Entity::find_by_id(id.0) + .filter(SsoCredential::Column::UserId.eq(*user_id)) + .one(&*db) + .await + .map_err(poem::error::InternalServerError)? + else { + return Ok(DeleteCredentialResponse::NotFound); + }; + + role.delete(&*db) + .await + .map_err(poem::error::InternalServerError)?; + Ok(DeleteCredentialResponse::Deleted) + } +} diff --git a/warpgate-admin/src/api/targets.rs b/warpgate-admin/src/api/targets.rs index bc6e5dd71..aeb70bc55 100644 --- a/warpgate-admin/src/api/targets.rs +++ b/warpgate-admin/src/api/targets.rs @@ -15,6 +15,8 @@ use warpgate_core::consts::BUILTIN_ADMIN_ROLE_NAME; use warpgate_db_entities::Target::TargetKind; use warpgate_db_entities::{Role, Target, TargetRoleAssignment}; +use super::TokenSecurityScheme; + #[derive(Object)] struct TargetDataRequest { name: String, @@ -44,6 +46,7 @@ impl ListApi { &self, db: Data<&Arc>>, search: Query>, + _auth: TokenSecurityScheme, ) -> poem::Result { let db = db.lock().await; @@ -68,6 +71,7 @@ impl ListApi { &self, db: Data<&Arc>>, body: Json, + _auth: TokenSecurityScheme, ) -> poem::Result { if body.name.is_empty() { return Ok(CreateTargetResponse::BadRequest(Json("name".into()))); @@ -133,6 +137,7 @@ impl DetailApi { &self, db: Data<&Arc>>, id: Path, + _auth: TokenSecurityScheme, ) -> poem::Result { let db = db.lock().await; @@ -157,6 +162,7 @@ impl DetailApi { db: Data<&Arc>>, body: Json, id: Path, + _auth: TokenSecurityScheme, ) -> poem::Result { let db = db.lock().await; @@ -195,6 +201,7 @@ impl DetailApi { &self, db: Data<&Arc>>, id: Path, + _auth: TokenSecurityScheme, ) -> poem::Result { let db = db.lock().await; @@ -263,6 +270,7 @@ impl RolesApi { &self, db: Data<&Arc>>, id: Path, + _auth: TokenSecurityScheme, ) -> poem::Result { let db = db.lock().await; @@ -291,6 +299,7 @@ impl RolesApi { db: Data<&Arc>>, id: Path, role_id: Path, + _auth: TokenSecurityScheme, ) -> poem::Result { let db = db.lock().await; @@ -326,6 +335,7 @@ impl RolesApi { db: Data<&Arc>>, id: Path, role_id: Path, + _auth: TokenSecurityScheme, ) -> poem::Result { let db = db.lock().await; diff --git a/warpgate-admin/src/api/tickets_detail.rs b/warpgate-admin/src/api/tickets_detail.rs index 13d6311ff..109514537 100644 --- a/warpgate-admin/src/api/tickets_detail.rs +++ b/warpgate-admin/src/api/tickets_detail.rs @@ -7,6 +7,8 @@ use sea_orm::{DatabaseConnection, EntityTrait, ModelTrait}; use tokio::sync::Mutex; use uuid::Uuid; +use super::TokenSecurityScheme; + pub struct Api; #[derive(ApiResponse)] @@ -29,6 +31,7 @@ impl Api { &self, db: Data<&Arc>>, id: Path, + _auth: TokenSecurityScheme, ) -> poem::Result { use warpgate_db_entities::Ticket; let db = db.lock().await; diff --git a/warpgate-admin/src/api/tickets_list.rs b/warpgate-admin/src/api/tickets_list.rs index 5e3ee8bc9..f2c09b8b9 100644 --- a/warpgate-admin/src/api/tickets_list.rs +++ b/warpgate-admin/src/api/tickets_list.rs @@ -12,6 +12,8 @@ use uuid::Uuid; use warpgate_common::helpers::hash::generate_ticket_secret; use warpgate_db_entities::Ticket; +use super::TokenSecurityScheme; + pub struct Api; #[derive(ApiResponse)] @@ -49,6 +51,7 @@ impl Api { async fn api_get_all_tickets( &self, db: Data<&Arc>>, + _auth: TokenSecurityScheme, ) -> poem::Result { use warpgate_db_entities::Ticket; @@ -69,6 +72,7 @@ impl Api { &self, db: Data<&Arc>>, body: Json, + _auth: TokenSecurityScheme, ) -> poem::Result { use warpgate_db_entities::Ticket; diff --git a/warpgate-admin/src/api/users.rs b/warpgate-admin/src/api/users.rs index aa1827f83..bb5267109 100644 --- a/warpgate-admin/src/api/users.rs +++ b/warpgate-admin/src/api/users.rs @@ -10,17 +10,20 @@ use sea_orm::{ }; use tokio::sync::Mutex; use uuid::Uuid; -use warpgate_common::helpers::hash::{hash_password, parse_hash}; use warpgate_common::{ - Role as RoleConfig, User as UserConfig, UserAuthCredential, UserRequireCredentialsPolicy, - WarpgateError, + Role as RoleConfig, User as UserConfig, UserRequireCredentialsPolicy, WarpgateError, }; use warpgate_db_entities::{Role, User, UserRoleAssignment}; +use super::TokenSecurityScheme; + +#[derive(Object)] +struct CreateUserRequest { + username: String, +} #[derive(Object)] struct UserDataRequest { username: String, - credentials: Vec, credential_policy: Option, } @@ -47,6 +50,7 @@ impl ListApi { &self, db: Data<&Arc>>, search: Query>, + _auth: TokenSecurityScheme, ) -> poem::Result { let db = db.lock().await; @@ -69,7 +73,8 @@ impl ListApi { async fn api_create_user( &self, db: Data<&Arc>>, - body: Json, + body: Json, + _auth: TokenSecurityScheme, ) -> poem::Result { if body.username.is_empty() { return Ok(CreateUserResponse::BadRequest(Json("name".into()))); @@ -80,10 +85,10 @@ impl ListApi { let values = User::ActiveModel { id: Set(Uuid::new_v4()), username: Set(body.username.clone()), - credentials: Set(serde_json::to_value(process_credentials(&body.credentials)) - .map_err(WarpgateError::from)?), - credential_policy: Set(serde_json::to_value(body.credential_policy.clone()) - .map_err(WarpgateError::from)?), + credential_policy: Set( + serde_json::to_value(UserRequireCredentialsPolicy::default()) + .map_err(WarpgateError::from)?, + ), }; let user = values.insert(&*db).await.map_err(WarpgateError::from)?; @@ -128,6 +133,7 @@ impl DetailApi { &self, db: Data<&Arc>>, id: Path, + _auth: TokenSecurityScheme, ) -> poem::Result { let db = db.lock().await; @@ -150,6 +156,7 @@ impl DetailApi { db: Data<&Arc>>, body: Json, id: Path, + _auth: TokenSecurityScheme, ) -> poem::Result { let db = db.lock().await; @@ -161,19 +168,8 @@ impl DetailApi { return Ok(UpdateUserResponse::NotFound); }; - let mut credentials = body.credentials.clone(); - for credential in credentials.iter_mut() { - if let UserAuthCredential::Password(ref mut c) = credential { - if parse_hash(c.hash.expose_secret()).is_err() { - c.hash = hash_password(c.hash.expose_secret()).into(); - } - } - } - let mut model: User::ActiveModel = user.into(); model.username = Set(body.username.clone()); - model.credentials = Set(serde_json::to_value(process_credentials(&body.credentials)) - .map_err(WarpgateError::from)?); model.credential_policy = Set(serde_json::to_value(body.credential_policy.clone()) .map_err(WarpgateError::from)?); @@ -192,6 +188,7 @@ impl DetailApi { &self, db: Data<&Arc>>, id: Path, + _auth: TokenSecurityScheme, ) -> poem::Result { let db = db.lock().await; @@ -216,18 +213,6 @@ impl DetailApi { } } -fn process_credentials(credentials: &[UserAuthCredential]) -> Vec { - let mut credentials: Vec = credentials.into(); - for credential in credentials.iter_mut() { - if let UserAuthCredential::Password(ref mut c) = credential { - if parse_hash(c.hash.expose_secret()).is_err() { - c.hash = hash_password(c.hash.expose_secret()).into(); - } - } - } - credentials -} - #[derive(ApiResponse)] enum GetUserRolesResponse { #[oai(status = 200)] @@ -265,6 +250,7 @@ impl RolesApi { &self, db: Data<&Arc>>, id: Path, + _auth: TokenSecurityScheme, ) -> poem::Result { let db = db.lock().await; @@ -293,6 +279,7 @@ impl RolesApi { db: Data<&Arc>>, id: Path, role_id: Path, + _auth: TokenSecurityScheme, ) -> poem::Result { let db = db.lock().await; @@ -328,6 +315,7 @@ impl RolesApi { db: Data<&Arc>>, id: Path, role_id: Path, + _auth: TokenSecurityScheme, ) -> poem::Result { let db = db.lock().await; diff --git a/warpgate-common/Cargo.toml b/warpgate-common/Cargo.toml index fa340b4e4..684c1a32d 100644 --- a/warpgate-common/Cargo.toml +++ b/warpgate-common/Cargo.toml @@ -8,9 +8,9 @@ version = "0.11.0" anyhow = "1.0" argon2 = "0.4" async-trait = "0.1" -bytes = "1.4" +bytes.workspace = true chrono = { version = "0.4", default-features = false, features = ["serde"] } -data-encoding = "2.3" +data-encoding.workspace = true delegate = "0.6" humantime-serde = "1.1" futures = "0.3" @@ -31,8 +31,8 @@ sea-orm = { version = "0.12.2", features = [ "runtime-tokio-rustls", "macros", ], default-features = false } -serde = "1.0" -serde_json = "1.0" +serde.workspace = true +serde_json.workspace = true thiserror = "1.0" tokio = { version = "1.20", features = ["tracing"] } tokio-rustls = "0.26" diff --git a/warpgate-common/src/config/mod.rs b/warpgate-common/src/config/mod.rs index 861b9dc78..c3a669e40 100644 --- a/warpgate-common/src/config/mod.rs +++ b/warpgate-common/src/config/mod.rs @@ -1,6 +1,7 @@ mod defaults; mod target; +use std::ops::Deref; use std::path::PathBuf; use std::time::Duration; @@ -16,6 +17,7 @@ use uuid::Uuid; use warpgate_sso::SsoProviderConfig; use crate::auth::CredentialKind; +use crate::helpers::hash::hash_password; use crate::helpers::otp::OtpSecretKey; use crate::{ListenEndpoint, Secret, WarpgateError}; @@ -37,6 +39,15 @@ pub enum UserAuthCredential { pub struct UserPasswordCredential { pub hash: Secret, } + +impl UserPasswordCredential { + pub fn from_password(password: &Secret) -> Self { + Self { + hash: Secret::new(hash_password(password.expose_secret())), + } + } +} + #[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, Object)] pub struct UserPublicKeyCredential { pub key: Secret, @@ -80,12 +91,25 @@ pub struct User { #[serde(default)] pub id: Uuid, pub username: String, - pub credentials: Vec, #[serde(skip_serializing_if = "Option::is_none", rename = "require")] pub credential_policy: Option, +} + +#[derive(Debug, Deserialize, Serialize, Clone, Object)] +pub struct UserDetails { + pub inner: User, + pub credentials: Vec, pub roles: Vec, } +impl Deref for UserDetails { + type Target = User; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + #[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, Hash, Object)] pub struct Role { #[serde(default)] @@ -315,18 +339,6 @@ pub enum ConfigProviderKind { #[derive(Debug, Deserialize, Serialize, Clone)] pub struct WarpgateConfigStore { - #[serde(default)] - #[serde(skip_serializing_if = "Vec::is_empty")] - pub targets: Vec, - - #[serde(default)] - #[serde(skip_serializing_if = "Vec::is_empty")] - pub users: Vec, - - #[serde(default)] - #[serde(skip_serializing_if = "Vec::is_empty")] - pub roles: Vec, - #[serde(default)] pub sso_providers: Vec, @@ -361,9 +373,6 @@ pub struct WarpgateConfigStore { impl Default for WarpgateConfigStore { fn default() -> Self { Self { - targets: vec![], - users: vec![], - roles: vec![], sso_providers: vec![], recordings: <_>::default(), external_host: None, diff --git a/warpgate-common/src/tls/rustls_helpers.rs b/warpgate-common/src/tls/rustls_helpers.rs index b407521a6..edf8b2628 100644 --- a/warpgate-common/src/tls/rustls_helpers.rs +++ b/warpgate-common/src/tls/rustls_helpers.rs @@ -24,9 +24,10 @@ pub async fn configure_tls_connector( accept_invalid_hostnames: bool, root_cert: Option<&[u8]>, ) -> Result { - let config = - ClientConfig::builder_with_provider(Arc::new(rustls::crypto::aws_lc_rs::default_provider())) - .with_safe_default_protocol_versions()?; + let config = ClientConfig::builder_with_provider(Arc::new( + rustls::crypto::aws_lc_rs::default_provider(), + )) + .with_safe_default_protocol_versions()?; let config = if accept_invalid_certs { config diff --git a/warpgate-core/Cargo.toml b/warpgate-core/Cargo.toml index 734b28a7c..823831405 100644 --- a/warpgate-core/Cargo.toml +++ b/warpgate-core/Cargo.toml @@ -12,9 +12,9 @@ warpgate-db-migrations = { version = "*", path = "../warpgate-db-migrations" } anyhow = { version = "1.0", features = ["std"] } argon2 = "0.4" async-trait = "0.1" -bytes = "1.3" +bytes.workspace = true chrono = { version = "0.4", default-features = false, features = ["serde"] } -data-encoding = "2.3" +data-encoding.workspace = true humantime-serde = "1.1" futures = "0.3" once_cell = "1.17" @@ -34,8 +34,8 @@ sea-orm = { version = "0.12", features = [ "runtime-tokio-rustls", "macros", ], default-features = false } -serde = "1.0" -serde_json = "1.0" +serde.workspace = true +serde_json.workspace = true thiserror = "1.0" tokio = { version = "1.20", features = ["tracing"] } totp-rs = { version = "5.0", features = ["otpauth"] } diff --git a/warpgate-core/src/config_providers/db.rs b/warpgate-core/src/config_providers/db.rs index 1464e8eb5..9dea376f6 100644 --- a/warpgate-core/src/config_providers/db.rs +++ b/warpgate-core/src/config_providers/db.rs @@ -16,11 +16,10 @@ use warpgate_common::auth::{ use warpgate_common::helpers::hash::verify_password_hash; use warpgate_common::helpers::otp::verify_totp; use warpgate_common::{ - Role as RoleConfig, Target as TargetConfig, User as UserConfig, UserAuthCredential, - UserPasswordCredential, UserPublicKeyCredential, UserSsoCredential, UserTotpCredential, - WarpgateError, + Role, Target, User, UserAuthCredential, UserPasswordCredential, UserPublicKeyCredential, + UserSsoCredential, UserTotpCredential, WarpgateError, }; -use warpgate_db_entities::{Role, Target, User, UserRoleAssignment}; +use warpgate_db_entities as entities; use super::ConfigProvider; @@ -36,29 +35,28 @@ impl DatabaseConfigProvider { #[async_trait] impl ConfigProvider for DatabaseConfigProvider { - async fn list_users(&mut self) -> Result, WarpgateError> { + async fn list_users(&mut self) -> Result, WarpgateError> { let db = self.db.lock().await; - let users = User::Entity::find() - .order_by_asc(User::Column::Username) + let users = entities::User::Entity::find() + .order_by_asc(entities::User::Column::Username) .all(&*db) .await?; - let users: Result, _> = users.into_iter().map(|t| t.try_into()).collect(); + let users: Result, _> = users.into_iter().map(|t| t.try_into()).collect(); Ok(users?) } - async fn list_targets(&mut self) -> Result, WarpgateError> { + async fn list_targets(&mut self) -> Result, WarpgateError> { let db = self.db.lock().await; - let targets = Target::Entity::find() - .order_by_asc(Target::Column::Name) + let targets = entities::Target::Entity::find() + .order_by_asc(entities::Target::Column::Name) .all(&*db) .await?; - let targets: Result, _> = - targets.into_iter().map(|t| t.try_into()).collect(); + let targets: Result, _> = targets.into_iter().map(|t| t.try_into()).collect(); Ok(targets?) } @@ -70,8 +68,8 @@ impl ConfigProvider for DatabaseConfigProvider { ) -> Result>, WarpgateError> { let db = self.db.lock().await; - let user_model = User::Entity::find() - .filter(User::Column::Username.eq(username)) + let user_model = entities::User::Entity::find() + .filter(entities::User::Column::Username.eq(username)) .one(&*db) .await?; @@ -80,7 +78,7 @@ impl ConfigProvider for DatabaseConfigProvider { return Ok(None); }; - let user: UserConfig = user_model.try_into()?; + let user = user_model.load_details(&db).await?; let supported_credential_types: HashSet = user .credentials @@ -92,7 +90,7 @@ impl ConfigProvider for DatabaseConfigProvider { supported_credential_types: supported_credential_types.clone(), }) as Box; - if let Some(req) = user.credential_policy { + if let Some(req) = user.credential_policy.clone() { let mut policy = PerProtocolCredentialPolicy { default: default_policy, protocols: HashMap::new(), @@ -147,6 +145,8 @@ impl ConfigProvider for DatabaseConfigProvider { &mut self, client_credential: &AuthCredential, ) -> Result, WarpgateError> { + let db = self.db.lock().await; + let AuthCredential::Sso { provider: client_provider, email: client_email, @@ -155,23 +155,23 @@ impl ConfigProvider for DatabaseConfigProvider { return Ok(None); }; - Ok(self - .list_users() + let Some(cred) = entities::SsoCredential::Entity::find() + .filter( + entities::SsoCredential::Column::Email.eq(client_email).and( + entities::SsoCredential::Column::Provider + .eq(client_provider) + .or(entities::SsoCredential::Column::Provider.is_null()), + ), + ) + .one(&*db) .await? - .iter() - .find(|x| { - for cred in x.credentials.iter() { - if let UserAuthCredential::Sso(UserSsoCredential { provider, email }) = cred { - if provider.as_ref().unwrap_or(client_provider) == client_provider - && email == client_email - { - return true; - } - } - } - false - }) - .map(|x| x.username.clone())) + else { + return Ok(None); + }; + + let user = cred.find_related(entities::User::Entity).one(&*db).await?; + + Ok(user.map(|x| x.username)) } async fn validate_credential( @@ -181,8 +181,8 @@ impl ConfigProvider for DatabaseConfigProvider { ) -> Result { let db = self.db.lock().await; - let user_model = User::Entity::find() - .filter(User::Column::Username.eq(username)) + let user_model = entities::User::Entity::find() + .filter(entities::User::Column::Username.eq(username)) .one(&*db) .await?; @@ -191,7 +191,7 @@ impl ConfigProvider for DatabaseConfigProvider { return Ok(false); }; - let user: UserConfig = user_model.try_into()?; + let user_details = user_model.load_details(&db).await?; match client_credential { AuthCredential::PublicKey { @@ -199,48 +199,59 @@ impl ConfigProvider for DatabaseConfigProvider { public_key_bytes, } => { let base64_bytes = BASE64.encode(public_key_bytes); + let openssh_public_key = format!("{kind} {base64_bytes}"); + debug!( + username = &user_details.username[..], + "Client key: {}", openssh_public_key + ); - let client_key = format!("{kind} {base64_bytes}"); - debug!(username = &user.username[..], "Client key: {}", client_key); - - return Ok(user.credentials.iter().any(|credential| match credential { - UserAuthCredential::PublicKey(UserPublicKeyCredential { - key: ref user_key, - }) => &client_key == user_key.expose_secret(), - _ => false, - })); + return Ok(user_details + .credentials + .iter() + .any(|credential| match credential { + UserAuthCredential::PublicKey(UserPublicKeyCredential { + key: ref user_key, + }) => &openssh_public_key == user_key.expose_secret(), + _ => false, + })); } AuthCredential::Password(client_password) => { - return Ok(user.credentials.iter().any(|credential| match credential { - UserAuthCredential::Password(UserPasswordCredential { - hash: ref user_password_hash, - }) => verify_password_hash( - client_password.expose_secret(), - user_password_hash.expose_secret(), - ) - .unwrap_or_else(|e| { - error!( - username = &user.username[..], - "Error verifying password hash: {}", e - ); - false - }), - _ => false, - })) + return Ok(user_details + .credentials + .iter() + .any(|credential| match credential { + UserAuthCredential::Password(UserPasswordCredential { + hash: ref user_password_hash, + }) => verify_password_hash( + client_password.expose_secret(), + user_password_hash.expose_secret(), + ) + .unwrap_or_else(|e| { + error!( + username = &user_details.username[..], + "Error verifying password hash: {}", e + ); + false + }), + _ => false, + })) } AuthCredential::Otp(client_otp) => { - return Ok(user.credentials.iter().any(|credential| match credential { - UserAuthCredential::Totp(UserTotpCredential { - key: ref user_otp_key, - }) => verify_totp(client_otp.expose_secret(), user_otp_key), - _ => false, - })) + return Ok(user_details + .credentials + .iter() + .any(|credential| match credential { + UserAuthCredential::Totp(UserTotpCredential { + key: ref user_otp_key, + }) => verify_totp(client_otp.expose_secret(), user_otp_key), + _ => false, + })) } AuthCredential::Sso { provider: client_provider, email: client_email, } => { - for credential in user.credentials.iter() { + for credential in user_details.credentials.iter() { if let UserAuthCredential::Sso(UserSsoCredential { ref provider, ref email, @@ -266,13 +277,13 @@ impl ConfigProvider for DatabaseConfigProvider { ) -> Result { let db = self.db.lock().await; - let target_model: Option = Target::Entity::find() - .filter(Target::Column::Name.eq(target_name)) + let target_model = entities::Target::Entity::find() + .filter(entities::Target::Column::Name.eq(target_name)) .one(&*db) .await?; - let user_model = User::Entity::find() - .filter(User::Column::Username.eq(username)) + let user_model = entities::User::Entity::find() + .filter(entities::User::Column::Username.eq(username)) .one(&*db) .await?; @@ -287,20 +298,20 @@ impl ConfigProvider for DatabaseConfigProvider { }; let target_roles: HashSet = target_model - .find_related(Role::Entity) + .find_related(entities::Role::Entity) .all(&*db) .await? .into_iter() - .map(Into::::into) + .map(Into::::into) .map(|x| x.name) .collect(); let user_roles: HashSet = user_model - .find_related(Role::Entity) + .find_related(entities::Role::Entity) .all(&*db) .await? .into_iter() - .map(Into::::into) + .map(Into::::into) .map(|x| x.name) .collect(); @@ -317,8 +328,8 @@ impl ConfigProvider for DatabaseConfigProvider { ) -> Result<(), WarpgateError> { let db = self.db.lock().await; - let user = User::Entity::find() - .filter(User::Column::Username.eq(username)) + let user = entities::User::Entity::find() + .filter(entities::User::Column::Username.eq(username)) .one(&*db) .await .map_err(WarpgateError::from)? @@ -326,7 +337,7 @@ impl ConfigProvider for DatabaseConfigProvider { let managed_role_names = match managed_role_names { Some(x) => x, - None => Role::Entity::find() + None => entities::Role::Entity::find() .all(&*db) .await? .into_iter() @@ -335,16 +346,16 @@ impl ConfigProvider for DatabaseConfigProvider { }; for role_name in managed_role_names.into_iter() { - let role = Role::Entity::find() - .filter(Role::Column::Name.eq(role_name.clone())) + let role = entities::Role::Entity::find() + .filter(entities::Role::Column::Name.eq(role_name.clone())) .one(&*db) .await .map_err(WarpgateError::from)? .ok_or_else(|| WarpgateError::RoleNotFound(role_name.clone()))?; - let assignment = UserRoleAssignment::Entity::find() - .filter(UserRoleAssignment::Column::UserId.eq(user.id)) - .filter(UserRoleAssignment::Column::RoleId.eq(role.id)) + let assignment = entities::UserRoleAssignment::Entity::find() + .filter(entities::UserRoleAssignment::Column::UserId.eq(user.id)) + .filter(entities::UserRoleAssignment::Column::RoleId.eq(role.id)) .one(&*db) .await .map_err(WarpgateError::from)?; @@ -352,7 +363,7 @@ impl ConfigProvider for DatabaseConfigProvider { match (assignment, assigned_role_names.contains(&role_name)) { (None, true) => { info!("Adding role {role_name} for user {username} (from SSO)"); - let values = UserRoleAssignment::ActiveModel { + let values = entities::UserRoleAssignment::ActiveModel { user_id: Set(user.id), role_id: Set(role.id), ..Default::default() diff --git a/warpgate-core/src/config_providers/file.rs b/warpgate-core/src/config_providers/file.rs deleted file mode 100644 index 763c5e552..000000000 --- a/warpgate-core/src/config_providers/file.rs +++ /dev/null @@ -1,308 +0,0 @@ -use std::collections::{HashMap, HashSet}; -use std::sync::Arc; - -use async_trait::async_trait; -use data_encoding::BASE64; -use tokio::sync::Mutex; -use tracing::*; -use warpgate_common::auth::{ - AllCredentialsPolicy, AnySingleCredentialPolicy, AuthCredential, CredentialKind, - CredentialPolicy, PerProtocolCredentialPolicy, -}; -use warpgate_common::helpers::hash::verify_password_hash; -use warpgate_common::helpers::otp::verify_totp; -use warpgate_common::{ - Target, User, UserAuthCredential, UserPasswordCredential, UserPublicKeyCredential, - UserSsoCredential, UserTotpCredential, WarpgateConfig, WarpgateError, -}; - -use super::ConfigProvider; - -pub struct FileConfigProvider { - config: Arc>, -} - -impl FileConfigProvider { - pub async fn new(config: &Arc>) -> Self { - Self { - config: config.clone(), - } - } -} - -#[async_trait] -impl ConfigProvider for FileConfigProvider { - async fn list_users(&mut self) -> Result, WarpgateError> { - Ok(self - .config - .lock() - .await - .store - .users - .iter() - .map(Clone::clone) - .collect::>()) - } - - async fn list_targets(&mut self) -> Result, WarpgateError> { - Ok(self - .config - .lock() - .await - .store - .targets - .iter() - .map(|x| x.to_owned()) - .collect::>()) - } - - async fn get_credential_policy( - &mut self, - username: &str, - supported_credential_types: &[CredentialKind], - ) -> Result>, WarpgateError> { - let user = { - self.config - .lock() - .await - .store - .users - .iter() - .find(|x| x.username == username) - .map(User::to_owned) - }; - let Some(user) = user else { - error!("Selected user not found: {}", username); - return Ok(None); - }; - - let supported_credential_types: HashSet = user - .credentials - .iter() - .map(|x| x.kind()) - .filter(|x| supported_credential_types.contains(x)) - .collect(); - let default_policy = Box::new(AnySingleCredentialPolicy { - supported_credential_types: supported_credential_types.clone(), - }) as Box; - - if let Some(req) = user.credential_policy { - let mut policy = PerProtocolCredentialPolicy { - default: default_policy, - protocols: HashMap::new(), - }; - - if let Some(p) = req.http { - policy.protocols.insert( - "HTTP", - Box::new(AllCredentialsPolicy { - supported_credential_types: supported_credential_types.clone(), - required_credential_types: p.into_iter().collect(), - }), - ); - } - if let Some(p) = req.mysql { - policy.protocols.insert( - "MySQL", - Box::new(AllCredentialsPolicy { - supported_credential_types: supported_credential_types.clone(), - required_credential_types: p.into_iter().collect(), - }), - ); - } - if let Some(p) = req.postgres { - policy.protocols.insert( - "PostgreSQL", - Box::new(AllCredentialsPolicy { - supported_credential_types: supported_credential_types.clone(), - required_credential_types: p.into_iter().collect(), - }), - ); - } - if let Some(p) = req.ssh { - policy.protocols.insert( - "SSH", - Box::new(AllCredentialsPolicy { - supported_credential_types, - required_credential_types: p.into_iter().collect(), - }), - ); - } - - Ok(Some( - Box::new(policy) as Box - )) - } else { - Ok(Some(default_policy)) - } - } - - async fn username_for_sso_credential( - &mut self, - client_credential: &AuthCredential, - ) -> Result, WarpgateError> { - let AuthCredential::Sso { - provider: client_provider, - email: client_email, - } = client_credential - else { - return Ok(None); - }; - - Ok(self - .config - .lock() - .await - .store - .users - .iter() - .find(|x| { - for cred in x.credentials.iter() { - if let UserAuthCredential::Sso(UserSsoCredential { provider, email }) = cred { - if provider.as_ref().unwrap_or(client_provider) == client_provider - && email == client_email - { - return true; - } - } - } - false - }) - .map(|x| x.username.clone())) - } - - async fn validate_credential( - &mut self, - username: &str, - client_credential: &AuthCredential, - ) -> Result { - let user = { - self.config - .lock() - .await - .store - .users - .iter() - .find(|x| x.username == username) - .map(User::to_owned) - }; - let Some(user) = user else { - error!("Selected user not found: {}", username); - return Ok(false); - }; - - match client_credential { - AuthCredential::PublicKey { - kind, - public_key_bytes, - } => { - let base64_bytes = BASE64.encode(public_key_bytes); - - let client_key = format!("{kind} {base64_bytes}"); - debug!(username = &user.username[..], "Client key: {}", client_key); - - return Ok(user.credentials.iter().any(|credential| match credential { - UserAuthCredential::PublicKey(UserPublicKeyCredential { - key: ref user_key, - }) => &client_key == user_key.expose_secret(), - _ => false, - })); - } - AuthCredential::Password(client_password) => { - return Ok(user.credentials.iter().any(|credential| match credential { - UserAuthCredential::Password(UserPasswordCredential { - hash: ref user_password_hash, - }) => verify_password_hash( - client_password.expose_secret(), - user_password_hash.expose_secret(), - ) - .unwrap_or_else(|e| { - error!( - username = &user.username[..], - "Error verifying password hash: {}", e - ); - false - }), - _ => false, - })) - } - AuthCredential::Otp(client_otp) => { - return Ok(user.credentials.iter().any(|credential| match credential { - UserAuthCredential::Totp(UserTotpCredential { - key: ref user_otp_key, - }) => verify_totp(client_otp.expose_secret(), user_otp_key), - _ => false, - })) - } - AuthCredential::Sso { - provider: client_provider, - email: client_email, - } => { - for credential in user.credentials.iter() { - if let UserAuthCredential::Sso(UserSsoCredential { - ref provider, - ref email, - }) = credential - { - if provider.as_ref().unwrap_or(client_provider) == client_provider { - return Ok(email == client_email); - } - } - } - return Ok(false); - } - _ => return Err(WarpgateError::InvalidCredentialType), - } - } - - async fn authorize_target( - &mut self, - username: &str, - target_name: &str, - ) -> Result { - let config = self.config.lock().await; - let user = config - .store - .users - .iter() - .find(|x| x.username == username) - .map(User::to_owned); - let target = config.store.targets.iter().find(|x| x.name == target_name); - - let Some(user) = user else { - error!("Selected user not found: {}", username); - return Ok(false); - }; - - let Some(target) = target else { - warn!("Selected target not found: {}", target_name); - return Ok(false); - }; - - let user_roles = user - .roles - .iter() - .map(|x| config.store.roles.iter().find(|y| &y.name == x)) - .filter_map(|x| x.to_owned().map(|x| x.name.clone())) - .collect::>(); - let target_roles = target - .allow_roles - .iter() - .map(|x| config.store.roles.iter().find(|y| &y.name == x)) - .filter_map(|x| x.to_owned().map(|x| x.name.clone())) - .collect::>(); - - let intersect = user_roles.intersection(&target_roles).count() > 0; - - Ok(intersect) - } - - async fn apply_sso_role_mappings( - &mut self, - _username: &str, - _managed_role_names: Option>, - _assigned_role_names: Vec, - ) -> Result<(), WarpgateError> { - Ok(()) - } -} diff --git a/warpgate-core/src/config_providers/mod.rs b/warpgate-core/src/config_providers/mod.rs index 6fa75e1c3..0d6e05454 100644 --- a/warpgate-core/src/config_providers/mod.rs +++ b/warpgate-core/src/config_providers/mod.rs @@ -1,10 +1,8 @@ mod db; -mod file; use std::sync::Arc; use async_trait::async_trait; pub use db::DatabaseConfigProvider; -pub use file::FileConfigProvider; use sea_orm::ActiveValue::Set; use sea_orm::{ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter}; use tokio::sync::Mutex; diff --git a/warpgate-core/src/db/mod.rs b/warpgate-core/src/db/mod.rs index 87d7563e2..5c8447e1e 100644 --- a/warpgate-core/src/db/mod.rs +++ b/warpgate-core/src/db/mod.rs @@ -1,4 +1,3 @@ -use std::collections::HashMap; use std::time::Duration; use anyhow::Result; @@ -10,13 +9,9 @@ use sea_orm::{ use tracing::*; use uuid::Uuid; use warpgate_common::helpers::fs::secure_file; -use warpgate_common::{ - ConfigProviderKind, TargetOptions, TargetWebAdminOptions, WarpgateConfig, WarpgateError, -}; +use warpgate_common::{TargetOptions, TargetWebAdminOptions, WarpgateConfig, WarpgateError}; use warpgate_db_entities::Target::TargetKind; -use warpgate_db_entities::{ - LogEntry, Role, Target, TargetRoleAssignment, User, UserRoleAssignment, -}; +use warpgate_db_entities::{LogEntry, Role, Target, TargetRoleAssignment}; use warpgate_db_migrations::migrate_database; use crate::consts::{BUILTIN_ADMIN_ROLE_NAME, BUILTIN_ADMIN_TARGET_NAME}; @@ -64,7 +59,7 @@ pub async fn connect_to_db(config: &WarpgateConfig) -> Result Result<(), WarpgateError> { use sea_orm::ActiveValue::Set; use warpgate_db_entities::{Recording, Session}; @@ -89,8 +84,6 @@ pub async fn populate_db( .await .map_err(WarpgateError::from)?; - let db_was_empty = Role::Entity::find().all(&*db).await?.is_empty(); - let admin_role = match Role::Entity::find() .filter(Role::Column::Name.eq(BUILTIN_ADMIN_ROLE_NAME)) .all(db) @@ -144,140 +137,6 @@ pub async fn populate_db( values.insert(&*db).await.map_err(WarpgateError::from)?; } - if db_was_empty && config.store.config_provider == ConfigProviderKind::Database { - migrate_config_into_db(db, config).await?; - } else if !config.store.targets.is_empty() { - warn!("Warpgate is now using the database for its configuration, but you still have leftover configuration in the config file."); - warn!("Configuration changes in the config file will be ignored."); - warn!("Remove `targets` and `roles` keys from the config to disable this warning."); - } - - Ok(()) -} - -async fn migrate_config_into_db( - db: &mut DatabaseConnection, - config: &mut WarpgateConfig, -) -> Result<(), WarpgateError> { - use sea_orm::ActiveValue::Set; - info!("Migrating config file into the database"); - - let mut role_lookup = HashMap::new(); - - for role_config in config.store.roles.iter() { - let role = match Role::Entity::find() - .filter(Role::Column::Name.eq(role_config.name.clone())) - .all(db) - .await? - .first() - { - Some(x) => x.to_owned(), - None => { - let values = Role::ActiveModel { - id: Set(Uuid::new_v4()), - name: Set(role_config.name.clone()), - }; - info!("Migrating role {}", role_config.name); - values.insert(&*db).await.map_err(WarpgateError::from)? - } - }; - role_lookup.insert(role_config.name.clone(), role.id); - } - config.store.roles = vec![]; - - for target_config in config.store.targets.iter() { - if TargetKind::WebAdmin == (&target_config.options).into() { - continue; - } - let target = match Target::Entity::find() - .filter(Target::Column::Kind.ne(TargetKind::WebAdmin)) - .filter(Target::Column::Name.eq(target_config.name.clone())) - .all(db) - .await? - .first() - { - Some(x) => x.to_owned(), - None => { - let values = Target::ActiveModel { - id: Set(Uuid::new_v4()), - name: Set(target_config.name.clone()), - kind: Set((&target_config.options).into()), - options: Set(serde_json::to_value(target_config.options.clone()) - .map_err(WarpgateError::from)?), - }; - - info!("Migrating target {}", target_config.name); - values.insert(&*db).await.map_err(WarpgateError::from)? - } - }; - - for role_name in target_config.allow_roles.iter() { - if let Some(role_id) = role_lookup.get(role_name) { - if TargetRoleAssignment::Entity::find() - .filter(TargetRoleAssignment::Column::TargetId.eq(target.id)) - .filter(TargetRoleAssignment::Column::RoleId.eq(*role_id)) - .all(db) - .await? - .is_empty() - { - let values = TargetRoleAssignment::ActiveModel { - target_id: Set(target.id), - role_id: Set(*role_id), - ..Default::default() - }; - values.insert(&*db).await.map_err(WarpgateError::from)?; - } - } - } - } - config.store.targets = vec![]; - - for user_config in config.store.users.iter() { - let user = match User::Entity::find() - .filter(User::Column::Username.eq(user_config.username.clone())) - .all(db) - .await? - .first() - { - Some(x) => x.to_owned(), - None => { - let values = User::ActiveModel { - id: Set(Uuid::new_v4()), - username: Set(user_config.username.clone()), - credentials: Set(serde_json::to_value(user_config.credentials.clone()) - .map_err(WarpgateError::from)?), - credential_policy: Set(serde_json::to_value( - user_config.credential_policy.clone(), - ) - .map_err(WarpgateError::from)?), - }; - - info!("Migrating user {}", user_config.username); - values.insert(&*db).await.map_err(WarpgateError::from)? - } - }; - - for role_name in user_config.roles.iter() { - if let Some(role_id) = role_lookup.get(role_name) { - if UserRoleAssignment::Entity::find() - .filter(UserRoleAssignment::Column::UserId.eq(user.id)) - .filter(UserRoleAssignment::Column::RoleId.eq(*role_id)) - .all(db) - .await? - .is_empty() - { - let values = UserRoleAssignment::ActiveModel { - user_id: Set(user.id), - role_id: Set(*role_id), - ..Default::default() - }; - values.insert(&*db).await.map_err(WarpgateError::from)?; - } - } - } - } - config.store.users = vec![]; - Ok(()) } diff --git a/warpgate-core/src/services.rs b/warpgate-core/src/services.rs index 0bfb55afd..fca480a9f 100644 --- a/warpgate-core/src/services.rs +++ b/warpgate-core/src/services.rs @@ -8,7 +8,7 @@ use warpgate_common::{ConfigProviderKind, WarpgateConfig}; use crate::db::{connect_to_db, populate_db}; use crate::recordings::SessionRecordings; -use crate::{AuthStateStore, ConfigProvider, DatabaseConfigProvider, FileConfigProvider, State}; +use crate::{AuthStateStore, ConfigProvider, DatabaseConfigProvider, State}; type ConfigProviderArc = Arc>; @@ -20,10 +20,11 @@ pub struct Services { pub state: Arc>, pub config_provider: ConfigProviderArc, pub auth_state_store: Arc>, + pub admin_token: Arc>>, } impl Services { - pub async fn new(mut config: WarpgateConfig) -> Result { + pub async fn new(mut config: WarpgateConfig, admin_token: Option) -> Result { let mut db = connect_to_db(&config).await?; populate_db(&mut db, &mut config).await?; let db = Arc::new(Mutex::new(db)); @@ -36,7 +37,7 @@ impl Services { let config_provider = match provider { ConfigProviderKind::File => { - Arc::new(Mutex::new(FileConfigProvider::new(&config).await)) as ConfigProviderArc + anyhow::bail!("File based config provider in no longer supported"); } ConfigProviderKind::Database => { Arc::new(Mutex::new(DatabaseConfigProvider::new(&db).await)) as ConfigProviderArc @@ -62,6 +63,7 @@ impl Services { state: State::new(&db), config_provider, auth_state_store, + admin_token: Arc::new(Mutex::new(admin_token)), }) } } diff --git a/warpgate-database-protocols/Cargo.toml b/warpgate-database-protocols/Cargo.toml index 685e0888f..d2931da89 100644 --- a/warpgate-database-protocols/Cargo.toml +++ b/warpgate-database-protocols/Cargo.toml @@ -14,7 +14,7 @@ authors = [ [dependencies] tokio = { version = "1.20", features = ["io-util"] } bitflags = { version = "1.3", default-features = false } -bytes = "1.4" +bytes.workspace = true futures-core = { version = "0.3", default-features = false } futures-util = { version = "0.3", default-features = false, features = [ "alloc", diff --git a/warpgate-db-entities/Cargo.toml b/warpgate-db-entities/Cargo.toml index d913cf7fb..893ac8d1f 100644 --- a/warpgate-db-entities/Cargo.toml +++ b/warpgate-db-entities/Cargo.toml @@ -5,6 +5,7 @@ name = "warpgate-db-entities" version = "0.11.0" [dependencies] +bytes = "1.4" chrono = { version = "0.4", default-features = false, features = ["serde"] } poem-openapi = { version = "5.1", features = ["chrono", "uuid"] } sea-orm = { version = "0.12", features = [ @@ -13,7 +14,7 @@ sea-orm = { version = "0.12", features = [ "with-uuid", "with-json", ], default-features = false } -serde = "1.0" -serde_json = "1.0" +serde.workspace = true +serde_json.workspace = true uuid = { version = "1.3", features = ["v4", "serde"] } warpgate-common = { version = "*", path = "../warpgate-common" } diff --git a/warpgate-db-entities/src/OtpCredential.rs b/warpgate-db-entities/src/OtpCredential.rs new file mode 100644 index 000000000..6d5bb6562 --- /dev/null +++ b/warpgate-db-entities/src/OtpCredential.rs @@ -0,0 +1,61 @@ +use sea_orm::entity::prelude::*; +use sea_orm::Set; +use serde::Serialize; +use uuid::Uuid; +use warpgate_common::{UserAuthCredential, UserTotpCredential}; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize)] +#[sea_orm(table_name = "credentials_otp")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: Uuid, + pub user_id: Uuid, + pub secret_key: Vec, +} + +#[derive(Copy, Clone, Debug, EnumIter)] +pub enum Relation { + User, +} + +impl RelationTrait for Relation { + fn def(&self) -> RelationDef { + match self { + Self::User => Entity::belongs_to(super::User::Entity) + .from(Column::UserId) + .to(super::User::Column::Id) + .into(), + } + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::User.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} + +impl From for UserTotpCredential { + fn from(credential: Model) -> Self { + UserTotpCredential { + key: credential.secret_key.into(), + } + } +} + +impl From for UserAuthCredential { + fn from(model: Model) -> Self { + Self::Totp(model.into()) + } +} + +impl From for ActiveModel { + fn from(credential: UserTotpCredential) -> Self { + Self { + secret_key: Set(credential.key.expose_secret().clone()), + ..Default::default() + } + } +} diff --git a/warpgate-db-entities/src/PasswordCredential.rs b/warpgate-db-entities/src/PasswordCredential.rs new file mode 100644 index 000000000..465c4164b --- /dev/null +++ b/warpgate-db-entities/src/PasswordCredential.rs @@ -0,0 +1,75 @@ +use sea_orm::entity::prelude::*; +use sea_orm::Set; +use serde::Serialize; +use uuid::Uuid; +use warpgate_common::{UserAuthCredential, UserPasswordCredential}; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize)] +#[sea_orm(table_name = "credentials_password")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: Uuid, + pub user_id: Uuid, + pub argon_hash: String, +} + +#[derive(Copy, Clone, Debug, EnumIter)] +pub enum Relation { + User, +} + +impl RelationTrait for Relation { + fn def(&self) -> RelationDef { + match self { + Self::User => Entity::belongs_to(super::User::Entity) + .from(Column::UserId) + .to(super::User::Column::Id) + .into(), + } + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::User.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} + +pub struct StrictModel { + pub id: Uuid, + pub credential: UserPasswordCredential, +} + +impl From for StrictModel { + fn from(model: Model) -> Self { + Self { + id: model.id, + credential: model.clone().into(), + } + } +} + +impl From for UserPasswordCredential { + fn from(credential: Model) -> Self { + UserPasswordCredential { + hash: credential.argon_hash.to_owned().into(), + } + } +} + +impl From for UserAuthCredential { + fn from(model: Model) -> Self { + Self::Password(model.into()) + } +} + +impl From for ActiveModel { + fn from(credential: UserPasswordCredential) -> Self { + Self { + argon_hash: Set(credential.hash.expose_secret().into()), + ..Default::default() + } + } +} diff --git a/warpgate-db-entities/src/PublicKeyCredential.rs b/warpgate-db-entities/src/PublicKeyCredential.rs new file mode 100644 index 000000000..0c572f383 --- /dev/null +++ b/warpgate-db-entities/src/PublicKeyCredential.rs @@ -0,0 +1,61 @@ +use sea_orm::entity::prelude::*; +use sea_orm::Set; +use serde::Serialize; +use uuid::Uuid; +use warpgate_common::{UserAuthCredential, UserPublicKeyCredential}; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize)] +#[sea_orm(table_name = "credentials_public_key")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: Uuid, + pub user_id: Uuid, + pub openssh_public_key: String, +} + +#[derive(Copy, Clone, Debug, EnumIter)] +pub enum Relation { + User, +} + +impl RelationTrait for Relation { + fn def(&self) -> RelationDef { + match self { + Self::User => Entity::belongs_to(super::User::Entity) + .from(Column::UserId) + .to(super::User::Column::Id) + .into(), + } + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::User.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} + +impl From for UserPublicKeyCredential { + fn from(credential: Model) -> Self { + UserPublicKeyCredential { + key: credential.openssh_public_key.into(), + } + } +} + +impl From for UserAuthCredential { + fn from(model: Model) -> Self { + Self::PublicKey(model.into()) + } +} + +impl From for ActiveModel { + fn from(credential: UserPublicKeyCredential) -> Self { + Self { + openssh_public_key: Set(credential.key.expose_secret().clone()), + ..Default::default() + } + } +} diff --git a/warpgate-db-entities/src/SsoCredential.rs b/warpgate-db-entities/src/SsoCredential.rs new file mode 100644 index 000000000..49d1d873f --- /dev/null +++ b/warpgate-db-entities/src/SsoCredential.rs @@ -0,0 +1,64 @@ +use sea_orm::entity::prelude::*; +use sea_orm::Set; +use serde::Serialize; +use uuid::Uuid; +use warpgate_common::{UserAuthCredential, UserSsoCredential}; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize)] +#[sea_orm(table_name = "credentials_sso")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: Uuid, + pub user_id: Uuid, + pub provider: Option, + pub email: String, +} + +#[derive(Copy, Clone, Debug, EnumIter)] +pub enum Relation { + User, +} + +impl RelationTrait for Relation { + fn def(&self) -> RelationDef { + match self { + Self::User => Entity::belongs_to(super::User::Entity) + .from(Column::UserId) + .to(super::User::Column::Id) + .into(), + } + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::User.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} + +impl From for UserSsoCredential { + fn from(credential: Model) -> Self { + UserSsoCredential { + provider: credential.provider, + email: credential.email, + } + } +} + +impl From for UserAuthCredential { + fn from(model: Model) -> Self { + Self::Sso(model.into()) + } +} + +impl From for ActiveModel { + fn from(credential: UserSsoCredential) -> Self { + Self { + provider: Set(credential.provider), + email: Set(credential.email), + ..Default::default() + } + } +} diff --git a/warpgate-db-entities/src/User.rs b/warpgate-db-entities/src/User.rs index b197c5669..8d0323bd9 100644 --- a/warpgate-db-entities/src/User.rs +++ b/warpgate-db-entities/src/User.rs @@ -2,7 +2,9 @@ use poem_openapi::Object; use sea_orm::entity::prelude::*; use serde::Serialize; use uuid::Uuid; -use warpgate_common::{User, UserAuthCredential, UserRequireCredentialsPolicy}; +use warpgate_common::{User, UserDetails, WarpgateError}; + +use crate::{OtpCredential, PasswordCredential, PublicKeyCredential, Role, SsoCredential}; #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Object)] #[sea_orm(table_name = "users")] @@ -11,7 +13,6 @@ pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub id: Uuid, pub username: String, - pub credentials: serde_json::Value, pub credential_policy: serde_json::Value, } @@ -25,24 +26,121 @@ impl Related for Entity { } } -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} +impl Related for Entity { + fn to() -> RelationDef { + Relation::OtpCredentials.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::PasswordCredentials.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::PublicKeyCredentials.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::SsoCredentials.def() + } +} + +#[derive(Copy, Clone, Debug, EnumIter)] +#[allow(clippy::enum_variant_names)] +pub enum Relation { + OtpCredentials, + PasswordCredentials, + PublicKeyCredentials, + SsoCredentials, +} + +impl RelationTrait for Relation { + fn def(&self) -> RelationDef { + match self { + Self::OtpCredentials => Entity::has_many(super::OtpCredential::Entity) + .from(Column::Id) + .to(super::OtpCredential::Column::UserId) + .into(), + Self::PasswordCredentials => Entity::has_many(super::PasswordCredential::Entity) + .from(Column::Id) + .to(super::PasswordCredential::Column::UserId) + .into(), + Self::PublicKeyCredentials => Entity::has_many(super::PublicKeyCredential::Entity) + .from(Column::Id) + .to(super::PublicKeyCredential::Column::UserId) + .into(), + Self::SsoCredentials => Entity::has_many(super::SsoCredential::Entity) + .from(Column::Id) + .to(super::SsoCredential::Column::UserId) + .into(), + } + } +} impl ActiveModelBehavior for ActiveModel {} impl TryFrom for User { - type Error = serde_json::Error; + type Error = WarpgateError; - fn try_from(model: Model) -> Result { - let credentials: Vec = serde_json::from_value(model.credentials)?; - let credential_policy: Option = - serde_json::from_value(model.credential_policy)?; - Ok(Self { + fn try_from(model: Model) -> Result { + Ok(User { id: model.id, username: model.username, - roles: vec![], + credential_policy: serde_json::from_value(model.credential_policy)?, + }) + } +} + +impl Model { + pub async fn load_details(self, db: &DatabaseConnection) -> Result { + let roles: Vec = self + .find_related(Role::Entity) + .all(db) + .await? + .into_iter() + .map(Into::::into) + .map(|x| x.name) + .collect(); + + let mut credentials = vec![]; + credentials.extend( + self.find_related(OtpCredential::Entity) + .all(db) + .await? + .into_iter() + .map(|x| x.into()), + ); + credentials.extend( + self.find_related(PasswordCredential::Entity) + .all(db) + .await? + .into_iter() + .map(|x| x.into()), + ); + credentials.extend( + self.find_related(SsoCredential::Entity) + .all(db) + .await? + .into_iter() + .map(|x| x.into()), + ); + credentials.extend( + self.find_related(PublicKeyCredential::Entity) + .all(db) + .await? + .into_iter() + .map(|x| x.into()), + ); + + Ok(warpgate_common::UserDetails { + inner: self.try_into()?, + roles, credentials, - credential_policy, }) } } diff --git a/warpgate-db-entities/src/lib.rs b/warpgate-db-entities/src/lib.rs index 0c3cd41e0..e01ae5971 100644 --- a/warpgate-db-entities/src/lib.rs +++ b/warpgate-db-entities/src/lib.rs @@ -2,9 +2,13 @@ pub mod KnownHost; pub mod LogEntry; +pub mod OtpCredential; +pub mod PasswordCredential; +pub mod PublicKeyCredential; pub mod Recording; pub mod Role; pub mod Session; +pub mod SsoCredential; pub mod Target; pub mod TargetRoleAssignment; pub mod Ticket; diff --git a/warpgate-db-migrations/Cargo.toml b/warpgate-db-migrations/Cargo.toml index 9e8cf87b6..77993b810 100644 --- a/warpgate-db-migrations/Cargo.toml +++ b/warpgate-db-migrations/Cargo.toml @@ -10,6 +10,7 @@ version = "0.11.0" [dependencies] tokio = { version = "1.20", features = ["macros", "rt-multi-thread"] } chrono = { version = "0.4", default-features = false, features = ["serde"] } +data-encoding.workspace = true sea-orm = { version = "0.12", features = [ "runtime-tokio-rustls", "macros", @@ -21,7 +22,8 @@ sea-orm-migration = { version = "0.12", default-features = false, features = [ "cli", ] } uuid = { version = "1.3", features = ["v4", "serde"] } -serde_json = "1.0" +serde_json.workspace = true +serde.workspace = true [features] postgres = ["sea-orm/sqlx-postgres"] diff --git a/warpgate-db-migrations/src/lib.rs b/warpgate-db-migrations/src/lib.rs index 62fb79ffd..ecb1a69c3 100644 --- a/warpgate-db-migrations/src/lib.rs +++ b/warpgate-db-migrations/src/lib.rs @@ -10,6 +10,7 @@ mod m00005_create_log_entry; mod m00006_add_session_protocol; mod m00007_targets_and_roles; mod m00008_users; +mod m00009_credential_models; pub struct Migrator; @@ -25,6 +26,7 @@ impl MigratorTrait for Migrator { Box::new(m00006_add_session_protocol::Migration), Box::new(m00007_targets_and_roles::Migration), Box::new(m00008_users::Migration), + Box::new(m00009_credential_models::Migration), ] } } diff --git a/warpgate-db-migrations/src/m00008_users.rs b/warpgate-db-migrations/src/m00008_users.rs index af7b3e1b9..e76e205a8 100644 --- a/warpgate-db-migrations/src/m00008_users.rs +++ b/warpgate-db-migrations/src/m00008_users.rs @@ -1,7 +1,7 @@ use sea_orm::Schema; use sea_orm_migration::prelude::*; -mod user { +pub mod user { use sea_orm::entity::prelude::*; use uuid::Uuid; diff --git a/warpgate-db-migrations/src/m00009_credential_models.rs b/warpgate-db-migrations/src/m00009_credential_models.rs new file mode 100644 index 000000000..f47ff72f2 --- /dev/null +++ b/warpgate-db-migrations/src/m00009_credential_models.rs @@ -0,0 +1,307 @@ +use credential_enum::UserAuthCredential; +use sea_orm::{ActiveModelTrait, EntityTrait, Schema, Set}; +use sea_orm_migration::prelude::*; +use uuid::Uuid; + +use super::m00008_users::user as User; + +pub mod otp_credential { + use sea_orm::entity::prelude::*; + use uuid::Uuid; + + use crate::m00008_users::user as User; + + #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] + #[sea_orm(table_name = "credentials_otp")] + pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: Uuid, + pub user_id: Uuid, + pub secret_key: Vec, + } + + #[derive(Copy, Clone, Debug, EnumIter)] + pub enum Relation { + User, + } + + impl RelationTrait for Relation { + fn def(&self) -> RelationDef { + match self { + Self::User => Entity::belongs_to(User::Entity) + .from(Column::UserId) + .to(User::Column::Id) + .into(), + } + } + } + + impl ActiveModelBehavior for ActiveModel {} +} + +pub mod password_credential { + use sea_orm::entity::prelude::*; + use uuid::Uuid; + + use crate::m00008_users::user as User; + + #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] + #[sea_orm(table_name = "credentials_password")] + pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: Uuid, + pub user_id: Uuid, + pub argon_hash: String, + } + + #[derive(Copy, Clone, Debug, EnumIter)] + pub enum Relation { + User, + } + + impl RelationTrait for Relation { + fn def(&self) -> RelationDef { + match self { + Self::User => Entity::belongs_to(User::Entity) + .from(Column::UserId) + .to(User::Column::Id) + .into(), + } + } + } + + impl ActiveModelBehavior for ActiveModel {} +} + +mod public_key_credential { + use sea_orm::entity::prelude::*; + use uuid::Uuid; + + use crate::m00008_users::user as User; + + #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] + #[sea_orm(table_name = "credentials_public_key")] + pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: Uuid, + pub user_id: Uuid, + pub openssh_public_key: String, + } + + #[derive(Copy, Clone, Debug, EnumIter)] + pub enum Relation { + User, + } + + impl RelationTrait for Relation { + fn def(&self) -> RelationDef { + match self { + Self::User => Entity::belongs_to(User::Entity) + .from(Column::UserId) + .to(User::Column::Id) + .into(), + } + } + } + + impl ActiveModelBehavior for ActiveModel {} +} + +mod sso_credential { + use sea_orm::entity::prelude::*; + use uuid::Uuid; + + use crate::m00008_users::user as User; + + #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] + #[sea_orm(table_name = "credentials_sso")] + pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: Uuid, + pub user_id: Uuid, + pub provider: Option, + pub email: String, + } + + #[derive(Copy, Clone, Debug, EnumIter)] + pub enum Relation { + User, + } + + impl RelationTrait for Relation { + fn def(&self) -> RelationDef { + match self { + Self::User => Entity::belongs_to(User::Entity) + .from(Column::UserId) + .to(User::Column::Id) + .into(), + } + } + } + + impl ActiveModelBehavior for ActiveModel {} +} + +mod credential_enum { + use serde::{Deserialize, Serialize}; + + mod serde_base64_secret { + use serde::Serializer; + + mod serde_base64 { + use data_encoding::BASE64; + use serde::{Deserialize, Serializer}; + + pub fn serialize>( + bytes: B, + serializer: S, + ) -> Result { + serializer.serialize_str(&BASE64.encode(bytes.as_ref())) + } + + pub fn deserialize<'de, D: serde::Deserializer<'de>, B: From>>( + deserializer: D, + ) -> Result { + let s = String::deserialize(deserializer)?; + Ok(BASE64 + .decode(s.as_bytes()) + .map_err(serde::de::Error::custom)? + .into()) + } + } + + pub fn serialize( + secret: &Vec, + serializer: S, + ) -> Result { + serde_base64::serialize(secret, serializer) + } + + pub fn deserialize<'de, D: serde::Deserializer<'de>>( + deserializer: D, + ) -> Result, D::Error> { + let inner = serde_base64::deserialize(deserializer)?; + Ok(inner) + } + } + + #[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] + #[serde(tag = "type")] + pub enum UserAuthCredential { + #[serde(rename = "password")] + Password(UserPasswordCredential), + #[serde(rename = "publickey")] + PublicKey(UserPublicKeyCredential), + #[serde(rename = "otp")] + Totp(UserTotpCredential), + #[serde(rename = "sso")] + Sso(UserSsoCredential), + } + + #[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] + pub struct UserPasswordCredential { + pub hash: String, + } + #[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] + pub struct UserPublicKeyCredential { + pub key: String, + } + #[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] + pub struct UserTotpCredential { + #[serde(with = "serde_base64_secret")] + pub key: Vec, + } + #[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] + pub struct UserSsoCredential { + pub provider: Option, + pub email: String, + } +} + +pub struct Migration; + +impl MigrationName for Migration { + fn name(&self) -> &str { + "m00009_credential_models" + } +} + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + let builder = manager.get_database_backend(); + let db = manager.get_connection(); + let schema = Schema::new(builder); + manager + .create_table(schema.create_table_from_entity(otp_credential::Entity)) + .await?; + manager + .create_table(schema.create_table_from_entity(password_credential::Entity)) + .await?; + manager + .create_table(schema.create_table_from_entity(public_key_credential::Entity)) + .await?; + manager + .create_table(schema.create_table_from_entity(sso_credential::Entity)) + .await?; + + let users = User::Entity::find().all(db).await?; + for user in users { + #[allow(clippy::unwrap_used)] + let credentials: Vec = + serde_json::from_value(user.credentials).unwrap(); + for credential in credentials { + match credential { + UserAuthCredential::Password(password) => { + let model = password_credential::ActiveModel { + id: Set(Uuid::new_v4()), + user_id: Set(user.id), + argon_hash: Set(password.hash), + }; + model.insert(db).await?; + } + UserAuthCredential::PublicKey(key) => { + let model = public_key_credential::ActiveModel { + id: Set(Uuid::new_v4()), + user_id: Set(user.id), + openssh_public_key: Set(key.key), + }; + model.insert(db).await?; + } + UserAuthCredential::Sso(sso) => { + let model = sso_credential::ActiveModel { + id: Set(Uuid::new_v4()), + user_id: Set(user.id), + provider: Set(sso.provider), + email: Set(sso.email), + }; + model.insert(db).await?; + } + UserAuthCredential::Totp(totp) => { + let model = otp_credential::ActiveModel { + id: Set(Uuid::new_v4()), + user_id: Set(user.id), + secret_key: Set(totp.key), + }; + model.insert(db).await?; + } + } + } + } + + manager + .alter_table( + Table::alter() + .table(User::Entity) + .drop_column(User::Column::Credentials) + .to_owned(), + ) + .await?; + + Ok(()) + } + + async fn down(&self, _manager: &SchemaManager) -> Result<(), DbErr> { + panic!("This migration is irreversible"); + } +} diff --git a/warpgate-protocol-http/Cargo.toml b/warpgate-protocol-http/Cargo.toml index 2f4449efe..41faae77b 100644 --- a/warpgate-protocol-http/Cargo.toml +++ b/warpgate-protocol-http/Cargo.toml @@ -9,7 +9,7 @@ anyhow = "1.0" async-trait = "0.1" chrono = { version = "0.4", default-features = false, features = ["serde"] } cookie = "0.17" -data-encoding = "2.3" +data-encoding.workspace = true delegate = "0.6" futures = "0.3" http = "1.0" @@ -28,8 +28,8 @@ reqwest = { version = "0.12", features = [ "rustls-tls-native-roots", "stream", ], default-features = false } -serde = "1.0" -serde_json = "1.0" +serde.workspace = true +serde_json.workspace = true tokio = { version = "1.20", features = ["tracing", "signal"] } tokio-tungstenite = { version = "0.24", features = ["rustls-tls-native-roots"] } tracing = "0.1" diff --git a/warpgate-protocol-http/src/api/auth.rs b/warpgate-protocol-http/src/api/auth.rs index d1b108e6e..32ab6be2b 100644 --- a/warpgate-protocol-http/src/api/auth.rs +++ b/warpgate-protocol-http/src/api/auth.rs @@ -17,7 +17,8 @@ use warpgate_core::Services; use super::common::logout; use crate::common::{ - authorize_session, endpoint_auth, get_auth_state_for_request, SessionAuthorization, SessionExt, + authorize_session, endpoint_auth, get_auth_state_for_request, RequestAuthorization, + SessionAuthorization, SessionExt, }; use crate::session::SessionStore; @@ -267,7 +268,7 @@ impl Api { async fn api_auth_state( &self, services: Data<&Services>, - auth: Option>, + auth: Option>, id: Path, ) -> poem::Result { let state_arc = get_auth_state(&id, &services, auth.map(|x| x.0)).await; @@ -286,7 +287,7 @@ impl Api { async fn api_approve_auth( &self, services: Data<&Services>, - auth: Option>, + auth: Option>, id: Path, ) -> poem::Result { let Some(state_arc) = get_auth_state(&id, &services, auth.map(|x| x.0)).await else { @@ -314,7 +315,7 @@ impl Api { async fn api_reject_auth( &self, services: Data<&Services>, - auth: Option>, + auth: Option>, id: Path, ) -> poem::Result { let Some(state_arc) = get_auth_state(&id, &services, auth.map(|x| x.0)).await else { @@ -329,11 +330,11 @@ impl Api { async fn get_auth_state( id: &Uuid, services: &Services, - auth: Option<&SessionAuthorization>, + auth: Option<&RequestAuthorization>, ) -> Option>> { let store = services.auth_state_store.lock().await; - let SessionAuthorization::User(username) = auth? else { + let RequestAuthorization::Session(SessionAuthorization::User(username)) = auth? else { return None; }; diff --git a/warpgate-protocol-http/src/api/mod.rs b/warpgate-protocol-http/src/api/mod.rs index 731cb5014..9e4674292 100644 --- a/warpgate-protocol-http/src/api/mod.rs +++ b/warpgate-protocol-http/src/api/mod.rs @@ -1,4 +1,5 @@ -use poem_openapi::OpenApi; +use poem_openapi::auth::ApiKey; +use poem_openapi::{OpenApi, SecurityScheme}; pub mod auth; mod common; @@ -7,8 +8,24 @@ pub mod sso_provider_detail; pub mod sso_provider_list; pub mod targets_list; +#[derive(SecurityScheme)] +#[oai(ty = "api_key", key_name = "X-Warpgate-Token", key_in = "header")] +#[allow(dead_code)] +pub struct TokenSecurityScheme(ApiKey); + +struct StubApi; + +#[OpenApi] +impl StubApi { + #[oai(path = "/__stub__", method = "get", operation_id = "__stub__")] + async fn stub(&self, _auth: TokenSecurityScheme) -> poem::Result<()> { + Ok(()) + } +} + pub fn get() -> impl OpenApi { ( + StubApi, auth::Api, info::Api, targets_list::Api, diff --git a/warpgate-protocol-http/src/api/targets_list.rs b/warpgate-protocol-http/src/api/targets_list.rs index 405b7e693..84d778600 100644 --- a/warpgate-protocol-http/src/api/targets_list.rs +++ b/warpgate-protocol-http/src/api/targets_list.rs @@ -8,7 +8,7 @@ use warpgate_common::TargetOptions; use warpgate_core::Services; use warpgate_db_entities::Target; -use crate::common::{endpoint_auth, SessionAuthorization}; +use crate::common::{endpoint_auth, RequestAuthorization, SessionAuthorization}; pub struct Api; @@ -36,9 +36,13 @@ impl Api { async fn api_get_all_targets( &self, services: Data<&Services>, - auth: Data<&SessionAuthorization>, + auth: Data<&RequestAuthorization>, search: Query>, ) -> poem::Result { + let RequestAuthorization::Session(auth) = *auth else { + return Ok(GetTargetsResponse::Ok(Json(vec![]))); + }; + let mut targets = { let mut config_provider = services.config_provider.lock().await; config_provider.list_targets().await? diff --git a/warpgate-protocol-http/src/catchall.rs b/warpgate-protocol-http/src/catchall.rs index bdbbdf192..ad2285831 100644 --- a/warpgate-protocol-http/src/catchall.rs +++ b/warpgate-protocol-http/src/catchall.rs @@ -10,7 +10,7 @@ use tracing::*; use warpgate_common::{Target, TargetHTTPOptions, TargetOptions}; use warpgate_core::{Services, WarpgateServerHandle}; -use crate::common::{SessionAuthorization, SessionExt}; +use crate::common::{RequestAuthorization, SessionAuthorization, SessionExt}; use crate::proxy::{proxy_normal_request, proxy_websocket_request}; #[derive(Deserialize)] @@ -63,7 +63,7 @@ async fn get_target_for_request( ) -> poem::Result> { let session = <&Session>::from_request_without_body(req).await?; let params: QueryParams = req.params()?; - let auth = Data::<&SessionAuthorization>::from_request_without_body(req).await?; + let auth = Data::<&RequestAuthorization>::from_request_without_body(req).await?; let selected_target_name; let need_role_auth; @@ -86,12 +86,16 @@ async fn get_target_for_request( None }; - match *auth { - SessionAuthorization::Ticket { target_name, .. } => { + let username = match *auth { + RequestAuthorization::Session(SessionAuthorization::Ticket { + target_name, + username, + }) => { selected_target_name = Some(target_name.clone()); need_role_auth = false; + username } - SessionAuthorization::User(_) => { + RequestAuthorization::Session(SessionAuthorization::User(username)) => { need_role_auth = true; selected_target_name = @@ -100,7 +104,9 @@ async fn get_target_for_request( } else { session.get_target_name() }); + username } + RequestAuthorization::AdminToken => return Ok(None), }; if let Some(target_name) = selected_target_name { @@ -127,7 +133,7 @@ async fn get_target_for_request( .config_provider .lock() .await - .authorize_target(auth.username(), &target.0.name) + .authorize_target(username, &target.0.name) .await? { return Ok(None); diff --git a/warpgate-protocol-http/src/common.rs b/warpgate-protocol-http/src/common.rs index bc1b6375c..88f6a5d4d 100644 --- a/warpgate-protocol-http/src/common.rs +++ b/warpgate-protocol-http/src/common.rs @@ -1,6 +1,7 @@ +use core::str; use std::sync::Arc; -use http::StatusCode; +use http::{HeaderName, StatusCode}; use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC}; use poem::session::Session; use poem::web::{Data, Redirect}; @@ -21,6 +22,7 @@ static AUTH_SESSION_KEY: &str = "auth"; static AUTH_STATE_ID_SESSION_KEY: &str = "auth_state_id"; static AUTH_SSO_LOGIN_STATE: &str = "auth_sso_login_state"; pub static SESSION_COOKIE_NAME: &str = "warpgate-http-session"; +static X_WARPGATE_TOKEN: HeaderName = HeaderName::from_static("x-warpgate-token"); #[derive(Serialize, Deserialize)] pub struct SsoLoginState { @@ -103,17 +105,25 @@ pub enum SessionAuthorization { impl SessionAuthorization { pub fn username(&self) -> &String { match self { - SessionAuthorization::User(username) => username, - SessionAuthorization::Ticket { username, .. } => username, + Self::User(username) => username, + Self::Ticket { username, .. } => username, } } } -async fn is_user_admin(req: &Request, auth: &SessionAuthorization) -> poem::Result { +#[derive(Clone, Serialize, Deserialize)] +pub enum RequestAuthorization { + Session(SessionAuthorization), + AdminToken, +} + +async fn is_user_admin(req: &Request, auth: &RequestAuthorization) -> poem::Result { let services = Data::<&Services>::from_request_without_body(req).await?; - let SessionAuthorization::User(username) = auth else { - return Ok(false); + let username = match auth { + RequestAuthorization::Session(SessionAuthorization::User(username)) => username, + RequestAuthorization::Session(SessionAuthorization::Ticket { .. }) => return Ok(false), + RequestAuthorization::AdminToken => return Ok(true), }; let mut config_provider = services.config_provider.lock().await; @@ -133,7 +143,7 @@ async fn is_user_admin(req: &Request, auth: &SessionAuthorization) -> poem::Resu pub fn endpoint_admin_auth(e: E) -> impl Endpoint { e.around(|ep, req| async move { - let auth = Data::<&SessionAuthorization>::from_request_without_body(&req).await?; + let auth = Data::<&RequestAuthorization>::from_request_without_body(&req).await?; if is_user_admin(&req, &auth).await? { return Ok(ep.call(req).await?.into_response()); } @@ -143,7 +153,7 @@ pub fn endpoint_admin_auth(e: E) -> impl Endpoint { pub fn page_admin_auth(e: E) -> impl Endpoint { e.around(|ep, req| async move { - let auth = Data::<&SessionAuthorization>::from_request_without_body(&req).await?; + let auth = Data::<&RequestAuthorization>::from_request_without_body(&req).await?; let session = <&Session>::from_request_without_body(&req).await?; if is_user_admin(&req, &auth).await? { return Ok(ep.call(req).await?.into_response()); @@ -158,11 +168,28 @@ pub async fn _inner_auth( req: Request, ) -> poem::Result> { let session = <&Session>::from_request_without_body(&req).await?; + let services = Data::<&Services>::from_request_without_body(&req).await?; + + let auth = match session.get_auth() { + Some(auth) => RequestAuthorization::Session(auth), + None => match req.headers().get(&X_WARPGATE_TOKEN) { + Some(token_from_header) => { + if Some( + token_from_header + .to_str() + .map_err(poem::error::BadRequest)?, + ) == services.admin_token.lock().await.as_deref() + { + RequestAuthorization::AdminToken + } else { + return Ok(None); + } + } + None => return Ok(None), + }, + }; - Ok(match session.get_auth() { - Some(auth) => Some(ep.data(auth).call(req).await?), - _ => None, - }) + Ok(Some(ep.data(auth).call(req).await?)) } pub fn endpoint_auth(e: E) -> impl Endpoint { diff --git a/warpgate-protocol-http/src/error.rs b/warpgate-protocol-http/src/error.rs index bd9fd7fd1..360a7780f 100644 --- a/warpgate-protocol-http/src/error.rs +++ b/warpgate-protocol-http/src/error.rs @@ -1,7 +1,9 @@ use http::StatusCode; use poem::IntoResponse; +use tracing::error; pub fn error_page(e: poem::Error) -> impl IntoResponse { + error!("{:?}", e); poem::web::Html(format!( r#" diff --git a/warpgate-web/src/admin/PublicKeyCredentialModal.svelte b/warpgate-web/src/admin/PublicKeyCredentialModal.svelte new file mode 100644 index 000000000..b1002fdfe --- /dev/null +++ b/warpgate-web/src/admin/PublicKeyCredentialModal.svelte @@ -0,0 +1,87 @@ + + + { + if (instance) { + opensshPublicKey = instance.opensshPublicKey + } + field?.focus() +}}> +
+ + Public key + + + + + + + +
+ + + +
+
+
+
diff --git a/warpgate-web/src/admin/SsoCredentialModal.svelte b/warpgate-web/src/admin/SsoCredentialModal.svelte new file mode 100644 index 000000000..8bc21c44c --- /dev/null +++ b/warpgate-web/src/admin/SsoCredentialModal.svelte @@ -0,0 +1,101 @@ + + + { + if (instance) { + provider = instance.provider ?? null + email = instance.email + } +}}> +
+ + Single sign-on + + + + + + + {#await api.getSsoProviders() then providers} + {#if !providers.length} + + You don't have any SSO providers configured. Add them to your config file first. + + {/if} + + + + {#each providers as provider} + + {/each} + + + {/await} + + +
+ + + +
+
+
+
diff --git a/warpgate-web/src/admin/User.svelte b/warpgate-web/src/admin/User.svelte index f8b2a094f..85ab33283 100644 --- a/warpgate-web/src/admin/User.svelte +++ b/warpgate-web/src/admin/User.svelte @@ -1,15 +1,12 @@ {#await load()} @@ -145,94 +82,11 @@ -
-

Credentials

- - - - - -
- -
- {#each user.credentials as credential} -
- {#if credential.kind === 'Password'} - - Password - {/if} - {#if credential.kind === 'PublicKey'} - - Public key - {abbreviatePublicKey(credential.key)} - {/if} - {#if credential.kind === 'Totp'} - - One-time password - {/if} - {#if credential.kind === 'Sso'} - - Single sign-on - - {credential.email} - {#if credential.provider} ({credential.provider}){/if} - - {/if} - - - { - editingCredential = credential - e.preventDefault() - }}> - Change - - { - deleteCredential(credential) - e.preventDefault() - }}> - Delete - -
-{/each} -
- -

Auth policy

-
- {#each policyProtocols as protocol} -
-
- {protocol.name} -
- {#if possibleCredentials[protocol.id]} - {@const _possibleCredentials = assertDefined(possibleCredentials[protocol.id])} - - {/if} -
- {/each} -
+

User roles

@@ -272,31 +126,3 @@ click={remove} >Remove
- -{#if editingCredential} - editingCredential = undefined} -/> -{/if} - - diff --git a/warpgate-web/src/admin/UserCredentialModal.svelte b/warpgate-web/src/admin/UserCredentialModal.svelte deleted file mode 100644 index ff67f79c0..000000000 --- a/warpgate-web/src/admin/UserCredentialModal.svelte +++ /dev/null @@ -1,290 +0,0 @@ - - - - - {#if credential.kind === 'Sso'} - Single sign-on - {/if} - {#if credential.kind === 'Password'} - Password - {/if} - {#if credential.kind === 'Totp'} - One-time password - {/if} - {#if credential.kind === 'PublicKey'} - Public key - {/if} - - - {#if credential.kind === 'Sso'} - - - - - {#await api.getSsoProviders() then providers} - {#if !providers.length} - - You don't have any SSO providers configured. Add them to your config file first. - - {/if} - - - - {#each providers as provider} - - {/each} - - - {/await} - {/if} - - {#if credential.kind === 'Password'} - - - - {/if} - - {#if credential.kind === 'PublicKey'} - - {/if} - - {#if credential.kind === 'Totp'} -
-
- OTP QR code -
-
- - -
-
- - - - {/if} -
- -
- - - -
-
-
- - diff --git a/warpgate-web/src/admin/lib/openapi-schema.json b/warpgate-web/src/admin/lib/openapi-schema.json index 907f8aa50..dfde19759 100644 --- a/warpgate-web/src/admin/lib/openapi-schema.json +++ b/warpgate-web/src/admin/lib/openapi-schema.json @@ -2,7 +2,7 @@ "openapi": "3.0.0", "info": { "title": "Warpgate Web Admin", - "version": "0.10.2" + "version": "0.11.0" }, "servers": [ { @@ -69,6 +69,11 @@ } } }, + "security": [ + { + "TokenSecurityScheme": [] + } + ], "operationId": "get_sessions" }, "delete": { @@ -77,6 +82,11 @@ "description": "" } }, + "security": [ + { + "TokenSecurityScheme": [] + } + ], "operationId": "close_all_sessions" } }, @@ -110,6 +120,11 @@ "description": "" } }, + "security": [ + { + "TokenSecurityScheme": [] + } + ], "operationId": "get_session" } }, @@ -143,6 +158,11 @@ } } }, + "security": [ + { + "TokenSecurityScheme": [] + } + ], "operationId": "get_session_recordings" } }, @@ -169,6 +189,11 @@ "description": "" } }, + "security": [ + { + "TokenSecurityScheme": [] + } + ], "operationId": "close_session" } }, @@ -202,6 +227,11 @@ "description": "" } }, + "security": [ + { + "TokenSecurityScheme": [] + } + ], "operationId": "get_recording" } }, @@ -234,6 +264,11 @@ } } }, + "security": [ + { + "TokenSecurityScheme": [] + } + ], "operationId": "get_roles" }, "post": { @@ -269,6 +304,11 @@ } } }, + "security": [ + { + "TokenSecurityScheme": [] + } + ], "operationId": "create_role" } }, @@ -302,6 +342,11 @@ "description": "" } }, + "security": [ + { + "TokenSecurityScheme": [] + } + ], "operationId": "get_role" }, "put": { @@ -346,6 +391,11 @@ "description": "" } }, + "security": [ + { + "TokenSecurityScheme": [] + } + ], "operationId": "update_role" }, "delete": { @@ -373,9 +423,226 @@ "description": "" } }, + "security": [ + { + "TokenSecurityScheme": [] + } + ], "operationId": "delete_role" } }, + "/tickets": { + "get": { + "responses": { + "200": { + "description": "", + "content": { + "application/json; charset=utf-8": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Ticket" + } + } + } + } + } + }, + "security": [ + { + "TokenSecurityScheme": [] + } + ], + "operationId": "get_tickets" + }, + "post": { + "requestBody": { + "content": { + "application/json; charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/CreateTicketRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json; charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/TicketAndSecret" + } + } + } + }, + "400": { + "description": "", + "content": { + "application/json; charset=utf-8": { + "schema": { + "type": "string" + } + } + } + } + }, + "security": [ + { + "TokenSecurityScheme": [] + } + ], + "operationId": "create_ticket" + } + }, + "/tickets/{id}": { + "delete": { + "parameters": [ + { + "name": "id", + "schema": { + "type": "string", + "format": "uuid" + }, + "in": "path", + "required": true, + "deprecated": false, + "explode": true + } + ], + "responses": { + "204": { + "description": "" + }, + "404": { + "description": "" + } + }, + "security": [ + { + "TokenSecurityScheme": [] + } + ], + "operationId": "delete_ticket" + } + }, + "/ssh/known-hosts": { + "get": { + "responses": { + "200": { + "description": "", + "content": { + "application/json; charset=utf-8": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SSHKnownHost" + } + } + } + } + } + }, + "security": [ + { + "TokenSecurityScheme": [] + } + ], + "operationId": "get_ssh_known_hosts" + } + }, + "/ssh/known-hosts/{id}": { + "delete": { + "parameters": [ + { + "name": "id", + "schema": { + "type": "string", + "format": "uuid" + }, + "in": "path", + "required": true, + "deprecated": false, + "explode": true + } + ], + "responses": { + "204": { + "description": "" + }, + "404": { + "description": "" + } + }, + "security": [ + { + "TokenSecurityScheme": [] + } + ], + "operationId": "delete_ssh_known_host" + } + }, + "/ssh/own-keys": { + "get": { + "responses": { + "200": { + "description": "", + "content": { + "application/json; charset=utf-8": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SSHKey" + } + } + } + } + } + }, + "security": [ + { + "TokenSecurityScheme": [] + } + ], + "operationId": "get_ssh_own_keys" + } + }, + "/logs": { + "post": { + "requestBody": { + "content": { + "application/json; charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/GetLogsRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json; charset=utf-8": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LogEntry" + } + } + } + } + } + }, + "security": [ + { + "TokenSecurityScheme": [] + } + ], + "operationId": "get_logs" + } + }, "/targets": { "get": { "parameters": [ @@ -405,6 +672,11 @@ } } }, + "security": [ + { + "TokenSecurityScheme": [] + } + ], "operationId": "get_targets" }, "post": { @@ -440,6 +712,11 @@ } } }, + "security": [ + { + "TokenSecurityScheme": [] + } + ], "operationId": "create_target" } }, @@ -473,6 +750,11 @@ "description": "" } }, + "security": [ + { + "TokenSecurityScheme": [] + } + ], "operationId": "get_target" }, "put": { @@ -517,6 +799,11 @@ "description": "" } }, + "security": [ + { + "TokenSecurityScheme": [] + } + ], "operationId": "update_target" }, "delete": { @@ -544,6 +831,11 @@ "description": "" } }, + "security": [ + { + "TokenSecurityScheme": [] + } + ], "operationId": "delete_target" } }, @@ -580,6 +872,11 @@ "description": "" } }, + "security": [ + { + "TokenSecurityScheme": [] + } + ], "operationId": "get_target_roles" } }, @@ -617,6 +914,11 @@ "description": "" } }, + "security": [ + { + "TokenSecurityScheme": [] + } + ], "operationId": "add_target_role" }, "delete": { @@ -655,6 +957,11 @@ "description": "" } }, + "security": [ + { + "TokenSecurityScheme": [] + } + ], "operationId": "delete_target_role" } }, @@ -687,14 +994,19 @@ } } }, - "operationId": "get_users" + "security": [ + { + "TokenSecurityScheme": [] + } + ], + "operationId": "get_users" }, "post": { "requestBody": { "content": { "application/json; charset=utf-8": { "schema": { - "$ref": "#/components/schemas/UserDataRequest" + "$ref": "#/components/schemas/CreateUserRequest" } } }, @@ -722,6 +1034,11 @@ } } }, + "security": [ + { + "TokenSecurityScheme": [] + } + ], "operationId": "create_user" } }, @@ -755,6 +1072,11 @@ "description": "" } }, + "security": [ + { + "TokenSecurityScheme": [] + } + ], "operationId": "get_user" }, "put": { @@ -796,6 +1118,11 @@ "description": "" } }, + "security": [ + { + "TokenSecurityScheme": [] + } + ], "operationId": "update_user" }, "delete": { @@ -820,6 +1147,11 @@ "description": "" } }, + "security": [ + { + "TokenSecurityScheme": [] + } + ], "operationId": "delete_user" } }, @@ -856,6 +1188,11 @@ "description": "" } }, + "security": [ + { + "TokenSecurityScheme": [] + } + ], "operationId": "get_user_roles" } }, @@ -893,6 +1230,11 @@ "description": "" } }, + "security": [ + { + "TokenSecurityScheme": [] + } + ], "operationId": "add_user_role" }, "delete": { @@ -928,11 +1270,29 @@ "description": "" } }, + "security": [ + { + "TokenSecurityScheme": [] + } + ], "operationId": "delete_user_role" } }, - "/tickets": { + "/users/{user_id}/credentials/passwords": { "get": { + "parameters": [ + { + "name": "user_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "in": "path", + "required": true, + "deprecated": false, + "explode": true + } + ], "responses": { "200": { "description": "", @@ -941,21 +1301,39 @@ "schema": { "type": "array", "items": { - "$ref": "#/components/schemas/Ticket" + "$ref": "#/components/schemas/ExistingPasswordCredential" } } } } } }, - "operationId": "get_tickets" + "security": [ + { + "TokenSecurityScheme": [] + } + ], + "operationId": "get_password_credentials" }, "post": { + "parameters": [ + { + "name": "user_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "in": "path", + "required": true, + "deprecated": false, + "explode": true + } + ], "requestBody": { "content": { "application/json; charset=utf-8": { "schema": { - "$ref": "#/components/schemas/CreateTicketRequest" + "$ref": "#/components/schemas/NewPasswordCredential" } } }, @@ -967,28 +1345,214 @@ "content": { "application/json; charset=utf-8": { "schema": { - "$ref": "#/components/schemas/TicketAndSecret" + "$ref": "#/components/schemas/ExistingPasswordCredential" } } } + } + }, + "security": [ + { + "TokenSecurityScheme": [] + } + ], + "operationId": "create_password_credential" + } + }, + "/users/{user_id}/credentials/passwords/{id}": { + "delete": { + "parameters": [ + { + "name": "user_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "in": "path", + "required": true, + "deprecated": false, + "explode": true }, - "400": { + { + "name": "id", + "schema": { + "type": "string", + "format": "uuid" + }, + "in": "path", + "required": true, + "deprecated": false, + "explode": true + } + ], + "responses": { + "204": { + "description": "" + }, + "404": { + "description": "" + } + }, + "security": [ + { + "TokenSecurityScheme": [] + } + ], + "operationId": "delete_password_credential" + } + }, + "/users/{user_id}/credentials/sso": { + "get": { + "parameters": [ + { + "name": "user_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "in": "path", + "required": true, + "deprecated": false, + "explode": true + } + ], + "responses": { + "200": { "description": "", "content": { "application/json; charset=utf-8": { "schema": { - "type": "string" + "type": "array", + "items": { + "$ref": "#/components/schemas/ExistingSsoCredential" + } } } } } }, - "operationId": "create_ticket" + "security": [ + { + "TokenSecurityScheme": [] + } + ], + "operationId": "get_sso_credentials" + }, + "post": { + "parameters": [ + { + "name": "user_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "in": "path", + "required": true, + "deprecated": false, + "explode": true + } + ], + "requestBody": { + "content": { + "application/json; charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NewSsoCredential" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json; charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ExistingSsoCredential" + } + } + } + } + }, + "security": [ + { + "TokenSecurityScheme": [] + } + ], + "operationId": "create_sso_credential" } }, - "/tickets/{id}": { + "/users/{user_id}/credentials/sso/{id}": { + "put": { + "parameters": [ + { + "name": "user_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "in": "path", + "required": true, + "deprecated": false, + "explode": true + }, + { + "name": "id", + "schema": { + "type": "string", + "format": "uuid" + }, + "in": "path", + "required": true, + "deprecated": false, + "explode": true + } + ], + "requestBody": { + "content": { + "application/json; charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NewSsoCredential" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json; charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ExistingSsoCredential" + } + } + } + }, + "404": { + "description": "" + } + }, + "security": [ + { + "TokenSecurityScheme": [] + } + ], + "operationId": "update_sso_credential" + }, "delete": { "parameters": [ + { + "name": "user_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "in": "path", + "required": true, + "deprecated": false, + "explode": true + }, { "name": "id", "schema": { @@ -1009,11 +1573,29 @@ "description": "" } }, - "operationId": "delete_ticket" + "security": [ + { + "TokenSecurityScheme": [] + } + ], + "operationId": "delete_sso_credential" } }, - "/ssh/known-hosts": { + "/users/{user_id}/credentials/public-keys": { "get": { + "parameters": [ + { + "name": "user_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "in": "path", + "required": true, + "deprecated": false, + "explode": true + } + ], "responses": { "200": { "description": "", @@ -1022,19 +1604,135 @@ "schema": { "type": "array", "items": { - "$ref": "#/components/schemas/SSHKnownHost" + "$ref": "#/components/schemas/ExistingPublicKeyCredential" } } } } } }, - "operationId": "get_ssh_known_hosts" + "security": [ + { + "TokenSecurityScheme": [] + } + ], + "operationId": "get_public_key_credentials" + }, + "post": { + "parameters": [ + { + "name": "user_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "in": "path", + "required": true, + "deprecated": false, + "explode": true + } + ], + "requestBody": { + "content": { + "application/json; charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NewPublicKeyCredential" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json; charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ExistingPublicKeyCredential" + } + } + } + } + }, + "security": [ + { + "TokenSecurityScheme": [] + } + ], + "operationId": "create_public_key_credential" } }, - "/ssh/known-hosts/{id}": { + "/users/{user_id}/credentials/public-keys/{id}": { + "put": { + "parameters": [ + { + "name": "user_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "in": "path", + "required": true, + "deprecated": false, + "explode": true + }, + { + "name": "id", + "schema": { + "type": "string", + "format": "uuid" + }, + "in": "path", + "required": true, + "deprecated": false, + "explode": true + } + ], + "requestBody": { + "content": { + "application/json; charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NewPublicKeyCredential" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json; charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ExistingPublicKeyCredential" + } + } + } + }, + "404": { + "description": "" + } + }, + "security": [ + { + "TokenSecurityScheme": [] + } + ], + "operationId": "update_public_key_credential" + }, "delete": { "parameters": [ + { + "name": "user_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "in": "path", + "required": true, + "deprecated": false, + "explode": true + }, { "name": "id", "schema": { @@ -1055,11 +1753,29 @@ "description": "" } }, - "operationId": "delete_ssh_known_host" + "security": [ + { + "TokenSecurityScheme": [] + } + ], + "operationId": "delete_public_key_credential" } }, - "/ssh/own-keys": { + "/users/{user_id}/credentials/otp": { "get": { + "parameters": [ + { + "name": "user_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "in": "path", + "required": true, + "deprecated": false, + "explode": true + } + ], "responses": { "200": { "description": "", @@ -1068,44 +1784,104 @@ "schema": { "type": "array", "items": { - "$ref": "#/components/schemas/SSHKey" + "$ref": "#/components/schemas/ExistingOtpCredential" } } } } } }, - "operationId": "get_ssh_own_keys" - } - }, - "/logs": { + "security": [ + { + "TokenSecurityScheme": [] + } + ], + "operationId": "get_otp_credentials" + }, "post": { + "parameters": [ + { + "name": "user_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "in": "path", + "required": true, + "deprecated": false, + "explode": true + } + ], "requestBody": { "content": { "application/json; charset=utf-8": { "schema": { - "$ref": "#/components/schemas/GetLogsRequest" + "$ref": "#/components/schemas/NewOtpCredential" } } }, "required": true }, "responses": { - "200": { + "201": { "description": "", "content": { "application/json; charset=utf-8": { "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/LogEntry" - } + "$ref": "#/components/schemas/ExistingOtpCredential" } } } } }, - "operationId": "get_logs" + "security": [ + { + "TokenSecurityScheme": [] + } + ], + "operationId": "create_otp_credential" + } + }, + "/users/{user_id}/credentials/otp/{id}": { + "delete": { + "parameters": [ + { + "name": "user_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "in": "path", + "required": true, + "deprecated": false, + "explode": true + }, + { + "name": "id", + "schema": { + "type": "string", + "format": "uuid" + }, + "in": "path", + "required": true, + "deprecated": false, + "explode": true + } + ], + "responses": { + "204": { + "description": "" + }, + "404": { + "description": "" + } + }, + "security": [ + { + "TokenSecurityScheme": [] + } + ], + "operationId": "delete_otp_credential" } } }, @@ -1134,6 +1910,17 @@ } } }, + "CreateUserRequest": { + "type": "object", + "required": [ + "username" + ], + "properties": { + "username": { + "type": "string" + } + } + }, "CredentialKind": { "type": "string", "enum": [ @@ -1144,6 +1931,65 @@ "WebUserApproval" ] }, + "ExistingOtpCredential": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "string", + "format": "uuid" + } + } + }, + "ExistingPasswordCredential": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "string", + "format": "uuid" + } + } + }, + "ExistingPublicKeyCredential": { + "type": "object", + "required": [ + "id", + "openssh_public_key" + ], + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "openssh_public_key": { + "type": "string" + } + } + }, + "ExistingSsoCredential": { + "type": "object", + "required": [ + "id", + "email" + ], + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "provider": { + "type": "string" + }, + "email": { + "type": "string" + } + } + }, "GetLogsRequest": { "type": "object", "properties": { @@ -1202,6 +2048,57 @@ } } }, + "NewOtpCredential": { + "type": "object", + "required": [ + "secret_key" + ], + "properties": { + "secret_key": { + "type": "array", + "items": { + "type": "integer", + "format": "uint8" + } + } + } + }, + "NewPasswordCredential": { + "type": "object", + "required": [ + "password" + ], + "properties": { + "password": { + "type": "string" + } + } + }, + "NewPublicKeyCredential": { + "type": "object", + "required": [ + "openssh_public_key" + ], + "properties": { + "openssh_public_key": { + "type": "string" + } + } + }, + "NewSsoCredential": { + "type": "object", + "required": [ + "email" + ], + "properties": { + "provider": { + "type": "string" + }, + "email": { + "type": "string" + } + } + }, "PaginatedSessionSnapshot": { "type": "object", "required": [ @@ -1814,9 +2711,7 @@ "type": "object", "required": [ "id", - "username", - "credentials", - "roles" + "username" ], "properties": { "id": { @@ -1826,180 +2721,25 @@ "username": { "type": "string" }, - "credentials": { - "type": "array", - "items": { - "$ref": "#/components/schemas/UserAuthCredential" - } - }, "credential_policy": { "$ref": "#/components/schemas/UserRequireCredentialsPolicy" - }, - "roles": { - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "UserAuthCredential": { - "type": "object", - "oneOf": [ - { - "$ref": "#/components/schemas/UserAuthCredential_UserPasswordCredential" - }, - { - "$ref": "#/components/schemas/UserAuthCredential_UserPublicKeyCredential" - }, - { - "$ref": "#/components/schemas/UserAuthCredential_UserTotpCredential" - }, - { - "$ref": "#/components/schemas/UserAuthCredential_UserSsoCredential" - } - ], - "discriminator": { - "propertyName": "kind", - "mapping": { - "Password": "#/components/schemas/UserAuthCredential_UserPasswordCredential", - "PublicKey": "#/components/schemas/UserAuthCredential_UserPublicKeyCredential", - "Totp": "#/components/schemas/UserAuthCredential_UserTotpCredential", - "Sso": "#/components/schemas/UserAuthCredential_UserSsoCredential" } } }, - "UserAuthCredential_UserPasswordCredential": { - "allOf": [ - { - "type": "object", - "required": [ - "kind" - ], - "properties": { - "kind": { - "type": "string", - "enum": [ - "Password" - ], - "example": "Password" - } - } - }, - { - "$ref": "#/components/schemas/UserPasswordCredential" - } - ] - }, - "UserAuthCredential_UserPublicKeyCredential": { - "allOf": [ - { - "type": "object", - "required": [ - "kind" - ], - "properties": { - "kind": { - "type": "string", - "enum": [ - "PublicKey" - ], - "example": "PublicKey" - } - } - }, - { - "$ref": "#/components/schemas/UserPublicKeyCredential" - } - ] - }, - "UserAuthCredential_UserSsoCredential": { - "allOf": [ - { - "type": "object", - "required": [ - "kind" - ], - "properties": { - "kind": { - "type": "string", - "enum": [ - "Sso" - ], - "example": "Sso" - } - } - }, - { - "$ref": "#/components/schemas/UserSsoCredential" - } - ] - }, - "UserAuthCredential_UserTotpCredential": { - "allOf": [ - { - "type": "object", - "required": [ - "kind" - ], - "properties": { - "kind": { - "type": "string", - "enum": [ - "Totp" - ], - "example": "Totp" - } - } - }, - { - "$ref": "#/components/schemas/UserTotpCredential" - } - ] - }, "UserDataRequest": { "type": "object", "required": [ - "username", - "credentials" + "username" ], "properties": { "username": { "type": "string" }, - "credentials": { - "type": "array", - "items": { - "$ref": "#/components/schemas/UserAuthCredential" - } - }, "credential_policy": { "$ref": "#/components/schemas/UserRequireCredentialsPolicy" } } }, - "UserPasswordCredential": { - "type": "object", - "required": [ - "hash" - ], - "properties": { - "hash": { - "type": "string" - } - } - }, - "UserPublicKeyCredential": { - "type": "object", - "required": [ - "key" - ], - "properties": { - "key": { - "type": "string" - } - } - }, "UserRequireCredentialsPolicy": { "type": "object", "properties": { @@ -2028,35 +2768,13 @@ } } } - }, - "UserSsoCredential": { - "type": "object", - "required": [ - "email" - ], - "properties": { - "provider": { - "type": "string" - }, - "email": { - "type": "string" - } - } - }, - "UserTotpCredential": { - "type": "object", - "required": [ - "key" - ], - "properties": { - "key": { - "type": "array", - "items": { - "type": "integer", - "format": "uint8" - } - } - } + } + }, + "securitySchemes": { + "TokenSecurityScheme": { + "type": "apiKey", + "name": "X-Warpgate-Token", + "in": "header" } } } diff --git a/warpgate-web/src/common/protocols.ts b/warpgate-web/src/common/protocols.ts index 5fdee76ba..8fe985982 100644 --- a/warpgate-web/src/common/protocols.ts +++ b/warpgate-web/src/common/protocols.ts @@ -1,5 +1,6 @@ import { shellEscape } from 'gateway/lib/shellEscape' import type { Info } from 'gateway/lib/api' +import { CredentialKind } from 'admin/lib/api' export interface ConnectionOptions { targetName?: string @@ -58,3 +59,10 @@ export function makeTargetURL (opt: ConnectionOptions): string { } return `${location.protocol}//${host}/?warpgate-target=${opt.targetName}` } + +export const possibleCredentials: Record> = { + ssh: new Set([CredentialKind.Password, CredentialKind.PublicKey, CredentialKind.Totp, CredentialKind.WebUserApproval]), + http: new Set([CredentialKind.Password, CredentialKind.Totp, CredentialKind.Sso]), + mysql: new Set([CredentialKind.Password]), + postgres: new Set([CredentialKind.Password]), +} diff --git a/warpgate-web/src/gateway/lib/openapi-schema.json b/warpgate-web/src/gateway/lib/openapi-schema.json index 66acf2e79..e8f4839eb 100644 --- a/warpgate-web/src/gateway/lib/openapi-schema.json +++ b/warpgate-web/src/gateway/lib/openapi-schema.json @@ -2,7 +2,7 @@ "openapi": "3.0.0", "info": { "title": "Warpgate HTTP proxy", - "version": "0.10.2" + "version": "0.11.0" }, "servers": [ { @@ -11,6 +11,21 @@ ], "tags": [], "paths": { + "/__stub__": { + "get": { + "responses": { + "200": { + "description": "" + } + }, + "security": [ + { + "TokenSecurityScheme": [] + } + ], + "operationId": "__stub__" + } + }, "/auth/login": { "post": { "requestBody": { @@ -600,6 +615,13 @@ } } } + }, + "securitySchemes": { + "TokenSecurityScheme": { + "type": "apiKey", + "name": "X-Warpgate-Token", + "in": "header" + } } } } diff --git a/warpgate/Cargo.toml b/warpgate/Cargo.toml index b9774d29f..46cfe83ba 100644 --- a/warpgate/Cargo.toml +++ b/warpgate/Cargo.toml @@ -9,17 +9,17 @@ ansi_term = "0.12" anyhow = { version = "1.0", features = ["backtrace"] } async-trait = "0.1" atty = "0.2" -bytes = "1.4" +bytes.workspace = true clap = { version = "4.0", features = ["derive"] } config = { version = "0.13", features = ["yaml"], default-features = false } console = { version = "0.15", default-features = false } console-subscriber = { version = "0.1", optional = true } -data-encoding = "2.3" +data-encoding.workspace = true dialoguer = "0.10" futures = "0.3" notify = "5.1" rcgen = { version = "0.10", features = ["zeroize"] } -serde_json = "1.0" +serde_json.workspace = true serde_yaml = "0.9" sea-orm = { version = "0.12.2", default-features = false } time = "0.3" diff --git a/warpgate/src/commands/recover_access.rs b/warpgate/src/commands/recover_access.rs index 71b6dd210..b4668820a 100644 --- a/warpgate/src/commands/recover_access.rs +++ b/warpgate/src/commands/recover_access.rs @@ -2,11 +2,11 @@ use anyhow::Result; use dialoguer::theme::ColorfulTheme; use sea_orm::{ActiveModelTrait, EntityTrait, QueryOrder, Set}; use tracing::*; +use uuid::Uuid; use warpgate_common::auth::CredentialKind; -use warpgate_common::helpers::hash::hash_password; -use warpgate_common::{Secret, User as UserConfig, UserAuthCredential, UserPasswordCredential}; +use warpgate_common::{Secret, User as UserConfig, UserPasswordCredential}; use warpgate_core::Services; -use warpgate_db_entities::User; +use warpgate_db_entities::{PasswordCredential, User}; use crate::commands::common::assert_interactive_terminal; use crate::config::load_config; @@ -15,7 +15,7 @@ pub(crate) async fn command(cli: &crate::Cli, username: &Option) -> Resu assert_interactive_terminal(); let config = load_config(&cli.config, true)?; - let services = Services::new(config.clone()).await?; + let services = Services::new(config.clone(), None).await?; warpgate_protocol_ssh::generate_host_keys(&config)?; warpgate_protocol_ssh::generate_client_keys(&config)?; @@ -47,9 +47,11 @@ pub(crate) async fn command(cli: &crate::Cli, username: &Option) -> Resu } }; - let password = dialoguer::Password::with_theme(&theme) - .with_prompt(format!("New password for {}", user.username)) - .interact()?; + let password = Secret::new( + dialoguer::Password::with_theme(&theme) + .with_prompt(format!("New password for {}", user.username)) + .interact()?, + ); if !dialoguer::Confirm::with_theme(&theme) .default(true) @@ -58,22 +60,25 @@ pub(crate) async fn command(cli: &crate::Cli, username: &Option) -> Resu std::process::exit(0); } - user.credentials - .push(UserAuthCredential::Password(UserPasswordCredential { - hash: Secret::new(hash_password(&password)), - })); + PasswordCredential::ActiveModel { + user_id: Set(user.id), + id: Set(Uuid::new_v4()), + ..UserPasswordCredential::from_password(&password).into() + } + .insert(&*db) + .await?; + user.credential_policy .get_or_insert_with(Default::default) .http = Some(vec![CredentialKind::Password]); - let model = User::ActiveModel { + User::ActiveModel { id: Set(user.id), - credentials: Set(serde_json::to_value(&user.credentials)?), credential_policy: Set(serde_json::to_value(Some(&user.credential_policy))?), ..Default::default() - }; - - model.update(&*db).await?; + } + .update(&*db) + .await?; info!("All done. You can now log in"); diff --git a/warpgate/src/commands/run.rs b/warpgate/src/commands/run.rs index 8416eeb08..bcc1eb2c1 100644 --- a/warpgate/src/commands/run.rs +++ b/warpgate/src/commands/run.rs @@ -16,12 +16,12 @@ use warpgate_protocol_ssh::SSHProtocolServer; use crate::config::{load_config, watch_config}; -pub(crate) async fn command(cli: &crate::Cli) -> Result<()> { +pub(crate) async fn command(cli: &crate::Cli, admin_token: Option) -> Result<()> { let version = env!("CARGO_PKG_VERSION"); info!(%version, "Warpgate"); let config = load_config(&cli.config, true)?; - let services = Services::new(config.clone()).await?; + let services = Services::new(config.clone(), admin_token).await?; install_database_logger(services.db.clone()); diff --git a/warpgate/src/commands/setup.rs b/warpgate/src/commands/setup.rs index 73b3f6446..0ec4ecad2 100644 --- a/warpgate/src/commands/setup.rs +++ b/warpgate/src/commands/setup.rs @@ -12,14 +12,13 @@ use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set}; use tracing::*; use uuid::Uuid; use warpgate_common::helpers::fs::{secure_directory, secure_file}; -use warpgate_common::helpers::hash::hash_password; use warpgate_common::{ - HttpConfig, ListenEndpoint, MySqlConfig, PostgresConfig, Secret, SshConfig, UserAuthCredential, + HttpConfig, ListenEndpoint, MySqlConfig, PostgresConfig, Secret, SshConfig, UserPasswordCredential, UserRequireCredentialsPolicy, WarpgateConfigStore, WarpgateError, }; use warpgate_core::consts::{BUILTIN_ADMIN_ROLE_NAME, BUILTIN_ADMIN_USERNAME}; use warpgate_core::Services; -use warpgate_db_entities::{Role, User, UserRoleAssignment}; +use warpgate_db_entities::{PasswordCredential, Role, User, UserRoleAssignment}; use crate::commands::common::{assert_interactive_terminal, is_docker}; use crate::config::load_config; @@ -263,25 +262,27 @@ pub(crate) async fn command(cli: &crate::Cli) -> Result<()> { // --- - let admin_password = if let Commands::UnattendedSetup { admin_password, .. } = &cli.command { - if let Some(admin_password) = admin_password { - admin_password.to_owned() - } else { - if let Ok(admin_password) = std::env::var("WARPGATE_ADMIN_PASSWORD") { - admin_password + let admin_password = Secret::new( + if let Commands::UnattendedSetup { admin_password, .. } = &cli.command { + if let Some(admin_password) = admin_password { + admin_password.to_owned() } else { - error!( + if let Ok(admin_password) = std::env::var("WARPGATE_ADMIN_PASSWORD") { + admin_password + } else { + error!( "You must supply the admin password either through the --admin-password option" ); - error!("or the WARPGATE_ADMIN_PASSWORD environment variable."); - std::process::exit(1); + error!("or the WARPGATE_ADMIN_PASSWORD environment variable."); + std::process::exit(1); + } } - } - } else { - dialoguer::Password::with_theme(&theme) - .with_prompt("Set a password for the Warpgate admin user") - .interact()? - }; + } else { + dialoguer::Password::with_theme(&theme) + .with_prompt("Set a password for the Warpgate admin user") + .interact()? + }, + ); // --- @@ -293,7 +294,7 @@ pub(crate) async fn command(cli: &crate::Cli) -> Result<()> { info!("Saved into {}", cli.config.display()); let config = load_config(&cli.config, true)?; - let services = Services::new(config.clone()).await?; + let services = Services::new(config.clone(), None).await?; warpgate_protocol_ssh::generate_host_keys(&config)?; warpgate_protocol_ssh::generate_client_keys(&config)?; @@ -319,11 +320,6 @@ pub(crate) async fn command(cli: &crate::Cli) -> Result<()> { let values = User::ActiveModel { id: Set(Uuid::new_v4()), username: Set(BUILTIN_ADMIN_USERNAME.to_owned()), - credentials: Set(serde_json::to_value(vec![UserAuthCredential::Password( - UserPasswordCredential { - hash: Secret::new(hash_password(&admin_password)), - }, - )])?), credential_policy: Set(serde_json::to_value( None::, )?), @@ -332,6 +328,14 @@ pub(crate) async fn command(cli: &crate::Cli) -> Result<()> { } }; + PasswordCredential::ActiveModel { + user_id: Set(admin_user.id), + id: Set(Uuid::new_v4()), + ..UserPasswordCredential::from_password(&admin_password).into() + } + .insert(&*db) + .await?; + if UserRoleAssignment::Entity::find() .filter(UserRoleAssignment::Column::UserId.eq(admin_user.id)) .filter(UserRoleAssignment::Column::RoleId.eq(admin_role.id)) diff --git a/warpgate/src/commands/test_target.rs b/warpgate/src/commands/test_target.rs index 6a84267d6..2f96c4dc7 100644 --- a/warpgate/src/commands/test_target.rs +++ b/warpgate/src/commands/test_target.rs @@ -7,7 +7,7 @@ use crate::config::load_config; pub(crate) async fn command(cli: &crate::Cli, target_name: &String) -> Result<()> { let config = load_config(&cli.config, true)?; - let services = Services::new(config.clone()).await?; + let services = Services::new(config.clone(), None).await?; let Some(target) = services .config_provider diff --git a/warpgate/src/main.rs b/warpgate/src/main.rs index de7b15cd0..8bcd45e58 100644 --- a/warpgate/src/main.rs +++ b/warpgate/src/main.rs @@ -70,7 +70,11 @@ pub(crate) enum Commands { /// Show Warpgate's SSH client keys ClientKeys, /// Run Warpgate - Run, + Run { + /// An API token that automatically maps to the first admin user + #[clap(long)] + admin_token: Option, + }, /// Create a password hash for use in the config file Check, /// Test the connection to a target host @@ -91,7 +95,9 @@ async fn _main() -> Result<()> { init_logging(load_config(&cli.config, false).ok().as_ref(), &cli).await; match &cli.command { - Commands::Run => crate::commands::run::command(&cli).await, + Commands::Run { admin_token } => { + crate::commands::run::command(&cli, admin_token.clone()).await + } Commands::Check => crate::commands::check::command(&cli).await, Commands::TestTarget { target_name } => { crate::commands::test_target::command(&cli, target_name).await