diff --git a/.github/workflows/_oidc_bff_code.yaml b/.github/workflows/_oidc_bff_code.yaml new file mode 100644 index 000000000..81af7ec8b --- /dev/null +++ b/.github/workflows/_oidc_bff_code.yaml @@ -0,0 +1,70 @@ +name: OIDC BFF Code + +on: + workflow_call: + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - name: Checkout source + uses: actions/checkout@v5 + + - name: Install stable toolchain + uses: actions-rust-lang/setup-rust-toolchain@v1.15.2 + with: + cache: false + components: clippy,rustfmt + + - name: Cache Rust Build + uses: Swatinem/rust-cache@v2.8.1 + with: + shared-key: backend/oidc-bff + workspaces: backend + + - name: Check Formatting + working-directory: backend/oidc-bff + run: > + cargo fmt + --check + + - name: Lint with Clippy + working-directory: backend/oidc-bff + run: > + cargo clippy + --all-targets + --all-features + --no-deps + -- + --deny warnings + + - name: Check Dependencies with Cargo Deny + uses: EmbarkStudios/cargo-deny-action@v2.0.13 + with: + command: check licenses ban + manifest-path: backend/Cargo.toml + + test: + runs-on: ubuntu-latest + steps: + - name: Checkout source + uses: actions/checkout@v5 + + - name: Install stable toolchain + uses: actions-rust-lang/setup-rust-toolchain@v1.15.2 + with: + cache: false + components: rustfmt + + - name: Cache Rust Build + uses: Swatinem/rust-cache@v2.8.1 + with: + shared-key: backend/oidc-bff + workspaces: backend + + - name: Run Tests + working-directory: backend/oidc-bff + run: > + cargo test + --all-targets + --all-features diff --git a/.github/workflows/_oidc_bff_container.yaml b/.github/workflows/_oidc_bff_container.yaml new file mode 100644 index 000000000..b848627c1 --- /dev/null +++ b/.github/workflows/_oidc_bff_container.yaml @@ -0,0 +1,53 @@ +name: OIDC BFF Container +on: + workflow_call: + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - name: Checkout Code + uses: actions/checkout@v5 + + - name: Generate Image Name + run: echo IMAGE_REPOSITORY=ghcr.io/$(echo "${{ github.repository }}" | tr '[:upper:]' '[:lower:]' | tr '[_]' '[\-]')-oidc-bff >> $GITHUB_ENV + + - name: Log in to GitHub Docker Registry + if: github.event_name != 'pull_request' + uses: docker/login-action@v3.6.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract Version from Tag + id: tags + run: echo version=$(echo "${{ github.ref }}" | awk -F '[@v]' '{print $3}') >> $GITHUB_OUTPUT + + - name: Docker Metadata + id: meta + uses: docker/metadata-action@v5.9.0 + with: + images: ${{ env.IMAGE_REPOSITORY }} + tags: | + type=raw,value=${{ steps.tags.outputs.version }} + type=raw,value=latest + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3.11.1 + + - name: Build Image + uses: docker/build-push-action@v6.18.0 + with: + context: backend + file: backend/Dockerfile.oidc-bff + target: deploy + push: true + load: ${{ !(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/oidc-bff@')) }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 9c7589bd5..2e57696cf 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -63,6 +63,20 @@ jobs: contents: read packages: write + oidc_bff_code: + # Deduplicate jobs from pull requests and branch pushes within the same repo. + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.repository + uses: ./.github/workflows/_oidc_bff_code.yaml + + oidc_bff_container: + # Deduplicate jobs from pull requests and branch pushes within the same repo. + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.repository + needs: oidc_bff_code + uses: ./.github/workflows/_oidc_bff_container.yaml + permissions: + contents: read + packages: write + supergraph_update: # Deduplicate jobs from pull requests and branch pushes within the same repo. if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.repository diff --git a/backend/Cargo.lock b/backend/Cargo.lock index bee1e794a..28424c077 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -12,6 +12,32 @@ dependencies = [ "regex", ] +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom 0.2.15", + "once_cell", + "version_check", +] + [[package]] name = "ahash" version = "0.8.12" @@ -34,6 +60,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "aliasable" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" + [[package]] name = "allocator-api2" version = "0.2.21" @@ -104,6 +136,9 @@ name = "anyhow" version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +dependencies = [ + "backtrace", +] [[package]] name = "argo-workflows-openapi" @@ -119,6 +154,12 @@ dependencies = [ "typify", ] +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + [[package]] name = "ascii_utils" version = "0.9.3" @@ -174,6 +215,22 @@ dependencies = [ "serde_json", ] +[[package]] +name = "astral-tokio-tar" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec179a06c1769b1e42e1e2cbe74c7dcdb3d6383c838454d063eaac5bbb7ebbe5" +dependencies = [ + "filetime", + "futures-core", + "libc", + "portable-atomic", + "rustc-hash", + "tokio", + "tokio-stream", + "xattr", +] + [[package]] name = "async-broadcast" version = "0.7.2" @@ -206,7 +263,7 @@ dependencies = [ "futures-util", "handlebars", "http 1.3.1", - "indexmap", + "indexmap 2.8.0", "mime", "multer", "num-traits", @@ -274,11 +331,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34ecdaff7c9cffa3614a9f9999bf9ee4c3078fe3ce4d6a6e161736b56febf2de" dependencies = [ "bytes", - "indexmap", + "indexmap 2.8.0", "serde", "serde_json", ] +[[package]] +name = "async-lock" +version = "3.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd03604047cee9b6ce9de9f70c6cd540a0520c813cbd49bae61f33ab80ed1dc" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + [[package]] name = "async-stream" version = "0.3.6" @@ -327,22 +395,57 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "auth-common" +version = "0.1.0" +dependencies = [ + "anyhow", + "axum", + "base64 0.22.1", + "chrono", + "http-body-util", + "oauth2", + "openidconnect", + "sea-orm", + "serde", + "serde_json", + "serde_yaml", + "sodiumoxide", + "tracing", +] + [[package]] name = "auth-daemon" version = "0.1.0" dependencies = [ "anyhow", + "auth-common", "axum", + "axum-reverse-proxy", + "axum-test", + "base64 0.22.1", + "chrono", "clap", "dotenvy", + "env_logger", + "http-body-util", + "migration", "mockito", + "oauth2", + "oauth2-test-server", + "openidconnect", "regex", "reqwest", + "rustls 0.23.35", + "sea-orm", "serde", "serde_json", + "serde_yaml", + "sodiumoxide", + "testcontainers", "thiserror 2.0.17", "tokio", - "tower-http", + "tower-http 0.6.6", "tracing", "tracing-subscriber", "url", @@ -368,19 +471,20 @@ dependencies = [ [[package]] name = "aws-lc-rs" -version = "1.13.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b756939cb2f8dc900aa6dcd505e6e2428e9cae7ff7b028c49e3946efa70878" +checksum = "5932a7d9d28b0d2ea34c6b3779d35e3dd6f6345317c34e73438c4f1f29144151" dependencies = [ "aws-lc-sys", + "untrusted 0.7.1", "zeroize", ] [[package]] name = "aws-lc-sys" -version = "0.28.0" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9f7720b74ed28ca77f90769a71fd8c637a0137f6fae4ae947e1050229cff57f" +checksum = "1826f2e4cfc2cd19ee53c42fbf68e2f81ec21108e0b7ecf6a71cf062137360fc" dependencies = [ "bindgen", "cc", @@ -469,7 +573,7 @@ dependencies = [ "http 0.2.12", "http 1.3.1", "once_cell", - "p256", + "p256 0.11.1", "percent-encoding", "ring 0.17.14", "sha2", @@ -559,13 +663,13 @@ dependencies = [ "http 1.3.1", "http-body 0.4.6", "hyper 0.14.32", - "hyper 1.7.0", + "hyper 1.8.1", "hyper-rustls 0.24.2", "hyper-rustls 0.27.5", "hyper-util", "pin-project-lite", "rustls 0.21.12", - "rustls 0.23.28", + "rustls 0.23.35", "rustls-native-certs 0.8.1", "rustls-pki-types", "tokio", @@ -685,11 +789,12 @@ dependencies = [ [[package]] name = "axum" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a18ed336352031311f4e0b4dd2ff392d4fbb370777c9d18d7fc9d7359f73871" +checksum = "5b098575ebe77cb6d14fc7f32749631a6e44edbef6b796f89b020e99ba20d425" dependencies = [ "axum-core", + "axum-macros", "base64 0.22.1", "bytes", "form_urlencoded", @@ -697,7 +802,7 @@ dependencies = [ "http 1.3.1", "http-body 1.0.1", "http-body-util", - "hyper 1.7.0", + "hyper 1.8.1", "hyper-util", "itoa", "matchit", @@ -712,7 +817,7 @@ dependencies = [ "sha1", "sync_wrapper", "tokio", - "tokio-tungstenite", + "tokio-tungstenite 0.28.0", "tower", "tower-layer", "tower-service", @@ -760,6 +865,72 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", +] + +[[package]] +name = "axum-reverse-proxy" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84bb2eec9f1a2c150ce1204d843d690db0da305f6f2848cbfd4a840c830b4f0b" +dependencies = [ + "axum", + "base64 0.21.7", + "bytes", + "futures-util", + "http 1.3.1", + "http-body 1.0.1", + "http-body-util", + "hyper 1.8.1", + "hyper-rustls 0.27.5", + "hyper-util", + "rand 0.9.2", + "rustls 0.23.35", + "sha1", + "tokio", + "tokio-tungstenite 0.21.0", + "tower", + "tracing", + "url", +] + +[[package]] +name = "axum-test" +version = "18.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3290e73c56c5cc4701cdd7d46b9ced1b4bd61c7e9f9c769a9e9e87ff617d75d2" +dependencies = [ + "anyhow", + "axum", + "bytes", + "bytesize", + "cookie", + "expect-json", + "http 1.3.1", + "http-body-util", + "hyper 1.8.1", + "hyper-util", + "mime", + "pretty_assertions", + "reserve-port", + "rust-multipart-rfc7578_2", + "serde", + "serde_json", + "serde_urlencoded", + "smallvec", + "tokio", + "tower", + "url", +] + [[package]] name = "backon" version = "1.5.1" @@ -771,12 +942,33 @@ dependencies = [ "tokio", ] +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-link 0.2.0", +] + [[package]] name = "base16ct" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce" +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base64" version = "0.21.7" @@ -805,27 +997,38 @@ version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3" +[[package]] +name = "bigdecimal" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "560f42649de9fa436b73517378a147ec21f6c997a546581df4b4b31677828934" +dependencies = [ + "autocfg", + "libm", + "num-bigint", + "num-integer", + "num-traits", + "serde", +] + [[package]] name = "bindgen" -version = "0.69.5" +version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ "bitflags", "cexpr", "clang-sys", "itertools 0.12.1", - "lazy_static", - "lazycell", "log", "prettyplease", "proc-macro2", "quote", "regex", - "rustc-hash 1.1.0", + "rustc-hash", "shlex", "syn 2.0.107", - "which", ] [[package]] @@ -837,6 +1040,18 @@ dependencies = [ "serde", ] +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -846,6 +1061,106 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bollard" +version = "0.19.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87a52479c9237eb04047ddb94788c41ca0d26eaff8b697ecfbb4c32f7fdc3b1b" +dependencies = [ + "async-stream", + "base64 0.22.1", + "bitflags", + "bollard-buildkit-proto", + "bollard-stubs", + "bytes", + "chrono", + "futures-core", + "futures-util", + "hex", + "home", + "http 1.3.1", + "http-body-util", + "hyper 1.8.1", + "hyper-named-pipe", + "hyper-rustls 0.27.5", + "hyper-util", + "hyperlocal", + "log", + "num", + "pin-project-lite", + "rand 0.9.2", + "rustls 0.23.35", + "rustls-native-certs 0.8.1", + "rustls-pemfile 2.2.0", + "rustls-pki-types", + "serde", + "serde_derive", + "serde_json", + "serde_repr", + "serde_urlencoded", + "thiserror 2.0.17", + "tokio", + "tokio-stream", + "tokio-util", + "tonic", + "tower-service", + "url", + "winapi", +] + +[[package]] +name = "bollard-buildkit-proto" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a885520bf6249ab931a764ffdb87b0ceef48e6e7d807cfdb21b751e086e1ad" +dependencies = [ + "prost", + "prost-types", + "tonic", + "tonic-prost", + "ureq", +] + +[[package]] +name = "bollard-stubs" +version = "1.49.1-rc.28.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5731fe885755e92beff1950774068e0cae67ea6ec7587381536fca84f1779623" +dependencies = [ + "base64 0.22.1", + "bollard-buildkit-proto", + "bytes", + "chrono", + "prost", + "serde", + "serde_json", + "serde_repr", + "serde_with", +] + +[[package]] +name = "borsh" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8646f98db542e39fc66e68a20b2144f6a732636df7c2354e74645faaa433ce" +dependencies = [ + "borsh-derive", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd1d3c0c2f5833f22386f252fe8ed005c7f59fdcddeef025c01b4c3b9fd9ac3" +dependencies = [ + "once_cell", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.107", +] + [[package]] name = "built" version = "0.8.0" @@ -858,6 +1173,28 @@ version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" +[[package]] +name = "bytecheck" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "byteorder" version = "1.5.0" @@ -866,9 +1203,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" dependencies = [ "serde", ] @@ -883,12 +1220,19 @@ dependencies = [ "either", ] +[[package]] +name = "bytesize" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bd91ee7b2422bcb158d90ef4d14f75ef67f340943fc4149891dcce8f8b972a3" + [[package]] name = "cc" -version = "1.2.17" +version = "1.2.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fcb57c740ae1daf453ae85f16e37396f672b039e00d9d866e07ddb24e328e3a" +checksum = "b97463e1064cb1b1c1384ad0a0b9c8abd0988e2a91f52606c80ef14aadb63e36" dependencies = [ + "find-msvc-tools", "jobserver", "libc", "shlex", @@ -968,7 +1312,7 @@ version = "4.5.49" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote", "syn 2.0.107", @@ -1020,27 +1364,56 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" [[package]] -name = "core-foundation" -version = "0.9.4" +name = "cookie" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" dependencies = [ - "core-foundation-sys", - "libc", + "percent-encoding", + "time", + "version_check", ] [[package]] -name = "core-foundation" -version = "0.10.0" +name = "cookie_store" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63" +checksum = "2eac901828f88a5241ee0600950ab981148a18f2f756900ffba1b125ca6a3ef9" dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation-sys" + "cookie", + "document-features", + "idna", + "log", + "publicsuffix", + "serde", + "serde_derive", + "serde_json", + "time", + "url", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" @@ -1096,6 +1469,24 @@ dependencies = [ "crc", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-queue" version = "0.3.12" @@ -1129,8 +1520,10 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" dependencies = [ + "generic-array", "rand_core 0.6.4", "subtle", + "zeroize", ] [[package]] @@ -1143,6 +1536,33 @@ dependencies = [ "typenum", ] +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", +] + [[package]] name = "darling" version = "0.20.11" @@ -1278,6 +1698,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d630bccd429a5bb5a64b5e94f693bfc48c9f8566418fda4c494cc94f911f87cc" dependencies = [ "powerfmt", + "serde", ] [[package]] @@ -1298,8 +1719,15 @@ dependencies = [ "proc-macro2", "quote", "syn 2.0.107", + "unicode-xid", ] +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + [[package]] name = "digest" version = "0.10.7" @@ -1323,6 +1751,26 @@ dependencies = [ "syn 2.0.107", ] +[[package]] +name = "docker_credential" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d89dfcba45b4afad7450a99b39e751590463e45c04728cf555d36bb66940de8" +dependencies = [ + "base64 0.21.7", + "serde", + "serde_json", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + [[package]] name = "dotenvy" version = "0.15.7" @@ -1348,11 +1796,58 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "413301934810f597c1d19ca71c8710e99a3f1ba28a0d2ebc01551a2daeea3c5c" dependencies = [ "der 0.6.1", - "elliptic-curve", - "rfc6979", + "elliptic-curve 0.12.3", + "rfc6979 0.3.1", + "signature 1.6.4", +] + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der 0.7.9", + "digest", + "elliptic-curve 0.13.8", + "rfc6979 0.4.0", + "signature 2.2.0", + "spki 0.7.3", +] + +[[package]] +name = "ed25519" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91cff35c70bba8a626e3185d8cd48cc11b5437e1a5bcd15b9b5fa3c64b6dfee7" +dependencies = [ "signature 1.6.4", ] +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8 0.10.2", + "signature 2.2.0", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519 2.2.3", + "serde", + "sha2", + "subtle", + "zeroize", +] + [[package]] name = "educe" version = "0.6.0" @@ -1380,20 +1875,50 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7bb888ab5300a19b8e5bceef25ac745ad065f3c9f7efc6de1b91958110891d3" dependencies = [ - "base16ct", + "base16ct 0.1.1", "crypto-bigint 0.4.9", "der 0.6.1", "digest", - "ff", + "ff 0.12.1", "generic-array", - "group", + "group 0.12.1", "pkcs8 0.9.0", "rand_core 0.6.4", - "sec1", + "sec1 0.3.0", + "subtle", + "zeroize", +] + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct 0.2.0", + "crypto-bigint 0.5.5", + "digest", + "ff 0.13.1", + "generic-array", + "group 0.13.0", + "hkdf", + "pem-rfc7468", + "pkcs8 0.10.2", + "rand_core 0.6.4", + "sec1 0.7.3", "subtle", "zeroize", ] +[[package]] +name = "email_address" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449" +dependencies = [ + "serde", +] + [[package]] name = "encoding_rs" version = "0.8.35" @@ -1423,12 +1948,46 @@ dependencies = [ "syn 2.0.107", ] +[[package]] +name = "env_filter" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + [[package]] name = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "erased-serde" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89e8918065695684b2b0702da20382d5ae6065cf3327bc2d6436bd49a71ce9f3" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + [[package]] name = "errno" version = "0.3.11" @@ -1450,6 +2009,16 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "etcetera" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de48cc4d1c1d97a20fd819def54b890cadde72ed3ad0c614822a0a433361be96" +dependencies = [ + "cfg-if", + "windows-sys 0.61.1", +] + [[package]] name = "event-listener" version = "5.4.0" @@ -1482,6 +2051,35 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "expect-json" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "422e7906e79941e5ac58c64dfd2da03e6ae3de62227f87606fbbe125d91080f9" +dependencies = [ + "chrono", + "email_address", + "expect-json-macros", + "num", + "regex", + "serde", + "serde_json", + "thiserror 2.0.17", + "typetag", + "uuid", +] + +[[package]] +name = "expect-json-macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bf7f5979e98460a0eb412665514594f68f366a32b85fa8d7ffb65bb1edee6a0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", +] + [[package]] name = "fast_chemail" version = "0.9.6" @@ -1497,6 +2095,17 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "ferroid" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0e9414a6ae93ef993ce40a1e02944f13d4508e2bf6f1ced1580ce6910f08253" +dependencies = [ + "portable-atomic", + "rand 0.9.2", + "web-time", +] + [[package]] name = "ff" version = "0.12.1" @@ -1507,6 +2116,40 @@ dependencies = [ "subtle", ] +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "filetime" +version = "0.2.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" +dependencies = [ + "cfg-if", + "libc", + "libredox", + "windows-sys 0.60.2", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" + [[package]] name = "flume" version = "0.11.1" @@ -1530,6 +2173,21 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -1545,6 +2203,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futures" version = "0.3.31" @@ -1659,6 +2323,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -1688,6 +2353,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + [[package]] name = "glob" version = "0.3.2" @@ -1734,7 +2405,7 @@ dependencies = [ "opentelemetry_sdk", "regex", "reqwest", - "rustls 0.23.28", + "rustls 0.23.35", "secrecy", "serde", "serde_json", @@ -1742,7 +2413,7 @@ dependencies = [ "thiserror 2.0.17", "tokio", "tokio-stream", - "tower-http", + "tower-http 0.6.6", "tower-service", "tracing", "url", @@ -1754,7 +2425,18 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7" dependencies = [ - "ff", + "ff 0.12.1", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff 0.13.1", "rand_core 0.6.4", "subtle", ] @@ -1771,7 +2453,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap", + "indexmap 2.8.0", "slab", "tokio", "tokio-util", @@ -1790,7 +2472,7 @@ dependencies = [ "futures-core", "futures-sink", "http 1.3.1", - "indexmap", + "indexmap 2.8.0", "slab", "tokio", "tokio-util", @@ -1811,6 +2493,15 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.8", +] + [[package]] name = "hashbrown" version = "0.15.2" @@ -1828,14 +2519,24 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" dependencies = [ - "hashbrown", + "hashbrown 0.15.2", ] [[package]] -name = "headers" -version = "0.4.0" +name = "hdrhistogram" +version = "7.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "322106e6bd0cba2d5ead589ddb8150a13d7c4217cf80d7c4f682ca994ccc6aa9" +checksum = "765c9198f173dd59ce26ff9f95ef0aafd0a0fe01fb9d72841bc5066a4c06511d" +dependencies = [ + "byteorder", + "num-traits", +] + +[[package]] +name = "headers" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "322106e6bd0cba2d5ead589ddb8150a13d7c4217cf80d7c4f682ca994ccc6aa9" dependencies = [ "base64 0.21.7", "bytes", @@ -1855,6 +2556,12 @@ dependencies = [ "http 1.3.1", ] +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "heck" version = "0.5.0" @@ -2011,9 +2718,9 @@ dependencies = [ [[package]] name = "hyper" -version = "1.7.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" dependencies = [ "atomic-waker", "bytes", @@ -2032,6 +2739,21 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-named-pipe" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73b7d8abf35697b81a825e386fc151e0d503e8cb5fcb93cc8669c376dfd6f278" +dependencies = [ + "hex", + "hyper 1.8.1", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", + "winapi", +] + [[package]] name = "hyper-rustls" version = "0.24.2" @@ -2056,16 +2778,16 @@ checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" dependencies = [ "futures-util", "http 1.3.1", - "hyper 1.7.0", + "hyper 1.8.1", "hyper-util", "log", - "rustls 0.23.28", + "rustls 0.23.35", "rustls-native-certs 0.8.1", "rustls-pki-types", "tokio", "tokio-rustls 0.26.2", "tower-service", - "webpki-roots", + "webpki-roots 0.26.8", ] [[package]] @@ -2074,32 +2796,68 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" dependencies = [ - "hyper 1.7.0", + "hyper 1.8.1", "hyper-util", "pin-project-lite", "tokio", "tower-service", ] +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper 1.8.1", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" dependencies = [ + "base64 0.22.1", "bytes", "futures-channel", "futures-core", "futures-util", "http 1.3.1", "http-body 1.0.1", - "hyper 1.7.0", + "hyper 1.8.1", + "ipnet", "libc", + "percent-encoding", "pin-project-lite", "socket2 0.6.0", + "system-configuration", "tokio", "tower-service", "tracing", + "windows-registry", +] + +[[package]] +name = "hyperlocal" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "986c5ce3b994526b3cd75578e62554abd09f0899d6206de48b3e96ab34ccc8c7" +dependencies = [ + "hex", + "http-body-util", + "hyper 1.8.1", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", ] [[package]] @@ -2277,6 +3035,17 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd62e6b5e86ea8eeeb8db1de02880a6abc01a397b2ebb64b5d74ac255318f5cb" +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + [[package]] name = "indexmap" version = "2.8.0" @@ -2284,22 +3053,70 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.15.2", "serde", ] +[[package]] +name = "indoc" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] + +[[package]] +name = "inherent" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c727f80bfa4a6c6e2508d2f05b6f4bfce242030bd88ed15ae5331c5b5d30fba7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", +] + +[[package]] +name = "inventory" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc61209c082fbeb19919bee74b176221b27223e27b65d781eb91af24eb1fb46e" +dependencies = [ + "rustversion", +] + [[package]] name = "ipnet" version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +[[package]] +name = "iri-string" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.12.1" @@ -2324,6 +3141,30 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[package]] +name = "jiff" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49cce2b81f2098e7e3efc35bc2e0a6b7abec9d34128283d7a26fa8f32a6dbb35" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-static" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "980af8b43c3ad5d8d349ace167ec8170839f753a42d233ba19e08afe1850fa69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", +] + [[package]] name = "jobserver" version = "0.1.33" @@ -2379,6 +3220,23 @@ dependencies = [ "serde_json", ] +[[package]] +name = "jsonwebtoken" +version = "10.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c76e1c7d7df3e34443b3621b459b066a7b79644f059fc8b2db7070c825fd417e" +dependencies = [ + "aws-lc-rs", + "base64 0.22.1", + "getrandom 0.2.15", + "js-sys", + "pem", + "serde", + "serde_json", + "signature 2.2.0", + "simple_asn1", +] + [[package]] name = "k8s-openapi" version = "0.26.0" @@ -2419,7 +3277,7 @@ dependencies = [ "http 1.3.1", "http-body 1.0.1", "http-body-util", - "hyper 1.7.0", + "hyper 1.8.1", "hyper-rustls 0.27.5", "hyper-timeout", "hyper-util", @@ -2427,7 +3285,7 @@ dependencies = [ "k8s-openapi", "kube-core", "pem", - "rustls 0.23.28", + "rustls 0.23.35", "secrecy", "serde", "serde_json", @@ -2436,7 +3294,7 @@ dependencies = [ "tokio", "tokio-util", "tower", - "tower-http", + "tower-http 0.6.6", "tracing", ] @@ -2479,13 +3337,13 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6aea4de4b562c5cc89ab10300bb63474ae1fa57ff5a19275f2e26401a323e3fd" dependencies = [ - "ahash", + "ahash 0.8.12", "async-broadcast", "async-stream", "backon", "educe", "futures", - "hashbrown", + "hashbrown 0.15.2", "hostname", "json-patch", "k8s-openapi", @@ -2509,12 +3367,6 @@ dependencies = [ "spin 0.9.8", ] -[[package]] -name = "lazycell" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" - [[package]] name = "lber" version = "0.4.2" @@ -2574,22 +3426,40 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" +[[package]] +name = "libredox" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df15f6eac291ed1cf25865b1ee60399f57e7c227e7f51bdbd4c5270396a9ed50" +dependencies = [ + "bitflags", + "libc", + "redox_syscall 0.6.0", +] + +[[package]] +name = "libsodium-sys" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b779387cd56adfbc02ea4a668e704f729be8d6a6abd2c27ca5ee537849a92fd" +dependencies = [ + "cc", + "libc", + "pkg-config", + "walkdir", +] + [[package]] name = "libsqlite3-sys" version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" dependencies = [ + "cc", "pkg-config", "vcpkg", ] -[[package]] -name = "linux-raw-sys" -version = "0.4.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" - [[package]] name = "linux-raw-sys" version = "0.9.3" @@ -2602,6 +3472,12 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + [[package]] name = "lock_api" version = "0.4.12" @@ -2610,6 +3486,7 @@ checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", + "serde", ] [[package]] @@ -2624,7 +3501,7 @@ version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" dependencies = [ - "hashbrown", + "hashbrown 0.15.2", ] [[package]] @@ -2658,6 +3535,14 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "migration" +version = "0.1.0" +dependencies = [ + "sea-orm-migration", + "tokio", +] + [[package]] name = "mime" version = "0.3.17" @@ -2680,6 +3565,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + [[package]] name = "mio" version = "1.0.3" @@ -2704,10 +3598,10 @@ dependencies = [ "http 1.3.1", "http-body 1.0.1", "http-body-util", - "hyper 1.7.0", + "hyper 1.8.1", "hyper-util", "log", - "rand 0.9.0", + "rand 0.9.2", "regex", "serde_json", "serde_urlencoded", @@ -2715,6 +3609,27 @@ dependencies = [ "tokio", ] +[[package]] +name = "moka" +version = "0.12.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8261cd88c312e0004c1d51baad2980c66528dfdb2bee62003e643a4d8f86b077" +dependencies = [ + "async-lock", + "crossbeam-channel", + "crossbeam-epoch", + "crossbeam-utils", + "equivalent", + "event-listener", + "futures-util", + "parking_lot", + "portable-atomic", + "rustc_version", + "smallvec", + "tagptr", + "uuid", +] + [[package]] name = "multer" version = "3.1.0" @@ -2732,6 +3647,23 @@ dependencies = [ "version_check", ] +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework 2.11.1", + "security-framework-sys", + "tempfile", +] + [[package]] name = "nom" version = "7.1.3" @@ -2751,6 +3683,20 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + [[package]] name = "num-bigint" version = "0.4.6" @@ -2778,6 +3724,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -2804,6 +3759,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -2825,36 +3791,195 @@ dependencies = [ ] [[package]] -name = "oid-registry" -version = "0.6.1" +name = "oauth2" +version = "5.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bedf36ffb6ba96c2eb7144ef6270557b52e54b20c0a8e1eb2ff99a6c6959bff" +checksum = "51e219e79014df21a225b1860a479e2dcd7cbd9130f4defd4bd0e191ea31d67d" dependencies = [ - "asn1-rs", + "base64 0.22.1", + "chrono", + "getrandom 0.2.15", + "http 1.3.1", + "rand 0.8.5", + "reqwest", + "serde", + "serde_json", + "serde_path_to_error", + "sha2", + "thiserror 1.0.69", + "url", ] [[package]] -name = "once_cell" -version = "1.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" - -[[package]] -name = "openssl-probe" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" - -[[package]] -name = "opentelemetry" -version = "0.31.0" +name = "oauth2-test-server" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b84bcd6ae87133e903af7ef497404dda70c60d0ea14895fc8a5e6722754fc2a0" +checksum = "e66b9483c4680a03f8f3a414e02d9e2b2d12702946d2fd05d58c3da4406630d2" dependencies = [ - "futures-core", - "futures-sink", - "js-sys", - "pin-project-lite", + "axum", + "base64 0.21.7", + "chrono", + "colored", + "futures", + "http 1.3.1", + "jsonwebtoken", + "once_cell", + "rand 0.8.5", + "reqwest", + "rsa", + "serde", + "serde_json", + "sha2", + "tokio", + "tower-http 0.5.2", + "tracing", + "tracing-subscriber", + "url", + "uuid", +] + +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + +[[package]] +name = "oid-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bedf36ffb6ba96c2eb7144ef6270557b52e54b20c0a8e1eb2ff99a6c6959bff" +dependencies = [ + "asn1-rs", +] + +[[package]] +name = "oidc-bff" +version = "0.1.0" +dependencies = [ + "anyhow", + "auth-common", + "axum", + "axum-reverse-proxy", + "base64 0.22.1", + "bytes", + "chrono", + "clap", + "dotenvy", + "http-body-util", + "hyper 1.8.1", + "migration", + "moka", + "oauth2", + "openidconnect", + "rustls 0.23.35", + "sea-orm", + "serde", + "serde_json", + "serde_yaml", + "sodiumoxide", + "thiserror 2.0.17", + "tokio", + "tower", + "tower-sessions", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "openidconnect" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c6709ba2ea764bbed26bce1adf3c10517113ddea6f2d4196e4851757ef2b2" +dependencies = [ + "base64 0.21.7", + "chrono", + "dyn-clone", + "ed25519-dalek", + "hmac", + "http 1.3.1", + "itertools 0.10.5", + "log", + "oauth2", + "p256 0.13.2", + "p384", + "rand 0.8.5", + "rsa", + "serde", + "serde-value", + "serde_json", + "serde_path_to_error", + "serde_plain", + "serde_with", + "sha2", + "subtle", + "thiserror 1.0.69", + "url", +] + +[[package]] +name = "openssl" +version = "0.10.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "opentelemetry" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b84bcd6ae87133e903af7ef497404dda70c60d0ea14895fc8a5e6722754fc2a0" +dependencies = [ + "futures-core", + "futures-sink", + "js-sys", + "pin-project-lite", "thiserror 2.0.17", "tracing", ] @@ -2921,7 +4046,7 @@ dependencies = [ "futures-util", "opentelemetry", "percent-encoding", - "rand 0.9.0", + "rand 0.9.2", "thiserror 2.0.17", "tokio", "tokio-stream", @@ -2936,6 +4061,39 @@ dependencies = [ "num-traits", ] +[[package]] +name = "ordered-float" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" +dependencies = [ + "num-traits", +] + +[[package]] +name = "ouroboros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0f050db9c44b97a94723127e6be766ac5c340c48f2c4bb3ffa11713744be59" +dependencies = [ + "aliasable", + "ouroboros_macro", + "static_assertions", +] + +[[package]] +name = "ouroboros_macro" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c7028bdd3d43083f6d8d4d5187680d0d3560d54df4cc9d752005268b41e64d0" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn 2.0.107", +] + [[package]] name = "outref" version = "0.5.2" @@ -2948,8 +4106,32 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51f44edd08f51e2ade572f141051021c5af22677e42b7dd28a88155151c33594" dependencies = [ - "ecdsa", - "elliptic-curve", + "ecdsa 0.14.8", + "elliptic-curve 0.12.3", + "sha2", +] + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa 0.16.9", + "elliptic-curve 0.13.8", + "primeorder", + "sha2", +] + +[[package]] +name = "p384" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" +dependencies = [ + "ecdsa 0.16.9", + "elliptic-curve 0.13.8", + "primeorder", "sha2", ] @@ -2977,11 +4159,36 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.10", "smallvec", "windows-targets 0.52.6", ] +[[package]] +name = "parse-display" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "914a1c2265c98e2446911282c6ac86d8524f495792c38c5bd884f80499c7538a" +dependencies = [ + "parse-display-derive", + "regex", + "regex-syntax", +] + +[[package]] +name = "parse-display-derive" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ae7800a4c974efd12df917266338e79a7a74415173caf7e70aa0a0707345281" +dependencies = [ + "proc-macro2", + "quote", + "regex", + "regex-syntax", + "structmeta", + "syn 2.0.107", +] + [[package]] name = "pem" version = "3.0.5" @@ -3052,6 +4259,15 @@ dependencies = [ "sha2", ] +[[package]] +name = "pgvector" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc58e2d255979a31caa7cabfa7aac654af0354220719ab7a68520ae7a91e8c0b" +dependencies = [ + "serde", +] + [[package]] name = "pin-project" version = "1.1.10" @@ -3121,6 +4337,31 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "pluralizer" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b3eba432a00a1f6c16f39147847a870e94e2e9b992759b503e330efec778cbe" +dependencies = [ + "once_cell", + "regex", +] + +[[package]] +name = "portable-atomic" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" + +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -3136,6 +4377,16 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "pretty_assertions" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" +dependencies = [ + "diff", + "yansi", +] + [[package]] name = "prettyplease" version = "0.2.32" @@ -3146,6 +4397,15 @@ dependencies = [ "syn 2.0.107", ] +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve 0.13.8", +] + [[package]] name = "proc-macro-crate" version = "3.3.0" @@ -3155,6 +4415,28 @@ dependencies = [ "toml_edit", ] +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.107", +] + [[package]] name = "proc-macro2" version = "1.0.101" @@ -3164,6 +4446,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "proc-macro2-diagnostics" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", + "version_check", + "yansi", +] + [[package]] name = "prost" version = "0.14.1" @@ -3187,6 +4482,51 @@ dependencies = [ "syn 2.0.107", ] +[[package]] +name = "prost-types" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9b4db3d6da204ed77bb26ba83b6122a73aeb2e87e25fbf7ad2e84c4ccbf8f72" +dependencies = [ + "prost", +] + +[[package]] +name = "psl-types" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" + +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "publicsuffix" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42ea446cab60335f76979ec15e12619a2165b5ae2c12166bef27d283a9fadf" +dependencies = [ + "idna", + "psl-types", +] + [[package]] name = "quinn" version = "0.11.7" @@ -3198,8 +4538,8 @@ dependencies = [ "pin-project-lite", "quinn-proto", "quinn-udp", - "rustc-hash 2.1.1", - "rustls 0.23.28", + "rustc-hash", + "rustls 0.23.35", "socket2 0.5.9", "thiserror 2.0.17", "tokio", @@ -3215,10 +4555,10 @@ checksum = "b820744eb4dc9b57a3398183639c511b5a26d2ed702cedd3febaa1393caa22cc" dependencies = [ "bytes", "getrandom 0.3.2", - "rand 0.9.0", + "rand 0.9.2", "ring 0.17.14", - "rustc-hash 2.1.1", - "rustls 0.23.28", + "rustc-hash", + "rustls 0.23.35", "rustls-pki-types", "slab", "thiserror 2.0.17", @@ -3256,6 +4596,12 @@ version = "5.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rand" version = "0.8.5" @@ -3269,13 +4615,12 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.0" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.3", - "zerocopy", ] [[package]] @@ -3325,6 +4670,15 @@ dependencies = [ "bitflags", ] +[[package]] +name = "redox_syscall" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec96166dafa0886eb81fe1c0a388bece180fbef2135f97c1e2cf8302e74b43b5" +dependencies = [ + "bitflags", +] + [[package]] name = "ref-cast" version = "1.0.25" @@ -3386,54 +4740,77 @@ version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "145bb27393fe455dd64d6cbc8d059adfa392590a45eadf079c01b11857e7b010" dependencies = [ - "hashbrown", + "hashbrown 0.15.2", "memchr", ] +[[package]] +name = "rend" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" +dependencies = [ + "bytecheck", +] + [[package]] name = "reqwest" -version = "0.12.15" +version = "0.12.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb" +checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" dependencies = [ "base64 0.22.1", "bytes", + "cookie", + "cookie_store", + "encoding_rs", "futures-channel", "futures-core", "futures-util", + "h2 0.4.8", "http 1.3.1", "http-body 1.0.1", "http-body-util", - "hyper 1.7.0", + "hyper 1.8.1", "hyper-rustls 0.27.5", + "hyper-tls", "hyper-util", - "ipnet", "js-sys", "log", "mime", - "once_cell", + "mime_guess", + "native-tls", "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.28", - "rustls-pemfile 2.2.0", + "rustls 0.23.35", "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", "sync_wrapper", "tokio", + "tokio-native-tls", "tokio-rustls 0.26.2", "tokio-util", "tower", + "tower-http 0.6.6", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "wasm-streams", "web-sys", - "webpki-roots", - "windows-registry", + "webpki-roots 1.0.4", +] + +[[package]] +name = "reserve-port" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21918d6644020c6f6ef1993242989bf6d4952d2e025617744f184c02df51c356" +dependencies = [ + "thiserror 2.0.17", ] [[package]] @@ -3447,6 +4824,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + [[package]] name = "ring" version = "0.16.20" @@ -3477,30 +4864,90 @@ dependencies = [ ] [[package]] -name = "rsa" -version = "0.9.8" +name = "rkyv" +version = "0.7.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9008cd6385b9e161d8229e1f6549dd23c3d022f132a2ea37ac3a10ac4935779b" +dependencies = [ + "bitvec", + "bytecheck", + "bytes", + "hashbrown 0.12.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "503d1d27590a2b0a3a4ca4c94755aa2875657196ecbf401a42eff41d7de532c0" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "rsa" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8 0.10.2", + "rand_core 0.6.4", + "signature 2.2.0", + "spki 0.7.3", + "subtle", + "zeroize", +] + +[[package]] +name = "rust-multipart-rfc7578_2" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c839d037155ebc06a571e305af66ff9fd9063a6e662447051737e1ac75beea41" +dependencies = [ + "bytes", + "futures-core", + "futures-util", + "http 1.3.1", + "mime", + "rand 0.9.2", + "thiserror 2.0.17", +] + +[[package]] +name = "rust_decimal" +version = "1.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b" +checksum = "35affe401787a9bd846712274d97654355d21b2a2c092a3139aabe31e9022282" dependencies = [ - "const-oid", - "digest", - "num-bigint-dig", - "num-integer", + "arrayvec", + "borsh", + "bytes", "num-traits", - "pkcs1", - "pkcs8 0.10.2", - "rand_core 0.6.4", - "signature 2.2.0", - "spki 0.7.3", - "subtle", - "zeroize", + "rand 0.8.5", + "rkyv", + "serde", + "serde_json", ] [[package]] -name = "rustc-hash" -version = "1.1.0" +name = "rustc-demangle" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" [[package]] name = "rustc-hash" @@ -3539,19 +4986,6 @@ dependencies = [ "nom", ] -[[package]] -name = "rustix" -version = "0.38.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" -dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", -] - [[package]] name = "rustix" version = "1.0.5" @@ -3561,7 +4995,7 @@ dependencies = [ "bitflags", "errno", "libc", - "linux-raw-sys 0.9.3", + "linux-raw-sys", "windows-sys 0.59.0", ] @@ -3579,16 +5013,16 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.28" +version = "0.23.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7160e3e10bf4535308537f3c4e1641468cd0e485175d6163087c0393c7d46643" +checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" dependencies = [ "aws-lc-rs", "log", "once_cell", "ring 0.17.14", "rustls-pki-types", - "rustls-webpki 0.103.3", + "rustls-webpki 0.103.8", "subtle", "zeroize", ] @@ -3637,11 +5071,12 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.11.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" +checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a" dependencies = [ "web-time", + "zeroize", ] [[package]] @@ -3656,9 +5091,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.3" +version = "0.103.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" +checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" dependencies = [ "aws-lc-rs", "ring 0.17.14", @@ -3708,6 +5143,18 @@ dependencies = [ "serde_json", ] +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + [[package]] name = "schemars" version = "1.0.4" @@ -3761,13 +5208,187 @@ dependencies = [ "untrusted 0.9.0", ] +[[package]] +name = "sea-bae" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f694a6ab48f14bc063cfadff30ab551d3c7e46d8f81836c51989d548f44a2a25" +dependencies = [ + "heck 0.4.1", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.107", +] + +[[package]] +name = "sea-orm" +version = "2.0.0-rc.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6dda57d64724c4c3e2b39ce17ca5f4084561656a3518b65b26edc5b36e4607" +dependencies = [ + "async-stream", + "async-trait", + "bigdecimal", + "chrono", + "derive_more", + "futures-util", + "itertools 0.14.0", + "log", + "ouroboros", + "pgvector", + "rust_decimal", + "sea-orm-macros", + "sea-query", + "sea-query-sqlx", + "sea-schema", + "serde", + "serde_json", + "sqlx", + "strum 0.27.2", + "thiserror 2.0.17", + "time", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "sea-orm-cli" +version = "2.0.0-rc.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d63b7fcf2623bfc47e4fcca48fd35f77fd376611935862a6e316991d035ac85c" +dependencies = [ + "chrono", + "clap", + "dotenvy", + "glob", + "indoc", + "regex", + "sea-schema", + "sqlx", + "tokio", + "tracing", + "tracing-subscriber", + "url", +] + +[[package]] +name = "sea-orm-macros" +version = "2.0.0-rc.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7674a565e093a4bfffbfd6d7fd79a5dc8d75463d442ffb44d0fc3a3dcce5a6" +dependencies = [ + "heck 0.5.0", + "pluralizer", + "proc-macro2", + "quote", + "sea-bae", + "syn 2.0.107", + "unicode-ident", +] + +[[package]] +name = "sea-orm-migration" +version = "2.0.0-rc.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02c77522b82141205bd99137be96b81b4540531f9ff7773b77d70f5749c39dcc" +dependencies = [ + "async-trait", + "clap", + "dotenvy", + "sea-orm", + "sea-orm-cli", + "sea-schema", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "sea-query" +version = "1.0.0-rc.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ebab2b9d558deec08e43887a63ed4d96d56b32cb9d98578bd1749e2c8c7e24" +dependencies = [ + "bigdecimal", + "chrono", + "inherent", + "ordered-float 4.6.0", + "rust_decimal", + "sea-query-derive", + "serde_json", + "time", + "uuid", +] + +[[package]] +name = "sea-query-derive" +version = "1.0.0-rc.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "365d236217f5daa4f40d3c9998ff3921351b53472da50308e384388162353b3a" +dependencies = [ + "darling 0.20.11", + "heck 0.4.1", + "proc-macro2", + "quote", + "syn 2.0.107", + "thiserror 2.0.17", +] + +[[package]] +name = "sea-query-sqlx" +version = "0.8.0-rc.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68873fa1776b4c25a26e7679f8ee22332978c721168ec1b0b32b6583d5a9381d" +dependencies = [ + "bigdecimal", + "chrono", + "rust_decimal", + "sea-query", + "serde_json", + "sqlx", + "time", + "uuid", +] + +[[package]] +name = "sea-schema" +version = "0.17.0-rc.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "845b7ed3e7a4f4458fe7218931b54e92be0dce01fc3c310d996c7b76d9a37ea5" +dependencies = [ + "async-trait", + "sea-query", + "sea-query-sqlx", + "sea-schema-derive", + "sqlx", +] + +[[package]] +name = "sea-schema-derive" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "debdc8729c37fdbf88472f97fd470393089f997a909e535ff67c544d18cfccf0" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "syn 2.0.107", +] + +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + [[package]] name = "sec1" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3be24c1842290c45df0a7bf069e0c268a747ad05a192f2fd7dcfdbc1cba40928" dependencies = [ - "base16ct", + "base16ct 0.1.1", "der 0.6.1", "generic-array", "pkcs8 0.9.0", @@ -3775,6 +5396,20 @@ dependencies = [ "zeroize", ] +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct 0.2.0", + "der 0.7.9", + "generic-array", + "pkcs8 0.10.2", + "subtle", + "zeroize", +] + [[package]] name = "secrecy" version = "0.10.3" @@ -3846,7 +5481,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" dependencies = [ - "ordered-float", + "ordered-float 2.10.1", "serde", ] @@ -3904,6 +5539,26 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_plain" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce1fc6db65a611022b23a0dec6975d63fb80a302cb3388835ff02c097258d50" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", +] + [[package]] name = "serde_spanned" version = "0.6.8" @@ -3937,13 +5592,44 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10574371d41b0d9b2cff89418eda27da52bcaff2cc8741db26382a77c29131f1" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.8.0", + "schemars 0.9.0", + "schemars 1.0.4", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08a72d8216842fdd57820dc78d840bef99248e35fb2554ff923319e60f2d686b" +dependencies = [ + "darling 0.21.3", + "proc-macro2", + "quote", + "syn 2.0.107", +] + [[package]] name = "serde_yaml" version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap", + "indexmap 2.8.0", "itoa", "ryu", "serde", @@ -3989,9 +5675,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.8" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures", @@ -4042,12 +5728,30 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "similar" version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" +[[package]] +name = "simple_asn1" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror 2.0.17", + "time", +] + [[package]] name = "slab" version = "0.4.9" @@ -4059,9 +5763,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.14.0" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" dependencies = [ "serde", ] @@ -4086,6 +5790,18 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "sodiumoxide" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e26be3acb6c2d9a7aac28482586a7856436af4cfe7100031d219de2d2ecb0028" +dependencies = [ + "ed25519 1.5.3", + "libc", + "libsodium-sys", + "serde", +] + [[package]] name = "spin" version = "0.5.2" @@ -4141,7 +5857,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" dependencies = [ "base64 0.22.1", + "bigdecimal", "bytes", + "chrono", "crc", "crossbeam-queue", "either", @@ -4150,14 +5868,15 @@ dependencies = [ "futures-intrusive", "futures-io", "futures-util", - "hashbrown", + "hashbrown 0.15.2", "hashlink", - "indexmap", + "indexmap 2.8.0", "log", "memchr", "once_cell", "percent-encoding", - "rustls 0.23.28", + "rust_decimal", + "rustls 0.23.35", "serde", "serde_json", "sha2", @@ -4168,7 +5887,8 @@ dependencies = [ "tokio-stream", "tracing", "url", - "webpki-roots", + "uuid", + "webpki-roots 0.26.8", ] [[package]] @@ -4192,7 +5912,7 @@ checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" dependencies = [ "dotenvy", "either", - "heck", + "heck 0.5.0", "hex", "once_cell", "proc-macro2", @@ -4217,9 +5937,11 @@ checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" dependencies = [ "atoi", "base64 0.22.1", + "bigdecimal", "bitflags", "byteorder", "bytes", + "chrono", "crc", "digest", "dotenvy", @@ -4240,6 +5962,7 @@ dependencies = [ "percent-encoding", "rand 0.8.5", "rsa", + "rust_decimal", "serde", "sha1", "sha2", @@ -4249,6 +5972,7 @@ dependencies = [ "thiserror 2.0.17", "time", "tracing", + "uuid", "whoami", ] @@ -4260,11 +5984,13 @@ checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" dependencies = [ "atoi", "base64 0.22.1", + "bigdecimal", "bitflags", "byteorder", + "chrono", "crc", "dotenvy", - "etcetera", + "etcetera 0.8.0", "futures-channel", "futures-core", "futures-util", @@ -4276,8 +6002,10 @@ dependencies = [ "log", "md-5", "memchr", + "num-bigint", "once_cell", "rand 0.8.5", + "rust_decimal", "serde", "serde_json", "sha2", @@ -4287,6 +6015,7 @@ dependencies = [ "thiserror 2.0.17", "time", "tracing", + "uuid", "whoami", ] @@ -4297,6 +6026,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" dependencies = [ "atoi", + "chrono", "flume", "futures-channel", "futures-core", @@ -4313,6 +6043,7 @@ dependencies = [ "time", "tracing", "url", + "uuid", ] [[package]] @@ -4321,6 +6052,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "static_assertions_next" version = "1.1.2" @@ -4339,10 +6076,33 @@ dependencies = [ ] [[package]] -name = "strsim" -version = "0.11.1" +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "structmeta" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e1575d8d40908d70f6fd05537266b90ae71b15dbbe7a8b7dffa2b759306d329" +dependencies = [ + "proc-macro2", + "quote", + "structmeta-derive", + "syn 2.0.107", +] + +[[package]] +name = "structmeta-derive" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +checksum = "152a0b65a590ff6c3da95cabe2353ee04e6167c896b28e3b14478c2636c922fc" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", +] [[package]] name = "strum" @@ -4368,7 +6128,7 @@ version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote", "rustversion", @@ -4381,7 +6141,7 @@ version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote", "rustversion", @@ -4448,6 +6208,39 @@ dependencies = [ "syn 2.0.107", ] +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tagptr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "telemetry" version = "0.1.2" @@ -4476,10 +6269,41 @@ dependencies = [ "fastrand", "getrandom 0.3.2", "once_cell", - "rustix 1.0.5", + "rustix", "windows-sys 0.59.0", ] +[[package]] +name = "testcontainers" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a347cac4368ba4f1871743adb27dc14829024d26b1763572404726b0b9943eb8" +dependencies = [ + "astral-tokio-tar", + "async-trait", + "bollard", + "bytes", + "docker_credential", + "either", + "etcetera 0.11.0", + "ferroid", + "futures", + "itertools 0.14.0", + "log", + "memchr", + "parse-display", + "pin-project-lite", + "reqwest", + "serde", + "serde_json", + "serde_with", + "thiserror 2.0.17", + "tokio", + "tokio-stream", + "tokio-util", + "url", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -4614,6 +6438,16 @@ dependencies = [ "syn 2.0.107", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + [[package]] name = "tokio-rustls" version = "0.24.1" @@ -4630,7 +6464,7 @@ version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" dependencies = [ - "rustls 0.23.28", + "rustls 0.23.35", "tokio", ] @@ -4645,6 +6479,18 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-tungstenite" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite 0.21.0", +] + [[package]] name = "tokio-tungstenite" version = "0.28.0" @@ -4654,7 +6500,7 @@ dependencies = [ "futures-util", "log", "tokio", - "tungstenite", + "tungstenite 0.28.0", ] [[package]] @@ -4699,7 +6545,7 @@ version = "0.22.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" dependencies = [ - "indexmap", + "indexmap 2.8.0", "serde", "serde_spanned", "toml_datetime", @@ -4713,16 +6559,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb7613188ce9f7df5bfe185db26c5814347d110db17920415cf2fbcad85e7203" dependencies = [ "async-trait", + "axum", "base64 0.22.1", "bytes", + "h2 0.4.8", "http 1.3.1", "http-body 1.0.1", "http-body-util", - "hyper 1.7.0", + "hyper 1.8.1", "hyper-timeout", "hyper-util", "percent-encoding", "pin-project", + "socket2 0.6.0", "sync_wrapper", "tokio", "tokio-stream", @@ -4758,13 +6607,14 @@ dependencies = [ [[package]] name = "tower" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", - "indexmap", + "hdrhistogram", + "indexmap 2.8.0", "pin-project-lite", "slab", "sync_wrapper", @@ -4775,6 +6625,39 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower-cookies" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "151b5a3e3c45df17466454bb74e9ecedecc955269bdedbf4d150dfa393b55a36" +dependencies = [ + "axum-core", + "cookie", + "futures-util", + "http 1.3.1", + "parking_lot", + "pin-project-lite", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" +dependencies = [ + "bitflags", + "bytes", + "http 1.3.1", + "http-body 1.0.1", + "http-body-util", + "pin-project-lite", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "tower-http" version = "0.6.6" @@ -4784,10 +6667,13 @@ dependencies = [ "base64 0.22.1", "bitflags", "bytes", + "futures-util", "http 1.3.1", "http-body 1.0.1", + "iri-string", "mime", "pin-project-lite", + "tower", "tower-layer", "tower-service", "tracing", @@ -4805,6 +6691,57 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" +[[package]] +name = "tower-sessions" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a05911f23e8fae446005fe9b7b97e66d95b6db589dc1c4d59f6a2d4d4927d3" +dependencies = [ + "async-trait", + "http 1.3.1", + "time", + "tokio", + "tower-cookies", + "tower-layer", + "tower-service", + "tower-sessions-core", + "tower-sessions-memory-store", + "tracing", +] + +[[package]] +name = "tower-sessions-core" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce8cce604865576b7751b7a6bc3058f754569a60d689328bb74c52b1d87e355b" +dependencies = [ + "async-trait", + "axum-core", + "base64 0.22.1", + "futures", + "http 1.3.1", + "parking_lot", + "rand 0.8.5", + "serde", + "serde_json", + "thiserror 2.0.17", + "time", + "tokio", + "tracing", +] + +[[package]] +name = "tower-sessions-memory-store" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb05909f2e1420135a831dd5df9f5596d69196d0a64c3499ca474c4bd3d33242" +dependencies = [ + "async-trait", + "time", + "tokio", + "tower-sessions-core", +] + [[package]] name = "tracing" version = "0.1.41" @@ -4892,6 +6829,25 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "tungstenite" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http 1.3.1", + "httparse", + "log", + "rand 0.8.5", + "sha1", + "thiserror 1.0.69", + "url", + "utf-8", +] + [[package]] name = "tungstenite" version = "0.28.0" @@ -4903,18 +6859,48 @@ dependencies = [ "http 1.3.1", "httparse", "log", - "rand 0.9.0", + "rand 0.9.2", "sha1", "thiserror 2.0.17", "utf-8", ] +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + [[package]] name = "typenum" version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +[[package]] +name = "typetag" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be2212c8a9b9bcfca32024de14998494cf9a5dfa59ea1b829de98bac374b86bf" +dependencies = [ + "erased-serde", + "inventory", + "once_cell", + "serde", + "typetag-impl", +] + +[[package]] +name = "typetag-impl" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27a7a9b72ba121f6f1f6c3632b85604cac41aedb5ddc70accbebb6cac83de846" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", +] + [[package]] name = "typify" version = "0.5.0" @@ -4931,7 +6917,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1eb359f7ffa4f9ebe947fa11a1b2da054564502968db5f317b7e37693cb2240" dependencies = [ - "heck", + "heck 0.5.0", "log", "proc-macro2", "quote", @@ -5025,6 +7011,34 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "ureq" +version = "3.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d39cb1dbab692d82a977c0392ffac19e188bd9186a9f32806f0aaa859d75585a" +dependencies = [ + "base64 0.22.1", + "log", + "percent-encoding", + "rustls 0.23.35", + "rustls-pki-types", + "ureq-proto", + "utf-8", + "webpki-roots 1.0.4", +] + +[[package]] +name = "ureq-proto" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d81f9efa9df032be5934a46a068815a10a042b494b6a58cb0a1a97bb5467ed6f" +dependencies = [ + "base64 0.22.1", + "http 1.3.1", + "httparse", + "log", +] + [[package]] name = "url" version = "2.5.4" @@ -5034,6 +7048,7 @@ dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", ] [[package]] @@ -5062,9 +7077,15 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.16.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" +checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" +dependencies = [ + "getrandom 0.3.2", + "js-sys", + "serde_core", + "wasm-bindgen", +] [[package]] name = "valuable" @@ -5244,15 +7265,12 @@ dependencies = [ ] [[package]] -name = "which" -version = "4.4.2" +name = "webpki-roots" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" dependencies = [ - "either", - "home", - "once_cell", - "rustix 0.38.44", + "rustls-pki-types", ] [[package]] @@ -5261,7 +7279,7 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6994d13118ab492c3c80c1f81928718159254c53c472bf9ce36f8dae4add02a7" dependencies = [ - "redox_syscall", + "redox_syscall 0.5.10", "wasite", ] @@ -5306,7 +7324,7 @@ dependencies = [ "windows-interface", "windows-link 0.1.1", "windows-result", - "windows-strings 0.4.0", + "windows-strings", ] [[package]] @@ -5345,38 +7363,29 @@ checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" [[package]] name = "windows-registry" -version = "0.4.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" +checksum = "b3bab093bdd303a1240bb99b8aba8ea8a69ee19d34c9e2ef9594e708a4878820" dependencies = [ + "windows-link 0.1.1", "windows-result", - "windows-strings 0.3.1", - "windows-targets 0.53.0", + "windows-strings", ] [[package]] name = "windows-result" -version = "0.3.2" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ "windows-link 0.1.1", ] [[package]] name = "windows-strings" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" -dependencies = [ - "windows-link 0.1.1", -] - -[[package]] -name = "windows-strings" -version = "0.4.0" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ "windows-link 0.1.1", ] @@ -5408,6 +7417,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.4", +] + [[package]] name = "windows-sys" version = "0.61.1" @@ -5450,18 +7468,19 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.0" +version = "0.53.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" +checksum = "2d42b7b7f66d2a06854650af09cfdf8713e427a439c97ad65a6375318033ac4b" dependencies = [ - "windows_aarch64_gnullvm 0.53.0", - "windows_aarch64_msvc 0.53.0", - "windows_i686_gnu 0.53.0", - "windows_i686_gnullvm 0.53.0", - "windows_i686_msvc 0.53.0", - "windows_x86_64_gnu 0.53.0", - "windows_x86_64_gnullvm 0.53.0", - "windows_x86_64_msvc 0.53.0", + "windows-link 0.2.0", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] [[package]] @@ -5478,9 +7497,9 @@ checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" [[package]] name = "windows_aarch64_msvc" @@ -5496,9 +7515,9 @@ checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_aarch64_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" [[package]] name = "windows_i686_gnu" @@ -5514,9 +7533,9 @@ checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnu" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" [[package]] name = "windows_i686_gnullvm" @@ -5526,9 +7545,9 @@ checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" [[package]] name = "windows_i686_msvc" @@ -5544,9 +7563,9 @@ checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_i686_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" [[package]] name = "windows_x86_64_gnu" @@ -5562,9 +7581,9 @@ checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnu" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" [[package]] name = "windows_x86_64_gnullvm" @@ -5580,9 +7599,9 @@ checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" [[package]] name = "windows_x86_64_msvc" @@ -5598,9 +7617,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "windows_x86_64_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" @@ -5623,7 +7642,7 @@ dependencies = [ "futures", "http 1.3.1", "http-body-util", - "hyper 1.7.0", + "hyper 1.8.1", "hyper-util", "log", "once_cell", @@ -5655,6 +7674,15 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "x509-parser" version = "0.15.1" @@ -5672,12 +7700,28 @@ dependencies = [ "time", ] +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix", +] + [[package]] name = "xmlparser" version = "0.13.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + [[package]] name = "yoke" version = "0.7.5" diff --git a/backend/Cargo.toml b/backend/Cargo.toml index c488a883b..b364d3d03 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -1,8 +1,9 @@ [workspace] members = [ "argo-workflows-openapi", + "auth-common", "auth-daemon", - "graph-proxy", + "graph-proxy", "oidc-bff", "oidc-bff/migration", "sessionspaces", "telemetry", ] @@ -10,7 +11,6 @@ resolver = "2" [workspace.dependencies] anyhow = { version = "1.0.100" } -built = { version = "0.8.0" } clap = { version = "4.5.49", features = ["derive", "env"] } chrono = { version = "0.4.42" } derive_more = { version = "2.0.1" , features = [ @@ -50,3 +50,4 @@ tracing-opentelemetry = { version = "0.32.0" } axum = { version = "0.8.6" } regex = "1.12.2" tower-http = { version = "0.6.6", features = ["cors"] } +sea-orm = { version = "2.0.0-rc", features = [ "sqlx-postgres", "runtime-tokio-rustls", "macros", "with-chrono" ] } \ No newline at end of file diff --git a/backend/Dockerfile.auth-daemon b/backend/Dockerfile.auth-daemon index 8556b5c38..c8166a7ee 100644 --- a/backend/Dockerfile.auth-daemon +++ b/backend/Dockerfile.auth-daemon @@ -7,6 +7,8 @@ RUN cargo install cargo-auditable COPY argo-workflows-openapi/Cargo.toml argo-workflows-openapi/Cargo.toml COPY graph-proxy/Cargo.toml graph-proxy/ COPY sessionspaces/Cargo.toml sessionspaces/ +COPY oidc-bff/Cargo.toml oidc-bff/ +COPY oidc-bff/migration/Cargo.toml oidc-bff/migration/ COPY auth-daemon/Cargo.toml auth-daemon/ COPY telemetry/build.rs telemetry/build.rs COPY telemetry/Cargo.toml telemetry/Cargo.toml @@ -18,6 +20,11 @@ RUN mkdir argo-workflows-openapi/src \ && echo "fn main() {}" > graph-proxy/src/main.rs \ && mkdir sessionspaces/src \ && echo "fn main() {}" > sessionspaces/src/main.rs \ + && mkdir oidc-bff/src \ + && touch oidc-bff/src/lib.rs \ + && echo "fn main() {}" > oidc-bff/src/main.rs \ + && mkdir oidc-bff/migration/src \ + && touch oidc-bff/migration/src/lib.rs \ && mkdir auth-daemon/src \ && echo "fn main() {}" > auth-daemon/src/main.rs \ && mkdir telemetry/src \ diff --git a/backend/Dockerfile.graph-proxy b/backend/Dockerfile.graph-proxy index 1cc9416fd..71d922df6 100644 --- a/backend/Dockerfile.graph-proxy +++ b/backend/Dockerfile.graph-proxy @@ -27,9 +27,6 @@ RUN mkdir graph-proxy/src \ RUN cargo build --release --package telemetry -RUN touch --date @0 graph-proxy/src/main.rs \ - && cargo build --release --package graph-proxy - COPY . . RUN touch graph-proxy/src/main.rs \ diff --git a/backend/Dockerfile.oidc-bff b/backend/Dockerfile.oidc-bff new file mode 100644 index 000000000..a67be847c --- /dev/null +++ b/backend/Dockerfile.oidc-bff @@ -0,0 +1,21 @@ +FROM docker.io/library/rust:1.91.0-bookworm AS build + +WORKDIR /app + +RUN cargo install cargo-auditable + +COPY Cargo.toml Cargo.lock ./ + +COPY . . + +RUN touch --date @0 oidc-bff/src/main.rs \ + && cargo build --release --package oidc-bff + +RUN touch oidc-bff/src/main.rs \ + && cargo auditable build --release --package oidc-bff + +FROM gcr.io/distroless/cc-debian12@sha256:0000f9dc0290f8eaf0ecceafbc35e803649087ea7879570fbc78372df7ac649b AS deploy + +COPY --from=build /app/target/release/oidc-bff /oidc-bff + +ENTRYPOINT ["/oidc-bff"] diff --git a/backend/auth-common/Cargo.toml b/backend/auth-common/Cargo.toml new file mode 100644 index 000000000..1ee732648 --- /dev/null +++ b/backend/auth-common/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "auth-common" +version = "0.1.0" +edition = "2024" +description = "Shared authentication logic for oidc-bff and auth-daemon" + +[dependencies] +anyhow = { workspace = true } +axum = { workspace = true } +chrono = { workspace = true } +http-body-util = "0.1.3" +oauth2 = "5.0.0" +openidconnect = "4.0.1" +sea-orm = { workspace = true, features = ["sqlx-sqlite", "sqlx-postgres", "runtime-tokio-rustls", "macros"] } +serde = { workspace = true } +serde_json = { workspace = true } +serde_yaml = "0.9.34" +sodiumoxide = "0.2.7" +base64 = "0.22.1" +tracing = { workspace = true } diff --git a/backend/auth-common/src/config.rs b/backend/auth-common/src/config.rs new file mode 100644 index 000000000..bf5b5f26e --- /dev/null +++ b/backend/auth-common/src/config.rs @@ -0,0 +1,29 @@ +//! Configuration loading helpers. + +use std::path::Path; + +use serde::de::DeserializeOwned; + +use crate::Result; + +/// Load configuration from a JSON or YAML file. +/// +/// File type is determined by extension: +/// - `.json` → JSON +/// - anything else → YAML +pub fn load_config_from_file>(path: P) -> Result { + let content = std::fs::read_to_string(&path)?; + match path.as_ref().extension().and_then(|e| e.to_str()) { + Some("json") => Ok(serde_json::from_str(&content)?), + // otherwise assume yaml + _ => Ok(serde_yaml::from_str(&content)?), + } +} + +/// Trait for configuration types that can be loaded from files. +pub trait LoadableConfig: Sized + DeserializeOwned { + /// Load configuration from a file path + fn from_file>(path: P) -> Result { + load_config_from_file(path) + } +} diff --git a/backend/auth-common/src/database.rs b/backend/auth-common/src/database.rs new file mode 100644 index 000000000..91ae74a6c --- /dev/null +++ b/backend/auth-common/src/database.rs @@ -0,0 +1,137 @@ +//! Database operations for token storage. +//! +//! Tokens are encrypted using libsodium sealed boxes before storage. + +use chrono::{DateTime, Duration, FixedOffset, Utc}; +use oauth2::RefreshToken; +use openidconnect::{IssuerUrl, SubjectIdentifier}; +use sea_orm::*; +use sodiumoxide::crypto::box_::{PublicKey, SecretKey}; + +use crate::entity::oidc_tokens; +use crate::{Result, TokenData}; + +/// Delete a user's token from the database by subject identifier. +/// This is called during logout to revoke workflow access. +pub async fn delete_token_from_database( + connection: &DatabaseConnection, + subject: &SubjectIdentifier, +) -> Result<()> { + oidc_tokens::Entity::delete_many() + .filter(oidc_tokens::Column::Subject.eq(subject.as_str())) + .exec(connection) + .await?; + tracing::info!(subject = %subject.as_str(), "Deleted token from database"); + Ok(()) +} + +/// Write (or update) a token to the database. +/// The refresh token is encrypted using a sealed box before storage. +pub async fn write_token_to_database( + connection: &DatabaseConnection, + token: &TokenData, + public_key: &PublicKey, +) -> Result<()> { + let encrypted_refresh_token = + sodiumoxide::crypto::sealedbox::seal(token.refresh_token.secret().as_bytes(), public_key); + + // TODO: offline_access tokens will expire if not used within 30 days. + // Keycloak returns the actual expiration date in the field "refresh_expires_in", + // we should use that instead of hardcoding 30 days. + let refresh_token_expires_at = Utc::now() + Duration::days(30); + + let token_update = oidc_tokens::ActiveModel { + issuer: Set(token.issuer.to_string()), + subject: Set(token.subject.to_string()), + encrypted_refresh_token: Set(encrypted_refresh_token), + expires_at: Set(Some(convert_time(refresh_token_expires_at))), + created_at: Set(Utc::now().into()), + updated_at: Set(Utc::now().into()), + ..Default::default() + }; + + oidc_tokens::Entity::insert(token_update) + .on_conflict( + sea_query::OnConflict::column(oidc_tokens::Column::Subject) + .update_columns([ + oidc_tokens::Column::Issuer, + oidc_tokens::Column::Subject, + oidc_tokens::Column::EncryptedRefreshToken, + oidc_tokens::Column::ExpiresAt, + oidc_tokens::Column::UpdatedAt, + // deliberately do not update CreatedAt + ]) + .to_owned(), + ) + .exec(connection) + .await?; + + tracing::debug!(subject = %token.subject.as_str(), "Wrote token to database"); + Ok(()) +} + +/// Read and decrypt a token from the database. +/// Requires both public and secret keys for decryption. +pub async fn read_token_from_database( + connection: &DatabaseConnection, + subject: &SubjectIdentifier, + issuer: Option<&IssuerUrl>, + public_key: &PublicKey, + secret_key: &SecretKey, +) -> Result { + tracing::debug!(subject = %subject.as_str(), "Fetching token from database"); + + // Build query: filter by Subject (and Issuer if provided) + let mut query = oidc_tokens::Entity::find() + .filter(oidc_tokens::Column::Subject.eq(subject.as_str())); + + if let Some(iss) = issuer { + query = query.filter(oidc_tokens::Column::Issuer.eq(iss.as_str())); + } + + let row = query.one(connection).await? + .ok_or_else(|| anyhow::anyhow!( + "No token row found for subject='{}' issuer={:?}", + subject.as_str(), + issuer + ))?; + + // Decrypt sealed box + let ciphertext = row.encrypted_refresh_token; + let decrypted = sodiumoxide::crypto::sealedbox::open(&ciphertext, public_key, secret_key) + .map_err(|_| anyhow::anyhow!("Unable to decrypt refresh token (sealedbox::open failed)"))?; + + let expires_at_utc = match row.expires_at { + Some(dt) => to_utc(dt), + None => Utc::now(), // Treat missing expiry as already expired + }; + + if expires_at_utc < Utc::now() { + return Err(anyhow::anyhow!("Stored refresh token has expired at {}", expires_at_utc).into()); + } + + let issuer = IssuerUrl::new(row.issuer)?; + let subject = SubjectIdentifier::new(row.subject); + let refresh_token = RefreshToken::new(String::from_utf8(decrypted)?); + + let token = TokenData::new( + issuer, + subject.clone(), + None, // Access token will be obtained on first request + Utc::now(), + refresh_token, + ); + + tracing::debug!(subject = %subject.as_str(), "Successfully read token from database"); + Ok(token) +} + +/// Convert UTC time to FixedOffset (for database storage) +fn convert_time(utc_time: DateTime) -> DateTime { + utc_time.with_timezone(&FixedOffset::east_opt(0).unwrap()) +} + +/// Convert DB stored FixedOffset time to Utc +fn to_utc(dt: DateTime) -> DateTime { + dt.with_timezone(&Utc) +} diff --git a/backend/auth-common/src/entity/mod.rs b/backend/auth-common/src/entity/mod.rs new file mode 100644 index 000000000..ca93c4e53 --- /dev/null +++ b/backend/auth-common/src/entity/mod.rs @@ -0,0 +1,7 @@ +//! SeaORM entities for authentication services. +//! +//! This module provides the database entities used by both `oidc-bff` and `auth-daemon` +//! for storing OIDC tokens. + +pub mod oidc_tokens; +pub use super::oidc_tokens::Entity as OidcTokens; diff --git a/backend/auth-common/src/entity/oidc_tokens.rs b/backend/auth-common/src/entity/oidc_tokens.rs new file mode 100644 index 000000000..23ee74526 --- /dev/null +++ b/backend/auth-common/src/entity/oidc_tokens.rs @@ -0,0 +1,22 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19 + +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm(table_name = "oidc_tokens")] +pub struct Model { + #[sea_orm(column_type = "Text")] + pub issuer: String, + #[sea_orm(primary_key, auto_increment = false, column_type = "Text")] + pub subject: String, + #[sea_orm(column_type = "VarBinary(StringLen::None)")] + pub encrypted_refresh_token: Vec, + pub expires_at: Option, + pub created_at: DateTimeWithTimeZone, + pub updated_at: DateTimeWithTimeZone, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/backend/auth-common/src/error.rs b/backend/auth-common/src/error.rs new file mode 100644 index 000000000..cb533acdc --- /dev/null +++ b/backend/auth-common/src/error.rs @@ -0,0 +1,91 @@ +//! Shared error type for authentication services. + +use axum::{ + http::StatusCode, + response::{IntoResponse, Response}, +}; + +/// Wrapper around `anyhow::Error` that implements `IntoResponse`. +#[derive(Debug)] +pub struct Error(pub anyhow::Error); + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + self.0.source() + } +} + +impl IntoResponse for Error { + fn into_response(self) -> Response { + // Log the actual error for debugging + tracing::error!(error = %self.0, "Request failed"); + + // Return generic message to client (don't leak internal details) + ( + StatusCode::INTERNAL_SERVER_ERROR, + "An internal error occurred", + ) + .into_response() + } +} + +impl From for Error { + fn from(err: anyhow::Error) -> Self { + Self(err) + } +} + +// Implement From for common error types instead of blanket impl +impl From for Error { + fn from(err: std::io::Error) -> Self { + Self(err.into()) + } +} + +impl From for Error { + fn from(err: sea_orm::DbErr) -> Self { + Self(err.into()) + } +} + +impl From for Error { + fn from(err: serde_json::Error) -> Self { + Self(err.into()) + } +} + +impl From for Error { + fn from(err: serde_yaml::Error) -> Self { + Self(err.into()) + } +} + +impl From for Error { + fn from(err: openidconnect::url::ParseError) -> Self { + Self(err.into()) + } +} + +impl From for Error { + fn from(err: std::string::FromUtf8Error) -> Self { + Self(err.into()) + } +} + +impl From for Error { + fn from(err: base64::DecodeError) -> Self { + Self(err.into()) + } +} + +impl From for Error { + fn from(err: axum::Error) -> Self { + Self(anyhow::anyhow!("Axum error: {}", err)) + } +} diff --git a/backend/auth-common/src/http_utils.rs b/backend/auth-common/src/http_utils.rs new file mode 100644 index 000000000..34a3291dc --- /dev/null +++ b/backend/auth-common/src/http_utils.rs @@ -0,0 +1,47 @@ +//! HTTP utilities for request manipulation. +//! +//! Provides helper functions for cloning requests and preparing headers. + +use axum::{ + body::Body, + extract::Request, + http::{self, HeaderValue}, +}; + +use crate::{Result, TokenData}; + +/// Clone a request by consuming the body and creating two identical requests. +/// +/// This is needed for retry logic where we might need to send the same request twice +/// (e.g., when a token refresh is needed mid-request). +/// +/// # Note +/// This is an inefficient method that reads the entire body into memory. +/// Consider streaming approaches for large request bodies. +pub async fn clone_request(req: Request) -> Result<(Request, Request)> { + let (parts, body) = req.into_parts(); + let bytes = http_body_util::BodyExt::collect(body).await?.to_bytes(); + let req1 = Request::from_parts(parts.clone(), Body::from(bytes.clone())); + let req2 = Request::from_parts(parts, Body::from(bytes)); + Ok((req1, req2)) +} + +/// Prepare request headers for proxying with authentication. +/// +/// - Adds the Authorization header with the Bearer token (if access_token is present) +/// - Removes the Cookie header (backend should authenticate via Bearer token, not cookies) +pub fn prepare_headers(req: &mut Request, token: &TokenData) { + if let Some(access_token) = &token.access_token { + let value = format!("Bearer {}", access_token.secret()); + tracing::debug!("Injecting Bearer token into request headers"); + + if let Ok(header_value) = HeaderValue::from_str(&value) { + req.headers_mut().insert(http::header::AUTHORIZATION, header_value); + } else { + tracing::warn!("Failed to create Authorization header value"); + } + } + + // Remove cookie header - backend should authenticate via Bearer token + req.headers_mut().remove(http::header::COOKIE); +} diff --git a/backend/auth-common/src/lib.rs b/backend/auth-common/src/lib.rs new file mode 100644 index 000000000..74d2b9d3e --- /dev/null +++ b/backend/auth-common/src/lib.rs @@ -0,0 +1,25 @@ +//! # auth-common +//! +//! Shared authentication logic for the `oidc-bff` and `auth-daemon` services. +//! +//! This crate provides: +//! - Common error types +//! - Token data structures +//! - Database entities for token storage +//! - Database operations for token storage +//! - HTTP utilities for request manipulation +//! - Configuration loading helpers + +pub mod config; +pub mod database; +pub mod entity; +pub mod error; +pub mod http_utils; +pub mod token; + +pub use entity::oidc_tokens; +pub use error::Error; +pub use token::TokenData; + +/// Common Result type using the shared Error +pub type Result = std::result::Result; diff --git a/backend/auth-common/src/token.rs b/backend/auth-common/src/token.rs new file mode 100644 index 000000000..748a025e5 --- /dev/null +++ b/backend/auth-common/src/token.rs @@ -0,0 +1,102 @@ +//! Token data structures shared between services. + +use std::time::Duration; + +use anyhow::anyhow; +use chrono::{DateTime, Utc}; +use openidconnect::{AccessToken, IssuerUrl, RefreshToken, SubjectIdentifier}; +use serde::{Deserialize, Serialize}; + +use crate::Result; + +/// Token data that can be stored in sessions or loaded from database. +/// +/// This structure is used by both `oidc-bff` (with required access_token for sessions) +/// and `auth-daemon` (with optional access_token when loaded from database). +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct TokenData { + pub issuer: IssuerUrl, + pub subject: SubjectIdentifier, + /// Access token - may be None if only refresh token is available (daemon startup) + pub access_token: Option, + pub access_token_expires_at: DateTime, + pub refresh_token: RefreshToken, +} + +impl TokenData { + /// Session key for storing token data + pub const SESSION_KEY: &'static str = "token_session_data"; + + pub fn new( + issuer: IssuerUrl, + subject: SubjectIdentifier, + access_token: Option, + access_token_expires_at: DateTime, + refresh_token: RefreshToken, + ) -> Self { + Self { + issuer, + subject, + access_token, + access_token_expires_at, + refresh_token, + } + } + + /// Create TokenData from an OAuth2 token response. + /// Requires the access token to be present. + pub fn from_token_response( + token_response: &T, + issuer: IssuerUrl, + subject: SubjectIdentifier, + ) -> Result { + let access_token = token_response.access_token().clone(); + let refresh_token = token_response + .refresh_token() + .ok_or_else(|| anyhow!("Token Response did not return a refresh token"))? + .clone(); + let access_token_expires_at = Utc::now() + + token_response + .expires_in() + .unwrap_or_else(|| Duration::from_secs(60)); + Ok(Self::new( + issuer, + subject, + Some(access_token), + access_token_expires_at, + refresh_token, + )) + } + + /// Update tokens in place from a token response + pub fn update_tokens_mut(&mut self, token_response: &T) { + let access_token = token_response.access_token().clone(); + let refresh_token = token_response.refresh_token(); + let access_token_expires_at = Utc::now() + + token_response + .expires_in() + .unwrap_or_else(|| Duration::from_secs(60)); + if let Some(refresh_token) = refresh_token { + self.refresh_token = refresh_token.clone(); + } + self.access_token = Some(access_token); + self.access_token_expires_at = access_token_expires_at; + } + + /// Create a new TokenData with updated tokens from a response + pub fn update_tokens(&self, token_response: &T) -> Self { + let mut clone = self.clone(); + clone.update_tokens_mut(token_response); + clone + } + + /// Check if the access token is expired or missing + pub fn access_token_is_expired(&self) -> bool { + self.access_token_expires_at <= Utc::now() || self.access_token.is_none() + } + + /// Get the access token secret string, if available + pub fn access_token_secret(&self) -> Option<&str> { + self.access_token.as_ref().map(|t| t.secret().as_str()) + } +} diff --git a/backend/auth-daemon/.devcontainer/devcontainer.json b/backend/auth-daemon/.devcontainer/devcontainer.json index 483c1588b..720464918 100644 --- a/backend/auth-daemon/.devcontainer/devcontainer.json +++ b/backend/auth-daemon/.devcontainer/devcontainer.json @@ -24,5 +24,11 @@ } }, "workspaceMount": "source=${localWorkspaceFolder}/../..,target=/workspace,type=bind", - "workspaceFolder": "/workspace/" + "workspaceFolder": "/workspace/", + "capAdd": [ + "SYS_PTRACE" + ], + "securityOpt": [ + "seccomp=unconfined" + ] } diff --git a/backend/auth-daemon/Cargo.toml b/backend/auth-daemon/Cargo.toml index eba8ac624..6af9ca5db 100644 --- a/backend/auth-daemon/Cargo.toml +++ b/backend/auth-daemon/Cargo.toml @@ -6,19 +6,36 @@ license-file = "../../LICENSE" [dependencies] anyhow.workspace = true -axum = { workspace = true } +auth-common = { path = "../auth-common" } +axum = { workspace = true, features = ["json"] } +axum-reverse-proxy = "1.0.3" clap = { workspace = true, features = ["env"] } dotenvy.workspace = true +openidconnect = "4.0.1" regex = { workspace = true } reqwest.workspace = true serde.workspace = true +serde_json.workspace = true +serde_yaml = "0.9.34" thiserror.workspace = true tokio = { workspace = true, features = ["full"] } tower-http = { workspace = true, features = ["cors"] } tracing.workspace = true tracing-subscriber = { workspace = true, features = ["env-filter"] } url.workspace = true +sea-orm = { workspace = true, features = ["sqlx-sqlite", "sqlx-postgres", "runtime-tokio-rustls", "macros"] } +chrono.workspace = true +sodiumoxide = "0.2.7" +http-body-util = "0.1.3" +oauth2 = "5.0.0" +base64 = "0.22.1" +rustls = "0.23.35" +axum-test = "18.4.1" +env_logger = "0.11.8" +oauth2-test-server = "0.1.3" +testcontainers = {version = "0.26.0", features = ["http_wait_plain"]} [dev-dependencies] mockito.workspace = true serde_json.workspace = true +migration = { version = "0.1.0", path = "../oidc-bff/migration" } diff --git a/backend/auth-daemon/src/config.rs b/backend/auth-daemon/src/config.rs new file mode 100644 index 000000000..b85d41c1a --- /dev/null +++ b/backend/auth-daemon/src/config.rs @@ -0,0 +1,32 @@ +use std::path::Path; + +use serde::{Deserialize, Serialize}; +use crate::Result; + +#[derive(Serialize, Deserialize)] +pub struct Config { + pub client_id: String, + pub client_secret: String, + pub oidc_provider_url: String, + pub graph_url: String, + pub port: u16, + pub postgres_user: String, + pub postgres_password: String, + pub postgres_database: String, + pub postgres_hostname: String, + pub postgres_port: u16, + pub encryption_public_key: String, + pub encryption_private_key: String, +} + +impl Config { + /// Load config from JSON or YAML file + pub fn from_file>(path: P) -> Result { + let content = std::fs::read_to_string(&path)?; + match path.as_ref().extension().and_then(|e| e.to_str()) { + Some("json") => Ok(serde_json::from_str(&content)?), + // otherwise assume yaml + _ => Ok(serde_yaml::from_str(&content)?), + } + } +} diff --git a/backend/auth-daemon/src/database.rs b/backend/auth-daemon/src/database.rs new file mode 100644 index 000000000..bda6f6e3f --- /dev/null +++ b/backend/auth-daemon/src/database.rs @@ -0,0 +1,7 @@ +//! Database operations for auth-daemon. +//! Re-exports shared database functions from auth-common. + +pub use auth_common::database::{ + read_token_from_database, + write_token_to_database, +}; diff --git a/backend/auth-daemon/src/error.rs b/backend/auth-daemon/src/error.rs new file mode 100644 index 000000000..48dc295a9 --- /dev/null +++ b/backend/auth-daemon/src/error.rs @@ -0,0 +1,4 @@ +//! Error handling for auth-daemon. +//! Re-exports the shared error type from auth-common. + +pub use auth_common::Error; diff --git a/backend/auth-daemon/src/inject_token.rs b/backend/auth-daemon/src/inject_token.rs new file mode 100644 index 000000000..dfa80b1f4 --- /dev/null +++ b/backend/auth-daemon/src/inject_token.rs @@ -0,0 +1,101 @@ +use crate::{database::write_token_to_database, state::{RouterState, TokenData}}; +use auth_common::http_utils::{clone_request, prepare_headers}; +use http_body_util::BodyExt; +use serde_json::Value; +use std::sync::Arc; +use axum::response::IntoResponse; + +use axum::{ + body::Body, + extract::{Request, State}, + middleware, response::Response, +}; + +use crate::Result; + +pub async fn inject_token( + State(state): State>, + req: Request, + next: middleware::Next, +) -> Result { + let token: Option = state.token.read().await.clone(); + if let Some(mut token) = token { + println!("Injecting token"); + if token.access_token_is_expired() { + println!("Access token is expired, refreshing"); + token = refresh_token_and_write_to_database(&state, &token).await?; + } + let mut req = clone_request(req).await?; + prepare_headers(&mut req.0, &token); + let response = next.clone().run(req.0).await; + let response = response_as_json(response).await?; + println!("DEBUG response json: {:?}", response); + if !is_good_response(&response) { + println!("Query failed, refreshing token and trying again"); + token = refresh_token_and_write_to_database(&state, &token).await?; + prepare_headers(&mut req.1, &token); + Ok(next.run(req.1).await) + } else { + Ok(axum::Json(response).into_response()) + } + } else { + println!("No token to inject"); + Ok(next.run(req).await) + } +} + +fn is_good_response(response: &Value) -> bool { + + if let Some(object) = response.as_object() { + if let Some(errors) = object.get("errors") { + return errors.as_array().map(|it| it.len() == 0).unwrap_or(false); + } else { + return true; + } + } + false +} + +async fn response_as_json(response: Response) -> Result { + + if !response.status().is_success() { + Err(anyhow::anyhow!("HTTP error: {}", response.status()))?; + } + + + let collected = response.into_body().collect().await + .map_err(|e| anyhow::anyhow!("collect body error: {}", e))?; + let bytes = collected.to_bytes(); + + let json: Value = serde_json::from_slice(&bytes) + .map_err(|e| anyhow::anyhow!("JSON parse error: {}", e))?; + Ok(json) +} + +// async fn set_token(state: &RouterState, new_token: TokenData) { +// let mut guard = state.token.write().await; +// *guard = Some(new_token); +// } + +// clone_request and prepare_headers are now provided by auth_common::http_utils + +async fn refresh_token_and_write_to_database( + state: &RouterState, + token: &TokenData, +) -> Result { + let token = refresh_token(state, token).await?; + write_token_to_database(&state.database_connection, &token, &state.public_key).await?; + Ok(token) +} + +async fn refresh_token(state: &RouterState, token: &TokenData) -> Result { + let token_response = state + .oidc_client + .exchange_refresh_token(&token.refresh_token) + .map_err(|e| anyhow::anyhow!("Failed to build refresh token request: {}", e))? + .request_async(&state.http_client) + .await + .map_err(|e| anyhow::anyhow!("Token refresh request failed: {}", e))?; + let token = token.update_tokens(&token_response); + Ok(token) +} diff --git a/backend/auth-daemon/src/main.rs b/backend/auth-daemon/src/main.rs index 7feae4e43..ea5935b43 100644 --- a/backend/auth-daemon/src/main.rs +++ b/backend/auth-daemon/src/main.rs @@ -1,11 +1,9 @@ #![forbid(unsafe_code)] #![doc = include_str!("../README.md")] -mod proxy; -use proxy::proxy; - mod healthcheck; use healthcheck::healthcheck; +use openidconnect::SubjectIdentifier; use std::{ net::{IpAddr, Ipv4Addr, SocketAddr}, @@ -14,9 +12,7 @@ use std::{ }; use axum::{ - Router, - http::Method, - routing::{get, post}, + Router, http::Method, middleware, routing::get }; use clap::Parser; use regex::Regex; @@ -25,31 +21,46 @@ use tokio::signal::unix::{SignalKind, signal}; use tower_http::cors::{AllowOrigin, CorsLayer}; use tracing::{debug, info}; use tracing_subscriber::EnvFilter; -use url::Url; + +use crate::{config::Config, state::RouterState}; +mod config; +mod state; +mod error; + +use axum_reverse_proxy::ReverseProxy; +mod inject_token; +mod database; + + +type Result = std::result::Result; + +#[derive(Parser, Debug)] +#[command(author, version, about)] +struct ServeArgs { + /// Path to config file (JSON or YAML) + #[arg( + short, + long, + env = "WORKFLOWS_AUTH_DAEMON_CONFIG", + default_value = "config.yaml" + )] + config: String, + #[arg( + env = "WORKFLOWS_AUTH_DAEMON_SUBJECT", + )] + subject: String, +} #[derive(Debug, Parser)] #[allow(clippy::large_enum_variant)] enum Cli { /// Starts a webserver - Serve(RouterState), + Serve(ServeArgs), } -#[derive(Debug, Parser)] -struct RouterState { - #[arg(short, long, env = "AUTH_DOMAIN")] - auth_domain: Url, - #[arg(short, long, env = "CLIENT_ID")] - client_id: String, - #[arg(short, long, env = "GRAPH_URL")] - graph_url: Url, - #[arg(short, long, env = "TOKEN")] - token: String, - #[arg(long, env = "PORT", default_value = "80")] - port: u16, -} #[tokio::main] -async fn main() -> anyhow::Result<()> { +async fn main() -> Result<()> { dotenvy::dotenv().ok(); let args = Cli::parse(); tracing_subscriber::fmt() @@ -58,8 +69,9 @@ async fn main() -> anyhow::Result<()> { match args { Cli::Serve(args) => { - let requested_port = args.port; - let router_state = Arc::new(args); + let config = Config::from_file(args.config)?; + let requested_port = config.port; + let router_state = Arc::new(RouterState::new(config, &SubjectIdentifier::new(args.subject)).await?); let router = setup_router(router_state, None)?; serve(router, IpAddr::V4(Ipv4Addr::UNSPECIFIED), requested_port).await?; @@ -71,6 +83,11 @@ async fn main() -> anyhow::Result<()> { fn setup_router(state: Arc, cors_allow: Option>) -> anyhow::Result { debug!("Setting up the router"); + + rustls::crypto::ring::default_provider() + .install_default() + .expect("Failed to install rust TLS cryptography"); + let cors_origin = if let Some(cors_allow) = cors_allow { info!("Allowing CORS Origin(s) matching: {:?}", cors_allow); AllowOrigin::predicate(move |origin, _| { @@ -85,8 +102,14 @@ fn setup_router(state: Arc, cors_allow: Option>) -> anyh AllowOrigin::default() }; - Ok(Router::new() - .route("/", post(proxy)) + let proxy: Router> = + ReverseProxy::new("/", state.config.graph_url.as_str()).into(); + + Ok(proxy + .layer(middleware::from_fn_with_state( + state.clone(), + inject_token::inject_token, + )) .with_state(state) .layer( CorsLayer::new() @@ -113,3 +136,132 @@ async fn shutdown_signal() { println!("Shutting down"); process::exit(0); } + +#[cfg(test)] +mod tests { + use std::sync::Arc; + use std::time::Duration; + + use mockito::Matcher; + use openidconnect::SubjectIdentifier; + use sea_orm::ActiveValue::Set; + use sea_orm::{Database, DatabaseConnection, EntityTrait}; + use serde_json::json; + use testcontainers::core::wait::HttpWaitStrategy; + use testcontainers::{GenericImage, ImageExt}; + use testcontainers::{ + core::WaitFor, + runners::AsyncRunner, +}; + + use base64::{Engine, engine::general_purpose::STANDARD as BASE64}; + + use crate::{config::Config, state::RouterState}; + use crate::{Result, setup_router}; + + async fn test_database(issuer_url: &str, refresh_token: &str) -> Result { + let db = Database::connect("sqlite::memory:").await?; + use migration::{Migrator, MigratorTrait}; + Migrator::up(&db, None).await?; + + let now: chrono::DateTime = chrono::Utc::now().into(); + let expires_at = now + Duration::from_secs(600); + + let public_key = sodiumoxide::crypto::box_::PublicKey::from_slice(&BASE64.decode("ZpJ703xR7atXbGXI20FkQk3J1qjLxodTP6yk92yPVGM=")?).expect("valid key"); + let encrypted_refresh_token = sodiumoxide::crypto::sealedbox::seal(refresh_token.as_bytes(), &public_key); + + auth_common::oidc_tokens::Entity::insert(auth_common::oidc_tokens::ActiveModel { + issuer: Set(issuer_url.into()), + subject: Set("test-subject".into()), + encrypted_refresh_token: Set(encrypted_refresh_token.into()), + expires_at: Set(expires_at.into()), + created_at: Set(now.clone().into()), + updated_at: Set(now.into()), + ..Default::default() + }).exec(&db) + .await?; + + Ok(db) + } + + + #[tokio::test] + async fn test() -> Result<()> { + + let _ = env_logger::try_init(); + + let mut graphql_server = mockito::Server::new_async().await; + let graphql_server_url = graphql_server.url(); + let graphql_mock = graphql_server.mock("POST", "/") + .match_header("Authorization", Matcher::Regex("Bearer .+".into())) + .with_status(200) + .with_body("{\"name\": \"workflow-name\"}") + .expect(1) + .create_async() + .await; + + let wait_strategy = HttpWaitStrategy::new("default/.well-known/openid-configuration").with_expected_status_code(200u16); + let oidc_container = GenericImage::new("ghcr.io/navikt/mock-oauth2-server", "3.0.1") + .with_wait_for(WaitFor::http(wait_strategy)) + .with_env_var("SERVER_PORT", "8080").with_startup_timeout(Duration::from_secs(60)).start().await.expect("failed to start mock OIDC server"); + let port = oidc_container.get_host_port_ipv4(8080).await + .map_err(|e| anyhow::anyhow!("Container error: {}", e))?; + + let mock_admin_url = format!("http://localhost:{}/default/token", port); + let params = [ + ("grant_type", "refresh_token"), + ("scope", "openid offline_access"), + ("subject", "test-subject"), + ("refresh_token", "test-refresh-token"), + ("client_id", "test-client"), + ]; + + let res: serde_json::Value = reqwest::Client::new() + .post(mock_admin_url) + .timeout(Duration::from_secs(10)) + .form(¶ms) + .send() + .await + .map_err(|e| anyhow::anyhow!("Failed to send token request: {}", e))? + .json() + .await + .map_err(|e| anyhow::anyhow!("Failed to parse token response: {}", e))?; + + let refresh_token = res["refresh_token"].as_str().expect("no refresh token"); + + let issuer_url = format!("http://localhost:{}/default", port); + + let db = test_database(&issuer_url, &refresh_token).await?; + + let config = Config{ + client_id: "test-client".into(), + client_secret: "".into(), + oidc_provider_url: issuer_url.into(), + graph_url: graphql_server_url.into(), + port: 6000, + postgres_user: "auth_user".into(), + postgres_password: "password".into(), + postgres_database: "auth_service".into(), + postgres_hostname: "database-hostname".into(), + postgres_port: 5432, + encryption_public_key: "ZpJ703xR7atXbGXI20FkQk3J1qjLxodTP6yk92yPVGM=".into(), + encryption_private_key: "yxjSYB/nvdAzktd83diOtADvp3RX/0Kx5V3FgK7YlXk=".into() }; + + let router_state = Arc::new(RouterState::with_database(config, &SubjectIdentifier::new("test-subject".into()), db).await?); + let router = setup_router(router_state, None)?; + let test_server = axum_test::TestServer::new(router)?; + + // + let response = test_server.post("/").content_type("application/json").json(&json!( + {"query": "mutation{ submitWorkflowTemplate(name: \"template-name\", visit: {proposalCode: \"xy\", proposalNumber: 1234, number: 5}, parameters: {}){ name } }" } + )).await; + + response.assert_status_ok(); + graphql_mock.assert_async().await; + + oidc_container.stop_with_timeout(Some(60)).await + .map_err(|e| anyhow::anyhow!("Container error: {}", e))?; + + Ok(()) + } +} \ No newline at end of file diff --git a/backend/auth-daemon/src/proxy.rs b/backend/auth-daemon/src/proxy.rs deleted file mode 100644 index a4bae2d23..000000000 --- a/backend/auth-daemon/src/proxy.rs +++ /dev/null @@ -1,205 +0,0 @@ -use std::sync::Arc; - -use axum::{Json, extract::State, http::StatusCode, response::IntoResponse}; -use reqwest::Client; -use serde::{Deserialize, Serialize}; -use tracing::info; -use url::Url; - -use crate::RouterState; - -pub async fn proxy( - State(router_state): State>, - Json(query): Json, -) -> Result { - let form = vec![ - ("grant_type", "refresh_token"), - ("client_id", &router_state.client_id), - ("refresh_token", &router_state.token), - ]; - - let auth_domain = match router_state.auth_domain.as_str().ends_with("/") { - true => format!("{}protocol/openid-connect/token", router_state.auth_domain), - false => format!("{}/protocol/openid-connect/token", router_state.auth_domain), - }; - info!("Auth domain: {}", auth_domain); - - let client = Client::new(); - - info!("Fetching access token"); - let res = client - .post(auth_domain) - .form(&form) - .send() - .await - .map_err(|_err| ProxyError::AuthQuery(router_state.auth_domain.clone()))?; - - if !res.status().is_success() { - let status = res.status(); - let body = res - .text() - .await - .unwrap_or_else(|_| "No message".to_string()); - return Err(ProxyError::KeycloakError(status, body)); - } - - info!("Decoding access token"); - let auth_data: AuthResponse = res.json().await?; - let graph_payload = GraphPayload::from(query.query); - - info!("Querying graph"); - let graph_request = client - .post(router_state.graph_url.clone()) - .bearer_auth(auth_data.access_token) - .json(&graph_payload) - .send() - .await?; - - info!("Decoding graph response"); - let graph_response: String = graph_request.text().await?; - - Ok(graph_response.to_string()) -} - -#[derive(Deserialize, Serialize)] -pub struct UserQuery { - query: String, -} - -#[derive(Deserialize)] -struct AuthResponse { - access_token: String, -} - -#[derive(Serialize)] -struct GraphPayload { - query: String, -} - -impl From for GraphPayload { - fn from(value: String) -> Self { - GraphPayload { query: value } - } -} - -#[derive(Debug, thiserror::Error)] -pub enum ProxyError { - #[error("Could not reach auth service at {0}")] - AuthQuery(Url), - #[error("Could not deserialise response: {0}")] - Deserialization(#[from] reqwest::Error), - #[error("Error on token exchange: status: {0}, message {1}")] - KeycloakError(StatusCode, String), -} - -impl IntoResponse for ProxyError { - fn into_response(self) -> axum::response::Response { - ( - StatusCode::INTERNAL_SERVER_ERROR, - format!("Something went wrong: {}", self), - ) - .into_response() - } -} - -#[cfg(test)] -mod tests { - use std::{str::FromStr, sync::Arc}; - - use axum::http::StatusCode; - use mockito::Server; - use url::Url; - - use crate::{ - RouterState, - proxy::{ProxyError, UserQuery, proxy}, - }; - - #[tokio::test] - async fn base_test() { - let client = "client"; - let token = "token"; - let port = 3000; - - let query = UserQuery { - query: "test-query".into(), - }; - let query_body = serde_json::to_string(&query).unwrap(); - - let mut auth = Server::new_async().await; - let mut graph = Server::new_async().await; - - auth.mock("POST", "/protocol/openid-connect/token") - .with_status(200) - .with_body(r#"{"access_token": "some-token"}"#) - .create_async() - .await; - - graph - .mock("POST", "/") - .with_status(200) - .with_header("content-type", "application/json") - .with_body(query_body) - .create_async() - .await; - - let router_state = RouterState { - auth_domain: Url::from_str(&auth.url()).unwrap(), - client_id: client.into(), - graph_url: Url::from_str(&graph.url()).unwrap(), - token: token.into(), - port, - }; - - let resp = proxy( - axum::extract::State(Arc::new(router_state)), - axum::Json(query), - ) - .await; - assert!(resp.is_ok()); - let result = resp.unwrap(); - assert_eq!(r#"{"query":"test-query"}"#, result); - } - - #[tokio::test] - async fn invalid_auth() { - let client = "client"; - let token = "token"; - let port = 3000; - - let query = UserQuery { - query: "test-query".into(), - }; - - let mut auth = Server::new_async().await; - - auth.mock("POST", "/protocol/openid-connect/token") - .with_status(401) - .with_body("Unauthorized") - .create_async() - .await; - - let router_state = RouterState { - auth_domain: Url::from_str(&auth.url()).unwrap(), - client_id: client.into(), - graph_url: Url::from_str("https://example.com").unwrap(), - token: token.into(), - port, - }; - - let resp = proxy( - axum::extract::State(Arc::new(router_state)), - axum::Json(query), - ) - .await; - assert!(resp.is_err()); - - match resp { - Err(ProxyError::KeycloakError(status, message)) => { - assert_eq!(status, StatusCode::UNAUTHORIZED); - assert_eq!(message, "Unauthorized"); - } - other => panic!("Expected Unauthorized error, got {:?}", other), - } - } -} diff --git a/backend/auth-daemon/src/state.rs b/backend/auth-daemon/src/state.rs new file mode 100644 index 000000000..c51841500 --- /dev/null +++ b/backend/auth-daemon/src/state.rs @@ -0,0 +1,95 @@ +use openidconnect::core::{CoreClient, CoreProviderMetadata}; +use sodiumoxide::crypto::box_::{PublicKey, SecretKey}; +use openidconnect::{IssuerUrl, SubjectIdentifier}; +use tokio::sync::RwLock; +use base64::{Engine, engine::general_purpose::STANDARD as BASE64}; +use oauth2::{ClientId, ClientSecret, EndpointMaybeSet, EndpointNotSet, EndpointSet, reqwest}; +use sea_orm::{Database, DatabaseConnection}; + +use crate::config::Config; +use crate::Result; +use crate::database::read_token_from_database; +use anyhow::anyhow; + +// Re-export TokenData from auth-common +pub use auth_common::TokenData; + +pub struct RouterState { + pub config: Config, + pub token: RwLock>, + pub http_client: reqwest::Client, + pub oidc_client: openidconnect::core::CoreClient< + EndpointSet, + EndpointNotSet, + EndpointNotSet, + EndpointNotSet, + EndpointMaybeSet, + EndpointMaybeSet, + >, + pub database_connection: DatabaseConnection, + pub public_key: PublicKey, +} + +impl RouterState { + + pub async fn new(config: Config, subject: impl Into<&SubjectIdentifier>) -> Result { + let database_url = format!( + "postgres://{}:{}@{}:{}/{}", + config.postgres_user, + config.postgres_password, + config.postgres_hostname, + config.postgres_port, + config.postgres_database + ); + let database_connection = Database::connect(&database_url).await?; + Self::with_database(config, subject, database_connection).await + } + + pub async fn with_database(config: Config, subject: impl Into<&SubjectIdentifier>, database_connection: impl Into) -> Result { + + let http_client = reqwest::ClientBuilder::new() + // Following redirects opens the client up to SSRF vulnerabilities. + .redirect(reqwest::redirect::Policy::none()) + .build() + .map_err(|e| anyhow!("Failed to build HTTP client: {}", e))?; + + // Use OpenID Connect Discovery to fetch the provider metadata. + let provider_metadata = CoreProviderMetadata::discover_async( + IssuerUrl::new(config.oidc_provider_url.to_string())?, + &http_client, + ) + .await + .map_err(|e| anyhow!("OIDC discovery failed: {}", e))?; + + let oidc_client = CoreClient::from_provider_metadata( + provider_metadata, + ClientId::new(config.client_id.to_string()), + if config.client_secret.is_empty() { + None + } else { + Some(ClientSecret::new(config.client_secret.to_string())) + }, + ); + + let public_key = PublicKey::from_slice(&BASE64.decode(&config.encryption_public_key)?) + .ok_or(anyhow!("Invalid public key"))?; + + let private_key = SecretKey::from_slice(&BASE64.decode(&config.encryption_private_key)?) + .ok_or(anyhow!("Invalid public key"))?; + + let subject = subject.into(); + let database_connection = database_connection.into(); + let token = read_token_from_database(&database_connection, subject, None, &public_key, &private_key).await?; + + Ok(Self {config, token: RwLock::new(Some(token)), + http_client, + oidc_client, + database_connection, + public_key, + }) + } +} + +// TokenData is now provided by auth_common - see `pub use auth_common::TokenData;` above + + diff --git a/backend/oidc-bff/.cargo/config.toml b/backend/oidc-bff/.cargo/config.toml new file mode 100644 index 000000000..f4d28a316 --- /dev/null +++ b/backend/oidc-bff/.cargo/config.toml @@ -0,0 +1,2 @@ +[build] +rustflags = ["-C", "link-arg=-fuse-ld=lld"] diff --git a/backend/oidc-bff/.devcontainer/Dockerfile b/backend/oidc-bff/.devcontainer/Dockerfile new file mode 100644 index 000000000..8575a669f --- /dev/null +++ b/backend/oidc-bff/.devcontainer/Dockerfile @@ -0,0 +1,7 @@ +FROM mcr.microsoft.com/devcontainers/rust:2-bookworm + +# Include lld linker to improve build times either by using environment variable +# RUSTFLAGS="-C link-arg=-fuse-ld=lld" or with Cargo's configuration file (i.e see .cargo/config.toml). +RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ + && apt-get -y install clang lld libssl-dev pkg-config postgresql-client \ + && apt-get autoremove -y && apt-get clean -y diff --git a/backend/oidc-bff/.devcontainer/devcontainer.json b/backend/oidc-bff/.devcontainer/devcontainer.json new file mode 100644 index 000000000..5ba89e0c5 --- /dev/null +++ b/backend/oidc-bff/.devcontainer/devcontainer.json @@ -0,0 +1,23 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/rust-postgres +{ + "name": "Rust and PostgreSQL", + "dockerComposeFile": "docker-compose.yml", + "service": "app", + "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", + + // Features to add to the dev container. More info: https://containers.dev/features. + // "features": {}, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [5432], + + // Use 'postCreateCommand' to run commands after the container is created. + "postCreateCommand": "cargo install sea-orm-cli" + + // Configure tool-specific properties. + // "customizations": {}, + + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" +} diff --git a/backend/oidc-bff/.devcontainer/docker-compose.yml b/backend/oidc-bff/.devcontainer/docker-compose.yml new file mode 100644 index 000000000..5c16f9cfe --- /dev/null +++ b/backend/oidc-bff/.devcontainer/docker-compose.yml @@ -0,0 +1,37 @@ +version: '3.8' + +volumes: + postgres-data: + +services: + app: + build: + context: . + dockerfile: Dockerfile + env_file: + # Ensure that the variables in .env match the same variables in devcontainer.json + - .env + + volumes: + - ../..:/workspaces:cached + + # Overrides default command so things don't shut down after the process ends. + command: sleep infinity + + # Runs app on the same network as the database container, allows "forwardPorts" in devcontainer.json function. + network_mode: service:db + + # Use "forwardPorts" in **devcontainer.json** to forward an app port locally. + # (Adding the "ports" property to this file will not forward from a Codespace.) + + db: + image: postgres:14.1 + restart: unless-stopped + volumes: + - postgres-data:/var/lib/postgresql/data + env_file: + # Ensure that the variables in .env match the same variables in devcontainer.json + - .env + + # Add "forwardPorts": ["5432"] to **devcontainer.json** to forward PostgreSQL locally. + # (Adding the "ports" property to this file will not forward from a Codespace.) \ No newline at end of file diff --git a/backend/oidc-bff/Cargo.toml b/backend/oidc-bff/Cargo.toml new file mode 100644 index 000000000..0d1f9420f --- /dev/null +++ b/backend/oidc-bff/Cargo.toml @@ -0,0 +1,46 @@ +[package] +name = "oidc-bff" +version = "0.1.0" +edition = "2024" +default-run = "oidc-bff" + +[lib] +name = "oidc_bff" # the library name as used in code: `use oidc_bff::...` +path = "src/lib.rs" + + +[[bin]] +name = "keygen" +path = "src/bin/keygen.rs" +publish = false + +[dependencies] +anyhow = { workspace = true, features = ["backtrace"] } +auth-common = { path = "../auth-common" } +axum = { workspace = true, features = ["macros"] } +axum-reverse-proxy = "1.0.3" +bytes = "1.11.0" +chrono.workspace = true +clap.workspace = true +dotenvy.workspace = true +http-body-util = "0.1.3" +hyper = "1.8.1" +moka = { version = "0.12.11", features = ["future"] } +oauth2 = "5.0.0" +openidconnect = { version = "4.0.1", features = ["timing-resistant-secret-traits"]} +rustls = { version = "0.23.35", features = ["ring"] } +serde.workspace = true +serde_json.workspace = true +thiserror.workspace = true +tokio = { workspace = true, features = ["full"] } +tower-sessions = "0.14.0" +sea-orm = {workspace=true} +migration = { version = "0.1.0", path = "migration" } +sodiumoxide = "0.2.7" +base64 = "0.22.1" +serde_yaml = "0.9.34" +tracing = { workspace = true } +tracing-subscriber = { workspace = true, features = ["env-filter"] } + +[dev-dependencies] +tower = "0.5.3" diff --git a/backend/oidc-bff/config.yaml b/backend/oidc-bff/config.yaml new file mode 100644 index 000000000..9b860a1c2 --- /dev/null +++ b/backend/oidc-bff/config.yaml @@ -0,0 +1,11 @@ +client_id: "workflows-ui-dev" +client_secret: "" +oidc_provider_url: "https://authn.diamond.ac.uk/realms/master" +port: 5173 +postgres_user: "postgres" +postgres_password: "postgres" +postgres_database: "postgres" +postgres_hostname: "localhost" +postgres_port: 5432 +encryption_public_key: "/8MLLEwz7CkTkUv9y1pq6Gcv2Aomlhpq7shhv95Lil0=" +encryption_private_key: "7f3saJVP6ISBaarRJ5KyNF0IFezCFDEmC556ygO3kQk=" \ No newline at end of file diff --git a/backend/oidc-bff/migration/Cargo.toml b/backend/oidc-bff/migration/Cargo.toml new file mode 100644 index 000000000..7d06f07ec --- /dev/null +++ b/backend/oidc-bff/migration/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "migration" +version = "0.1.0" +edition = "2024" +rust-version = "1.85.0" +publish = false + +[lib] +name = "migration" +path = "src/lib.rs" + +[dependencies] +tokio = { version = "1", features = ["macros", "rt", "rt-multi-thread"] } + +[dependencies.sea-orm-migration] +version = "2.0.0-rc" +features = [ + # Enable at least one `ASYNC_RUNTIME` and `DATABASE_DRIVER` feature if you want to run migration via CLI. + # View the list of supported features at https://www.sea-ql.org/SeaORM/docs/install-and-config/database-and-async-runtime. + # e.g. + "runtime-tokio-rustls", # `ASYNC_RUNTIME` feature + "sqlx-postgres", # `DATABASE_DRIVER` feature +] diff --git a/backend/oidc-bff/migration/README.md b/backend/oidc-bff/migration/README.md new file mode 100644 index 000000000..3b438d89e --- /dev/null +++ b/backend/oidc-bff/migration/README.md @@ -0,0 +1,41 @@ +# Running Migrator CLI + +- Generate a new migration file + ```sh + cargo run -- generate MIGRATION_NAME + ``` +- Apply all pending migrations + ```sh + cargo run + ``` + ```sh + cargo run -- up + ``` +- Apply first 10 pending migrations + ```sh + cargo run -- up -n 10 + ``` +- Rollback last applied migrations + ```sh + cargo run -- down + ``` +- Rollback last 10 applied migrations + ```sh + cargo run -- down -n 10 + ``` +- Drop all tables from the database, then reapply all migrations + ```sh + cargo run -- fresh + ``` +- Rollback all applied migrations, then reapply all migrations + ```sh + cargo run -- refresh + ``` +- Rollback all applied migrations + ```sh + cargo run -- reset + ``` +- Check the status of all migrations + ```sh + cargo run -- status + ``` diff --git a/backend/oidc-bff/migration/src/lib.rs b/backend/oidc-bff/migration/src/lib.rs new file mode 100644 index 000000000..2c605afb9 --- /dev/null +++ b/backend/oidc-bff/migration/src/lib.rs @@ -0,0 +1,12 @@ +pub use sea_orm_migration::prelude::*; + +mod m20220101_000001_create_table; + +pub struct Migrator; + +#[async_trait::async_trait] +impl MigratorTrait for Migrator { + fn migrations() -> Vec> { + vec![Box::new(m20220101_000001_create_table::Migration)] + } +} diff --git a/backend/oidc-bff/migration/src/m20220101_000001_create_table.rs b/backend/oidc-bff/migration/src/m20220101_000001_create_table.rs new file mode 100644 index 000000000..c47c5dbbb --- /dev/null +++ b/backend/oidc-bff/migration/src/m20220101_000001_create_table.rs @@ -0,0 +1,56 @@ +use sea_orm_migration::{prelude::*, schema::*}; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .table(OidcTokens::Table) + .if_not_exists() + .col(ColumnDef::new(OidcTokens::Issuer).text().not_null()) + .col(ColumnDef::new(OidcTokens::Subject).text().not_null()) + .col( + ColumnDef::new(OidcTokens::EncryptedRefreshToken) + .binary() + .not_null(), + ) + .col(ColumnDef::new(OidcTokens::ExpiresAt).timestamp_with_time_zone()) + .col( + ColumnDef::new(OidcTokens::CreatedAt) + .timestamp_with_time_zone() + .not_null() + .default(Expr::current_timestamp()), + ) + .col( + ColumnDef::new(OidcTokens::UpdatedAt) + .timestamp_with_time_zone() + .not_null() + .default(Expr::current_timestamp()), + ) + .primary_key(Index::create().col(OidcTokens::Subject).primary()) + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(OidcTokens::Table).to_owned()) + .await + } +} + +#[derive(Iden)] +enum OidcTokens { + Table, + Issuer, + Subject, + EncryptedRefreshToken, + ExpiresAt, + CreatedAt, + UpdatedAt, +} diff --git a/backend/oidc-bff/migration/src/main.rs b/backend/oidc-bff/migration/src/main.rs new file mode 100644 index 000000000..8723c2c13 --- /dev/null +++ b/backend/oidc-bff/migration/src/main.rs @@ -0,0 +1,7 @@ +use sea_orm_migration::prelude::*; + +#[tokio::main] +async fn main() { + cli::run_cli(migration::Migrator).await; + +} diff --git a/backend/oidc-bff/src/admin_auth.rs b/backend/oidc-bff/src/admin_auth.rs new file mode 100644 index 000000000..4f3ea6de8 --- /dev/null +++ b/backend/oidc-bff/src/admin_auth.rs @@ -0,0 +1,78 @@ +//! Admin authentication middleware for protecting sensitive endpoints. +//! +//! This module provides a simple token-based authentication layer for admin-only +//! routes like the debug endpoint. Authentication is performed via a secret token +//! passed in the `X-Admin-Token` header. +//! +//! # Configuration +//! +//! Set the `WORKFLOWS_ADMIN_TOKEN` environment variable to enable admin endpoints. +//! If not set, all admin requests will be rejected with 401 Unauthorized. +//! +//! # Example +//! +//! ```ignore +//! use axum::{middleware, routing::get, Router}; +//! use crate::admin_auth::require_admin_auth; +//! +//! let router = Router::new() +//! .route("/debug", get(debug_handler)) +//! .layer(middleware::from_fn(require_admin_auth)); +//! ``` +//! +//! Then call with: +//! ```bash +//! curl -H "X-Admin-Token: your-secret" http://localhost:8080/debug +//! ``` + +use axum::{ + extract::Request, + http::StatusCode, + middleware::Next, + response::{IntoResponse, Response}, +}; + +/// The header name for the admin authentication token. +pub const ADMIN_TOKEN_HEADER: &str = "X-Admin-Token"; + +/// Environment variable name for the admin token. +pub const ADMIN_TOKEN_ENV: &str = "WORKFLOWS_ADMIN_TOKEN"; + +/// Middleware function that checks for admin authentication. +/// +/// This middleware reads the expected token from the `WORKFLOWS_ADMIN_TOKEN` +/// environment variable and compares it against the `X-Admin-Token` header +/// in incoming requests. +/// +/// # Returns +/// +/// - `200 OK` with the inner handler's response if authentication succeeds +/// - `401 Unauthorized` if the token is missing, invalid, or not configured +pub async fn require_admin_auth(req: Request, next: Next) -> Response { + // Check if admin token is configured + let Ok(expected_token) = std::env::var(ADMIN_TOKEN_ENV) else { + tracing::warn!("Admin endpoint accessed but no admin token configured"); + return (StatusCode::UNAUTHORIZED, "Admin token not configured").into_response(); + }; + + // Check for the admin token header + let provided_token = req + .headers() + .get(ADMIN_TOKEN_HEADER) + .and_then(|v| v.to_str().ok()); + + match provided_token { + Some(token) if token == expected_token => { + // Token matches, proceed with the request + next.run(req).await + } + Some(_) => { + tracing::warn!("Admin endpoint accessed with invalid token"); + (StatusCode::UNAUTHORIZED, "Invalid admin token").into_response() + } + None => { + tracing::debug!("Admin endpoint accessed without token"); + (StatusCode::UNAUTHORIZED, "Admin token required").into_response() + } + } +} diff --git a/backend/oidc-bff/src/auth_session_data.rs b/backend/oidc-bff/src/auth_session_data.rs new file mode 100644 index 000000000..d7a7f6886 --- /dev/null +++ b/backend/oidc-bff/src/auth_session_data.rs @@ -0,0 +1,37 @@ +use openidconnect::{ + CsrfToken, Nonce, PkceCodeVerifier, +}; +use serde::{Deserialize, Serialize}; + +// Re-export TokenData from auth-common for use as session data +// Note: In BFF context, access_token should always be Some() after successful authentication +pub use auth_common::TokenData as TokenSessionData; + +#[derive(Debug, Serialize, Deserialize)] +pub struct LoginSessionData { + pub csrf_token: CsrfToken, + pub pcke_verifier: PkceCodeVerifier, + pub nonce: Nonce, +} + +impl Clone for LoginSessionData { + fn clone(&self) -> Self { + Self { + csrf_token: self.csrf_token.clone(), + pcke_verifier: PkceCodeVerifier::new(self.pcke_verifier.secret().clone()), + nonce: self.nonce.clone(), + } + } +} + +impl LoginSessionData { + pub const SESSION_KEY: &str = "auth_session_data"; + + pub fn new(csrf_token: CsrfToken, pcke_verifier: PkceCodeVerifier, nonce: Nonce) -> Self { + Self { + csrf_token, + pcke_verifier, + nonce, + } + } +} diff --git a/backend/oidc-bff/src/bin/keygen.rs b/backend/oidc-bff/src/bin/keygen.rs new file mode 100644 index 000000000..a49b24626 --- /dev/null +++ b/backend/oidc-bff/src/bin/keygen.rs @@ -0,0 +1,23 @@ +use base64::{Engine, engine::general_purpose::STANDARD as BASE64}; +use sodiumoxide::crypto::box_::gen_keypair; + +fn main() { + // Initialize sodiumoxide (required before using crypto functions) + if sodiumoxide::init().is_err() { + eprintln!("Failed to initialize libsodium"); + std::process::exit(1); + } + + // Generate a new sealed-box keypair + let (public_key, secret_key) = gen_keypair(); + + // Base64 encode for easy storage in environment variables + let public_b64 = BASE64.encode(public_key.0); + let secret_b64 = BASE64.encode(secret_key.0); + + println!("Public Key:"); + println!("{}", public_b64); + println!(); + println!("Private Key:"); + println!("{}", secret_b64); +} diff --git a/backend/oidc-bff/src/callback.rs b/backend/oidc-bff/src/callback.rs new file mode 100644 index 000000000..e05082926 --- /dev/null +++ b/backend/oidc-bff/src/callback.rs @@ -0,0 +1,116 @@ +use std::sync::Arc; + +use axum::debug_handler; +use axum::extract::{Query, State}; +use openidconnect::{ + AccessTokenHash, AuthorizationCode, CsrfToken, + OAuth2TokenResponse, RedirectUrl, TokenResponse, +}; +use serde::{Deserialize, Serialize}; +use tower_sessions::Session; + +use crate::Result; +use crate::auth_session_data::{LoginSessionData, TokenSessionData}; +use crate::database::write_token_to_database; +use crate::state::AppState; + +#[derive(Serialize, Deserialize)] +pub struct CallbackQuery { + pub code: String, + pub state: String, +} +use anyhow::anyhow; + +#[debug_handler] +pub async fn callback( + State(state): State>, + Query(params): Query, + session: Session, +) -> Result { + // Retrieve data from the users session + let auth_session_data: LoginSessionData = session + .remove(LoginSessionData::SESSION_KEY) + .await + .map_err(|e| anyhow!("Session error: {}", e))? + .ok_or(anyhow!("session expired"))?; + + // Once the user has been redirected to the redirect URL, you'll have access to the + // authorization code. For security reasons, your code should verify that the `state` + // parameter returned by the server matches `csrf_state`. + + if auth_session_data.csrf_token != CsrfToken::new(params.state) { + return Err(anyhow!("invalid state").into()); + } + let redirect_url = std::borrow::Cow::Owned(RedirectUrl::new( + // "http://localhost:5173/auth/callback".to_string(), + "https://staging.workflows.diamond.ac.uk/auth/callback".to_string(), + )?); + // Now you can exchange it for an access token and ID token. + let token_response = state + .oidc_client + .exchange_code(AuthorizationCode::new(params.code.to_string())) + .map_err(|e| anyhow!("Failed to build code exchange request: {}", e))? + // Set the PKCE code verifier. + .set_pkce_verifier(auth_session_data.pcke_verifier) + .set_redirect_uri(redirect_url) + .request_async(&state.http_client) + .await + .map_err(|e| anyhow!("Token exchange request failed: {}", e))?; + + // Extract the ID token claims after verifying its authenticity and nonce. + let id_token = token_response + .id_token() + .ok_or_else(|| anyhow!("Server did not return an ID token"))?; + let id_token_verifier = state.oidc_client.id_token_verifier(); + let claims = id_token.claims(&id_token_verifier, &auth_session_data.nonce) + .map_err(|e| anyhow!("Failed to verify ID token: {}", e))?; + + // Verify the access token hash to ensure that the access token hasn't been substituted for + // another user's. + if let Some(expected_access_token_hash) = claims.access_token_hash() { + let actual_access_token_hash = AccessTokenHash::from_token( + token_response.access_token(), + id_token.signing_alg().map_err(|e| anyhow!("Signing alg error: {}", e))?, + id_token.signing_key(&id_token_verifier).map_err(|e| anyhow!("Signing key error: {}", e))?, + ).map_err(|e| anyhow!("Access token hash error: {}", e))?; + if actual_access_token_hash != *expected_access_token_hash { + return Err(anyhow!("Invalid access token").into()); + } + } + + // The authenticated user's identity is now available. See the IdTokenClaims struct for a + // complete listing of the available claims. + let response = format!( + "User {} with e-mail address {} has authenticated successfully", + claims.subject().as_str(), + claims + .email() + .map(|email| email.as_str()) + .unwrap_or(""), + ); + + // If available, we can use the user info endpoint to request additional information. + + // // The user_info request uses the AccessToken returned in the token response. To parse custom + // // claims, use UserInfoClaims directly (with the desired type parameters) rather than using the + // // CoreUserInfoClaims type alias. + // let userinfo: CoreUserInfoClaims = client + // .user_info(token_response.access_token().to_owned(), None)? + // .request_async(&http_client) + // .await + // .map_err(|err| anyhow!("Failed requesting user info: {}", err))?; + + // See the OAuth2TokenResponse trait for a listing of other available fields such as + // access_token() and refresh_token(). + let token_data = TokenSessionData::from_token_response( + &token_response, + claims.issuer().clone(), + claims.subject().clone(), + )?; + write_token_to_database(&state.database_connection, &token_data, &state.public_key).await?; + session + .insert(TokenSessionData::SESSION_KEY, token_data) + .await + .map_err(|e| anyhow!("Failed to save token to session: {}", e))?; + Ok(response) +} diff --git a/backend/oidc-bff/src/config.rs b/backend/oidc-bff/src/config.rs new file mode 100644 index 000000000..f2c58a390 --- /dev/null +++ b/backend/oidc-bff/src/config.rs @@ -0,0 +1,32 @@ +use std::path::Path; + +use crate::Result; +use serde::Deserialize; +use serde::Serialize; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Config { + pub client_id: String, + pub client_secret: String, + pub oidc_provider_url: String, + pub port: u16, + pub postgres_user: String, + pub postgres_password: String, + pub postgres_database: String, + pub postgres_hostname: String, + pub postgres_port: u16, + pub encryption_public_key: String, + pub encryption_private_key: String, +} + +impl Config { + /// Load config from JSON or YAML file + pub fn from_file>(path: P) -> Result { + let content = std::fs::read_to_string(&path)?; + match path.as_ref().extension().and_then(|e| e.to_str()) { + Some("json") => Ok(serde_json::from_str(&content)?), + // otherwise assume yaml + _ => Ok(serde_yaml::from_str(&content)?), + } + } +} diff --git a/backend/oidc-bff/src/database.rs b/backend/oidc-bff/src/database.rs new file mode 100644 index 000000000..2c4382250 --- /dev/null +++ b/backend/oidc-bff/src/database.rs @@ -0,0 +1,8 @@ +//! Database operations for oidc-bff. +//! Re-exports shared database functions from auth-common and provides BFF-specific migration. + +// Re-export shared database functions +pub use auth_common::database::{ + write_token_to_database, + delete_token_from_database, +}; diff --git a/backend/oidc-bff/src/entity/mod.rs b/backend/oidc-bff/src/entity/mod.rs new file mode 100644 index 000000000..889654a14 --- /dev/null +++ b/backend/oidc-bff/src/entity/mod.rs @@ -0,0 +1,5 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19 + +pub mod prelude; + +pub mod oidc_tokens; diff --git a/backend/oidc-bff/src/entity/oidc_tokens.rs b/backend/oidc-bff/src/entity/oidc_tokens.rs new file mode 100644 index 000000000..23ee74526 --- /dev/null +++ b/backend/oidc-bff/src/entity/oidc_tokens.rs @@ -0,0 +1,22 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19 + +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm(table_name = "oidc_tokens")] +pub struct Model { + #[sea_orm(column_type = "Text")] + pub issuer: String, + #[sea_orm(primary_key, auto_increment = false, column_type = "Text")] + pub subject: String, + #[sea_orm(column_type = "VarBinary(StringLen::None)")] + pub encrypted_refresh_token: Vec, + pub expires_at: Option, + pub created_at: DateTimeWithTimeZone, + pub updated_at: DateTimeWithTimeZone, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/backend/oidc-bff/src/entity/prelude.rs b/backend/oidc-bff/src/entity/prelude.rs new file mode 100644 index 000000000..b303a78d8 --- /dev/null +++ b/backend/oidc-bff/src/entity/prelude.rs @@ -0,0 +1,3 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19 + +pub use super::oidc_tokens::Entity as OidcTokens; diff --git a/backend/oidc-bff/src/error.rs b/backend/oidc-bff/src/error.rs new file mode 100644 index 000000000..0f5fad566 --- /dev/null +++ b/backend/oidc-bff/src/error.rs @@ -0,0 +1,4 @@ +//! Error handling for oidc-bff. +//! Re-exports the shared error type from auth-common. + +pub use auth_common::Error; diff --git a/backend/oidc-bff/src/healthcheck.rs b/backend/oidc-bff/src/healthcheck.rs new file mode 100644 index 000000000..c9be25c8a --- /dev/null +++ b/backend/oidc-bff/src/healthcheck.rs @@ -0,0 +1,5 @@ +use axum::http::StatusCode; + +pub async fn healthcheck() -> StatusCode { + StatusCode::ACCEPTED +} diff --git a/backend/oidc-bff/src/inject_token_from_session.rs b/backend/oidc-bff/src/inject_token_from_session.rs new file mode 100644 index 000000000..736b8167f --- /dev/null +++ b/backend/oidc-bff/src/inject_token_from_session.rs @@ -0,0 +1,70 @@ +use crate::{database::write_token_to_database, state::AppState}; +use auth_common::http_utils::{clone_request, prepare_headers}; +use std::sync::Arc; +use tower_sessions::Session; + +use axum::{ + extract::{Request, State}, + http::StatusCode, + middleware, +}; + +use crate::Result; + +use crate::auth_session_data::TokenSessionData; + +pub async fn inject_token_from_session( + State(state): State>, + session: Session, + req: Request, + next: middleware::Next, +) -> Result { + // Read token from session + let token: Option = session.get(TokenSessionData::SESSION_KEY).await + .map_err(|e| anyhow::anyhow!("Failed to read session: {}", e))?; + if let Some(mut token) = token { + if token.access_token_is_expired() { + token = refresh_token_and_update_session(&state, &token, &session).await?; + } + let mut req = clone_request(req).await?; + prepare_headers(&mut req.0, &token); + let response = next.clone().run(req.0).await; + if response.status() == StatusCode::UNAUTHORIZED { + token = refresh_token_and_update_session(&state, &token, &session).await?; + prepare_headers(&mut req.1, &token); + Ok(next.run(req.1).await) + } else { + Ok(response) + } + } else { + Ok(next.run(req).await) + } +} + +// clone_request and prepare_headers are now provided by auth_common::http_utils + +async fn refresh_token_and_update_session( + state: &AppState, + token: &TokenSessionData, + session: &Session, +) -> Result { + let token = refresh_token(state, token).await?; + write_token_to_database(&state.database_connection, &token, &state.public_key).await?; + session + .insert(TokenSessionData::SESSION_KEY, token.clone()) + .await + .map_err(|e| anyhow::anyhow!("Failed to update session: {}", e))?; + Ok(token) +} + +async fn refresh_token(state: &AppState, token: &TokenSessionData) -> Result { + let token_response = state + .oidc_client + .exchange_refresh_token(&token.refresh_token) + .map_err(|e| anyhow::anyhow!("Failed to build refresh token request: {}", e))? + .request_async(&state.http_client) + .await + .map_err(|e| anyhow::anyhow!("Token refresh request failed: {}", e))?; + let token = token.update_tokens(&token_response); + Ok(token) +} diff --git a/backend/oidc-bff/src/lib.rs b/backend/oidc-bff/src/lib.rs new file mode 100644 index 000000000..28bea8c1e --- /dev/null +++ b/backend/oidc-bff/src/lib.rs @@ -0,0 +1,6 @@ +//! OIDC Backend-for-Frontend (BFF) library. +//! +//! Re-exports auth-common entity for backwards compatibility. + +// Re-export auth-common's entity module for backwards compatibility with existing code +pub use auth_common::entity; \ No newline at end of file diff --git a/backend/oidc-bff/src/login.rs b/backend/oidc-bff/src/login.rs new file mode 100644 index 000000000..00420838e --- /dev/null +++ b/backend/oidc-bff/src/login.rs @@ -0,0 +1,47 @@ +use std::sync::Arc; + +use axum::extract::State; +use axum::response::Redirect; +use openidconnect::core::CoreAuthenticationFlow; +use openidconnect::{ CsrfToken, Nonce, PkceCodeChallenge, RedirectUrl, Scope}; +use tower_sessions::Session; + +use crate::Result; +use crate::auth_session_data::LoginSessionData; +use crate::state::AppState; + +#[axum::debug_handler] +pub async fn login(State(state): State>, session: Session) -> Result { + // Set the URL the user will be redirected to after the authorization process. + // .set_redirect_uri(RedirectUrl::new("https://localhost/callback".to_string())?); + let oidc_client = state.oidc_client.clone().set_redirect_uri(RedirectUrl::new( + // "http://localhost:5173/auth/callback".to_string(), + "https://staging.workflows.diamond.ac.uk/auth/callback".to_string(), + )?); + // .set_redirect_uri(RedirectUrl::new("https://workflows.diamond.ac.uk".to_string())?) + // Generate a PKCE challenge. + let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256(); + + // Generate the full authorization URL. + let (auth_url, csrf_token, nonce) = oidc_client + .authorize_url( + CoreAuthenticationFlow::AuthorizationCode, + CsrfToken::new_random, + Nonce::new_random, + ) + // Set the desired scopes. + .add_scope(Scope::new("openid".to_string())) + .add_scope(Scope::new("offline_access".to_string())) + // Set the PKCE code challenge. + .set_pkce_challenge(pkce_challenge) + .url(); + + // Store data in the users session + let auth_session_data = LoginSessionData::new(csrf_token, pkce_verifier, nonce); + session + .insert(LoginSessionData::SESSION_KEY, auth_session_data) + .await + .map_err(|e| anyhow::anyhow!("Failed to save session: {}", e))?; + + Ok(Redirect::temporary(auth_url.as_str())) +} diff --git a/backend/oidc-bff/src/main.rs b/backend/oidc-bff/src/main.rs new file mode 100644 index 000000000..ef3eef859 --- /dev/null +++ b/backend/oidc-bff/src/main.rs @@ -0,0 +1,138 @@ +mod healthcheck; +mod config; +mod login; +mod auth_session_data; +mod state; +mod callback; +mod database; +mod error; +mod admin_auth; + +use clap::Parser; +use config::Config; +use tokio::signal::unix::{Signal, SignalKind, signal}; +use tower_sessions::{MemoryStore, Session, SessionManagerLayer}; +use std::{ + net::{Ipv4Addr, SocketAddr}, sync::Arc, process +}; +use state::AppState; + +type Result = std::result::Result; + +use axum::{ + Router, + extract::State, + middleware, + response::IntoResponse, + routing::{get, post}, +}; +use axum_reverse_proxy::ReverseProxy; + +use crate::auth_session_data::{TokenSessionData}; +mod inject_token_from_session; + +#[derive(Parser, Debug)] +#[command(author, version, about)] +struct Args { + /// Path to config file (JSON or YAML) + //TODO: Change this from env variable to hardcoded + #[arg( + short, + long, + env = "WORKFLOWS_OIDC_BFF_CONFIG", + default_value = "config.yaml" + )] + config: String, +} + +#[tokio::main] +async fn main() -> Result<()> { + dotenvy::dotenv().ok(); + let args: Args = Args::try_parse() + .map_err(|e| anyhow::anyhow!("CLI argument error: {}", e))?; + let config = Config::from_file(args.config)?; + let port = config.port; + let appstate = Arc::new(AppState::new(config).await?); + + // Migration has been removed and its use can be added later if needed + + rustls::crypto::ring::default_provider() + .install_default() + .expect("Failed to install rust TLS cryptography"); + + let router = create_router(appstate); + serve(router, port).await +} + +fn create_router(state: Arc) -> Router { + let session_store = MemoryStore::default(); + let session_layer = SessionManagerLayer::new(session_store) + .with_secure(false) + // .with_expiry(Expiry::OnInactivity(Duration::seconds(600))) + ; + + let proxy: Router<()> = + ReverseProxy::new("/", "https://staging.workflows.diamond.ac.uk/graphql").into(); + + let router = Router::new() + .fallback_service(proxy) + .layer(middleware::from_fn_with_state( + state.clone(), + inject_token_from_session::inject_token_from_session, + )) + .route("/auth/login", get(login::login)) + .route("/auth/callback", get(callback::callback)) + .route("/auth/logout", post(logout)) + .route("/healthcheck", get(healthcheck::healthcheck)) + .layer(session_layer); + + + router.with_state(state) +} + +async fn serve(router: Router, port: u16) -> Result<()> { + let listener = + tokio::net::TcpListener::bind(SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), port)).await?; + let service = router.into_make_service(); + axum::serve(listener, service) + .with_graceful_shutdown(shutdown_signal()) + .await?; + Ok(()) +} + +async fn shutdown_signal() { + let mut sigterm: Signal = signal(SignalKind::terminate()).expect("Failed to listen for SIGTERM"); + sigterm.recv().await; + println!("Shutting Down"); + process::exit(0); +} + +/// Logout handler that: +/// 1. Retrieves the user's token from the session (to get subject ID) +/// 2. Deletes the token from the database (so workflows can't use it) +/// 3. Clears the session (so browser requests are no longer authenticated) +async fn logout( + State(state): State>, + session: Session, +) -> Result { + // Get the token data to find the subject for database deletion + let token_session_data: Option = + session.get(TokenSessionData::SESSION_KEY).await + .map_err(|e| anyhow::anyhow!("Failed to read session: {}", e))?; + + // If we have token data, delete it from the database + if let Some(token_data) = token_session_data { + database::delete_token_from_database( + &state.database_connection, + &token_data.subject, + ) + .await?; + } + + // Clear the entire session (removes both login and token data) + session.flush().await + .map_err(|e| anyhow::anyhow!("Failed to flush session: {}", e))?; + + Ok(axum::http::StatusCode::OK) +} + diff --git a/backend/oidc-bff/src/state.rs b/backend/oidc-bff/src/state.rs new file mode 100644 index 000000000..1155f9a4d --- /dev/null +++ b/backend/oidc-bff/src/state.rs @@ -0,0 +1,74 @@ +use crate::Result; +use crate::config::Config; +use anyhow::anyhow; +use base64::{Engine, engine::general_purpose::STANDARD as BASE64}; +use oauth2::{ClientId, ClientSecret, EndpointMaybeSet, EndpointNotSet, EndpointSet, reqwest}; +use openidconnect::IssuerUrl; +use openidconnect::core::{CoreClient, CoreProviderMetadata}; +use sea_orm::Database; +use sea_orm::DatabaseConnection; +use sodiumoxide::crypto::box_::PublicKey; +#[derive(Debug, Clone)] +pub struct AppState { + pub config: Config, + pub http_client: reqwest::Client, + pub oidc_client: openidconnect::core::CoreClient< + EndpointSet, + EndpointNotSet, + EndpointNotSet, + EndpointNotSet, + EndpointMaybeSet, + EndpointMaybeSet, + >, + pub database_connection: DatabaseConnection, + pub public_key: PublicKey, +} + +impl AppState { + pub async fn new(config: Config) -> Result { + let http_client = reqwest::ClientBuilder::new() + // Following redirects opens the client up to SSRF vulnerabilities. + .redirect(reqwest::redirect::Policy::none()) + .build() + .map_err(|e| anyhow!("Failed to build HTTP client: {}", e))?; + + // Use OpenID Connect Discovery to fetch the provider metadata. + let provider_metadata = CoreProviderMetadata::discover_async( + IssuerUrl::new(config.oidc_provider_url.to_string())?, + &http_client, + ) + .await + .map_err(|e| anyhow!("OIDC discovery failed: {}", e))?; + + let oidc_client = CoreClient::from_provider_metadata( + provider_metadata, + ClientId::new(config.client_id.to_string()), + if config.client_secret.is_empty() { + None + } else { + Some(ClientSecret::new(config.client_secret.to_string())) + }, + ); + + let database_url = format!( + "postgres://{}:{}@{}:{}/{}", + config.postgres_user, + config.postgres_password, + config.postgres_hostname, + config.postgres_port, + config.postgres_database + ); + let database_connection = Database::connect(&database_url).await?; + + let public_key = PublicKey::from_slice(&BASE64.decode(&config.encryption_public_key)?) + .ok_or(anyhow!("Invalid public key"))?; + + Ok(AppState { + config, + http_client, + oidc_client, + database_connection, + public_key, + }) + } +} diff --git a/charts/apps/staging-values.yaml b/charts/apps/staging-values.yaml index 146268c16..e81fb61da 100644 --- a/charts/apps/staging-values.yaml +++ b/charts/apps/staging-values.yaml @@ -57,7 +57,7 @@ sealedsecrets: workflows: enabled: true - targetRevision: HEAD + targetRevision: drh/bff-pkce-dev extraValueFiles: - staging-values.yaml @@ -69,7 +69,7 @@ graphProxy: dashboard: enabled: true - targetRevision: HEAD + targetRevision: drh/bff-pkce-dev extraValueFiles: - staging-values.yaml diff --git a/charts/apps/templates/dashboard-application.yaml b/charts/apps/templates/dashboard-application.yaml index 330003ac9..fbb55e805 100644 --- a/charts/apps/templates/dashboard-application.yaml +++ b/charts/apps/templates/dashboard-application.yaml @@ -8,7 +8,7 @@ metadata: argocd.argoproj.io/sync-wave: "2" spec: destination: - namespace: dashboard + namespace: workflows server: {{ .Values.destination.server }} project: default source: diff --git a/charts/apps/values.yaml b/charts/apps/values.yaml index fc72db35e..5875ff34e 100644 --- a/charts/apps/values.yaml +++ b/charts/apps/values.yaml @@ -122,7 +122,7 @@ sealedsecrets: workflows: enabled: true - targetRevision: HEAD + targetRevision: drh/bff-pkce-dev extraValuesFiles: [] valuesObject: {} @@ -134,7 +134,7 @@ graphProxy: dashboard: enabled: true - targetRevision: HEAD + targetRevision: drh/bff-pkce-dev extraValuesFiles: [] valuesObject: {} diff --git a/charts/dashboard/staging-values.yaml b/charts/dashboard/staging-values.yaml index b2584c29e..e9a728754 100644 --- a/charts/dashboard/staging-values.yaml +++ b/charts/dashboard/staging-values.yaml @@ -12,6 +12,19 @@ ingress: paths: - path: / pathType: Prefix + service: + name: dashboard + port: 80 + - path: /api + pathType: Prefix + service: + name: oidc-bff + port: 80 + - path: /auth + pathType: Prefix + service: + name: oidc-bff + port: 80 tls: true secretName: dashboard-tls-cert diff --git a/charts/dashboard/templates/ingress.yaml b/charts/dashboard/templates/ingress.yaml index e4d34abb5..3ebbb7419 100644 --- a/charts/dashboard/templates/ingress.yaml +++ b/charts/dashboard/templates/ingress.yaml @@ -32,9 +32,9 @@ spec: {{- end }} backend: service: - name: {{ include "common.names.fullname" $ }} + name: {{ .service.name }} port: - number: {{ $.Values.service.port }} + number: {{ .service.port }} {{- end }} {{- end }} {{- end }} diff --git a/charts/dashboard/templates/oidc-bff.yaml b/charts/dashboard/templates/oidc-bff.yaml new file mode 100644 index 000000000..eeffd75c4 --- /dev/null +++ b/charts/dashboard/templates/oidc-bff.yaml @@ -0,0 +1,70 @@ +apiVersion: v1 +kind: Service +metadata: + name: oidc-bff + namespace: {{ .Release.Namespace }} +spec: + type: ClusterIP + selector: + app: oidc-bff + ports: + - name: oidc-bff + port: 80 + targetPort: 80 + protocol: TCP +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: oidc-bff +spec: + replicas: 1 + selector: + matchLabels: + app: oidc-bff + template: + metadata: + labels: + app: oidc-bff + spec: + imagePullSecrets: {{ include "common.images.renderPullSecrets" (dict "images" (list $.Values.oidcBff.image) "context" $ ) }} + containers: + - name: oidc-bff + image: {{ include "common.images.image" ( dict "imageRoot" $.Values.oidcBff.image "global" $.Values.global "chart" $.Chart ) }} + imagePullPolicy: {{ $.Values.oidcBff.image.pullPolicy }} + ports: + - containerPort: 80 + livenessProbe: + httpGet: + path: /healthcheck + port: 80 + initialDelaySeconds: 3 + periodSeconds: 3 + readinessProbe: + httpGet: + path: /healthcheck + port: 80 + initialDelaySeconds: 5 + periodSeconds: 5 + startupProbe: + httpGet: + path: /healthcheck + port: 80 + failureThreshold: 3 + periodSeconds: 5 + env: + - name: WORKFLOWS_OIDC_BFF_CONFIG + value: "/etc/oidc-bff/config.yaml" + - name: RUST_BACKTRACE + value: "1" + volumeMounts: + - name: oidc-bff-config + mountPath: /etc/oidc-bff + readOnly: true + volumes: + - name: oidc-bff-config + secret: + secretName: {{ .Values.oidcBff.configuration.secretName | default "oidc-bff-config" }} + items: + - key: config.yaml + path: config.yaml diff --git a/charts/dashboard/templates/service.yaml b/charts/dashboard/templates/service.yaml index cbc43766e..1a6eeabf2 100644 --- a/charts/dashboard/templates/service.yaml +++ b/charts/dashboard/templates/service.yaml @@ -20,3 +20,4 @@ spec: targetPort: dashboard protocol: TCP {{- end }} + diff --git a/charts/dashboard/values.yaml b/charts/dashboard/values.yaml index 9b9529def..ed83d6af6 100644 --- a/charts/dashboard/values.yaml +++ b/charts/dashboard/values.yaml @@ -43,6 +43,19 @@ ingress: paths: - path: / pathType: Prefix + service: + name: dashboard + port: 80 + - path: /api + pathType: Prefix + service: + name: oidc-bff + port: 80 + - path: /auth + pathType: Prefix + service: + name: oidc-bff + port: 80 tls: false annotations: nginx.ingress.kubernetes.io/force-ssl-redirect: "true" @@ -54,3 +67,14 @@ serviceAccount: create: true name: "" annotations: [] + +oidcBff: + image: + registry: ghcr.io + repository: diamondlightsource/workflows-oidc-bff + tag: "davehadley" + digest: "sha256:e259494171d5115d060dd9f3d9da971312d39e9afabd91478c0b7a49c7c5f6d3" + pullPolicy: IfNotPresent + pullSecrets: [] + configuration: + secretName: oidc-bff-config \ No newline at end of file diff --git a/charts/workflows-cluster/staging-values.yaml b/charts/workflows-cluster/staging-values.yaml index 0970e87f1..dc252576d 100644 --- a/charts/workflows-cluster/staging-values.yaml +++ b/charts/workflows-cluster/staging-values.yaml @@ -51,7 +51,7 @@ vcluster: - staging-values.yaml path: charts/apps repoURL: https://github.com/DiamondLightSource/workflows.git - targetRevision: HEAD + targetRevision: drh/bff-pkce-dev syncPolicy: automated: prune: true @@ -63,13 +63,15 @@ vcluster: byName: "/letsencrypt-argo-cd-staging-workflows-diamond-ac-uk": "argocd/argo-cd-tls-cert" "/letsencrypt-argo-workflows-staging-workflows-diamond-ac-uk": "workflows/workflows-tls-cert" - "/letsencrypt-staging-workflows-diamond-ac-uk": "dashboard/dashboard-tls-cert" + "/letsencrypt-staging-workflows-diamond-ac-uk": "workflows/dashboard-tls-cert" "/argo-server-sso": "workflows/argo-server-sso" "/sessionspaces-ispyb": "kube-system/sessionspaces-ispyb" "/artifact-s3-secret": "graph-proxy/artifact-s3-secret" "/s3-artifact": "workflows/artifact-s3" + "/oidc-bff-config": "workflows/oidc-bff-config" "/postgres-passwords": "workflows/postgres-passwords" "/postgres-argo-workflows-password": "workflows/postgres-argo-workflows-password" + "/postgres-auth-service-password": "workflows/postgres-auth-service-password" "/postgres-application-passwords": "workflows/postgres-application-passwords" "/postgres-initdb-script": "workflows/postgres-initdb-script" @@ -82,6 +84,7 @@ authenticationConfiguration: - issuer: url: https://authn.diamond.ac.uk/realms/master audiences: + - workflows-dashboard-staging - workflows-cluster-staging - graph audienceMatchPolicy: MatchAny diff --git a/charts/workflows-cluster/templates/oidc-bff.yaml b/charts/workflows-cluster/templates/oidc-bff.yaml new file mode 100644 index 000000000..8afd7be74 --- /dev/null +++ b/charts/workflows-cluster/templates/oidc-bff.yaml @@ -0,0 +1,13 @@ +--- +apiVersion: bitnami.com/v1alpha1 +kind: SealedSecret +metadata: + name: oidc-bff-config + namespace: workflows +spec: + encryptedData: + config.yaml: AgBmJplbabGvBLH9zqvt3YANVjBPUaARwJu/H0pBx/k5FpCSASfP0vKx7/XL4JCg01+awygBW8a8t7bBChs6yXTcatXGWKeVE1eX2go9XPmwGM9OKR8cLoFlufQx++0N6HhMAOuonCsbM33X9RbmfYm5Azxontmk9+W6nQqT+7UkcpPheC2f0tFKyJVvIVbrUOsJmls1TgmQUFZIj5dWtOrgChhC6rSbogMWF2WtFpT4RRlCKZUXMggFPTw82fjytCVxDXaxnjwD2Wzj33dY9gxy10iGp/MUQefZ2AM3iMqcgu2u0ENe5dCO8bKVcaemytRuKRiK6DUbKq2pa5gATUG9pDUhbW3g9Aa7sQyjvOSun+G4S+1+n9+EMTaYcv79eVkAnl07XHe6p5T3Bsl42iZdL8gMahtFwYH31yybWFI57Q66zyIFj7oTzr8zvyYwyYj/VpAA6gMKxAF2eBXwNOhKd7LnUjI7iitBiDxIBfNQImkfcfDaAQSm84JPZW1YJl6jolvJTQAckjLMrrzDKR4X953Qv9BsPZBjS16MVg3hkZR9Db3XdP2lSQngsT/WMDSJ44X0fxNHU6Hamv2hanvYw1kCJCeESYfA+aWtAcsZtjX2axRfiuWHTxVZ5nUcTsCuxbIpla/iS7jG5i81BZu79ilEKj2rJjm6of1y5dWehZm93DQjQK++jgU8Bil9UARvrLG2hsrodpl0FTSYUMAyF2Lct3FYp9yr5juMKWNqXeK4z/2E/fX3DbQUK0vNCpUXvhwTPq0KOtlnTBbQHJanHjROI5IMe2Mtmoypzne/K/pWJSGz7b6+n5jBGZJjaOeH77+f/mAFu5M3K/KL2yxEjU55BuHnzXhHgDQxErwM61NJQx5OhJI0zLcVln593N0c8J73Ei7I5yaEYo/AVorADwv0tjWztUgCplE/4GHAgbV9Fats2dmZw90FnBpZALDOJZInujrxGze+//rex6s8tzk2xEUXIZMZvPOKiIUhp65+m1PX2aQwNOjO/v3GJUHdnvjpCk8PXAAq9l8rlyOTE1QimBwaLzIA8CI4vh/iD+dUh+TpWbs4wopJyA9jBUMCRhZLNvXuHG4bXygZc5b2OJ8Az+qMU1LpTcrLa1g7/qQHOhMv4EyoqbluMLFY8RtaeT59GtBmwvvVCkCoLDQazCxvkHRr0f/vTiKE7sG/CQ+rlDxL4DqGJsgcQYJYWYbBaykr1nOBsoxeIUBKFzJY7o8PP9LwBlf7PmKpQFhcT+xyIdOZJFSVDtiqVr+jWwTJm+iE+3WI/M/QYwmhuLcRf8M= + template: + metadata: + name: oidc-bff-config + namespace: workflows diff --git a/charts/workflows-cluster/templates/sealed-secret-postgres-application-passwords.yaml b/charts/workflows-cluster/templates/sealed-secret-postgres-application-passwords.yaml new file mode 100644 index 000000000..a7d121aaf --- /dev/null +++ b/charts/workflows-cluster/templates/sealed-secret-postgres-application-passwords.yaml @@ -0,0 +1,19 @@ +--- +apiVersion: bitnami.com/v1alpha1 +kind: SealedSecret +metadata: + creationTimestamp: null + name: postgres-application-passwords + namespace: workflows +spec: + encryptedData: + passwords: AgAaHsA0p6ZAyHb0lUl2rtGkv6OG7f6b6d95MPjsI5sogEAzz0YL8nuSx8x7HHbtqT03dd7MmHV9gawNo0CGhGQjGFwLrtFhI67EcufAE6/5q7hX6DQAxm5/ZTZPvFffOuLMCS/PAJzZAYcBKW0Adhk8qCmcQ+Ps4G4GKcCY6kD4/BuoDvaGodvie2it16ejPHEeZ8cMzfYN+/NMRQ+6q2dmwqb/Gf5kc6HJMbynaSkQotD2OFbnkzvYUdAcjpxLMsw4Ia5kP2Hz11mgwV1LqrGW70nDHFmojN4q/+PR+VrNd7FYN1ZgwoIbIoKPbF/E1chSmwGGU8CqwJxXATS+jYz1m7X5RR2PgL0fFdMHXB5jiXoGdZqTQKyt6okXxvF1fMthmZw5VrZBiTeE9pQnDCZEHq4rAXKuoWDjsrruMGATI/4JPive+snWDgpTEzHa40k+HXIj1/lHqiQfAQaRyLS5a89j+g20berJ8R/q6oYZhVEG4noKnntRcfKeT+DaeOeRoQ5M9Jn3AUfqke3R3sJJvYFTo0mU1F6fXq9YrOJRz3cQzPEGDqladBH8D9KHcl5dKs2PzZaAbpUlvT8YVadVWTtMm46etqBpAOE7rPYRQzhrmRHdRXOrN862p5qyhZLXgIvJASWheVaBbwIGK2PKpFSk1juzkZ9sAOFTkLD7BNJYn3rq6RFPl7CYjQ8ecNOQGGYPRIDDfAmmqHujiarAJXXqvyB8tjeSXqAd50ThkZGf7dO/zRUGAyI8a1KO3wYU + usernames: AgBTq5CHMVyTx6l2Ihqz4EE64Dm7G2wXSAQEnZr+4BA4nwU1bz+1xVNCen8AN7t8iPA5gO+8SyPIXKj9ZhqtjO+ltY2vjvIY2QXEQXOjDozfo+Q9TxgQ3jyKN3mr4hv2OxgVx+Obz8mD0rNPDO0cqI3SwUZQleDV/87dL09leUCIWFnNxHbhaLhuDNdPKTdw7ORSplHmTa35j1x9u5xOiwfOzUYshOYRcccqNzvKsp6Ptj2QWZkqiALn0FIkWC4uZ9KN0eQxv9w/ZLr/ixY5TI7tbBW5oiNYR6iQ0IK2xAOCl4tbTk/S8qkQBVbwxbvKrsNnR2FQR9hs8iimr1HDY1gFt2nt+3qY0Cle88Bngy0Yt/o7J7pZTV1rgr3SzpogcmIu/LCA1wln4OnMlhv/U+oMsoxVL/FssJupjFbX+5UiVzMpYYZ7CTLqiGnn29A+0VCp1bNaTTTYx1R5DGZc9JQj5+XkGCitCicf0GrSRs7nEjk/O/1aILE6CISpvqvXbMncjJysgVr8CdUoKwsdqhleRQ01llmhVj20LYhoGtMPe23DejGIrItlNlphp9dT4Sl0qvLPf3pmOaladabwCF0NXub9q4zpN5sM6p88xISHUIyD0Qh7T7WJ1wk0RBDC+onfZmWK8iZsP4XT2ZpbruwMJWriF9GlH+NSZhzwbym+QOBLV7x8jFebTF8P5lEUVf6oaGIa5dCQHAy0N3H0UzXE1U+XSBvo6eg= + template: + metadata: + creationTimestamp: null + labels: + argocd.argoproj.io/instance: workflows + name: postgres-application-passwords + namespace: workflows + type: Opaque diff --git a/charts/workflows-cluster/templates/sealed-secret-postgres-argo-workflows-password.yaml b/charts/workflows-cluster/templates/sealed-secret-postgres-argo-workflows-password.yaml new file mode 100644 index 000000000..7d76405fb --- /dev/null +++ b/charts/workflows-cluster/templates/sealed-secret-postgres-argo-workflows-password.yaml @@ -0,0 +1,19 @@ +--- +apiVersion: bitnami.com/v1alpha1 +kind: SealedSecret +metadata: + creationTimestamp: null + name: postgres-argo-workflows-password + namespace: workflows +spec: + encryptedData: + password: AgABMdb+f+AyfmSgwm9NHvG13yK7Js3krlaH2nWoQAnupq1zZW7fFsmCnna1FW67RDzY0ZPOCc0T7yyWsEzVWuupBQWliRh9FaROQZ21wt6GWeQZD65tf0lqu9pLORwhMbaW8oSTTx/GAY2ojyz1YX0npsYfEdfh32YAEgpfrqZN8GAjdhqQUwGhsrhMIcxS4vilO8pk98Q6G+9hajduPNl766qW+IMyRnK1iZQdSU3i7r4neiLCaMh+cLpdbYfIYwG+LlChGWmHfYIucIH0pMb7bkCWzIdtQRobbcwK+atcD+nAEEl6IicyhEU+b/qHrGjCNvUQ5jq7JFYy6Ri0lKzFSpwVH4MkdzZWUpttsqo3EnKayQB8ZXZH5hTrpspifhhiI0D3s90relTAhn7UNz5fwbRtdTBpevhQzp5IZVuAMuvDdzcISh3LoBe7Vr/UUVHGOMLVyqzGY9z80VO95tJV+iDO2TNpEE3gNl4Gaxm9Dr3zDBmDyo8uKUuLYE38lVzB4EJmgo/vuE8OIdKdZGK905PIGQ6O5AidZbL3nEUvIHXoGrYiE0pZjyF4uVJpu65Dw5A3GDH6W6l5iK/sCTcL9LQdPX7o/AOfUWyNJbD4O6FAau3d1m5gie5kVVT8zzMhVH+BUxwT1JvRTxL3J6nazKlo0Xcoq4UdDai+I/dFg3di7aHo9obyWvcLAYPRMvhY3357PaxOsDAYkvaEbwPoFGTPIPLz8/o= + username: AgCfSEt91Uw44NHBeRVfIZOuu7zVlZFu80R9VifqNnxN2A/ggpIaHVqozXsStTeb/Qkl/axG2feP6hC2ilHVti20XHzDD98tTeo5p5j9yfStudLL/rIYZnNsO12uVUjKDPbOTrxgxtjJkRdDlO3Zd1Aqrsk11xZieeu9RaGBqY1qRVUkZb2bIM9kege/3wL8eTO9jF2ogMEybEtCBoOridBdmLMCPncYHt36NcPXi/8cpJOgj2icEFAQJT6MKlkdigiREMDXLByscJO0EseonIyiLdK/1WgRx/nUUMoqyt7CTqTej41cUZCM9ufjLC8VuM8OCtAHVKu4OOf76ya3wGp2OBf7ajBUQe0NbZ3+ckyrSgodQv92jemKCPkW2GeN2+t+M3/kTn9xQ0lc+TPpRR7JnlbHNYvCXLLtrtGG3AzkeyZJZ4FKBaIL5V5dS6ywSLvMsPlmjci1jfCswsHOE9uCVt456v968cvGFOpOXneHTALc46r/vjvJ58Nrtkayik3glhRsS7kPH26AJ2HhOOu6FwIp//+Emd2W3fw7MEWizu9tNCPJMUm9HrO7I59db74Cb7hZoBpzhNyzi7mfwQJCGCDU/ahlbYSWiknc2EDE0mCNZTIpyTPVYv7gc66yyroZSq4KHnaQl6pKLOCl3pHLa9xP6nuZfGqN5gaf+YCu6Qi05Ktq0IEHqIKNs/AXZE3iXVHzamhyUwFBqwnpDA== + template: + metadata: + creationTimestamp: null + labels: + argocd.argoproj.io/instance: workflows + name: postgres-argo-workflows-password + namespace: workflows + type: Opaque diff --git a/charts/workflows-cluster/templates/sealed-secret-postgres-auth-service-password.yaml b/charts/workflows-cluster/templates/sealed-secret-postgres-auth-service-password.yaml new file mode 100644 index 000000000..d5fb5f767 --- /dev/null +++ b/charts/workflows-cluster/templates/sealed-secret-postgres-auth-service-password.yaml @@ -0,0 +1,19 @@ +--- +apiVersion: bitnami.com/v1alpha1 +kind: SealedSecret +metadata: + creationTimestamp: null + name: postgres-auth-service-password + namespace: workflows +spec: + encryptedData: + password: AgDHejDxPArV97Etg07L0oUlMDTIFlluz3cqYfMfJe/MBoPWLN8wxWq/Vpmy/0+LAW5aj9sIPnsXCE0Pfq4tXdr7sLdhNHOA0Efy2m5WLcoyTzbTU4/f/beXIGsabDFVvgEvIrEYIu2TOJj1mTLFmESWbYG4Z822oXdKEdmAE4Zvf8wmSkGLfOoFXYueNBaQ7N2BNIeA1+8pywuHMmoyeTOyxk6J743vRiPqEK940H+BT5bhuZz370FJVeXnKq+SBgsckqVZ42Rp7VW2qW1+j+tAku6AHcRhCxV0KLPjf08oQrzcY60blBvW4AUiwLrSghK88DfswBENQKtYlYO9nt5mV7ZaBD1dnSetCM867fRiSyTXXN0PJAXZjgAqkdQ5W8cBA5wImD0cF+wsZShZyVEO2qYGL5THrUATjHkSF2zGfhLSGHIlJlmsC527Yo2J0SovqjY4g3qTI8VxdugOax7yj/W8y2iwUxjokTvtwplIjM0nXNxwF7KqkbIOgxiW3IdtMyI7PQxTgO69jnch4odnzCgFToE7lOs/uVo1lNH+iU7a0o/o/5ksL8MqjX56zU1BYuGOtq+eGL9B3t7jnN91SkJzO9glRzuo15wRARKMdJ2Et2tqkfw6qQlCVnpvj8r4xApUnsMbvyrR6odP2jg5Sr2mgYnoN2krb6jjJwiBFeC3FZcJVctBUa60ZPpRiftXvUCpjhU4qMaABrk66k2xZZTD346wcMg= + username: AgBRyZIlJrZnf0VtwQE1iqJbwxwcE9d638Ii21F75xCj1eVGne0t1oCysZpi9ZLOjhPYTWZADBW8VwYM6OchzMRcTAO3M4tsALsb2tsf8id9zMl7He1NIeprLeKr3hNQQ4Ktcf+CNrStr/f/b5Ei3Po8aA1Aq3VOiuBd+C+0P/nHJWcEGl0hbw3GEytjILHl7JwjMg40B6j2Diq3Z3o7fry17iJ98V0ta1kyQBpAZdlmPr+mSXpllpl915dhepE86//k9c+2ZIN7+WmLln4MYmznakbcZpiN5QAhuxnJO69G8wYcwSsJdI0B4fOy3iNMc/zNCWLbd3pgmz0h5hvumU1XZ42ftujTvhDb6eagBWPMWr7gguNSjWw6zwgtCPSVt+lzRBEu9MUBAY1TA3Zk5MvMXAYPTlix0vN6lPZweB2u1C/f/GqAV7BzTDMzxj58Fa/r/B1PEkOvgZygZ16TojXOQlkcMT7DfPf9Yf4YWL59i1RBzUvw2lgDB1glZ4GqMcxYHTfTR6j4feQJzmvVh2FgP0kthsxAXhO0a/sZ4EHfxVeN51PKA3BPuFdX56HLAkYvj2Osmuy+fVC8UGsDW9ik5fndzhaqxap/9qzLYUtU2AtEI3tPKvJhpM0QQvFockUgY6J31y1IivOo+gGGS1DCnRVnNDPHXETE2DBidSwBSThTiuV8+7LMrCyoAvaFEBFhENcrbtX5Eb8= + template: + metadata: + creationTimestamp: null + labels: + argocd.argoproj.io/instance: workflows + name: postgres-auth-service-password + namespace: workflows + type: Opaque diff --git a/charts/workflows-cluster/templates/sealed-secret-postgres-initdb-script.yaml b/charts/workflows-cluster/templates/sealed-secret-postgres-initdb-script.yaml new file mode 100644 index 000000000..cc0fa322d --- /dev/null +++ b/charts/workflows-cluster/templates/sealed-secret-postgres-initdb-script.yaml @@ -0,0 +1,15 @@ +--- +apiVersion: bitnami.com/v1alpha1 +kind: SealedSecret +metadata: + creationTimestamp: null + name: postgres-initdb-script + namespace: workflows +spec: + encryptedData: + init.sql: AgB4hcgbt4yJhii5v3cJ5WZBuGOcRBS4PaZeT8kjHSc9gNAfxyCPM0EtwBaVZMfE2dinj3nk+2GDsoVsoYaKl8XXz7Ey0xfdPfypLLGmTaAUGFWFRL9ygrJV8T1wZB6rI33erTgO/NQCu35umLYq8xyFkIZolrZEOFP6hHShBMbz51yIjRdwvTVkYItfiEOKmHmHs4psyWvsK+KL58zhB/7PZZ7Twi4+rdBmB+wV0NMy6lpq41mQNz12tLVQUlvM0KKyUApow1FVXuBbeZkKgTSKEgHYOtrmrWwoqAl2qK1hf4B0jRocZAv1WCOcevbTb+IFqRdAUiOHDHc5TnCuSUNiffmTwyRXlvLgvoJLElFUaamsEESgzAS7+qkwzSeyrZ3F+qmeXXCIfnKUL0kzCo9kMgXGqOjJ6gibo4wFSL4QUctv6GT4rAGIqyUyLvrCoU0QhNpPfhZkXQf5FUz/n0wHD1qbisPLEyqsCGcZR45RClrhncza1RLWb/xWhHAsVUTeolrSZXpZOnEYOsjuIF2Gyf8029HMMQMhgguThhBho93MwhA7BUnbSbuMrQlQ+R7TryxlplOsWZfPGBEs1M/YYCVXs4+PRHPvttYps9KHcpvN+WlObE+IIxtVbozVaVEBz26j9xZezC7hFXYTifRXEpgRExfRtNtRIpel/xYpkAoQsBbJ+Xsi23Vrp/uyjU2ogXJoziGHaWs8/YKnvD5bA4AXnNAm3zLokCeXJ1ei3O8iFZ5R2vxXUSvDFC4oOxLMj+nB89xR7/0z7L5BxMe8v7gtOZ1q1qd543LgNvuysSOb8CT24bc3gc9Ueqk8nFZTg+R2L6avDUJuzPXRS0zl8cCfxZ/GEEkzPsVYTY6wTJm3GJ6wnf3Mwyzk+dTBViPhjzj96XMMAqEzWVHKpfQkxjmeUfoRynK7/quQvsWl2tLGZofDHGKAAB6ZdVirGHTqCqqidgVz8QNIz9ECybDJQpilwzWxHUHTCu0YoBidXwjR25BOI0Q= + template: + metadata: + creationTimestamp: null + name: postgres-initdb-script + namespace: workflows diff --git a/charts/workflows-cluster/templates/sealed-secret-postgres-passwords.yaml b/charts/workflows-cluster/templates/sealed-secret-postgres-passwords.yaml new file mode 100644 index 000000000..9d51b149f --- /dev/null +++ b/charts/workflows-cluster/templates/sealed-secret-postgres-passwords.yaml @@ -0,0 +1,19 @@ +--- +apiVersion: bitnami.com/v1alpha1 +kind: SealedSecret +metadata: + creationTimestamp: null + name: postgres-passwords + namespace: workflows +spec: + encryptedData: + password: AgBD1UtI2bqrlgkSPQVTOE0L7baaX/42Bq0w9209GiA7FztZkaOxGsOroAW0cCKifbbK9E4BlTPyFu6X1onw8OSowpmZ8bih5amuroYECugw/g669kfh//G7nKi44o26XoI9CajDJUVb+LkXHZFEJ8Xb9xnclxdR4U4klCriIow1cUnCkif2ELa1MMZ+mVvAIdsArIehymNfCpCRZzNu35u38Yf0EYAOCeh5W0LT5cyKp1pql8AGAGbpSSzojJxWpx8N0lA8toeKjOmVegKK2jS7q+4IfF8In2UU8E/DtcMTUbvorYwLPVKtDA7AlHTH8qU1+tosjTHQa5s8U0XvAGKuboiy7vPVDpypMo+tP5LKab9cdFkJumT+PFoIyJkacJnzs2TEzGAAi+PAgr/6yUBgsFejtV0yjFXNbh0vPhVdDQaqt+ylm451bTQo1FxgS6sOFYHnUo4Cj7+0vnMu8f994GZilaqdkY20aRWn/6pg66K1d/koDXB0hDJncx5xsZ1Zgx8Bo1B7rm0v7cyDmFaFEv4BSdcU9XMor98tRMOMQgdHaKBkyz4T/eViwmfoGI/9VA1PaqvO5cEZgkQ6Rn5LqvmkvNanKtrgn+BTsvlximUskQq0NklDl84EUEvieT4OmzNIAWiVTVImZ3/erVQFau7sILc8U+s5H6srGJO45wtFx4r+eKMGC/ZK4Ie128vVWn8e5JFDZ8UAJChyeV4aCzQw1Uw2soQ= + repmgr-password: AgDGDte8on0ZOp1Ts++H8D0ZtVDS6LrTZj6XQ1pdER1WoACEjuzg/VhEmqMBP52Rla1lU6agGbZph65tdS6cbf2c2pUSvTnh2agVncdADMjSZonG1dh5wW0fQj9k0WxB+40gv2iWlW3jsaOhYPSCaI38vSL4yjnTr0IVxTIRDBF3Lst+P4jSTgQxUaHzDy7OtRTsGMJDky/BCDFR4fh4MAJxFCoDLnPPG8FBsVVoPz4UeeOMNdq4Yj4/BH677G1PCbsJGGbbSOPn4zr1Fh0q9pJ4ge+vE82bhoqBkb++v2jYtjyCgicS0Tj+KaA6y87/6RTveOpDyooetomwa3JwVawzPWY2wMFB6NrPGeie97wMiYGTFZzcnVZycqfAp5LuSrWdqx+lvJEg2UOyXXHiT3v4qTJxvsDbONuVkHB/ogYi2o66ZBaAUObzFi5G4LHZg5qzxkTroO3T7OTZTSnqAPoyApHHo/t0uSPAWbv+wyqBaZQJxV4cQ7Eyf3wni7UKIXLK139D7Ah2/BrxCM0l4pL8gmFnCjkGKgJiDgG3C+rQXZc+lbjdw4XaLjdTh/T5fg5q1ve0ZErFIa+ugyoLmU17hFlIIbDKaw3nIQJP/abOkx08lLlAZLNuDhGj/HQR7gzEgiLXYWHpBWgUFYRrlAcF2pnq774QL7mnuHbDTZERu5eiC0TPI1FqpYWqujeIB/RIZw9caAzdB9fPJMvbeYQCIdY4uTbQlrs= + template: + metadata: + creationTimestamp: null + labels: + argocd.argoproj.io/instance: workflows + name: postgres-passwords + namespace: workflows + type: Opaque diff --git a/charts/workflows/staging-values.yaml b/charts/workflows/staging-values.yaml index 23db28579..236103dd5 100644 --- a/charts/workflows/staging-values.yaml +++ b/charts/workflows/staging-values.yaml @@ -19,6 +19,10 @@ oauth2-proxy: - aud emailClaim: email userIDClaim: fedid + extraAudiences: + - workflows-dashboard-staging + - workflows-cluster-staging + - graph ingress: hosts: - argo-workflows.staging.workflows.diamond.ac.uk diff --git a/charts/workflows/templates/_helpers.tpl b/charts/workflows/templates/_helpers.tpl new file mode 100644 index 000000000..55fb510b0 --- /dev/null +++ b/charts/workflows/templates/_helpers.tpl @@ -0,0 +1,28 @@ +{{/* +Create a new password for the argo_workflows user in postgres +*/}} +{{- define "workflows.argoWorkflowsPostgresPassword" }} +{{- $existing := (lookup "v1" "Secret" .Release.Namespace "postgres-argo-workflows-password") }} + {{- if $existing }} + {{- index $existing.data "password" | b64dec }} + {{- else }} + {{- if not (index .Release "argoWorkflowsPostgresPassword" ) -}} + {{- $_ := set .Release "argoWorkflowsPostgresPassword" (randAlphaNum 24) }} + {{- end }} + {{- index .Release "argoWorkflowsPostgresPassword" }} + {{- end }} +{{- end }} +{{/* +Create a new password for the auth_user in postgres +*/}} +{{- define "workflows.authServicePostgresPassword" }} +{{- $existing := (lookup "v1" "Secret" .Release.Namespace "postgres-auth-service-password") }} + {{- if $existing }} + {{- index $existing.data "password" | b64dec }} + {{- else }} + {{- if not (index .Release "authServicePostgresPassword" ) -}} + {{- $_ := set .Release "authServicePostgresPassword" (randAlphaNum 24) }} + {{- end }} + {{- index .Release "authServicePostgresPassword" }} + {{- end }} +{{- end }} \ No newline at end of file diff --git a/charts/workflows/values.yaml b/charts/workflows/values.yaml index b355cd44c..ff09cb5a0 100644 --- a/charts/workflows/values.yaml +++ b/charts/workflows/values.yaml @@ -175,6 +175,10 @@ oauth2-proxy: - aud emailClaim: email userIDClaim: fedid + extraAudiences: + - workflows-dashboard-staging + - workflows-cluster-staging + - graph extraArgs: - --cookie-refresh=55s extraVolumes: diff --git a/docs/explanations/bff-architecture-analysis.md b/docs/explanations/bff-architecture-analysis.md new file mode 100644 index 000000000..387a27bbf --- /dev/null +++ b/docs/explanations/bff-architecture-analysis.md @@ -0,0 +1,1365 @@ +# Backend-For-Frontend (BFF) Architecture Analysis + +## Table of Contents + +1. [Executive Summary](#executive-summary) +2. [Architecture Overview](#architecture-overview) +3. [Component Deep Dive](#component-deep-dive) + - [auth-common](#auth-common) + - [oidc-bff](#oidc-bff) + - [auth-daemon](#auth-daemon) +4. [BFF Compliance Assessment](#bff-compliance-assessment) +5. [Implementation Progress](#implementation-progress) +6. [Remaining Improvements](#remaining-improvements) +7. [Implementation Recommendations](#implementation-recommendations) +8. [References (Verified with Source Quotes)](#references-verified-with-source-quotes) +9. [Recommended Reading](#recommended-reading) +10. [Actionable Tasks / Tickets](#actionable-tasks--tickets) +11. [Summary](#summary) + +--- + +## Executive Summary + +This document analyzes the current authentication backend implementation against the **Backend-For-Frontend (BFF)** architectural pattern as defined by IETF best practices. The system consists of three core components: + +| Component | Purpose | +|-----------|---------| +| `auth-common` | Shared library for token management, database operations, and HTTP utilities | +| `oidc-bff` | Browser-facing BFF that handles OAuth login flows and request proxying | +| `auth-daemon` | Workflow sidecar that injects tokens for server-to-server communication | + +The implementation **successfully follows many BFF principles** and has undergone significant refactoring to address technical debt. Several critical issues have been resolved, but some areas still require attention for full security compliance. + +--- + +## Architecture Overview + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ BROWSER │ +│ ┌──────────────────────────────────────────────────────────────────────┐ │ +│ │ Frontend Application (JavaScript/React) │ │ +│ │ - Never sees OAuth tokens │ │ +│ │ - Receives only session cookie │ │ +│ └────────────────────────────┬─────────────────────────────────────────┘ │ +└───────────────────────────────┼─────────────────────────────────────────────┘ + │ HTTP (Session Cookie) + ▼ +┌─────────────────────────────────────────────────────────────────────────────┐ +│ oidc-bff │ +│ ┌────────────────────────────────────────────────────────────────────────┐ │ +│ │ Confidential OAuth Client │ │ +│ │ - Client ID + Secret │ │ +│ │ - OIDC Discovery │ │ +│ │ - Authorization Code + PKCE │ │ +│ ├────────────────────────────────────────────────────────────────────────┤ │ +│ │ Session Management │ │ +│ │ - tower_sessions (MemoryStore) │ │ +│ │ - CSRF protection (state parameter) │ │ +│ │ - Nonce validation │ │ +│ ├────────────────────────────────────────────────────────────────────────┤ │ +│ │ Token Management │ │ +│ │ - Access tokens stored in session (server-side) │ │ +│ │ - Refresh tokens encrypted → PostgreSQL │ │ +│ │ - Automatic token refresh │ │ +│ ├────────────────────────────────────────────────────────────────────────┤ │ +│ │ Request Proxy │ │ +│ │ - Strips cookies │ │ +│ │ - Injects Bearer token │ │ +│ │ - Forwards to resource server │ │ +│ └────────────────────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────────────┘ + │ + │ Bearer Token + ▼ +┌─────────────────────────────────────────────────────────────────────────────┐ +│ Resource Server (GraphQL) │ +│ staging.workflows.diamond.ac.uk/graphql │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +### Workflow Authentication Flow + +``` +┌────────────────────────────────────────────────────────────────────────────┐ +│ WORKFLOW POD │ +│ ┌────────────────────────────────────────────────────────────────────-┐ │ +│ │ Workflow Container │ │ +│ │ - Makes HTTP requests to localhost │ │ +│ │ - No token awareness │ │ +│ └─────────────────────────────┬──────────────────────────────────────-┘ │ +│ │ HTTP (no auth) │ +│ ▼ │ +│ ┌─────────────────────────────────────────────────────────────────-───┐ │ +│ │ auth-daemon (Sidecar) │ │ +│ │ - Reads refresh token from PostgreSQL (encrypted) │ │ +│ │ - Obtains access token via token refresh │ │ +│ │ - Injects Bearer token into outgoing requests │ │ +│ │ - Proxies to resource server │ │ +│ └─────────────────────────────┬─────────────────────────────────-─────┘ │ +└────────────────────────────────┼──────────────────────────────────────-────┘ + │ Bearer Token + ▼ +┌────────────────────────────────────────────────────────────────────────────┐ +│ Resource Server (GraphQL) │ +└────────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Component Deep Dive + +### auth-common + +**Purpose**: Shared library providing common authentication functionality to both `oidc-bff` and `auth-daemon`. + +#### File Structure + +``` +auth-common/src/ +├── lib.rs # Module exports +├── config.rs # Configuration loading utilities +├── database.rs # Token CRUD operations with encryption +├── error.rs # Shared error type (anyhow wrapper) +├── http_utils.rs # Request cloning & header preparation +├── token.rs # TokenData struct definition +└── entity/ + ├── mod.rs + └── oidc_tokens.rs # SeaORM entity for token storage +``` + +#### Key Data Structures + +##### TokenData (`token.rs`) + +```rust +pub struct TokenData { + pub issuer: IssuerUrl, // OAuth provider URL + pub subject: SubjectIdentifier, // User's unique ID + pub access_token: Option, // Short-lived (optional) + pub access_token_expires_at: DateTime, + pub refresh_token: RefreshToken, // Long-lived +} +``` + +**Why `access_token` is `Option`?** +- In `oidc-bff`: Always `Some()` after successful authentication +- In `auth-daemon`: Initially `None` when loaded from database (only refresh token is stored); populated after first token refresh + +##### Database Schema (`oidc_tokens.rs`) + +```rust +pub struct Model { + pub issuer: String, // OAuth provider + pub subject: String, // PRIMARY KEY (user ID) + pub encrypted_refresh_token: Vec, // Sealed box encrypted + pub expires_at: Option, + pub created_at: DateTimeWithTimeZone, + pub updated_at: DateTimeWithTimeZone, +} +``` + +**Key Design Decision**: Subject is the primary key, meaning one token per user per system. Conflict resolution uses `ON CONFLICT ... UPDATE`. + +#### Encryption Strategy + +```rust +// Write: Encrypt refresh token using libsodium sealed box +let encrypted = sodiumoxide::crypto::sealedbox::seal( + token.refresh_token.secret().as_bytes(), + public_key +); + +// Read: Decrypt using both public and secret keys +let decrypted = sodiumoxide::crypto::sealedbox::open( + &ciphertext, + public_key, + secret_key +); +``` + +**Why Sealed Boxes?** +- Encrypts data such that only the holder of the private key can decrypt +- No authentication between writer and reader (by design) +- `oidc-bff` only needs public key (encrypt-only) +- `auth-daemon` needs both keys (decrypt to use tokens) + +#### HTTP Utilities (`http_utils.rs`) + +```rust +// Clone request for retry logic (token refresh mid-request) +pub async fn clone_request(req: Request) -> Result<(Request, Request)> + +// Prepare headers for proxying +pub fn prepare_headers(req: &mut Request, token: &TokenData) { + // Add: Authorization: Bearer + // Remove: Cookie header (backend uses Bearer, not cookies) +} +``` + +--- + +### oidc-bff + +**Purpose**: Browser-facing Backend-For-Frontend service handling OAuth login and request proxying. + +#### File Structure + +``` +oidc-bff/src/ +├── main.rs # Server entry point & router setup +├── config.rs # BFF-specific configuration +├── state.rs # Shared application state (OIDC client, DB) +├── login.rs # OAuth login initiation +├── callback.rs # OAuth callback handler +├── auth_session_data.rs # Session data structures +├── inject_token_from_session.rs # Proxy middleware +├── auth_proxy.rs # Alternative proxy-only mode +├── admin_auth.rs # Debug endpoint protection +├── database.rs # Re-exports from auth-common +├── error.rs # Re-exports from auth-common +├── healthcheck.rs # Kubernetes probes +├── counter.rs # Debug/test session handling +└── lib.rs # Re-exports for library use +``` + +#### OAuth Flow Implementation + +##### 1. Login Initiation (`login.rs`) + +```rust +pub async fn login(State(state), session: Session) -> Result { + // Generate PKCE challenge (proof of possession) + let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256(); + + // Generate authorization URL with: + // - CSRF token (state parameter) + // - Nonce (ID token replay protection) + // - PKCE challenge + // - Scopes: openid, offline_access + let (auth_url, csrf_token, nonce) = oidc_client + .authorize_url(CoreAuthenticationFlow::AuthorizationCode, ...) + .add_scope(Scope::new("openid")) + .add_scope(Scope::new("offline_access")) // Enables refresh tokens + .set_pkce_challenge(pkce_challenge) + .url(); + + // Store verification data in session (NOT sent to browser) + session.insert(LoginSessionData::SESSION_KEY, LoginSessionData { + csrf_token, + pcke_verifier, // PKCE verifier stays server-side + nonce, + }); + + // Redirect to authorization server + Redirect::temporary(auth_url) +} +``` + +##### 2. Callback Handler (`callback.rs`) + +```rust +pub async fn callback(State(state), Query(params), session: Session) -> Result { + // 1. Retrieve and remove login session data + let auth_data: LoginSessionData = session.remove(SESSION_KEY).await?; + + // 2. Verify CSRF token (state parameter) + if auth_data.csrf_token != CsrfToken::new(params.state) { + return Err("invalid state"); + } + + // 3. Exchange authorization code for tokens + let token_response = oidc_client + .exchange_code(AuthorizationCode::new(params.code)) + .set_pkce_verifier(auth_data.pcke_verifier) // Prove we initiated + .request_async(&http_client) + .await?; + + // 4. Verify ID token + let id_token = token_response.id_token()?; + let claims = id_token.claims(&id_token_verifier, &auth_data.nonce)?; + + // 5. Verify access token hash (prevents token substitution) + if let Some(expected_hash) = claims.access_token_hash() { + let actual_hash = AccessTokenHash::from_token( + token_response.access_token(), + id_token.signing_alg()?, + id_token.signing_key(&id_token_verifier)?, + )?; + if actual_hash != *expected_hash { + return Err("Invalid access token"); + } + } + + // 6. Store tokens + let token_data = TokenSessionData::from_token_response(&token_response, ...); + write_token_to_database(&db, &token_data, &public_key).await?; // Encrypted + session.insert(TokenSessionData::SESSION_KEY, token_data).await?; // For proxying + + Ok("Login successful") +} +``` + +##### 3. Request Proxy (`inject_token_from_session.rs`) + +```rust +pub async fn inject_token_from_session( + State(state), + session: Session, + req: Request, + next: Next, +) -> Result { + // 1. Load token from session + let token: Option = session.get(TOKEN_KEY).await?; + + if let Some(mut token) = token { + // 2. Refresh if expired + if token.access_token_is_expired() { + token = refresh_token_and_update_session(&state, &token, &session).await?; + } + + // 3. Clone request (for retry) + let mut req = clone_request(req).await?; + + // 4. Inject Bearer token, strip cookies + prepare_headers(&mut req.0, &token); + + // 5. Forward request + let response = next.run(req.0).await; + + // 6. Retry on 401 (token may have been revoked server-side) + if response.status() == StatusCode::UNAUTHORIZED { + token = refresh_token_and_update_session(&state, &token, &session).await?; + prepare_headers(&mut req.1, &token); + return Ok(next.run(req.1).await); + } + + Ok(response) + } else { + // No session - forward without auth + Ok(next.run(req).await) + } +} +``` + +#### Session Management + +```rust +// main.rs - Router setup +let session_store = MemoryStore::default(); // In-memory sessions +let session_layer = SessionManagerLayer::new(session_store) + .with_secure(false); // ⚠️ ISSUE: Should be true in production! +``` + +**Session Data Stored**: +1. `LoginSessionData` - Temporary (during OAuth flow): + - CSRF token + - PKCE verifier + - Nonce + +2. `TokenSessionData` - Persistent (after login): + - Issuer URL + - Subject ID + - Access token (in memory) + - Access token expiry + - Refresh token + +--- + +### auth-daemon + +**Purpose**: Sidecar service for Kubernetes workflow pods that authenticates API requests on behalf of a specific user. + +#### File Structure + +``` +auth-daemon/src/ +├── main.rs # CLI & server entry point +├── config.rs # Daemon-specific configuration +├── state.rs # RouterState with token cache +├── inject_token.rs # Token injection middleware +├── database.rs # Re-exports from auth-common +├── error.rs # Re-exports from auth-common +└── healthcheck.rs # Kubernetes probes +``` + +#### Key Differences from oidc-bff + +| Aspect | oidc-bff | auth-daemon | +|--------|----------|-------------| +| Token Source | OAuth callback | Database lookup | +| Session | Browser cookies | None (stateless) | +| User Context | From session | CLI argument (`--subject`) | +| Encryption Keys | Public only | Public + Private | +| Purpose | Human users | Automated workflows | + +#### Initialization Flow + +```rust +// main.rs +#[tokio::main] +async fn main() { + // 1. Parse subject from environment/CLI + let subject = args.subject; // WORKFLOWS_AUTH_DAEMON_SUBJECT + + // 2. Load config with encryption keys + let config = Config::from_file(args.config)?; + + // 3. Initialize state (loads token from DB) + let state = RouterState::new(config, &SubjectIdentifier::new(subject)).await?; + + // 4. Start proxy server + let router = setup_router(state)?; + serve(router, port).await?; +} +``` + +```rust +// state.rs - RouterState::new() +pub async fn new(config: Config, subject: &SubjectIdentifier) -> Result { + // 1. Connect to database + let db = Database::connect(&database_url).await?; + + // 2. Setup OIDC client for token refresh + let oidc_client = CoreClient::from_provider_metadata(...); + + // 3. Load encryption keys + let public_key = PublicKey::from_slice(&decode(&config.encryption_public_key)?)?; + let private_key = SecretKey::from_slice(&decode(&config.encryption_private_key)?)?; + + // 4. Read and decrypt token from database + let token = read_token_from_database(&db, subject, None, &public_key, &private_key).await?; + // Note: token.access_token is None at this point + + Ok(Self { + token: RwLock::new(Some(token)), + oidc_client, + ... + }) +} +``` + +#### Token Injection (`inject_token.rs`) + +```rust +pub async fn inject_token( + State(state), + req: Request, + next: Next, +) -> Result { + let token = state.token.read().await.clone(); + + if let Some(mut token) = token { + // Refresh on startup or expiry + if token.access_token_is_expired() { + token = refresh_token_and_write_to_database(&state, &token).await?; + } + + let mut req = clone_request(req).await?; + prepare_headers(&mut req.0, &token); + + let response = next.run(req.0).await; + let response_json = response_as_json(response).await?; + + // Retry on GraphQL errors (different from HTTP 401) + if !is_good_response(&response_json) { + token = refresh_token_and_write_to_database(&state, &token).await?; + prepare_headers(&mut req.1, &token); + return Ok(next.run(req.1).await); + } + + Ok(Json(response_json).into_response()) + } else { + Ok(next.run(req).await) + } +} +``` + +--- + +## BFF Compliance Assessment + +### Fully Implemented + +| Principle | Implementation | +|-----------|----------------| +| **Confidential Client** | Client ID + Secret stored server-side in `AppState` | +| **Token Isolation** | Access/refresh tokens never sent to browser | +| **Authorization Code + PKCE** | Both implemented in `login.rs` | +| **CSRF Protection** | State parameter verified in `callback.rs` | +| **Nonce Validation** | ID token nonce checked in `callback.rs` | +| **Access Token Hash Verification** | Prevents token substitution attacks | +| **Cookie-Based Sessions** | `tower_sessions` handles session cookies | +| **Request Proxying** | `axum_reverse_proxy` with token injection | +| **Cookie Stripping** | `prepare_headers()` removes Cookie header | +| **Encrypted Token Storage** | Sealed boxes for database refresh tokens | + +### Partially Implemented + +| Principle | Current State | Issue | +|-----------|---------------|-------| +| **Session Security** | `with_secure(false)` | Cookies transmitted over HTTP | +| **Session Store** | `MemoryStore` | Lost on restart, not scalable | +| **Outbound Validation** | Hardcoded single URL | No allowlist validation | + +### Missing + +| Principle | Impact | Recommendation | +|-----------|--------|----------------| +| **CORS Configuration** | No explicit CORS on BFF | Add strict CORS layer | +| **Custom Header CSRF** | Relies only on state parameter | Add `X-Requested-With` check | +| **Session Expiry** | Commented out | Implement idle timeout | +| **Check Session Endpoint** | Missing | Add `/auth/session` endpoint | +| **Proper Logout Redirect** | Returns 200 OK | Redirect to application | +| **Refresh Token Expiry** | Hardcoded 30 days | Use `refresh_expires_in` | + +--- + +## Implementation Progress + +This section tracks the improvements identified in the original `suggestions.md` technical debt document and their current status. + +### Completed Items + +#### 1. Shared Crate Structure (P4 → Done) + +The `auth-common` crate has been created and consolidates shared code: + +``` +backend/auth-common/src/ +├── lib.rs # Module exports +├── config.rs # Configuration loading utilities +├── database.rs # Token CRUD with encryption +├── error.rs # Shared error type +├── http_utils.rs # Request cloning & header prep +├── token.rs # TokenData struct +└── entity/ + └── oidc_tokens.rs +``` + +**What was extracted:** + +| Code | From | To | +|-----------------------------|------------------|-----------------------------| +| `Error` type | both services | `auth_common::error` | +| `TokenData` struct | both services | `auth_common::token` | +| `write_token_to_database()` | both services | `auth_common::database` | +| `read_token_from_database()`| auth-daemon | `auth_common::database` | +| `delete_token_from_database()`| oidc-bff | `auth_common::database` | +| `clone_request()` | both services | `auth_common::http_utils` | +| `prepare_headers()` | both services | `auth_common::http_utils` | +| SeaORM entity | both services | `auth_common::entity` | + +--- + +#### 2. Logout Function Fixed (P0 → Done) + +**Before** (empty function): +```rust +async fn logout() {} +``` + +**After** (proper implementation in `oidc-bff/src/main.rs`): +```rust +async fn logout( + State(state): State>, + session: Session, +) -> Result { + // Get token data to find subject for database deletion + let token_session_data: Option = + session.get(TokenSessionData::SESSION_KEY).await?; + + // Delete from database so workflows can't use the token + if let Some(token_data) = token_session_data { + database::delete_token_from_database( + &state.database_connection, + &token_data.subject, + ).await?; + } + + // Clear the session + session.flush().await?; + Ok(axum::http::StatusCode::OK) +} +``` + +--- + +#### 3. Error Information Leakage Fixed (P0 → Done) + +**Before** (leaked internal details): +```rust +impl IntoResponse for Error { + fn into_response(self) -> Response { + (StatusCode::INTERNAL_SERVER_ERROR, + format!("Something went wrong: {}", self.0)) // Exposed! + .into_response() + } +} +``` + +**After** (`auth-common/src/error.rs`): +```rust +impl IntoResponse for Error { + fn into_response(self) -> Response { + // Log for debugging (server-side only) + tracing::error!(error = %self.0, "Request failed"); + + // Generic message to client + (StatusCode::INTERNAL_SERVER_ERROR, + "An internal error occurred") + .into_response() + } +} +``` + +--- + +#### 4. Graceful Shutdown in auth-daemon (P1 → Partially Done) + +Graceful shutdown is implemented via `with_graceful_shutdown()`: +```rust +axum::serve(listener, router.into_make_service()) + .with_graceful_shutdown(shutdown_signal()) + .await?; +``` + +**Note**: The `process::exit(0)` in `shutdown_signal()` still bypasses cleanup: +```rust +async fn shutdown_signal() { + sigterm.recv().await; + println!("Shutting down"); + process::exit(0); // Still problematic! +} +``` + +--- + +#### 5. Unwraps Fixed in Shared Code (P1 → Done) + +**Before** (in duplicated code): +```rust +HeaderValue::from_str(&value).unwrap() // Could panic +``` + +**After** (`auth-common/src/http_utils.rs`): +```rust +if let Ok(header_value) = HeaderValue::from_str(&value) { + req.headers_mut().insert(http::header::AUTHORIZATION, header_value); +} else { + tracing::warn!("Failed to create Authorization header value"); +} +``` + +**Note**: `oidc-bff/src/inject_token.rs` still has an unwrap at line 87 (this file appears to be a duplicate/older version not using `auth_common::http_utils`). + +--- + +#### 6. Debug Endpoint Protection (P2 → Done) + +Debug routes now protected with admin auth middleware: +```rust +#[cfg(debug_assertions)] +{ + router = router.route( + "/debug", + get(debug).layer(middleware::from_fn(admin_auth::require_admin_auth)), + ); +} +``` + +Admin auth requires `X-Admin-Token` header matching `WORKFLOWS_ADMIN_TOKEN` env var. + +--- + +### Partially Completed + +#### 7. Duplicate inject_token Files + +There are **two** inject_token files in oidc-bff: +- `inject_token_from_session.rs` - Uses `auth_common::http_utils` ✅ +- `inject_token.rs` - Has duplicated code, unwraps ❌ + +The `main.rs` uses `inject_token_from_session`, but `inject_token.rs` should be removed or consolidated. + +--- + +### Not Yet Implemented + +#### 8. Hardcoded URLs (P0 - Still Open) + +**Locations still hardcoded:** +- `oidc-bff/src/main.rs:72` - Proxy URL +- `oidc-bff/src/login.rs:21` - Callback URL +- `oidc-bff/src/callback.rs:46` - Callback URL + +**Recommendation**: Add to `Config`: +```rust +pub struct Config { + pub graph_url: String, // For proxy + pub callback_url: String, // For OAuth redirects + pub base_url: String, // Application base URL + // ... existing fields +} +``` + +--- + +#### 9. `process::exit(0)` in Shutdown (P0 - Still Open) + +**Location:** `auth-daemon/src/main.rs:136-137` + +```rust +async fn shutdown_signal() { + sigterm.recv().await; + println!("Shutting down"); + process::exit(0); // Bypasses graceful shutdown +} +``` + +**Fix**: Remove `process::exit(0)` and let the function return naturally: +```rust +async fn shutdown_signal() { + sigterm.recv().await; + tracing::info!("Received SIGTERM, initiating graceful shutdown"); + // Let axum handle the rest +} +``` + +--- + +#### 10. No Logging in oidc-bff (P1 - Still Open) + +`oidc-bff/src/main.rs` has no `tracing_subscriber` initialization. + +**Fix**: Add to `main()`: +```rust +tracing_subscriber::fmt() + .with_env_filter(EnvFilter::from_env("LOG_LEVEL")) + .init(); +``` + +--- + +#### 11. `println!` Instead of Logging (P1 - Still Open) + +**Remaining occurrences** (auth-daemon/src/inject_token.rs): +```rust +println!("Injecting token"); +println!("Access token is expired, refreshing"); +println!("DEBUG response json: {:?}", response); +println!("Query failed, refreshing token and trying again"); +println!("No token to inject"); +``` + +Also in `auth-daemon/src/main.rs:136`: +```rust +println!("Shutting down"); +``` + +**Fix**: Replace with tracing macros: +```rust +tracing::debug!("Injecting token"); +tracing::debug!(response = ?response, "Response received"); +tracing::info!("Shutting down gracefully"); +``` + +--- + +#### 12. Missing Graceful Shutdown in oidc-bff (P1 - Still Open) + +**Location:** `oidc-bff/src/main.rs:97-102` + +```rust +async fn serve(router: Router, port: u16) -> Result<()> { + let listener = tokio::net::TcpListener::bind(...).await?; + axum::serve(listener, service).await?; // No graceful shutdown + Ok(()) +} +``` + +**Fix**: Add shutdown signal handler like auth-daemon has. + +--- + +#### 13. MemoryStore Documentation (P1 - Still Open) + +The `MemoryStore` limitation is not documented in code. + +**Fix**: Add warning comment: +```rust +// WARNING: MemoryStore is for development only! +// Sessions are lost on restart and not shared across replicas. +// For production, use tower-sessions-redis-store or tower-sessions-sqlx-store. +let session_store = MemoryStore::default(); +``` + +--- + +#### 14. TLS Provider Location (P2 - Still Open) + +TLS setup is in `setup_router()` in auth-daemon, which could be called multiple times in tests. + +**Recommendation**: Move to `main()` before router setup. + +--- + +#### 15. Inconsistent Health Endpoints (P2 - Still Open) + +- `oidc-bff`: `/healthcheck` +- `auth-daemon`: `/healthz` + +**Recommendation**: Standardize on `/healthz` (Kubernetes convention). + +--- + +#### 16. Refresh Token Expiry TODO (P2 - Still Open) + +**Location:** `auth-common/src/database.rs` + +```rust +// TODO: offline_access tokens will expire if not used within 30 days. +// Keycloak returns the actual expiration date in "refresh_expires_in" +let refresh_token_expires_at = Utc::now() + Duration::days(30); +``` + +**Fix**: Parse `refresh_expires_in` from token response. + +--- + +#### 17. Debug Routes Always Exposed (P3 - Still Open) + +The `/read` and `/write` counter routes are always exposed, not just in debug builds: +```rust +.route("/read", get(counter::counter_read)) +.route("/write", get(counter::counter_write)) +``` + +**Fix**: Move behind `#[cfg(debug_assertions)]` or remove entirely. + +--- + +## Remaining Improvements + +### Session Security (CRITICAL) + +**Current Code** (`main.rs`): +```rust +let session_layer = SessionManagerLayer::new(session_store) + .with_secure(false); // INSECURE +``` + +**Problem**: Session cookies sent over HTTP, vulnerable to MITM attacks. + +**Fix**: +```rust +let session_layer = SessionManagerLayer::new(session_store) + .with_secure(true) // HTTPS only + .with_same_site(SameSite::Strict) // CSRF protection + .with_http_only(true) // No JS access + .with_expiry(Expiry::OnInactivity(Duration::hours(1))); +``` + +--- + +### Session Store Scalability + +**Current**: `MemoryStore::default()` + +**Problems**: +- Sessions lost on pod restart +- Cannot scale horizontally +- Memory exhaustion risk + +**Options**: + +| Store | Pros | Cons | +|-------|------|------| +| Redis | Fast, shared state | Extra infrastructure | +| PostgreSQL | Reuses existing DB | Slower | +| Signed cookies | Stateless, scalable | Limited size | + +--- + +### Missing Session Check Endpoint + +**BFF Best Practice**: +``` +Browser → /auth/session → { authenticated: true/false, user: {...} } + → If false → /auth/login +``` + +**Add endpoint**: +```rust +#[derive(Serialize)] +pub struct SessionStatus { + pub authenticated: bool, + pub subject: Option, + pub email: Option, +} + +pub async fn check_session(session: Session) -> Json { + let token: Option = session.get(TOKEN_KEY).await.ok().flatten(); + Json(SessionStatus { + authenticated: token.is_some(), + subject: token.as_ref().map(|t| t.subject.to_string()), + email: None, + }) +} +``` + +--- + +### CSRF Protection Enhancement + +**Current**: Only OAuth state parameter validation + +**Add custom header check** for non-OAuth routes: +```rust +pub async fn require_same_origin(req: Request, next: Next) -> Response { + if req.method() != Method::GET { + if !req.headers().contains_key("X-Requested-With") { + return StatusCode::FORBIDDEN.into_response(); + } + } + next.run(req).await +} +``` + +--- + +## Implementation Recommendations + +### Priority 1 (Security Critical) + +| Item | Status | Action | +|------|--------|--------| +| Fix `process::exit(0)` | ❌ | Remove from shutdown handler | +| Move hardcoded URLs to config | ❌ | Add `graph_url`, `callback_url` to Config | +| Enable secure session cookies | ❌ | Set `with_secure(true)` in production | +| Implement session expiry | ❌ | Add idle/absolute timeouts | + +### Priority 2 (Production Readiness) + +| Item | Status | Action | +|------|--------|--------| +| Add tracing to oidc-bff | ❌ | Initialize `tracing_subscriber` | +| Replace `println!` with tracing | ❌ | Update auth-daemon logging | +| Add graceful shutdown to oidc-bff | ❌ | Add `with_graceful_shutdown()` | +| Document MemoryStore limitation | ❌ | Add warning comment | +| Add session check endpoint | ❌ | Create `/auth/session` route | + +### Priority 3 (Code Quality) + +| Item | Status | Action | +|------|--------|--------| +| Remove duplicate inject_token.rs | ❌ | Delete `oidc-bff/src/inject_token.rs` | +| Standardize health endpoints | ❌ | Change to `/healthz` | +| Move TLS setup to main() | ❌ | Refactor auth-daemon | +| Fix refresh token expiry | ❌ | Parse `refresh_expires_in` | +| Remove debug counter routes | ❌ | Gate behind feature flag | + +### Priority 4 (Already Done) + +| Item | Status | +|------|--------| +| Create shared crate | ✅ `auth-common` | +| Fix empty logout function | ✅ With DB cleanup | +| Fix error information leakage | ✅ Generic messages | +| Fix unwraps in shared code | ✅ `http_utils.rs` | +| Protect debug endpoint | ✅ Admin auth middleware | + +--- + +## Appendix: Configuration Example + +```yaml +# config.yaml (production) +client_id: "workflows-frontend" +client_secret: "${OIDC_CLIENT_SECRET}" +oidc_provider_url: "https://auth.diamond.ac.uk/realms/master" +port: 8080 + +# NEW: URLs that should be configurable +graph_url: "https://workflows.diamond.ac.uk/graphql" +callback_url: "https://workflows.diamond.ac.uk/auth/callback" +base_url: "https://workflows.diamond.ac.uk" + +# Session (when implemented) +session_secure: true +session_idle_timeout_seconds: 3600 +session_max_lifetime_seconds: 28800 + +# Database +postgres_hostname: "${DB_HOST}" +postgres_port: 5432 +postgres_database: "auth_service" +postgres_user: "${DB_USER}" +postgres_password: "${DB_PASSWORD}" + +# Encryption +encryption_public_key: "${ENCRYPTION_PUBLIC_KEY}" +encryption_private_key: "" # Not needed for BFF (only auth-daemon) +``` + +--- + +## References + +This section contains verified references with direct quotes from authoritative specifications. + +### Standards & Specifications + +#### 1. OpenID Connect Core 1.0 +**Source**: https://openid.net/specs/openid-connect-core-1_0.html + +**Authorization Code Flow (Section 3.1)**: +> "The Authorization Code Flow returns an Authorization Code to the Client, which can then exchange it for an ID Token and an Access Token directly. This provides the benefit of not exposing any tokens to the User Agent and possibly exposing them to others with access to the User Agent." + +**ID Token Validation Requirements (Section 3.1.3.7)**: +> The Client MUST validate the ID Token in the Token Response in the following manner: +> 1. If the ID Token is encrypted, decrypt it... +> 2. The Issuer Identifier for the OpenID Provider (which is typically obtained during Discovery) MUST exactly match the value of the iss (issuer) Claim. +> 3. The Client MUST validate that the aud (audience) Claim contains its client_id value... +> 4. If the ID Token contains multiple audiences, the Client SHOULD verify that an azp Claim is present. +> 5. The current time MUST be before the time represented by the exp Claim. + +**Nonce Requirement (Section 3.1.3.7)**: +> "The value of the nonce Claim MUST be checked to verify that it is the same value as the one that was sent in the Authentication Request. The Client SHOULD check the nonce value for replay attacks." + +**Access Token Hash Validation (Section 3.1.3.8)**: +> "If the ID Token contains the at_hash Claim, the Client MAY use it to verify that the issued Access Token is the correct one... Hash the octets of the ASCII representation of the access_token with the hash algorithm specified... Take the left-most half of the hash and base64url-encode it." + +**Token Lifetimes (Section 16.18)**: +> "Access Tokens might not be revocable by the Authorization Server. Access Token lifetimes SHOULD therefore be kept to single use or very short lifetimes." + +**TLS Requirements (Section 3.1.2 and 16.17)**: +> "Communication with the Authorization Endpoint MUST utilize TLS. See Section 16.17 (TLS Requirements) for more information on using TLS." + +--- + +#### 2. RFC 7636 - Proof Key for Code Exchange (PKCE) +**Source**: https://datatracker.ietf.org/doc/html/rfc7636 + +**Abstract**: +> "OAuth 2.0 public clients utilizing the Authorization Code Grant are susceptible to the authorization code interception attack. This specification describes the attack as well as a technique to mitigate against the threat through the use of Proof Key for Code Exchange (PKCE, pronounced 'pixy')." + +**Attack Description (Section 1)**: +> "In this attack, the attacker intercepts the authorization code returned from the authorization endpoint within a communication path not protected by Transport Layer Security (TLS), such as inter-application communication within the client's operating system. Once the attacker has gained access to the authorization code, it can use it to obtain the access token." + +**Mitigation (Section 1)**: +> "To mitigate this attack, this extension utilizes a dynamically created cryptographically random key called 'code verifier'. A unique code verifier is created for every authorization request, and its transformed value, called 'code challenge', is sent to the authorization server to obtain the authorization code." + +**Code Verifier Requirements (Section 4.1)**: +> "code_verifier = high-entropy cryptographic random STRING using the unreserved characters [A-Z] / [a-z] / [0-9] / '-' / '.' / '_' / '~' from Section 2.3 of [RFC3986], with a minimum length of 43 characters and a maximum length of 128 characters." + +**S256 Method (Section 4.2)**: +> "code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))" +> "If the client is capable of using 'S256', it MUST use 'S256', as 'S256' is Mandatory To Implement (MTI) on the server." + +**Entropy Requirements (Section 7.1)**: +> "The security model relies on the fact that the code verifier is not learned or guessed by the attacker. It is vitally important to adhere to this principle... The client SHOULD create a 'code_verifier' with a minimum of 256 bits of entropy." + +--- + +#### 3. OAuth 2.0 for Browser-Based Applications (IETF Draft) +**Source**: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-browser-based-apps + +**Note**: This specification is currently an IETF draft and defines the BFF (Backend-For-Frontend) pattern for browser applications. Key recommendations include (as described in Section 6.1): + +- **Backend-For-Frontend Pattern**: The BFF acts as a confidential OAuth client on behalf of the browser +- **Token Storage**: Access tokens and refresh tokens are kept on the backend, never exposed to the browser +- **Session Cookies**: The browser should receive only an HTTP-only, Secure session cookie, per the draft's recommendations +- **Request Proxying**: All authenticated API requests flow through the BFF + +For complete details on the BFF architecture pattern and its security considerations, see Section 6.1 of the specification. + +--- + +#### 4. RFC 6749 - OAuth 2.0 Authorization Framework +**Source**: https://datatracker.ietf.org/doc/html/rfc6749 + +**Authorization Code Grant (Referenced by RFC 7636)**: +- Defines the foundational OAuth 2.0 flows +- Section 4.1 specifies the Authorization Code Grant flow +- Section 4.1.1 describes Authorization Request parameters +- Section 4.1.3 describes Access Token Request parameters + +See RFC 6749 Section 4.1 for the normative definition of the Authorization Code Grant flow. + +--- + +### Framework Documentation + +| Framework | Documentation URL | Purpose in Codebase | +|----------------------|-----------------------------------------------------------------|-------------------------------------------------------| +| **Axum** | https://docs.rs/axum/latest/axum/ | Web framework for HTTP routing and middleware | +| **Tower Sessions** | https://docs.rs/tower-sessions/latest/tower_sessions/ | Session management with cookie backend | +| **OpenIDConnect Rust** | https://docs.rs/openidconnect/latest/openidconnect/ | OIDC client implementation | +| **SeaORM** | https://www.sea-ql.org/SeaORM/ | Async ORM for PostgreSQL token storage | +| **sodiumoxide** | https://docs.rs/sodiumoxide/latest/sodiumoxide/ | libsodium bindings for sealed box encryption | + +--- + +### Security Best Practices + +#### OWASP Session Management Cheat Sheet +**Source**: https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html + +Key recommendations relevant to this implementation: +- Use secure, HTTP-only cookies for session identifiers +- Implement session expiration (idle and absolute timeouts) +- Regenerate session IDs after authentication +- Store minimal data in sessions + +#### OWASP CSRF Prevention Cheat Sheet +**Source**: https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html + +Key recommendations: +- Use SameSite cookie attribute (Strict or Lax) +- Implement synchronizer token pattern or double-submit cookies +- Verify Origin/Referer headers for sensitive operations + + +## Recommended Reading + +### For Understanding the Architecture +1. Read the [Architecture Overview](#architecture-overview) section and diagrams +2. Review the [Component Deep Dive](#component-deep-dive) for implementation details +3. Check the [BFF Compliance Assessment](#bff-compliance-assessment) for security posture + +### For Implementation Work +1. Read **RFC 7636 (PKCE)** - Essential for understanding the code verifier/challenge flow in `login.rs` +2. Read **OpenID Connect Core 1.0 Section 3.1** - Explains the validation logic in `callback.rs` +3. Review the [Implementation Progress](#implementation-progress) section for completed vs remaining work + +### Essential Specification Sections + +| Topic | Specification | Section | +|-------|---------------|---------| +| Authorization Code Flow | OpenID Connect Core 1.0 | Section 3.1 | +| ID Token Claims | OpenID Connect Core 1.0 | Section 2 | +| ID Token Validation | OpenID Connect Core 1.0 | Section 3.1.3.7 | +| PKCE Protocol | RFC 7636 | Section 4 | +| Code Challenge Methods | RFC 7636 | Section 4.2 | +| Security Considerations | RFC 7636 | Section 7 | + +--- + +## Future Tickets + +This section breaks down the remaining improvements into small, manageable tasks. + +### Ticket Category: Security Critical (P0) + +#### TICKET-001: Remove process::exit(0) from Shutdown Handler +**File**: `backend/auth-daemon/src/main.rs:136-137` +**Description**: The `process::exit(0)` bypasses graceful shutdown, preventing cleanup. +**Acceptance Criteria**: +- [ ] Remove `process::exit(0)` from `shutdown_signal()` function +- [ ] Replace `println!("Shutting down")` with `tracing::info!("Received SIGTERM, initiating graceful shutdown")` +- [ ] Verify graceful shutdown completes via integration test +**Effort**: Small (< 30 mins) + +--- + +#### TICKET-002: Move Hardcoded URLs to Configuration +**Files**: +- `backend/oidc-bff/src/main.rs:72` (proxy URL) +- `backend/oidc-bff/src/login.rs:21` (callback URL) +- `backend/oidc-bff/src/callback.rs:46` (callback URL) +**Description**: URLs are hardcoded to staging environment. +**Acceptance Criteria**: +- [ ] Add `graph_url`, `callback_url`, `base_url` to `Config` struct +- [ ] Update `login.rs` to use config callback URL +- [ ] Update `callback.rs` to use config callback URL +- [ ] Update `main.rs` to use config graph URL +- [ ] Update `config.yaml` with new fields +- [ ] Document new config options in README +**Effort**: Medium (1-2 hours) + +--- + +#### TICKET-003: Enable Secure Session Cookie Flags +**File**: `backend/oidc-bff/src/main.rs` (SessionManagerLayer) +**Description**: Session cookies are currently sent over HTTP (`with_secure(false)`). +**Acceptance Criteria**: +- [ ] Set `with_secure(true)` for production +- [ ] Add `with_same_site(SameSite::Strict)` +- [ ] Add `with_http_only(true)` +- [ ] Make secure flag configurable via environment variable +- [ ] Add warning log if secure=false +**Effort**: Small (< 30 mins) + +--- + +#### TICKET-004: Implement Session Expiry +**File**: `backend/oidc-bff/src/main.rs` +**Description**: Session has no expiry, allowing indefinite session lifetimes. +**Acceptance Criteria**: +- [ ] Add `session_idle_timeout` to Config +- [ ] Add `session_max_lifetime` to Config +- [ ] Apply `with_expiry(Expiry::OnInactivity(...))` to SessionManagerLayer +- [ ] Document session timeout behavior +**Effort**: Small (30 mins) + +--- + +### Ticket Category: Production Readiness (P1) + +#### TICKET-005: Initialize Tracing in oidc-bff +**File**: `backend/oidc-bff/src/main.rs` +**Description**: No logging infrastructure is initialized. +**Acceptance Criteria**: +- [ ] Add `tracing` and `tracing-subscriber` dependencies to Cargo.toml +- [ ] Initialize `tracing_subscriber` with `EnvFilter` in main() +- [ ] Add structured logging for key operations +- [ ] Verify logs appear when running locally +**Effort**: Small (< 30 mins) + +--- + +#### TICKET-006: Replace println! with Tracing in auth-daemon +**Files**: +- `backend/auth-daemon/src/inject_token.rs` (5 occurrences) +- `backend/auth-daemon/src/main.rs:136` (1 occurrence) +**Description**: Debug output uses `println!` instead of structured logging. +**Acceptance Criteria**: +- [ ] Replace all `println!` with appropriate `tracing::*` macros +- [ ] Use `tracing::debug!` for normal flow messages +- [ ] Use `tracing::info!` for significant events (shutdown) +- [ ] Use structured fields (e.g., `tracing::debug!(response = ?response)`) +**Effort**: Small (< 30 mins) + +--- + +#### TICKET-007: Add Graceful Shutdown to oidc-bff +**File**: `backend/oidc-bff/src/main.rs:97-102` +**Description**: Server has no graceful shutdown handler. +**Acceptance Criteria**: +- [ ] Add `shutdown_signal()` async function +- [ ] Apply `with_graceful_shutdown()` to axum::serve +- [ ] Handle SIGTERM and SIGINT signals +- [ ] Verify in-flight requests complete before shutdown +**Effort**: Small (30 mins) + +--- + +#### TICKET-008: Add Session Check Endpoint +**File**: `backend/oidc-bff/src/` (new file: `session.rs`) +**Description**: No endpoint to check session status. +**Acceptance Criteria**: +- [ ] Create `SessionStatus` response struct +- [ ] Implement `check_session()` handler +- [ ] Add `/auth/session` route to router +- [ ] Return `{ authenticated: bool, subject?: string }` +- [ ] Add API documentation +**Effort**: Medium (1 hour) + +--- + +#### TICKET-009: Document MemoryStore Limitation +**File**: `backend/oidc-bff/src/main.rs` +**Description**: MemoryStore limitations not documented. +**Acceptance Criteria**: +- [ ] Add warning comment above MemoryStore initialization +- [ ] Document alternatives (Redis, PostgreSQL) +- [ ] Add note in README about production session store requirements +**Effort**: Small (< 15 mins) + +--- + +### Ticket Category: Code Quality (P2) + +#### TICKET-010: Remove Duplicate inject_token.rs +**Files**: +- `backend/oidc-bff/src/inject_token.rs` (to be removed) +- `backend/oidc-bff/src/inject_token_from_session.rs` (keep) +**Description**: Two inject_token files exist, causing confusion. +**Acceptance Criteria**: +- [ ] Verify `main.rs` uses `inject_token_from_session` +- [ ] Remove `inject_token.rs` +- [ ] Update any imports that may reference it +- [ ] Run tests to ensure nothing breaks +**Effort**: Small (< 15 mins) + +--- + +#### TICKET-011: Standardize Health Endpoints +**Files**: +- `backend/oidc-bff/src/main.rs` (`/healthcheck`) +- `backend/auth-daemon/src/main.rs` (`/healthz`) +**Description**: Inconsistent health endpoint naming. +**Acceptance Criteria**: +- [ ] Change oidc-bff to use `/healthz` +- [ ] Update Kubernetes manifests if needed +- [ ] Document standard endpoint in README +**Effort**: Small (< 30 mins) + +--- + +#### TICKET-012: Parse Refresh Token Expiry +**File**: `backend/auth-common/src/database.rs` +**Description**: Refresh token expiry is hardcoded to 30 days. +**Acceptance Criteria**: +- [ ] Update `write_token_to_database()` to accept expiry parameter +- [ ] Parse `refresh_expires_in` from token response in callback +- [ ] Use actual expiry instead of hardcoded 30 days +- [ ] Add fallback to 30 days if not provided +**Effort**: Medium (1 hour) + +--- + +#### TICKET-013: Gate Debug Routes Behind Feature Flag +**File**: `backend/oidc-bff/src/main.rs` +**Description**: `/read` and `/write` counter routes exposed in production. +**Acceptance Criteria**: +- [ ] Move counter routes behind `#[cfg(debug_assertions)]` +- [ ] Or add feature flag in Cargo.toml +- [ ] Verify routes not accessible in release build +**Effort**: Small (< 15 mins) + +--- + +#### TICKET-014: Move TLS Provider Setup +**File**: `backend/auth-daemon/src/main.rs` +**Description**: TLS setup in `setup_router()` may be called multiple times in tests. +**Acceptance Criteria**: +- [ ] Move `rustls::crypto::ring::default_provider().install_default()` to main() +- [ ] Ensure it's only called once +- [ ] Add fallback handling if already installed +**Effort**: Small (< 30 mins) + +--- + +### Task Summary + +| Priority | Tickets | +|----------|---------| +| P0 (Security Critical) | TICKET-001 to TICKET-004 | +| P1 (Production Readiness) | TICKET-005 to TICKET-009 | +| P2 (Code Quality) | TICKET-010 to TICKET-014 | + +--- + +## Summary + +The current implementation has made **significant progress** since the initial technical debt assessment: + +### Completed +- Shared `auth-common` crate extracted +- Logout function properly implemented with database cleanup +- Error messages no longer leak internal details +- Debug endpoints protected with admin auth +- Unwraps fixed in shared HTTP utilities + +### Remaining Work +1. **Security Critical**: `process::exit(0)`, hardcoded URLs, session cookie flags +2. **Production Readiness**: Logging, graceful shutdown in oidc-bff, session store +3. **Code Quality**: Remove duplicate files, standardize endpoints + +--- + +*Document Version: 1.1* +*Last Updated: 30/01/2026 diff --git a/examples/conventional-templates/workflow-auth.yaml b/examples/conventional-templates/workflow-auth.yaml new file mode 100644 index 000000000..5220f6fe1 --- /dev/null +++ b/examples/conventional-templates/workflow-auth.yaml @@ -0,0 +1,66 @@ +apiVersion: argoproj.io/v1alpha1 +kind: ClusterWorkflowTemplate +metadata: + name: workflow-of-workflows + labels: + workflows.diamond.ac.uk/science-group: workflows-examples + annotations: + workflows.argoproj.io/title: Workflow Of Workflows +spec: + entrypoint: entry + templates: + - name: entry + dag: + tasks: + - name: start-auth + template: auth + - name: delay + template: wait + dependencies: [start-auth] + - name: conditional-steps-workflow + templateRef: + name: conditional-steps + template: workflow-entry + clusterScope: true + dependencies: [delay] + + - name: wait + script: + image: docker.io/library/bash:5.3 + command: [bash] + source: | + echo "Waiting for initial access token to expire..." + sleep 60 + + - name: auth + daemon: true + retryStrategy: + limit: 10 + container: + image: ghcr.io/diamondlightsource/workflows-auth-daemon:0.1.0 + readinessProbe: + httpGet: + path: /healthz + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 1 + env: + - name: PORT + value: "8080" + - name: AUTH_DOMAIN + value: https://authn.diamond.ac.uk/realms/master + - name: CLIENT_ID + value: workflows-cluster-staging + # value: workflows-cluster + - name: LOG_LEVEL + value: debug + - name: GRAPH_URL + value: https://staging.workflows.diamond.ac.uk/graphql + # value: https://workflows.diamond.ac.uk/graphql + - name: TOKEN + valueFrom: + secretKeyRef: + name: token + key: token + args: + - serve diff --git a/examples/conventional-templates/workflow-of-workflows.yaml b/examples/conventional-templates/workflow-of-workflows.yaml new file mode 100644 index 000000000..fa46aee55 --- /dev/null +++ b/examples/conventional-templates/workflow-of-workflows.yaml @@ -0,0 +1,74 @@ +apiVersion: argoproj.io/v1alpha1 +kind: WorkflowTemplate +metadata: + name: workflow-of-workflows-1 + labels: + workflows.diamond.ac.uk/science-group: workflows-examples + annotations: + workflows.argoproj.io/title: Workflow Of Workflows +spec: + entrypoint: entry + templates: + - name: entry + steps: + - - name: auth + template: auth + + - - name: test + template: curl + arguments: + parameters: + - name: cmd + value: | + curl --request POST http://{{steps.auth.ip}}:6000 -H "Content-Type: application/json" -d '{"query": "mutation{ submitWorkflowTemplate(name: \"conditional-steps\", visit: {proposalCode: \"bi\", proposalNumber: 22491, number: 1}, parameters: {}){ name } }" }' + + - name: auth + daemon: true + retryStrategy: + limit: 10 + inputs: + artifacts: + - name: auth-config + path: /etc/workflows-auth-daemon/config.yaml + raw: + data: | + client_id: "workflows-dashboard-staging" + client_secret: "" + oidc_provider_url: "https://authn.diamond.ac.uk/realms/master" + port: 6000 + postgres_user: "auth_user" + postgres_password: "redacted" + postgres_database: "auth_service" + postgres_hostname: "workflows-postgresql-ha-pgpool.workflows.svc.cluster.local" + postgres_port: 5432 + encryption_public_key: "redacted" + encryption_private_key: "redacted" + graph_url: "https://staging.workflows.diamond.ac.uk/graphql" + + container: + image: ghcr.io/diamondlightsource/workflows-auth-daemon@sha256:728e9b59d3e6734c7ffc30cfa0dc73057268c13875441b4f2d2188b543834c41 + readinessProbe: + httpGet: + path: /healthz + port: 6000 + initialDelaySeconds: 5 + periodSeconds: 1 + env: + - name: WORKFLOWS_AUTH_DAEMON_CONFIG + value: /etc/workflows-auth-daemon/config.yaml + - name: WORKFLOWS_AUTH_DAEMON_SUBJECT + value: d4cd0e39-c80a-434d-81d2-0ed81be969e2 + - name: RUST_BACKTRACE + value: "1" + args: + - serve + + - name: curl + inputs: + parameters: + - name: cmd + container: + image: curlimages/curl:8.15.0 + command: ["/bin/sh", "-c"] + args: ["{{inputs.parameters.cmd}}"] +