feat: OIDC support (#3017)

* oidc

* update

* feat: add support for multiselect in python

* d

* d

* progress

* all

* all

* all
This commit is contained in:
Ruben Fiszel
2024-01-16 23:32:26 +01:00
committed by GitHub
parent 8207e95e94
commit a24645ee00
29 changed files with 724 additions and 39 deletions
+310 -21
View File
@@ -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",
+1
View File
@@ -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" }
@@ -0,0 +1 @@
-- Add down migration script here
@@ -0,0 +1,2 @@
-- Add up migration script here
ALTER TABLE token ADD COLUMN IF NOT EXISTS job UUID;
+1
View File
@@ -781,6 +781,7 @@ async fn handle_zombie_jobs<R: rsmq_async::RsmqConnection + Send + Sync + Clone>
"ephemeral-script",
*SCRIPT_TOKEN_EXPIRY,
&job.email,
&job.id,
)
.await
.expect("could not create job token");
+2 -2
View File
@@ -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}"
))
+2 -1
View File
@@ -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}
+23
View File
@@ -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
+5 -1
View File
@@ -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::<ApiAuthed>())
.route_layer(from_extractor::<users::Tokened>())
.nest("/jobs", jobs::global_root_service())
.nest("/oidc", oidc::global_service())
.nest(
"/saml",
saml::global_service().layer(Extension(Arc::new(sp_extension.0))),
+296
View File
@@ -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<Keys> {
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<String> {
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<DB>,
) -> windmill_common::error::JsonResult<CoreJsonWebKeySet> {
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<CoreProviderMetadata> {
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<CoreProviderMetadata> {
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<String>,
flow_path: Option<String>,
groups: Vec<String>,
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<DB>,
Path((w_id, audience)): Path<(String, String)>,
) -> windmill_common::error::Result<String> {
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::<JobClaim, CoreGenderClaim>::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())
}
+2
View File
@@ -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;
+2
View File
@@ -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(),
+15 -2
View File
@@ -144,7 +144,9 @@ pub async fn get_reserved_variables(
flow_path: Option<String>,
schedule_path: Option<String>,
step_id: Option<String>,
) -> [ContextualVariable; 15] {
root_flow_id: Option<String>,
jwt_token: Option<String>,
) -> [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(),
},
]
}
+2 -1
View File
@@ -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
@@ -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;
+4
View File
@@ -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();
@@ -48,7 +48,7 @@ pub async fn handle_dedicated_process(
job_dir: &str,
context_envs: HashMap<String, String>,
envs: HashMap<String, String>,
reserved_variables: [variables::ContextualVariable; 15],
reserved_variables: [variables::ContextualVariable; 17],
common_bun_proc_envs: HashMap<String, String>,
args: Vec<&str>,
mut killpill_rx: tokio::sync::broadcast::Receiver<()>,
@@ -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;
@@ -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;
+7 -3
View File
@@ -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<String> {
// 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?;
@@ -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) {
@@ -33,8 +33,14 @@ export const settings: Record<string, Setting[]> = {
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',
+1
View File
@@ -0,0 +1 @@
windmill-api/
Binary file not shown.
Binary file not shown.
+3
View File
@@ -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", "")
Binary file not shown.
Binary file not shown.
+27 -5
View File
@@ -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<string> {
// });
// }
export async function denoS3LightClientSettings(s3_resource_path: string | undefined): Promise<DenoS3LightClientSettings> {
export async function denoS3LightClientSettings(
s3_resource_path: string | undefined
): Promise<DenoS3LightClientSettings> {
!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<string> {
!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));
}