From a24645ee008ef751eb4138b9766107f61b8b3bd3 Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Tue, 16 Jan 2024 23:32:26 +0100 Subject: [PATCH] feat: OIDC support (#3017) * oidc * update * feat: add support for multiselect in python * d * d * progress * all * all * all --- backend/Cargo.lock | 331 ++++++++++++++++-- backend/Cargo.toml | 1 + .../20240108130114_add_oidc.down.sql | 1 + .../migrations/20240108130114_add_oidc.up.sql | 2 + backend/src/monitor.rs | 1 + backend/tests/worker.rs | 4 +- backend/windmill-api/Cargo.toml | 3 +- backend/windmill-api/openapi.yaml | 23 ++ backend/windmill-api/src/lib.rs | 6 +- backend/windmill-api/src/oidc.rs | 296 ++++++++++++++++ backend/windmill-api/src/resources.rs | 2 + backend/windmill-api/src/variables.rs | 2 + backend/windmill-common/src/variables.rs | 17 +- backend/windmill-worker/Cargo.toml | 3 +- backend/windmill-worker/src/bun_executor.rs | 2 + backend/windmill-worker/src/common.rs | 4 + .../windmill-worker/src/dedicated_worker.rs | 2 +- backend/windmill-worker/src/deno_executor.rs | 2 + .../windmill-worker/src/python_executor.rs | 4 + backend/windmill-worker/src/worker.rs | 10 +- .../src/lib/components/TestConnection.svelte | 3 +- .../src/lib/components/instanceSettings.ts | 8 +- python-client/.gitignore | 1 + .../wmill/dist/wmill-1.245.0-py3-none-any.whl | Bin 0 -> 7356 bytes python-client/wmill/dist/wmill-1.245.0.tar.gz | Bin 0 -> 7564 bytes python-client/wmill/wmill/client.py | 3 + .../dist/wmill_pg-1.245.0-py3-none-any.whl | Bin 0 -> 2299 bytes .../wmill_pg/dist/wmill_pg-1.245.0.tar.gz | Bin 0 -> 1933 bytes typescript-client/client.ts | 32 +- 29 files changed, 724 insertions(+), 39 deletions(-) create mode 100644 backend/migrations/20240108130114_add_oidc.down.sql create mode 100644 backend/migrations/20240108130114_add_oidc.up.sql create mode 100644 backend/windmill-api/src/oidc.rs create mode 100644 python-client/wmill/dist/wmill-1.245.0-py3-none-any.whl create mode 100644 python-client/wmill/dist/wmill-1.245.0.tar.gz create mode 100644 python-client/wmill_pg/dist/wmill_pg-1.245.0-py3-none-any.whl create mode 100644 python-client/wmill_pg/dist/wmill_pg-1.245.0.tar.gz diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 5ee1b04922..4024a9bd7b 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -775,7 +775,7 @@ dependencies = [ "hmac", "http", "once_cell", - "p256", + "p256 0.11.1", "percent-encoding", "ring 0.17.7", "sha2 0.10.8", @@ -1027,6 +1027,12 @@ 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.13.1" @@ -1898,8 +1904,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]] @@ -1912,6 +1920,34 @@ dependencies = [ "typenum", ] +[[package]] +name = "curve25519-dalek" +version = "4.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89b8c6a2e4b1f45971ad09761aafb85514a84744b67a95e32c3cc1352d1f65c" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest 0.10.7", + "fiat-crypto", + "platforms", + "rustc_version 0.4.0", + "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.48", +] + [[package]] name = "darling" version = "0.14.4" @@ -2439,11 +2475,49 @@ 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.8", + "digest 0.10.7", + "elliptic-curve 0.13.8", + "rfc6979 0.4.0", + "signature 2.2.0", + "spki 0.7.3", +] + +[[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.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f628eaec48bfd21b865dc2950cfa014450c01d2fa2b69a86c2fd5844ec523c0" +dependencies = [ + "curve25519-dalek", + "ed25519", + "serde", + "sha2 0.10.8", + "subtle", + "zeroize", +] + [[package]] name = "either" version = "1.9.0" @@ -2459,16 +2533,37 @@ 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 0.10.7", - "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 0.10.7", + "ff 0.13.0", + "generic-array", + "group 0.13.0", + "hkdf", + "pem-rfc7468 0.7.0", + "pkcs8 0.10.2", + "rand_core 0.6.4", + "sec1 0.7.3", "subtle", "zeroize", ] @@ -2708,6 +2803,22 @@ dependencies = [ "subtle", ] +[[package]] +name = "ff" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27573eac26f4dd11e2b1916c3fe1baa56407c83c71a773a8ba17ec0bca03b6b7" + [[package]] name = "filetime" version = "0.2.23" @@ -3143,6 +3254,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -3263,7 +3375,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.0", "rand_core 0.6.4", "subtle", ] @@ -3612,6 +3735,7 @@ checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" dependencies = [ "equivalent", "hashbrown 0.14.3", + "serde", ] [[package]] @@ -4542,6 +4666,26 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" +[[package]] +name = "oauth2" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c38841cdd844847e3e7c8d29cef9dcfed8877f8f56f9071f77843ecf3baf937f" +dependencies = [ + "base64 0.13.1", + "chrono", + "getrandom 0.2.12", + "http", + "rand 0.8.5", + "reqwest", + "serde", + "serde_json", + "serde_path_to_error", + "sha2 0.10.8", + "thiserror", + "url", +] + [[package]] name = "object" version = "0.32.2" @@ -4625,6 +4769,38 @@ dependencies = [ "serde_json", ] +[[package]] +name = "openidconnect" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62d6050f6a84b81f23c569f5607ad883293e57491036e318fafe6fc4895fadb1" +dependencies = [ + "base64 0.13.1", + "chrono", + "dyn-clone", + "ed25519-dalek", + "hmac", + "http", + "itertools 0.10.5", + "log", + "oauth2", + "p256 0.13.2", + "p384", + "rand 0.8.5", + "rsa 0.9.6", + "serde", + "serde-value", + "serde_derive", + "serde_json", + "serde_path_to_error", + "serde_plain", + "serde_with", + "sha2 0.10.8", + "subtle", + "thiserror", + "url", +] + [[package]] name = "openssl" version = "0.10.62" @@ -4675,6 +4851,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "ordered-float" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" +dependencies = [ + "num-traits", +] + [[package]] name = "outref" version = "0.5.1" @@ -4693,8 +4878,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 0.10.8", +] + +[[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 0.10.8", +] + +[[package]] +name = "p384" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70786f51bcc69f6a4c0360e063a4cac5419ef7c5cd5b3c99ad70f3be5ba79209" +dependencies = [ + "ecdsa 0.16.9", + "elliptic-curve 0.13.8", + "primeorder", "sha2 0.10.8", ] @@ -5056,6 +5265,12 @@ dependencies = [ "array-init-cursor", ] +[[package]] +name = "platforms" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "626dec3cac7cc0e1577a2ec3fc496277ec2baa084bebad95bb6fdbfae235f84c" + [[package]] name = "pmutil" version = "0.5.3" @@ -5543,6 +5758,15 @@ dependencies = [ "syn 2.0.48", ] +[[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 = "1.3.1" @@ -6130,6 +6354,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 = "riff" version = "1.0.1" @@ -6647,7 +6881,7 @@ 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", @@ -6655,6 +6889,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.8", + "generic-array", + "pkcs8 0.10.2", + "subtle", + "zeroize", +] + [[package]] name = "security-framework" version = "2.9.2" @@ -6728,6 +6976,16 @@ dependencies = [ "serde_json", ] +[[package]] +name = "serde-value" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" +dependencies = [ + "ordered-float", + "serde", +] + [[package]] name = "serde-wasm-bindgen" version = "0.4.5" @@ -6872,6 +7130,35 @@ dependencies = [ "v8", ] +[[package]] +name = "serde_with" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64cd236ccc1b7a29e7e2739f27c0b2dd199804abc4290e32f59f3b68d6405c23" +dependencies = [ + "base64 0.21.7", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.1.0", + "serde", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93634eb5f75a2323b16de4748022ac4297f9e76b6dced2be287a099f41b5e788" +dependencies = [ + "darling 0.20.3", + "proc-macro2", + "quote", + "syn 2.0.48", +] + [[package]] name = "serde_yaml" version = "0.9.30" @@ -7520,9 +7807,9 @@ dependencies = [ [[package]] name = "strsim" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +checksum = "ccbca6f34534eb78dbee83f6b2c9442fea7113f43d9e80ea320f0972ae5dc08d" [[package]] name = "strum" @@ -9315,12 +9602,13 @@ dependencies = [ "hf-hub", "hmac", "hyper", - "itertools 0.12.0", + "itertools 0.9.0", "lazy_static", "magic-crypt", "mail-send", "mime_guess", "object_store", + "openidconnect", "polars", "polars-io", "prometheus", @@ -9405,7 +9693,7 @@ dependencies = [ "hex", "hmac", "hyper", - "itertools 0.12.0", + "itertools 0.9.0", "lazy_static", "magic-crypt", "prometheus", @@ -9464,7 +9752,7 @@ version = "1.245.1" dependencies = [ "anyhow", "gosyn", - "itertools 0.12.0", + "itertools 0.9.0", "lazy_static", "regex", "windmill-parser", @@ -9486,7 +9774,7 @@ name = "windmill-parser-py" version = "1.245.1" dependencies = [ "anyhow", - "itertools 0.12.0", + "itertools 0.9.0", "rustpython-parser", "serde_json", "windmill-parser", @@ -9498,7 +9786,7 @@ version = "1.245.1" dependencies = [ "anyhow", "async-recursion", - "itertools 0.12.0", + "itertools 0.9.0", "lazy_static", "phf 0.11.2", "regex", @@ -9563,14 +9851,14 @@ dependencies = [ "anyhow", "async-recursion", "axum", - "bigdecimal 0.4.2", + "bigdecimal 0.3.1", "chrono", "chrono-tz", "cron", "futures-core", "hex", "hmac", - "itertools 0.12.0", + "itertools 0.9.0", "lazy_static", "prometheus", "regex", @@ -9613,13 +9901,14 @@ dependencies = [ "gcp_auth", "git-version", "hex", - "itertools 0.12.0", + "itertools 0.9.0", "jsonwebtoken", "lazy_static", "mysql_async", "native-tls", "nix", "once_cell", + "openidconnect", "pem 3.0.3", "postgres-native-tls", "prometheus", diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 16ff05491b..31d7bea8ee 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -216,3 +216,4 @@ aws-config = "1.0.3" polars = { version = "0.35.2", features = ["lazy", "parquet", "aws", "csv", "dtype-full"] } polars-io = { version = "0.35.2", features = ["csv"] } object_store = { version = "0.8.0", features = ["aws"] } +openidconnect = { version = "3.4.0" } diff --git a/backend/migrations/20240108130114_add_oidc.down.sql b/backend/migrations/20240108130114_add_oidc.down.sql new file mode 100644 index 0000000000..d2f607c5b8 --- /dev/null +++ b/backend/migrations/20240108130114_add_oidc.down.sql @@ -0,0 +1 @@ +-- Add down migration script here diff --git a/backend/migrations/20240108130114_add_oidc.up.sql b/backend/migrations/20240108130114_add_oidc.up.sql new file mode 100644 index 0000000000..4b8c8abbdf --- /dev/null +++ b/backend/migrations/20240108130114_add_oidc.up.sql @@ -0,0 +1,2 @@ +-- Add up migration script here +ALTER TABLE token ADD COLUMN IF NOT EXISTS job UUID; \ No newline at end of file diff --git a/backend/src/monitor.rs b/backend/src/monitor.rs index 758b0e02ac..16e6d003eb 100644 --- a/backend/src/monitor.rs +++ b/backend/src/monitor.rs @@ -781,6 +781,7 @@ async fn handle_zombie_jobs "ephemeral-script", *SCRIPT_TOKEN_EXPIRY, &job.email, + &job.id, ) .await .expect("could not create job token"); diff --git a/backend/tests/worker.rs b/backend/tests/worker.rs index 7a4448d4ad..33ba7fa915 100644 --- a/backend/tests/worker.rs +++ b/backend/tests/worker.rs @@ -300,7 +300,7 @@ mod suspend_resume { let second = completed.next().await.unwrap(); // print_job(second, &db).await; - let token = windmill_worker::create_token_for_owner(&db, "test-workspace", "u/test-user", "", 100, "").await.unwrap(); + let token = windmill_worker::create_token_for_owner(&db, "test-workspace", "u/test-user", "", 100, "", &Uuid::nil()).await.unwrap(); let secret = reqwest::get(format!( "http://localhost:{port}/api/w/test-workspace/jobs/job_signature/{second}/0?token={token}&approver=ruben" )) @@ -401,7 +401,7 @@ mod suspend_resume { /* ... and send a request resume it. */ let second = completed.next().await.unwrap(); - let token = windmill_worker::create_token_for_owner(&db, "test-workspace", "u/test-user", "", 100, "").await.unwrap(); + let token = windmill_worker::create_token_for_owner(&db, "test-workspace", "u/test-user", "", 100, "", &Uuid::nil()).await.unwrap(); let secret = reqwest::get(format!( "http://localhost:{port}/api/w/test-workspace/jobs/job_signature/{second}/0?token={token}" )) diff --git a/backend/windmill-api/Cargo.toml b/backend/windmill-api/Cargo.toml index e0734c4394..cd2e6c49f9 100644 --- a/backend/windmill-api/Cargo.toml +++ b/backend/windmill-api/Cargo.toml @@ -9,7 +9,7 @@ name = "windmill_api" path = "src/lib.rs" [features] -enterprise = ["windmill-queue/enterprise", "windmill-audit/enterprise", "windmill-git-sync/enterprise"] +enterprise = ["windmill-queue/enterprise", "windmill-audit/enterprise", "windmill-git-sync/enterprise", "openidconnect"] stripe = ["async-stripe"] enterprise_saml = ["samael"] benchmark = [] @@ -89,3 +89,4 @@ aws-sdk-s3.workspace = true polars.workspace = true polars-io.workspace = true object_store.workspace = true +openidconnect = { workspace = true, optional = true} diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index 5b81fa38e1..c7f72764aa 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -1681,6 +1681,29 @@ paths: items: $ref: "#/components/schemas/TruncatedToken" + /w/{workspace}/oidc/token/{audience}: + post: + summary: get OIDC token (ee only) + operationId: getOidcToken + tags: + - oidc + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - name: audience + in: path + required: true + schema: + type: string + + responses: + "200": + description: new oidc token + content: + text/plain: + schema: + type: string + + /w/{workspace}/variables/create: post: summary: create variable diff --git a/backend/windmill-api/src/lib.rs b/backend/windmill-api/src/lib.rs index 7d7f49cbcc..2f4bdcb21c 100644 --- a/backend/windmill-api/src/lib.rs +++ b/backend/windmill-api/src/lib.rs @@ -63,6 +63,8 @@ pub mod job_helpers; pub mod job_metrics; pub mod jobs; pub mod oauth2; + +mod oidc; mod openai; mod raw_apps; mod resources; @@ -212,7 +214,8 @@ pub async fn run_server( users::workspaced_service().layer(Extension(argon2.clone())), ) .nest("/variables", variables::workspaced_service()) - .nest("/workspaces", workspaces::workspaced_service()), + .nest("/workspaces", workspaces::workspaced_service()) + .nest("/oidc", oidc::workspaced_service()), ) .nest("/workspaces", workspaces::global_service()) .nest( @@ -235,6 +238,7 @@ pub async fn run_server( .route_layer(from_extractor::()) .route_layer(from_extractor::()) .nest("/jobs", jobs::global_root_service()) + .nest("/oidc", oidc::global_service()) .nest( "/saml", saml::global_service().layer(Extension(Arc::new(sp_extension.0))), diff --git a/backend/windmill-api/src/oidc.rs b/backend/windmill-api/src/oidc.rs new file mode 100644 index 0000000000..95ef4da450 --- /dev/null +++ b/backend/windmill-api/src/oidc.rs @@ -0,0 +1,296 @@ +/* + * Author: Ruben Fiszel + * Copyright: Windmill Labs, Inc 2023 + * This file and its contents are licensed under the AGPLv3 License. + * Please see the included NOTICE for copyright information and + * LICENSE-AGPL for a copy of the license. + */ + +use std::process::Command; + +use anyhow; + +#[cfg(feature = "enterprise")] +use openidconnect::{ + core::{ + CoreClaimName, CoreJsonWebKeySet, CoreJwsSigningAlgorithm, CoreProviderMetadata, + CoreResponseType, CoreRsaPrivateSigningKey, CoreSubjectIdentifierType, + }, + AdditionalClaims, AuthUrl, EmptyAdditionalProviderMetadata, IssuerUrl, JsonWebKeyId, + JsonWebKeySetUrl, ResponseTypes, +}; + +#[cfg(feature = "enterprise")] +impl AdditionalClaims for JobClaim {} + +use crate::db::DB; +use axum::extract::Path; +use axum::routing::get; +use axum::Extension; +use axum::{Json, Router}; +use serde::{Deserialize, Serialize}; + +#[cfg(feature = "enterprise")] +pub fn global_service() -> Router { + Router::new() + .route( + "/.well-known/openid-configuration", + get(openid_configuration), + ) + .route("/jwks", get(jwks)) +} +#[cfg(not(feature = "enterprise"))] +pub fn global_service() -> Router { + Router::new() +} + +#[cfg(not(feature = "enterprise"))] +pub fn workspaced_service() -> Router { + Router::new() +} + +#[cfg(feature = "enterprise")] +pub fn workspaced_service() -> Router { + Router::new().route("/token/:audience", get(gen_token)) +} + +#[derive(Debug, Clone, serde::Serialize)] +struct Keys { + private_key: String, +} + +async fn gen_pems(db: &DB) -> anyhow::Result { + let private_key_cmd = Command::new("openssl") + .arg("genrsa") + .arg("--traditional") + .arg("2048") + .output() + .expect("failed to execute process"); + + let private_key = String::from_utf8(private_key_cmd.stdout).unwrap(); + + tracing::debug!("Generated private key: {}", private_key); + let keys = Keys { private_key }; + + sqlx::query!( + "INSERT INTO global_settings (name, value) VALUES ('rsa_keys', $1)", + serde_json::to_value(&keys).unwrap() + ) + .execute(db) + .await?; + + Ok(keys) +} + +#[cfg(feature = "enterprise")] +async fn get_private_key(db: &DB) -> anyhow::Result { + let key = sqlx::query_scalar!( + "SELECT value->>'private_key' FROM global_settings WHERE name = 'rsa_keys'", + ) + .fetch_optional(db) + .await? + .flatten(); + + if let Some(key) = key { + return Ok(key); + } else { + let keys = gen_pems(db).await?; + return Ok(keys.private_key); + } +} + +#[cfg(feature = "enterprise")] +pub async fn jwks( + Extension(db): Extension, +) -> windmill_common::error::JsonResult { + use openidconnect::PrivateSigningKey; + + let private_key = get_private_key(&db).await?; + let jwks = CoreJsonWebKeySet::new(vec![CoreRsaPrivateSigningKey::from_pem( + &private_key, + Some(JsonWebKeyId::new("windmill".to_string())), + ) + .map_err(|e| anyhow::anyhow!("Failed to parse PEM: {}", e))? + .as_verification_key()]); + + Ok(Json(jwks)) +} + +#[cfg(feature = "enterprise")] +pub async fn openid_configuration() -> windmill_common::error::JsonResult { + use windmill_common::BASE_URL; + + let base_url = BASE_URL.read().await.clone(); + return get_provider_metadata(base_url) + .map(Json) + .map_err(|e| e.into()); +} + +#[cfg(feature = "enterprise")] +pub fn get_provider_metadata(base_url: String) -> anyhow::Result { + let provider_metadata = CoreProviderMetadata::new( + IssuerUrl::new(format!("{base_url}/api/oidc/"))?, + AuthUrl::new(format!("{base_url}/api/oidc/"))?, + JsonWebKeySetUrl::new(format!("{base_url}/api/oidc/jwks"))?, + vec![ + // Optional: support the implicit flow. + ResponseTypes::new(vec![CoreResponseType::Token, CoreResponseType::IdToken]), // Other flows including hybrid flows may also be specified here. + ], + vec![CoreSubjectIdentifierType::Public], + vec![CoreJwsSigningAlgorithm::RsaSsaPssSha256], + EmptyAdditionalProviderMetadata {}, + ) + // Recommended: specify the supported ID token claims. + .set_claims_supported(Some(vec![ + // Providers may also define an enum instead of using CoreClaimName. + CoreClaimName::new("sub".to_string()), + CoreClaimName::new("aud".to_string()), + CoreClaimName::new("email".to_string()), + CoreClaimName::new("email_verified".to_string()), + CoreClaimName::new("exp".to_string()), + CoreClaimName::new("iat".to_string()), + CoreClaimName::new("iss".to_string()), + CoreClaimName::new("name".to_string()), + CoreClaimName::new("given_name".to_string()), + CoreClaimName::new("family_name".to_string()), + CoreClaimName::new("picture".to_string()), + CoreClaimName::new("locale".to_string()), + ])); + return Ok(provider_metadata); +} + +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Eq, Serialize)] +struct JobClaim { + job_id: String, + path: Option, + flow_path: Option, + groups: Vec, + username: String, + email: String, + workspace: String, +} + +use crate::db::ApiAuthed; +use crate::users::Tokened; + +#[cfg(feature = "enterprise")] +pub async fn gen_token( + authed: ApiAuthed, + token: Tokened, + Extension(db): Extension, + Path((w_id, audience)): Path<(String, String)>, +) -> windmill_common::error::Result { + use chrono::{Duration, Utc}; + use openidconnect::{ + core::{CoreGenderClaim, CoreJsonWebKeyType, CoreJweContentEncryptionAlgorithm}, + Audience, EndUserEmail, IdToken, IdTokenClaims, StandardClaims, SubjectIdentifier, + }; + use windmill_queue::get_queued_job; + + use crate::users::get_groups_for_user; + + let private_key = get_private_key(&db).await?; + + let username = authed.username; + let email = authed.email; + + let job_id = { + let job = sqlx::query_scalar!("SELECT job FROM token WHERE token = $1", token.token) + .fetch_optional(&db) + .await? + .flatten(); + if job.is_none() { + return Err(anyhow::anyhow!("Token not found").into()); + } else { + job.unwrap() + } + }; + let mut tx = db.begin().await?; + let job = get_queued_job(job_id, &w_id, &mut tx).await?; + tx.commit().await?; + + let job = job.ok_or_else(|| anyhow::anyhow!("Queued job {} not found", job_id))?; + let issue_url = crate::BASE_URL.read().await.clone(); + let flow_path = if let Some(uuid) = job.parent_job { + sqlx::query_scalar!("SELECT script_path FROM queue WHERE id = $1", uuid) + .fetch_optional(&db) + .await? + .flatten() + } else { + None + }; + + let groups = get_groups_for_user(&w_id, &username, &email, &db) + .await + .ok() + .unwrap_or_default(); + + let id_token = IdToken::< + JobClaim, + CoreGenderClaim, + CoreJweContentEncryptionAlgorithm, + CoreJwsSigningAlgorithm, + CoreJsonWebKeyType, + >::new( + IdTokenClaims::::new( + // Specify the issuer URL for the OpenID Connect Provider. + IssuerUrl::new(issue_url) + .map_err(|e| anyhow::anyhow!("Failed to generate IssueUrl: {}", e))?, + // The audience is usually a single entry with the client ID of the client for whom + // the ID token is intended. This is a required claim. + vec![Audience::new(audience)], + // The ID token expiration is usually much shorter than that of the access or refresh + // tokens issued to clients. + Utc::now() + Duration::seconds(300), + // The issue time is usually the current time. + Utc::now(), + // Set the standard claims defined by the OpenID Connect Core spec. + StandardClaims::new( + // Stable subject identifiers are recommended in place of e-mail addresses or other + // potentially unstable identifiers. This is the only required claim. + SubjectIdentifier::new("windmill".to_string()), + ) + // Optional: specify the user's e-mail address. This should only be provided if the + // client has been granted the 'profile' or 'email' scopes. + .set_email(Some(EndUserEmail::new(job.email.clone()))) + // Optional: specify whether the provider has verified the user's e-mail address. + .set_email_verified(Some(true)), + // OpenID Connect Providers may supply custom claims by providing a struct that + // implements the AdditionalClaims trait. This requires manually using the + // generic IdTokenClaims struct rather than the CoreIdTokenClaims type alias, + // however. + JobClaim { + job_id: job_id.to_string(), + path: job.script_path, + flow_path, + username: job.created_by, + email: job.email, + workspace: job.workspace_id, + groups, + }, + ), + // The private key used for signing the ID token. For confidential clients (those able + // to maintain a client secret), a CoreHmacKey can also be used, in conjunction + // with one of the CoreJwsSigningAlgorithm::HmacSha* signing algorithms. When using an + // HMAC-based signing algorithm, the UTF-8 representation of the client secret should + // be used as the HMAC key. + &CoreRsaPrivateSigningKey::from_pem( + &private_key, + Some(JsonWebKeyId::new("windmill".to_string())), + ) + .map_err(|e| anyhow::anyhow!("Invalid private key: {}", e))?, + // Uses the RS256 signature algorithm. This crate supports any RS*, PS*, or HS* + // signature algorithm. + CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, + // When returning the ID token alongside an access token (e.g., in the Authorization Code + // flow), it is recommended to pass the access token here to set the `at_hash` claim + // automatically. + None, + // When returning the ID token alongside an authorization code (e.g., in the implicit + // flow), it is recommended to pass the authorization code here to set the `c_hash` claim + // automatically. + None, + ) + .map_err(|e| anyhow::anyhow!("Failed to generate token: {}", e))?; + Ok(id_token.to_string()) +} diff --git a/backend/windmill-api/src/resources.rs b/backend/windmill-api/src/resources.rs index 8093853808..d6451c4d41 100644 --- a/backend/windmill-api/src/resources.rs +++ b/backend/windmill-api/src/resources.rs @@ -544,6 +544,8 @@ pub async fn transform_json_value<'c>( flow_path, job.schedule_path.clone(), job.flow_step_id.clone(), + job.root_job.map(|x| x.to_string()), + None, ) .await; diff --git a/backend/windmill-api/src/variables.rs b/backend/windmill-api/src/variables.rs index 758ba63677..b419f37c3b 100644 --- a/backend/windmill-api/src/variables.rs +++ b/backend/windmill-api/src/variables.rs @@ -69,6 +69,8 @@ async fn list_contextual_variables( Some("u/user/encapsulating_flow_path".to_string()), Some("u/user/triggering_flow_path".to_string()), Some("c".to_string()), + Some("017e0ad5-f499-73b6-5488-92a61c5196dd".to_string()), + Some("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c".to_string()), ) .await .to_vec(), diff --git a/backend/windmill-common/src/variables.rs b/backend/windmill-common/src/variables.rs index 15799d17e4..73098b399c 100644 --- a/backend/windmill-common/src/variables.rs +++ b/backend/windmill-common/src/variables.rs @@ -144,7 +144,9 @@ pub async fn get_reserved_variables( flow_path: Option, schedule_path: Option, step_id: Option, -) -> [ContextualVariable; 15] { + root_flow_id: Option, + jwt_token: Option, +) -> [ContextualVariable; 17] { let state_path = { let trigger = if schedule_path.is_some() { username.to_string() @@ -233,11 +235,17 @@ pub async fn get_reserved_variables( value: flow_id.unwrap_or_else(|| "".to_string()), description: "Job id of the encapsulating flow if the job is a flow step".to_string(), }, + ContextualVariable { + name: "WM_ROOT_FLOW_JOB_ID".to_string(), + value: root_flow_id.unwrap_or_else(|| "".to_string()), + description: "Job id of the root flow if the job is a flow step".to_string(), + }, ContextualVariable { name: "WM_FLOW_PATH".to_string(), value: flow_path.unwrap_or_else(|| "".to_string()), description: "Path of the encapsulating flow if the job is a flow step".to_string(), }, + ContextualVariable { name: "WM_SCHEDULE_PATH".to_string(), value: schedule_path.unwrap_or_else(|| "".to_string()), @@ -269,6 +277,11 @@ pub async fn get_reserved_variables( name: "WM_OBJECT_PATH".to_string(), value: object_path, description: "Script or flow step execution unique path, useful for storing results in an external service".to_string(), - } + }, + ContextualVariable { + name: "WM_OIDC_JWT".to_string(), + value: jwt_token.unwrap_or_else(|| "".to_string()), + description: "OIDC JWT token (EE only)".to_string(), + }, ] } diff --git a/backend/windmill-worker/Cargo.toml b/backend/windmill-worker/Cargo.toml index de8fd33767..2a9bd40b89 100644 --- a/backend/windmill-worker/Cargo.toml +++ b/backend/windmill-worker/Cargo.toml @@ -10,7 +10,7 @@ path = "src/lib.rs" [features] default = [] -enterprise = ["windmill-queue/enterprise", "windmill-git-sync/enterprise", "dep:gcp_auth", "dep:jsonwebtoken", "dep:pem", "dep:sha2", "dep:tiberius", "dep:tokio-util"] +enterprise = ["windmill-queue/enterprise", "windmill-git-sync/enterprise", "dep:gcp_auth", "dep:jsonwebtoken", "dep:openidconnect", "dep:pem", "dep:sha2", "dep:tiberius", "dep:tokio-util"] benchmark = ["windmill-queue/benchmark"] flamegraph = [] @@ -78,6 +78,7 @@ reqwest.workspace = true hex.workspace = true tiberius = { workspace = true, optional = true } tokio-util = { workspace = true, optional = true } +openidconnect = { workspace = true, optional = true} [build-dependencies] deno_fetch.workspace = true diff --git a/backend/windmill-worker/src/bun_executor.rs b/backend/windmill-worker/src/bun_executor.rs index 3d5a2df11e..9055a02d0f 100644 --- a/backend/windmill-worker/src/bun_executor.rs +++ b/backend/windmill-worker/src/bun_executor.rs @@ -733,6 +733,8 @@ pub async fn start_worker( None, None, None, + None, + None, ) .await; let context_envs = build_envs_map(context.to_vec()).await; diff --git a/backend/windmill-worker/src/common.rs b/backend/windmill-worker/src/common.rs index 4758e3a001..35cf2c25ad 100644 --- a/backend/windmill-worker/src/common.rs +++ b/backend/windmill-worker/src/common.rs @@ -274,6 +274,8 @@ pub async fn transform_json_value( flow_path, job.schedule_path.clone(), job.flow_step_id.clone(), + job.root_job.clone().map(|x| x.to_string()), + None, ) .await; @@ -383,6 +385,8 @@ pub async fn get_reserved_variables( flow_path, job.schedule_path.clone(), job.flow_step_id.clone(), + job.root_job.clone().map(|x| x.to_string()), + None, ) .await .to_vec(); diff --git a/backend/windmill-worker/src/dedicated_worker.rs b/backend/windmill-worker/src/dedicated_worker.rs index 62e6a6a24f..7f1c926c2b 100644 --- a/backend/windmill-worker/src/dedicated_worker.rs +++ b/backend/windmill-worker/src/dedicated_worker.rs @@ -48,7 +48,7 @@ pub async fn handle_dedicated_process( job_dir: &str, context_envs: HashMap, envs: HashMap, - reserved_variables: [variables::ContextualVariable; 15], + reserved_variables: [variables::ContextualVariable; 17], common_bun_proc_envs: HashMap, args: Vec<&str>, mut killpill_rx: tokio::sync::broadcast::Receiver<()>, diff --git a/backend/windmill-worker/src/deno_executor.rs b/backend/windmill-worker/src/deno_executor.rs index c0057b2343..c85066b1a4 100644 --- a/backend/windmill-worker/src/deno_executor.rs +++ b/backend/windmill-worker/src/deno_executor.rs @@ -423,6 +423,8 @@ pub async fn start_worker( None, None, None, + None, + None, ) .await; let context_envs = build_envs_map(context.to_vec()).await; diff --git a/backend/windmill-worker/src/python_executor.rs b/backend/windmill-worker/src/python_executor.rs index ee0743ea71..249e4d1a76 100644 --- a/backend/windmill-worker/src/python_executor.rs +++ b/backend/windmill-worker/src/python_executor.rs @@ -888,6 +888,8 @@ pub async fn start_worker( None, None, None, + None, + None, ) .await .to_vec(); @@ -1005,6 +1007,8 @@ for line in sys.stdin: None, None, None, + None, + None, ) .await; diff --git a/backend/windmill-worker/src/worker.rs b/backend/windmill-worker/src/worker.rs index 5dae852ff8..485443f6ac 100644 --- a/backend/windmill-worker/src/worker.rs +++ b/backend/windmill-worker/src/worker.rs @@ -113,6 +113,7 @@ pub async fn create_token_for_owner_in_bg( let w_id = job.workspace_id.clone(); let owner = job.permissioned_as.clone(); let email = job.email.clone(); + let job_id = job.id.clone(); tokio::spawn(async move { let token = create_token_for_owner( &db.clone(), @@ -121,6 +122,7 @@ pub async fn create_token_for_owner_in_bg( "ephemeral-script", *SCRIPT_TOKEN_EXPIRY, &email, + &job_id, ) .await .expect("could not create job token"); @@ -138,6 +140,7 @@ pub async fn create_token_for_owner( label: &str, expires_in: u64, email: &str, + job_id: &Uuid, ) -> error::Result { // TODO: Bad implementation. We should not have access to this DB here. if let Some(token) = JOB_TOKEN.as_ref() { @@ -156,15 +159,16 @@ pub async fn create_token_for_owner( sqlx::query_scalar!( "INSERT INTO token - (workspace_id, token, owner, label, expiration, super_admin, email) - VALUES ($1, $2, $3, $4, now() + ($5 || ' seconds')::interval, $6, $7)", + (workspace_id, token, owner, label, expiration, super_admin, email, job) + VALUES ($1, $2, $3, $4, now() + ($5 || ' seconds')::interval, $6, $7, $8)", &w_id, token, owner, label, expires_in.to_string(), is_super_admin, - email + email, + job_id ) .execute(db) .await?; diff --git a/frontend/src/lib/components/TestConnection.svelte b/frontend/src/lib/components/TestConnection.svelte index 332f800c63..a172e3a954 100644 --- a/frontend/src/lib/components/TestConnection.svelte +++ b/frontend/src/lib/components/TestConnection.svelte @@ -111,7 +111,8 @@ workspace: $workspaceStore!, id: job, requestBody: { - reason: 'Connection did not resolve after 5s' + reason: + 'Connection did not resolve after 5s. Do you have native workers or a worker group listening to the proper tag available?' } }) } catch (err) { diff --git a/frontend/src/lib/components/instanceSettings.ts b/frontend/src/lib/components/instanceSettings.ts index b20840d0a3..f4532493a5 100644 --- a/frontend/src/lib/components/instanceSettings.ts +++ b/frontend/src/lib/components/instanceSettings.ts @@ -33,8 +33,14 @@ export const settings: Record = { fieldType: 'text', placeholder: 'https://windmill.com', storage: 'setting', + error: 'Base url must start with http:// or https:// and not end with / or a space', isValid: (value: string | undefined) => - value ? value?.startsWith('http') && value.includes('://') && !value?.endsWith('/') : true + value + ? value?.startsWith('http') && + value.includes('://') && + !value?.endsWith('/') && + !value?.endsWith(' ') + : true }, { label: 'Request Size Limit In MB', diff --git a/python-client/.gitignore b/python-client/.gitignore index e69de29bb2..0a6e1f82df 100644 --- a/python-client/.gitignore +++ b/python-client/.gitignore @@ -0,0 +1 @@ +windmill-api/ diff --git a/python-client/wmill/dist/wmill-1.245.0-py3-none-any.whl b/python-client/wmill/dist/wmill-1.245.0-py3-none-any.whl new file mode 100644 index 0000000000000000000000000000000000000000..f361ba393a27ced62ab26980585a36089d420d21 GIT binary patch literal 7356 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%0A=g%rpd>bd5HR4kdM5uZP81%95bfiA+)OO# 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 literal 0 HcmV?d00001 diff --git a/python-client/wmill/wmill/client.py b/python-client/wmill/wmill/client.py index 1daf1f3d02..b965bd98b9 100644 --- a/python-client/wmill/wmill/client.py +++ b/python-client/wmill/wmill/client.py @@ -219,6 +219,9 @@ class Windmill: def get_job(self, job_id: str) -> dict: return self.get(f"/w/{self.workspace}/jobs_u/get/{job_id}").json() + def get_id_token(self, audience: str) -> dict: + return self.get(f"/w/{self.workspace}/oidc/token/{audience}").text + def get_job_status(self, job_id: str) -> JobStatus: job = self.get_job(job_id) job_type = job.get("type", "") diff --git a/python-client/wmill_pg/dist/wmill_pg-1.245.0-py3-none-any.whl b/python-client/wmill_pg/dist/wmill_pg-1.245.0-py3-none-any.whl new file mode 100644 index 0000000000000000000000000000000000000000..5e487f5b880b7c1781082e243b5079c7d6695e78 GIT binary patch literal 2299 zcmaKt2{e>@AIE28?6NO~!th>8#xS-b8BB&5VT^3ACd#sxAu*Cv<8%L10(vgxA)6-yo)`@%f4QqjeANwRsMwGg#YxS_e zsQQnd^m0NI-u0}fa2$BcGb?sOw3B6K?C+2j5+QQjYVs)d^q|s7T~^q^2}MGX)7YDJ zi2bzAjQeJ&2+5yIL2X^@h*1cyx2x$X*zUiob6BuR?f$h7zdLFqB;*1$5R1iJ=0XE6 zj!C$MeMoXFETg+)i5hQZlx4kA1|M6XY#8P%Cd9tAtTK<>hq@w7{1-ggRD-yJV3P!a z0od?Z>nObo!BvyZ(Ec?^FlDxWLI2;K`Kshc@tY>ilx;d#GVE01CX(i&l*MQtvOZdB z`gYd0ctpJ6XM<1?V3L1YvUpS0^Rz4JjiD3fr!^PuMz(t>Ao8jX=v{*cN9@`yWOsBu z_<^#p;xz!4i#P2RLid`YY(Y2u**7#4K>}cIw!Mub+`3?QiZ4FrC%W2Z(wql zw1R!euj*U_m|Z{CCYLvn-deqBtm*f@@t5faK9(7$ z_7%)|Hfa-&jvIzpG{mrxsevtamRjinHBMae1sipD#SIJ%!T5`VFYP8X(^}cRs^_NU z7LlE_H-ojY3@)+|C!QnI( z@tn!JGBq^Sv&BD(^n8+!|( z#=TX!Y2_R#O~xvGt{ED2Ldc#!7cr@^u#;>9i&mpNGs(I6oZKMTeGI6JZUE6|hlW+~`2--j@mPXHw4cG+z^Dr~Ac$MHbqU>1+#0eal zm_KLKKN4DBlXi(lsp?=qoi^S=r>NHPYyqE+Cdtwlo0}nFLe(#)@CNny(O096jp;`-B1@fmKSlFlSdWI!zM8u0yrQ!%_iZkH zOPn&DI3aT3Nb&n&az-K$Xki*$fUGDAqBj<;a!DYPTLdkC`KkCxdl7f;bqAR@ro*f} zZP9mRs^ao8{Ue^oB1SrVUE*RHG5NtuMzBOzjfz{c^0n^KQV@Q_t~*>snuk*_!N;0Z@c-!BP+u&~2e#?Y-p6U+c+FB^S;R}Tu~GeAsXH8peKX+tawl&*u0 zICIqSc4?M5Jp8Ng#$KqFiA!ovU zso=jua=(55DEw;wb&vf!gK;2y8iT&p^Ebdhn`0mCf0F!#2JHJj?XO(hqd+{L QEj*mNg3}BJyr19x0O#3~j{pDw literal 0 HcmV?d00001 diff --git a/python-client/wmill_pg/dist/wmill_pg-1.245.0.tar.gz b/python-client/wmill_pg/dist/wmill_pg-1.245.0.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..e539cc5f8882bec7eeb2e5362bc2390890191493 GIT binary patch literal 1933 zcmV;82XgoyiwFn+00002|95R^Y;0d}XDu-@wsZGEr&Ayjk`VJPAZVNC(|^AONb1FLdgH0%>s|1CkVpW# zSS%LY#R8;5I^Unto1YmCnArO&a7ujX-*LLV&9f?Z9nbal@Xg*6AkQQf(EZ-G32z@q zl1J>o9qjk_ok4ftk>N|n8FpWyJ%j&yTt_?%r|H6WiP!5Br*n3Ec=Ya=M8V^(|3R1_>)#!E?jCl``v2;9hP4GCbg^VOO)|LOjQxl&M;eA7NHQce=F=%ga~ z$b8w^Zq{i5;j?G>x19TzYGx!tA3l6Y3%N{U#G^D364y415DM5FN0i6S)(BxhR7^vj zNqm4m;37`;l(iySd`%N;|qL6!<<2NLq9a|+405uvy(R`KjBMOw99I%rFp3_ zZwfJwYwl%N3WN|P-v98IlKd(BzxTQScZ~mk68}G_{r}ME_q?Io?U9#Vx9b`2|84qz z_&k^2Wg?R(duA_QAlfB}quJ;H;FiaJm{WL55Bwcv+jK@lwQ1t=^IiBS_4D77PB#S06 zgikparLmTru#k!uskA@x_#;*2x6d)3;{YNp8vD@l z06X~k>h>1TD45A=1@$b1r|q)8z~h=?wIcB9CsD*@)jke@Qt=D|+)}FPj#Thbp7T;|T5{`={4+86ACvzv`5zPi z)$!ljUrnbx=5jhEY4MfwKaO`d{{zpSiT|DgbCE=tQ~_F*UwL6x{x<{T|Be4Q{@?ij z&;5Vt6aKjO|BwFP^BniC|96H1eNGZ@KPSJ=-&~} zq-evIJC2#J(lI00-b(=+-hlgZtSf=<#L`)S4h_Rjeb{8Ru>Bfa>R@Fo^E6~*XxLUq zE0YP(pCmD}$`cpMvh@FNsz*#W$5jE9RObQmOpT)!bDeO?WAJS;qjE{`Ib)bhpxH?t zseDzG2zAOcPYBZ89EwG@IqIo~sgB7$mHlUG%+OyOUvs%ka;dWn(0}8*#+nSoOLF0} zk}?Gma5C4avARaT7b(MydOQuKM@hL#r0Av_wN~=sEhxS)8#E7PeNvU-#;%SV1iz80 zd{!uCS1@kHOm30ZO?P+)GbJ633%uY!gs&5EnWfZcm5R=o%tgFW(lIE42|Z^jg~N59 z6vxn)&`s5dV8KbIRF6UF6zlUkr2{qhdLlLrlwvE&a%>sdu>{ozOnI61X>QciR#Vg3 z$dxXpzRu-ndnPKsQ>%ZAs-ocMwPXh zAj8D}CjK|^zrmj^{;$ug9%udkZvL<9-HHFZ!(QLS|4)Hu_#G9O5VPsiB^7<09-rd= zz%{G?+g$&rzx-ssIr;I|NA~}}xBqv&{vH4C_6Fwu=ab+alT>Y=?Z1}i-6QM~7oE@u zcC7U+5VWqVj0BzMQAE{t_>bNFKtCrDv(+V!5x(#426|gw4S{~v7emmYzRfYh+c!C? zLYqaDha>#?wGMPv-G{JG_5F$wetpm-!}hBlaQq>pS;ps_0oSTWJR0F&&)-15sYtFs z-cc_XZ%6oOJCD=8-;9nnyIuQ- zBnEqx_65W?BP_vsJ7CI}L2?bdZ*v>^F>XVf(80jKz`(%3z`(%3z`(%3z`(%3z`(%3 Tz`(%Zzk`1RcvLNx08jt`z<%UW literal 0 HcmV?d00001 diff --git a/typescript-client/client.ts b/typescript-client/client.ts index 75ebdff7c8..148b6c7af9 100644 --- a/typescript-client/client.ts +++ b/typescript-client/client.ts @@ -1,6 +1,12 @@ -import { ResourceService, VariableService, JobService, HelpersService } from "./index"; +import { + ResourceService, + VariableService, + JobService, + HelpersService, +} from "./index"; import { OpenAPI } from "./index"; import type { DenoS3LightClientSettings } from "./index"; +import { OidcService } from "./src"; export { AdminService, @@ -269,18 +275,20 @@ export async function databaseUrlFromResource(path: string): Promise { // }); // } -export async function denoS3LightClientSettings(s3_resource_path: string | undefined): Promise { +export async function denoS3LightClientSettings( + s3_resource_path: string | undefined +): Promise { !clientSet && setClient(); const workspace = getWorkspace(); const s3Resource = await HelpersService.s3ResourceInfo({ workspace: workspace, requestBody: { - s3_resource_path: s3_resource_path - } + s3_resource_path: s3_resource_path, + }, }); let settings: DenoS3LightClientSettings = { ...s3Resource, - } + }; return settings; } @@ -316,6 +324,20 @@ export function getResumeEndpoints(approver?: string): Promise<{ return getResumeUrls(approver); } +/** + * Get an OIDC jwt token for auth to external services (e.g: Vault, AWS) (ee only) + * @param audience audience of the token + * @returns jwt token + */ +export async function getIdToken(audience: string): Promise { + !clientSet && setClient(); + const workspace = getWorkspace(); + return await OidcService.getOidcToken({ + workspace, + audience, + }); +} + export function base64ToUint8Array(data: string): Uint8Array { return Uint8Array.from(atob(data), (c) => c.charCodeAt(0)); }