diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 9dc463ff1..eab8f55e8 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -629,19 +629,23 @@ dependencies = [ "anyhow", "async-trait", "axum", + "axum-extra", "base64 0.22.1", "chrono", "http-body-util", + "jsonwebtoken", "migration", "oauth2", "openidconnect", + "reqwest 0.12.28", + "rstest 0.28.0", "rustls 0.23.46", "sea-orm", "serde", "serde_json", "serde_yaml", "sodiumoxide", - "thiserror 2.0.21", + "testcontainers", "tokio", "tracing", ] @@ -654,29 +658,26 @@ dependencies = [ "async-trait", "auth-core", "axum", + "axum-extra", "axum-reverse-proxy", "axum-test", - "bytes", "chrono", "clap", "dotenvy", "hyper 1.9.0", + "jsonwebtoken", "migration", "mockito", - "moka", + "openidconnect", "regex", "reqwest 0.12.28", "sea-orm", "serde", "serde_json", "testcontainers", - "thiserror 2.0.21", "tokio", - "tokio-tungstenite 0.28.0", - "tower", "tower-http", "tower-sessions", - "tower-sessions-cookie-store", "tracing", "tracing-subscriber", "url", @@ -1654,12 +1655,7 @@ version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" dependencies = [ - "base64 0.22.1", - "hmac 0.12.1", "percent-encoding", - "rand 0.8.6", - "sha2 0.10.9", - "subtle", "time", "version_check", ] @@ -2799,13 +2795,14 @@ dependencies = [ [[package]] name = "graph-proxy" -version = "0.4.0" +version = "0.4.1" dependencies = [ "anyhow", "argo-workflows-openapi", "async-graphql", "async-graphql-axum", "async-stream", + "auth-core", "aws-credential-types", "aws-sdk-s3", "axum", @@ -2829,7 +2826,7 @@ dependencies = [ "opentelemetry_sdk", "regex", "reqwest 0.12.28", - "rstest", + "rstest 0.26.1", "rustls 0.23.46", "schemars 1.2.1", "secrecy", @@ -3839,9 +3836,9 @@ dependencies = [ [[package]] name = "jsonwebtoken" -version = "10.3.0" +version = "11.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0529410abe238729a60b108898784df8984c87f6054c9c4fcacc47e4803c1ce1" +checksum = "e75fe14a82d81e5f5af639997db37d8b96045938a7ac6ab18cdbe1c7467e05e1" dependencies = [ "aws-lc-rs", "base64 0.22.1", @@ -3852,6 +3849,7 @@ dependencies = [ "serde_json", "signature 2.2.0", "simple_asn1", + "zeroize", ] [[package]] @@ -5622,7 +5620,18 @@ checksum = "f5a3193c063baaa2a95a33f03035c8a72b83d97a54916055ba22d35ed3839d49" dependencies = [ "futures-timer", "futures-util", - "rstest_macros", + "rstest_macros 0.26.1", +] + +[[package]] +name = "rstest" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b376e512f30a7686a8dbc6aeaace99b3ade1c26c1c63473473a817ade226dafc" +dependencies = [ + "futures-timer", + "futures-util", + "rstest_macros 0.28.0", ] [[package]] @@ -5643,6 +5652,24 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "rstest_macros" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88512dbfb5b73a0dfa38aef7bc73b5657035fce477b923faf12d332dd83051ea" +dependencies = [ + "cfg-if", + "glob", + "proc-macro-crate", + "proc-macro2", + "quote", + "regex", + "relative-path", + "rustc_version", + "syn 2.0.119", + "unicode-ident", +] + [[package]] name = "rust-multipart-rfc7578_2" version = "0.8.0" @@ -7465,11 +7492,7 @@ checksum = "d25a406cddcc431a75d3d9afc6a7c0f7428d4891dd973e4d54c56b46127bf857" dependencies = [ "futures-util", "log", - "rustls 0.23.46", - "rustls-native-certs 0.8.3", - "rustls-pki-types", "tokio", - "tokio-rustls 0.26.4", "tungstenite 0.28.0", ] @@ -7722,25 +7745,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "tower-sessions-cookie-store" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8831be41b579a91b2b4084e2a8b5a383ac4e73a93c2a0dbb0c1d232a7e3f7247" -dependencies = [ - "async-trait", - "base64 0.22.1", - "http 1.4.2", - "serde", - "serde_json", - "time", - "tower-cookies", - "tower-layer", - "tower-service", - "tower-sessions-core", - "tracing", -] - [[package]] name = "tower-sessions-core" version = "0.14.0" @@ -7870,8 +7874,6 @@ dependencies = [ "httparse", "log", "rand 0.9.4", - "rustls 0.23.46", - "rustls-pki-types", "sha1 0.10.6", "thiserror 2.0.21", "utf-8", @@ -8953,6 +8955,20 @@ name = "zeroize" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] [[package]] name = "zerotrie" diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 4698cffa6..0765ae74f 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -17,13 +17,13 @@ async-trait = { version = "0.1.89" } anyhow = { version = "1.0.100" } clap = { version = "4.5.49", features = ["derive", "env"] } chrono = { version = "0.4.42" } -derive_more = { version = "2.0.1" , features = [ +derive_more = { version = "2.0.1", features = [ "deref", "deref_mut", "from", "from_str", "into", -]} +] } axum-reverse-proxy = { version = "1.3.0" } dotenvy = { version = "0.15.7" } mockito = { version = "1.7.0" } @@ -31,7 +31,7 @@ reqwest = { version = "0.12.15", default-features = false, features = [ "rustls-tls", "json", "blocking", - "stream" + "stream", ] } serde = { version = "1.0.228", features = ["derive"] } serde_json = { version = "1.0.145" } @@ -48,24 +48,41 @@ tracing-subscriber = { version = "0.3.20" } url = { version = "2.5.4" } k8s-openapi = { version = "0.28.0", features = ["v1_35"] } kube = { version = "4.2.0", features = ["runtime", "derive"] } -ldap3 = { version = "0.11.5", default-features = false, features = ["tls-rustls"] } -sqlx = { version = "0.8.6", default-features = false, features = ["runtime-tokio", "tls-rustls", "mysql", "time"] } +ldap3 = { version = "0.11.5", default-features = false, features = [ + "tls-rustls", +] } +sqlx = { version = "0.8.6", default-features = false, features = [ + "runtime-tokio", + "tls-rustls", + "mysql", + "time", +] } strum = { version = "0.27.2", features = ["derive"] } time = { version = "0.3.47" } openidconnect = "4.0.1" opentelemetry = { version = "0.32.0" } opentelemetry_sdk = { version = "0.32.1", features = ["rt-tokio"] } -opentelemetry-otlp = { version = "0.32.0", features = ["metrics", "grpc-tonic"] } +opentelemetry-otlp = { version = "0.32.0", features = [ + "metrics", + "grpc-tonic", +] } opentelemetry-semantic-conventions = "0.32.0" tracing-opentelemetry = { version = "0.33.0" } axum = { version = "0.8.6" } +axum-extra = { version = "0.12.6", features = ["typed-header"] } regex = "1.12.2" rustls = "0.23.35" testcontainers = { version = "0.27.2", features = ["http_wait_plain"] } tower-http = { version = "0.6.6", features = ["cors"] } -sea-orm = { version = "2.0.0-rc", features = [ "sqlx-postgres", "runtime-tokio-rustls", "macros", "with-chrono" ] } +sea-orm = { version = "2.0.0-rc", features = [ + "sqlx-postgres", + "runtime-tokio-rustls", + "macros", + "with-chrono", +] } tokio-tungstenite = "0.28.0" tower = "0.5.3" moka = { version = "0.12.11", features = ["future"] } env_logger = "0.11.8" axum-test = "18.4.1" +jsonwebtoken = { version = "11.1.0", features = ["aws_lc_rs"] } diff --git a/backend/auth-core/Cargo.toml b/backend/auth-core/Cargo.toml index 9d3831cb9..40d15cf29 100644 --- a/backend/auth-core/Cargo.toml +++ b/backend/auth-core/Cargo.toml @@ -15,20 +15,26 @@ publish = false [dependencies] anyhow = { workspace = true, features = ["backtrace"] } -async-trait = {workspace = true } +async-trait = { workspace = true } axum = { workspace = true, features = ["macros"] } +axum-extra = { workspace = true } base64 = "0.22.1" chrono = { workspace = true } http-body-util = "0.1.3" +jsonwebtoken = { workspace = true } migration = { version = "0.1.0", path = "migration" } oauth2 = "5.0.0" -openidconnect = { workspace = true, features = ["timing-resistant-secret-traits"] } +openidconnect = { workspace = true, features = [ + "timing-resistant-secret-traits", +] } +reqwest.workspace = true +rstest = "0.28.0" rustls = { workspace = true, features = ["aws-lc-rs"] } sea-orm = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } serde_yaml = { workspace = true } sodiumoxide = "0.2.7" -thiserror = { workspace = true } +testcontainers = { workspace = true } tokio = { workspace = true, features = ["full"] } tracing = { workspace = true } diff --git a/backend/auth-core/src/lib.rs b/backend/auth-core/src/lib.rs index f3350b58f..80a6eb212 100644 --- a/backend/auth-core/src/lib.rs +++ b/backend/auth-core/src/lib.rs @@ -6,6 +6,8 @@ pub mod healthcheck; pub mod middleware; pub mod oidc; pub mod request; +pub mod test_oidc_server; +pub mod validate_token; pub use async_trait; pub use base64; pub use oauth2; diff --git a/backend/graph-proxy/src/test_oidc_server.rs b/backend/auth-core/src/test_oidc_server.rs similarity index 100% rename from backend/graph-proxy/src/test_oidc_server.rs rename to backend/auth-core/src/test_oidc_server.rs index 6c6296569..8ba1b1ccc 100644 --- a/backend/graph-proxy/src/test_oidc_server.rs +++ b/backend/auth-core/src/test_oidc_server.rs @@ -2,8 +2,8 @@ use std::time::Duration; use anyhow::Context; use axum_extra::headers::Authorization; -use testcontainers::core::wait::HttpWaitStrategy; use testcontainers::core::WaitFor; +use testcontainers::core::wait::HttpWaitStrategy; use testcontainers::runners::AsyncRunner; use testcontainers::{ContainerAsync, GenericImage, ImageExt}; diff --git a/backend/graph-proxy/src/validate_token.rs b/backend/auth-core/src/validate_token.rs similarity index 96% rename from backend/graph-proxy/src/validate_token.rs rename to backend/auth-core/src/validate_token.rs index 0200b8d76..d82d85570 100644 --- a/backend/graph-proxy/src/validate_token.rs +++ b/backend/auth-core/src/validate_token.rs @@ -1,20 +1,20 @@ use axum::http::Uri; -use axum_extra::headers::{authorization::Bearer, Authorization}; -use jsonwebtoken::{decode, decode_header, jwk::JwkSet, Algorithm, DecodingKey, Validation}; +use axum_extra::headers::{Authorization, authorization::Bearer}; +use jsonwebtoken::{Algorithm, DecodingKey, Validation, decode, decode_header, jwk::JwkSet}; use openidconnect::{ - core::CoreClient, AccessToken, ClientId, ClientSecret, EndpointMaybeSet, EndpointNotSet, - EndpointSet, IntrospectionUrl, IssuerUrl, TokenIntrospectionResponse, + AccessToken, ClientId, ClientSecret, EndpointMaybeSet, EndpointNotSet, EndpointSet, + IntrospectionUrl, IssuerUrl, TokenIntrospectionResponse, core::CoreClient, }; use serde::{Deserialize, Serialize}; use openidconnect::{ + AdditionalProviderMetadata, ProviderMetadata, core::{ CoreAuthDisplay, CoreClaimName, CoreClaimType, CoreClientAuthMethod, CoreGrantType, CoreJsonWebKey, CoreJweContentEncryptionAlgorithm, CoreJweKeyManagementAlgorithm, CoreResponseMode, CoreResponseType, CoreSubjectIdentifierType, }, - AdditionalProviderMetadata, ProviderMetadata, }; use serde_json::Value; diff --git a/backend/auth-gateway/Cargo.toml b/backend/auth-gateway/Cargo.toml index 6be6bced6..f4a848e60 100644 --- a/backend/auth-gateway/Cargo.toml +++ b/backend/auth-gateway/Cargo.toml @@ -11,30 +11,28 @@ path = "src/bin/keygen.rs" publish = false [dependencies] -async-trait = {workspace = true } +async-trait = { workspace = true } auth-core = { path = "../auth-core" } anyhow = { workspace = true, features = ["backtrace"] } axum = { workspace = true, features = ["macros"] } -axum-reverse-proxy = { workspace = true, features=["full"] } -bytes = "1.11.0" +axum-extra = { workspace = true } +axum-reverse-proxy = { workspace = true, features = ["full"] } chrono.workspace = true clap.workspace = true dotenvy.workspace = true hyper = "1.8.1" -moka.workspace = true +openidconnect.workspace = true serde.workspace = true serde_json.workspace = true -thiserror.workspace = true tokio = { workspace = true, features = ["full"] } tower-sessions = "0.14.0" tower-http = { workspace = true } regex = { workspace = true } reqwest = { workspace = true } -tower-sessions-cookie-store = "0.5.2" -tower = { workspace = true } -tokio-tungstenite = { workspace = true, features = ["rustls-tls-native-roots"] } tracing = { workspace = true } tracing-subscriber = { workspace = true, features = ["env-filter"] } +jsonwebtoken = { workspace = true } + [dev-dependencies] axum-test = { workspace = true } diff --git a/backend/auth-gateway/src/client_login.rs b/backend/auth-gateway/src/client_login.rs new file mode 100644 index 000000000..b7982438c --- /dev/null +++ b/backend/auth-gateway/src/client_login.rs @@ -0,0 +1,161 @@ +use crate::auth_session_data::TokenSessionData; +use crate::state::AppState; + +use auth_core::database::write_token_to_database; +use auth_core::oauth2::{ClientId, ClientSecret, Scope}; +use auth_core::oidc::OidcClient; +use auth_core::openidconnect::core::CoreProviderMetadata; +use auth_core::openidconnect::{IssuerUrl, SubjectIdentifier, TokenResponse}; +use auth_core::validate_token::{ValidatedAuthToken, ValidationMethod}; +use axum_extra::TypedHeader; +use axum_extra::headers::Authorization; +use axum_extra::headers::authorization::Bearer; +use jsonwebtoken::dangerous::insecure_decode; + +use axum::response::{IntoResponse, Response}; +use axum::{Json, extract::State}; + +use hyper::StatusCode; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; + +#[derive(Debug, Deserialize, Serialize)] +pub struct IdTokenClaims { + iss: String, + preferred_username: String, + sub: String, + name: Option, + fedid: Option, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct ClientCredentials { + pub client_id: String, + pub client_secret: String, +} + +pub struct AppError { + status: StatusCode, + message: String, +} +impl IntoResponse for AppError { + fn into_response(self) -> Response { + (self.status, self.message).into_response() + } +} + +impl AppError { + fn new(status: StatusCode, message: impl Into) -> Self { + Self { + status, + message: message.into(), + } + } + + fn new_500(message: impl Into) -> Self { + Self::new(StatusCode::INTERNAL_SERVER_ERROR, message) + } +} + +pub async fn client_credential_login( + State(state): State>, + TypedHeader(auth_header): TypedHeader>, + Json(client_creds): Json, +) -> Result { + let http_client = reqwest::ClientBuilder::new() + .redirect(reqwest::redirect::Policy::none()) + .build() + .map_err(|e| { + AppError::new( + e.status().unwrap_or(StatusCode::INTERNAL_SERVER_ERROR), + e.to_string(), + ) + })?; + + let provider_metadata = + CoreProviderMetadata::discover_async(state.oidc_provider_url.clone(), &http_client) + .await + .map_err(|e| AppError { + status: StatusCode::SERVICE_UNAVAILABLE, + message: format!("OIDC discovery failed: {e}"), + })?; + + let auth_token = state + .token_validator + .validate_token(Some(auth_header), ValidationMethod::Jwt) + .await; + + match auth_token { + ValidatedAuthToken::Valid(_) => {} + ValidatedAuthToken::Invalid => { + return Err(AppError::new( + StatusCode::UNAUTHORIZED, + "Invalid token".to_string(), + )); + } + ValidatedAuthToken::Missing => { + return Err(AppError::new( + StatusCode::UNAUTHORIZED, + "Missing auth token".to_string(), + )); + } + ValidatedAuthToken::Failed(e) => { + return Err(AppError::new_500(e)); + } + }; + + let client = OidcClient::from_provider_metadata( + provider_metadata, + ClientId::new(client_creds.client_id), + Some(ClientSecret::new(client_creds.client_secret)), + ); + let token_response = match client + .exchange_client_credentials() + .unwrap_or(Err(AppError::new_500( + "Client credentials flow not supported", + ))?) + .add_scope(Scope::new("offline_access".to_string())) + .add_scope(Scope::new("openid".to_string())) + .add_scope(Scope::new("fedid".to_string())) + .request_async(&http_client) + .await + { + Ok(resp) => resp, + Err(e) => { + tracing::warn!("Client credential exchange failed: {e}"); + return Err(AppError::new( + StatusCode::UNAUTHORIZED, + "Invalid client credentials", + )); + } + }; + + let id_token = token_response.id_token().ok_or(AppError::new( + StatusCode::BAD_GATEWAY, + "No ID token was obtained for these client credentials".to_string(), + ))?; + + let claims: IdTokenClaims = insecure_decode::(&id_token.to_string()) + .map_err(|_| AppError::new_500("Unexpected ID token format".to_string()))? + .claims; + + let token_data = TokenSessionData::from_token_response( + &token_response, + IssuerUrl::new(claims.iss) + .map_err(|_| AppError::new_500("Could not convert iss claim to URL".to_string()))?, + SubjectIdentifier::new(claims.sub), + claims.name, + Some(claims.preferred_username), + claims.fedid, + ) + .map_err(|_| AppError::new_500("Failed to construct token session data"))?; + + write_token_to_database(&state.database_connection, &token_data, &state.public_key) + .await + .map_err(|e| { + tracing::error!("Database write failed: {:?}", e); + AppError::new_500("Failed to write token to database") + })?; + + Ok(StatusCode::OK) +} diff --git a/backend/auth-gateway/src/main.rs b/backend/auth-gateway/src/main.rs index 3c8249d65..870112c31 100644 --- a/backend/auth-gateway/src/main.rs +++ b/backend/auth-gateway/src/main.rs @@ -1,5 +1,6 @@ mod auth_session_data; mod callback; +mod client_login; mod config; mod login; mod state; @@ -159,6 +160,10 @@ fn create_router( // it any other routes should be wrapped by CorsLayer above if not having a similar bearer // token authentication to `/auth/status` .route("/auth/status", get(status).layer(status_cors)) + .route( + "/auth/client-creds", + post(client_login::client_credential_login), + ) .with_state(state) } diff --git a/backend/auth-gateway/src/state.rs b/backend/auth-gateway/src/state.rs index 462712a5b..84ae42d00 100644 --- a/backend/auth-gateway/src/state.rs +++ b/backend/auth-gateway/src/state.rs @@ -1,5 +1,7 @@ use async_trait::async_trait; use axum::http::StatusCode; +use hyper::Uri; +use openidconnect::IssuerUrl; use regex::Regex; use std::sync::Arc; use tower_sessions::Session; @@ -14,6 +16,7 @@ use auth_core::oidc::{ DbConnection, HttpClient, OidcClient, SodiumPublicKey, create_db_connection, create_oidc_client, decode_public_key, exchange_refresh_token, }; +use auth_core::validate_token::TokenValidator; #[derive(Debug, Clone)] pub struct AppState { @@ -25,6 +28,8 @@ pub struct AppState { pub callback_default_return_to_url: String, pub cors_allow: Option>, pub session_secure: bool, + pub oidc_provider_url: IssuerUrl, + pub token_validator: TokenValidator, } impl AppState { @@ -46,6 +51,15 @@ impl AppState { .map(|it| Regex::new(&it).expect("Invalid regex in config")) .collect() }); + let oidc_issuer_url = Uri::try_from(config.common.oidc_provider_url.as_str())?; + let token_validator = TokenValidator::new( + &oidc_issuer_url, + config.common.client_id, + config.common.client_secret, + vec!["graph"], + ) + .await?; + Ok(AppState { http_client, oidc_client, @@ -55,6 +69,8 @@ impl AppState { callback_default_return_to_url: config.callback_default_return_to_url, cors_allow, session_secure: config.session_secure.unwrap_or(true), + oidc_provider_url: IssuerUrl::new(config.common.oidc_provider_url)?, + token_validator, }) } } diff --git a/backend/auth-gateway/src/tests.rs b/backend/auth-gateway/src/tests.rs index 6f0c90255..6a387e05a 100644 --- a/backend/auth-gateway/src/tests.rs +++ b/backend/auth-gateway/src/tests.rs @@ -7,9 +7,12 @@ use auth_core::config::CommonConfig; use auth_core::entity::oidc_tokens; use auth_core::oidc::create_oidc_client; use auth_core::sea_orm::{ColumnTrait, Database, EntityTrait, PaginatorTrait, QueryFilter}; +use auth_core::validate_token::TokenValidator; use axum::http::{StatusCode, header}; use axum_test::TestServer; +use hyper::Uri; use mockito::{Matcher, Server}; +use openidconnect::IssuerUrl; use regex::Regex; use serde_json::json; use testcontainers::core::wait::HttpWaitStrategy; @@ -90,6 +93,17 @@ async fn test_app_state_with( ) .expect("valid test public key"); + let uri: Uri = oidc_provider_url + .parse() + .expect("OIDC provider URL should be convertible to Uri"); + let token_validator = + TokenValidator::new(&uri, config.client_id, config.client_secret, vec!["test"]) + .await + .expect("test validator is setup"); + + let issuer_url = IssuerUrl::new(oidc_provider_url.to_string()) + .expect("OIDC provider URL should be convertible to IssuerUrl"); + Arc::new(AppState { http_client, oidc_client, @@ -99,6 +113,8 @@ async fn test_app_state_with( callback_default_return_to_url: "http://localhost/dashboard".to_string(), cors_allow, session_secure: false, + token_validator, + oidc_provider_url: issuer_url, }) } diff --git a/backend/graph-proxy/Cargo.toml b/backend/graph-proxy/Cargo.toml index 5b5ea1567..0452c30e6 100644 --- a/backend/graph-proxy/Cargo.toml +++ b/backend/graph-proxy/Cargo.toml @@ -7,20 +7,21 @@ license-file = "../../LICENSE" [dependencies] anyhow = { workspace = true } +auth-core = { path = "../auth-core" } aws-credential-types = { version = "1.2.2" } -aws-sdk-s3 = { version = "1.82.0" , features = ["behavior-version-latest"]} +aws-sdk-s3 = { version = "1.82.0", features = ["behavior-version-latest"] } argo-workflows-openapi = { path = "../argo-workflows-openapi" } async-graphql = { version = "7.0.17", features = ["chrono", "url"] } async-graphql-axum = { version = "7.0.17" } axum = { workspace = true } -axum-extra = { version = "0.12.1", features = ["typed-header"] } +axum-extra = { workspace = true } chrono = { workspace = true } clap = { workspace = true } derive_more = { workspace = true } dotenvy = { workspace = true } lazy_static = { version = "1.5.0" } mime_guess = "2.0.5" -regex = { workspace = true} +regex = { workspace = true } reqwest = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } @@ -44,12 +45,15 @@ opentelemetry = { workspace = true } opentelemetry_sdk = { workspace = true } openidconnect = { workspace = true } rstest = "0.26.1" -jsonwebtoken = { version = "10.3.0", features = ["aws_lc_rs"] } +jsonwebtoken = { workspace = true } schemars = "1.2.1" tempfile = "3.27.0" [dev-dependencies] -graphql-ws-client = { version = "0.13.0", features = ["client-graphql-client", "tungstenite-0.28"] } +graphql-ws-client = { version = "0.13.0", features = [ + "client-graphql-client", + "tungstenite-0.28", +] } graphql_client = "0.16.0" mockito = { workspace = true } testcontainers = { workspace = true } diff --git a/backend/graph-proxy/src/graphql/auth_guard.rs b/backend/graph-proxy/src/graphql/auth_guard.rs index 75ebb404f..d82de22bf 100644 --- a/backend/graph-proxy/src/graphql/auth_guard.rs +++ b/backend/graph-proxy/src/graphql/auth_guard.rs @@ -2,7 +2,7 @@ use std::fmt::Display; use async_graphql::{Context, Error, ErrorExtensions, Guard, Result}; -use crate::validate_token::ValidatedAuthToken; +use auth_core::validate_token::ValidatedAuthToken; /// Checks that OIDC authn token is valid pub struct AuthGuard; diff --git a/backend/graph-proxy/src/graphql/mod.rs b/backend/graph-proxy/src/graphql/mod.rs index 78bdedfd4..0e89d2ac4 100644 --- a/backend/graph-proxy/src/graphql/mod.rs +++ b/backend/graph-proxy/src/graphql/mod.rs @@ -15,8 +15,8 @@ mod workflow_templates; /// GraphQL operations related to workflows mod workflows; +use crate::graphql::auth_guard::AuthGuard; use crate::RouterState; -use crate::{graphql::auth_guard::AuthGuard, validate_token::ValidationMethod}; use self::{ subscription::WorkflowsSubscription, @@ -29,6 +29,7 @@ use async_graphql::{ MergedSubscription, Object, Schema, SchemaBuilder, SimpleObject, Union, Value, ID, }; use async_graphql_axum::{GraphQLRequest, GraphQLResponse}; +use auth_core::validate_token::ValidationMethod; use axum::extract::State; use axum_extra::{ headers::{authorization::Bearer, Authorization}, diff --git a/backend/graph-proxy/src/graphql/subscription.rs b/backend/graph-proxy/src/graphql/subscription.rs index 3885d3f5d..098219995 100644 --- a/backend/graph-proxy/src/graphql/subscription.rs +++ b/backend/graph-proxy/src/graphql/subscription.rs @@ -4,6 +4,7 @@ use argo_workflows_openapi::{ }; use async_graphql::{Context, SimpleObject, Subscription}; use async_stream::stream; +use auth_core::validate_token::ValidatedAuthToken; use eventsource_stream::Eventsource; use futures_util::{Stream, StreamExt}; use serde::Deserialize; @@ -15,7 +16,6 @@ use crate::{ VisitInput, }, s3client::{Client as S3Client, S3Bucket}, - validate_token::ValidatedAuthToken, ArgoServerUrl, }; @@ -667,6 +667,7 @@ mod tests { use std::{env, fs, path::PathBuf}; use async_graphql::Request; + use auth_core::validate_token::ValidatedAuthToken; use axum_extra::headers::Authorization; use futures_util::StreamExt; use mockito::Matcher; @@ -676,7 +677,6 @@ mod tests { use crate::graphql::root_schema_builder; use crate::graphql::Visit; - use crate::validate_token::ValidatedAuthToken; use crate::{ArgoServerUrl, Client, S3Bucket, S3ClientArgs}; fn test_token() -> ValidatedAuthToken { diff --git a/backend/graph-proxy/src/graphql/subscription_integration.rs b/backend/graph-proxy/src/graphql/subscription_integration.rs index 95fafcd04..91d75493c 100644 --- a/backend/graph-proxy/src/graphql/subscription_integration.rs +++ b/backend/graph-proxy/src/graphql/subscription_integration.rs @@ -24,6 +24,7 @@ use async_graphql::{ }, Data, Executor, Result, }; +use auth_core::validate_token::{TokenValidator, ValidationMethod}; use axum::{ body::{Body, HttpBody}, extract::{ @@ -47,10 +48,7 @@ use futures_util::{ use opentelemetry::KeyValue; use tower_service::Service; -use crate::{ - metrics::MetricsState, - validate_token::{TokenValidator, ValidationMethod}, -}; +use crate::metrics::MetricsState; /// A GraphQL protocol extractor. /// @@ -390,12 +388,12 @@ where #[cfg(test)] mod tests { use super::*; - use crate::{ - metrics::{noop::NoopMeterProvider, Metrics}, + use crate::metrics::{noop::NoopMeterProvider, Metrics}; + use async_graphql::{Context, EmptyMutation, Object, Schema, Subscription}; + use auth_core::{ test_oidc_server::TestOidcServer, - validate_token::ValidatedAuthToken, + validate_token::{TokenValidator, ValidatedAuthToken}, }; - use async_graphql::{Context, EmptyMutation, Object, Schema, Subscription}; use axum::{http::Uri, routing::get_service, Router}; use axum_extra::headers::HeaderMapExt; use futures_util::Stream; diff --git a/backend/graph-proxy/src/graphql/triggers.rs b/backend/graph-proxy/src/graphql/triggers.rs index f38b605fd..350fd0f21 100644 --- a/backend/graph-proxy/src/graphql/triggers.rs +++ b/backend/graph-proxy/src/graphql/triggers.rs @@ -455,11 +455,11 @@ impl TriggerMutation { mod tests { use crate::{ graphql::triggers::{TriggerMutation, TriggerQuery}, - validate_token::ValidatedAuthToken, KubernetesApiUrl, }; use async_graphql::{EmptySubscription, PathSegment, Pos, Schema, ServerError}; + use auth_core::validate_token::ValidatedAuthToken; use axum_extra::headers::Authorization; use jsonwebtoken::{encode, EncodingKey, Header}; use mockito::{Matcher, ServerGuard}; diff --git a/backend/graph-proxy/src/graphql/workflow_templates.rs b/backend/graph-proxy/src/graphql/workflow_templates.rs index 5b247076b..fd07f4340 100644 --- a/backend/graph-proxy/src/graphql/workflow_templates.rs +++ b/backend/graph-proxy/src/graphql/workflow_templates.rs @@ -4,7 +4,7 @@ use super::{ workflows::Workflow, VisitInput, CLIENT, }; -use crate::{graphql::auth_guard::AuthGuard, validate_token::ValidatedAuthToken}; +use crate::graphql::auth_guard::AuthGuard; use crate::{graphql::filters::WorkflowTemplatesFilter, ArgoServerUrl, SessionSpacesApiUrl}; use anyhow::anyhow; use argo_workflows_openapi::APIResult; @@ -12,6 +12,7 @@ use async_graphql::{ connection::{Connection, CursorType, Edge, EmptyFields, OpaqueCursor}, Context, Json, Object, SimpleObject, }; +use auth_core::validate_token::ValidatedAuthToken; use kube::{ api::{ApiResource, DynamicObject}, core::GroupVersionKind, @@ -425,11 +426,12 @@ fn to_argo_parameter(name: String, value: Value) -> Result, serde #[cfg(test)] mod tests { - use crate::{graphql::auth_guard::AuthErrorCode, validate_token::ValidatedAuthToken}; + use crate::graphql::auth_guard::AuthErrorCode; use super::WorkflowTemplatesQuery; use anyhow::Ok; use async_graphql::{EmptyMutation, EmptySubscription, Schema}; + use auth_core::validate_token::ValidatedAuthToken; use axum_extra::headers::Authorization; use rstest::rstest; use serde_json::json; diff --git a/backend/graph-proxy/src/graphql/workflows.rs b/backend/graph-proxy/src/graphql/workflows.rs index 09aa6d915..4120750cb 100644 --- a/backend/graph-proxy/src/graphql/workflows.rs +++ b/backend/graph-proxy/src/graphql/workflows.rs @@ -1,7 +1,6 @@ use super::{Visit, VisitInput, CLIENT}; use crate::{ graphql::{auth_guard::AuthGuard, filters::WorkflowFilter, subscription::get_auth_token}, - validate_token::ValidatedAuthToken, ArgoServerUrl, S3Bucket, }; use argo_workflows_openapi::{ @@ -13,6 +12,7 @@ use async_graphql::{ connection::{Connection, CursorType, Edge, EmptyFields, OpaqueCursor}, Context, Enum, Object, SimpleObject, Union, ID, }; +use auth_core::validate_token::ValidatedAuthToken; use aws_sdk_s3::presigning::PresigningConfig; use axum_extra::headers::{authorization::Bearer, Authorization}; use chrono::{DateTime, Utc}; @@ -1009,8 +1009,8 @@ impl WorkflowCreator { mod tests { use crate::graphql::auth_guard::AuthErrorCode; use crate::graphql::{root_schema_builder, Authorization, Visit}; - use crate::validate_token::ValidatedAuthToken; use crate::{ArgoServerUrl, Client, S3Bucket, S3ClientArgs}; + use auth_core::validate_token::ValidatedAuthToken; use jsonwebtoken::{encode, EncodingKey, Header}; use rstest::rstest; use serde::{Deserialize, Serialize}; diff --git a/backend/graph-proxy/src/main.rs b/backend/graph-proxy/src/main.rs index a9f6184fa..44c9dc49d 100644 --- a/backend/graph-proxy/src/main.rs +++ b/backend/graph-proxy/src/main.rs @@ -13,18 +13,12 @@ mod sessionspaces; /// Graph-proxy-specific metrics mod metrics; -/// Validate Bearer tokens -mod validate_token; - -#[cfg(test)] -mod test_oidc_server; - use crate::{ graphql::subscription_integration::GraphQLSubscription, metrics::{Metrics, MetricsState}, - validate_token::TokenValidator, }; use async_graphql::{http::GraphiQLSource, SDLExportOptions}; +use auth_core::validate_token::TokenValidator; use axum::{ http::Uri, response::Html, diff --git a/workflows-cli/Cargo.lock b/workflows-cli/Cargo.lock index 56e3fbf95..3364c76e0 100644 --- a/workflows-cli/Cargo.lock +++ b/workflows-cli/Cargo.lock @@ -295,6 +295,22 @@ dependencies = [ "serde_json", ] +[[package]] +name = "astral-tokio-tar" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b18457efd137254e016bbde5e1d88df61c4e1a5ae2223746e56123bac6af2463" +dependencies = [ + "futures-core", + "libc", + "portable-atomic", + "rustc-hash", + "rustix", + "tokio", + "tokio-stream", + "xattr", +] + [[package]] name = "async-stream" version = "0.3.6" @@ -350,19 +366,23 @@ dependencies = [ "anyhow", "async-trait", "axum", + "axum-extra", "base64 0.22.1", "chrono", "http-body-util", + "jsonwebtoken", "migration", "oauth2", "openidconnect", + "reqwest 0.12.28", + "rstest", "rustls", "sea-orm", "serde", "serde_json", "serde_yaml", "sodiumoxide", - "thiserror 2.0.21", + "testcontainers", "tokio", "tracing", ] @@ -380,6 +400,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b281d307588d634de920874890732659e2e7672f72b5e10e81badc1a8a83621e" dependencies = [ "aws-lc-sys", + "untrusted 0.7.1", "zeroize", ] @@ -449,6 +470,28 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum-extra" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be44683b41ccb9ab2d23a5230015c9c3c55be97a25e4428366de8873103f7970" +dependencies = [ + "axum", + "axum-core", + "bytes", + "futures-core", + "futures-util", + "headers", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "axum-macros" version = "0.5.1" @@ -478,6 +521,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + [[package]] name = "base64ct" version = "1.8.3" @@ -543,6 +592,80 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bollard" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee04c4c84f1f811b017f2fbb7dd8815c976e7ca98593de9c1e2afad0f636bff4" +dependencies = [ + "async-stream", + "base64 0.22.1", + "bitflags", + "bollard-buildkit-proto", + "bollard-stubs", + "bytes", + "futures-core", + "futures-util", + "hex", + "home", + "http", + "http-body-util", + "hyper", + "hyper-named-pipe", + "hyper-rustls", + "hyper-util", + "hyperlocal", + "log", + "num", + "pin-project-lite", + "rand 0.9.5", + "rustls", + "rustls-native-certs", + "rustls-pki-types", + "serde", + "serde_derive", + "serde_json", + "serde_urlencoded", + "thiserror 2.0.21", + "time", + "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.52.1-rc.29.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f0a8ca8799131c1837d1282c3f81f31e76ceb0ce426e04a7fe1ccee3287c066" +dependencies = [ + "base64 0.22.1", + "bollard-buildkit-proto", + "bytes", + "prost", + "serde", + "serde_json", + "serde_repr", + "time", +] + [[package]] name = "borrow-or-share" version = "0.2.4" @@ -1035,6 +1158,17 @@ dependencies = [ "syn 3.0.9", ] +[[package]] +name = "docker_credential" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29547a1dc60885a552306986316bc9701ba120c1a8db6769fa68691529ad373d" +dependencies = [ + "base64 0.22.1", + "serde", + "serde_json", +] + [[package]] name = "dotenvy" version = "0.15.7" @@ -1175,6 +1309,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.2", +] + [[package]] name = "event-listener" version = "5.4.2" @@ -1202,6 +1346,17 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" +[[package]] +name = "ferroid" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee93edf3c501f0035bbeffeccfed0b79e14c311f12195ec0e661e114a0f60da4" +dependencies = [ + "portable-atomic", + "rand 0.10.2", + "web-time", +] + [[package]] name = "ff" version = "0.13.1" @@ -1369,6 +1524,17 @@ version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" +[[package]] +name = "futures-macro" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + [[package]] name = "futures-sink" version = "0.3.34" @@ -1381,6 +1547,12 @@ version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" +[[package]] +name = "futures-timer" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af43fadb8a98512d547e37b4e92e0ced13e205c061b87b4623eff01d918d6968" + [[package]] name = "futures-util" version = "0.3.34" @@ -1390,6 +1562,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-io", + "futures-macro", "futures-sink", "futures-task", "memchr", @@ -1555,6 +1728,30 @@ dependencies = [ "hashbrown 0.15.3", ] +[[package]] +name = "headers" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc2700e3fe555c3310aa7286cac6167449f2c87e0eb58769c9208a1c58a1d106" +dependencies = [ + "base64 0.22.1", + "bytes", + "headers-core", + "http", + "httpdate", + "mime", + "sha1", +] + +[[package]] +name = "headers-core" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" +dependencies = [ + "http", +] + [[package]] name = "heck" version = "0.4.1" @@ -1667,6 +1864,20 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-named-pipe" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fab3637d6b04a8037af8a266fdf6cf92ea957e8c53981a2bf6136572531025bf" +dependencies = [ + "hex", + "hyper", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + [[package]] name = "hyper-rustls" version = "0.27.9" @@ -1683,6 +1894,19 @@ dependencies = [ "webpki-roots 1.0.9", ] +[[package]] +name = "hyper-timeout" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" +dependencies = [ + "hyper", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + [[package]] name = "hyper-tls" version = "0.6.0" @@ -1722,6 +1946,21 @@ dependencies = [ "tracing", ] +[[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", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + [[package]] name = "iana-time-zone" version = "0.1.65" @@ -2072,10 +2311,11 @@ dependencies = [ [[package]] name = "jsonwebtoken" -version = "11.0.0" +version = "11.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "881733cbc631fc9e472e24447ce32a64bedf2da498d6d8570b08edc87de71f65" +checksum = "e75fe14a82d81e5f5af639997db37d8b96045938a7ac6ab18cdbe1c7467e05e1" dependencies = [ + "aws-lc-rs", "base64 0.22.1", "getrandom 0.2.17", "js-sys", @@ -2201,9 +2441,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" @@ -2682,6 +2922,31 @@ dependencies = [ "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.119", +] + [[package]] name = "pem" version = "3.0.6" @@ -2716,6 +2981,26 @@ dependencies = [ "serde", ] +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -2765,6 +3050,12 @@ dependencies = [ "regex", ] +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" + [[package]] name = "potential_utf" version = "0.1.4" @@ -2829,6 +3120,38 @@ dependencies = [ "yansi", ] +[[package]] +name = "prost" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528ac67416ff8646872a3c02cad9cc4ee5dc9f9540c9b10771855c95cb2e5ae1" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf" +dependencies = [ + "anyhow", + "itertools 0.14.0", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "prost-types" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f94967dc7688f3054c7fac87473ffae4cc4c3904800e2d9f5b857246d8963b0a" +dependencies = [ + "prost", +] + [[package]] name = "ptr_meta" version = "0.1.4" @@ -3102,6 +3425,12 @@ version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" +[[package]] +name = "relative-path" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" + [[package]] name = "rend" version = "0.4.2" @@ -3119,7 +3448,9 @@ checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ "base64 0.22.1", "bytes", + "futures-channel", "futures-core", + "futures-util", "http", "http-body", "http-body-util", @@ -3142,12 +3473,14 @@ dependencies = [ "tokio", "tokio-native-tls", "tokio-rustls", + "tokio-util", "tower", "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-streams", "web-sys", "webpki-roots 1.0.9", ] @@ -3160,6 +3493,7 @@ checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" dependencies = [ "base64 0.22.1", "bytes", + "encoding_rs", "futures-channel", "futures-core", "futures-util", @@ -3172,6 +3506,7 @@ dependencies = [ "hyper-util", "js-sys", "log", + "mime", "percent-encoding", "pin-project-lite", "quinn", @@ -3213,7 +3548,7 @@ dependencies = [ "cfg-if", "getrandom 0.2.17", "libc", - "untrusted", + "untrusted 0.9.0", "windows-sys 0.52.0", ] @@ -3266,6 +3601,35 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rstest" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b376e512f30a7686a8dbc6aeaace99b3ade1c26c1c63473473a817ade226dafc" +dependencies = [ + "futures-timer", + "futures-util", + "rstest_macros", +] + +[[package]] +name = "rstest_macros" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88512dbfb5b73a0dfa38aef7bc73b5657035fce477b923faf12d332dd83051ea" +dependencies = [ + "cfg-if", + "glob", + "proc-macro-crate", + "proc-macro2", + "quote", + "regex", + "relative-path", + "rustc_version", + "syn 2.0.119", + "unicode-ident", +] + [[package]] name = "rust_decimal" version = "1.42.1" @@ -3300,9 +3664,9 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.2" +version = "1.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +checksum = "e5e76742e5294153d4dde1207dbec5bdd81eef6fabdc0d2118babadabfd858aa" dependencies = [ "bitflags", "errno", @@ -3385,7 +3749,7 @@ dependencies = [ "aws-lc-rs", "ring", "rustls-pki-types", - "untrusted", + "untrusted 0.9.0", ] [[package]] @@ -3759,6 +4123,17 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_repr" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -4144,7 +4519,7 @@ dependencies = [ "chrono", "crc", "dotenvy", - "etcetera", + "etcetera 0.8.0", "futures-channel", "futures-core", "futures-util", @@ -4228,6 +4603,29 @@ 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.119", +] + +[[package]] +name = "structmeta-derive" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "152a0b65a590ff6c3da95cabe2353ee04e6167c896b28e3b14478c2636c922fc" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "strum" version = "0.28.0" @@ -4327,6 +4725,38 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "testcontainers" +version = "0.27.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfd5785b5483672915ed5fe3cddf9f546802779fc1eceff0a6fb7321fac81c1e" +dependencies = [ + "astral-tokio-tar", + "async-trait", + "bollard", + "bytes", + "docker_credential", + "either", + "etcetera 0.11.0", + "ferroid", + "futures", + "http", + "itertools 0.14.0", + "log", + "memchr", + "parse-display", + "pin-project-lite", + "reqwest 0.13.4", + "serde", + "serde_json", + "serde_with", + "thiserror 2.0.21", + "tokio", + "tokio-stream", + "tokio-util", + "url", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -4543,6 +4973,47 @@ dependencies = [ "winnow 1.0.4", ] +[[package]] +name = "tonic" +version = "0.14.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2cdfc673aabd30fe8fee92e65837b644c37c5d47f0f97ae6d5e0f1237d15e1b" +dependencies = [ + "async-trait", + "axum", + "base64 0.22.1", + "bytes", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-timeout", + "hyper-util", + "percent-encoding", + "pin-project", + "socket2", + "sync_wrapper", + "tokio", + "tokio-stream", + "tokio-util", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tonic-prost" +version = "0.14.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eba464054e2226f182a34489b8b079bf4c6e3e7ab5fd7fc30e024f24aa92a484" +dependencies = [ + "bytes", + "prost", + "tonic", +] + [[package]] name = "tower" version = "0.5.3" @@ -4551,9 +5022,12 @@ checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", + "indexmap 2.9.0", "pin-project-lite", + "slab", "sync_wrapper", "tokio", + "tokio-util", "tower-layer", "tower-service", "tracing", @@ -4693,12 +5167,45 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + [[package]] name = "untrusted" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "ureq" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cde93ff05758a36a851a398a772b0b79a82fbe3c186a46f2696baaeaf84c966b" +dependencies = [ + "base64 0.23.1", + "log", + "percent-encoding", + "rustls", + "rustls-pki-types", + "ureq-proto", + "utf8-zero", +] + +[[package]] +name = "ureq-proto" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841583fc36089d7d59419568ae157f43a5a03a2e5a81ba87c77d1596ef46e922" +dependencies = [ + "base64 0.23.1", + "http", + "httparse", + "log", +] + [[package]] name = "url" version = "2.5.8" @@ -4712,6 +5219,12 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "utf8-zero" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -4859,6 +5372,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "web-sys" version = "0.3.103" @@ -5225,6 +5751,16 @@ dependencies = [ "tap", ] +[[package]] +name = "xattr" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "963d6fab92d5504b971dc8bd90ca743c2575e590bc8071ea5bc571598667ae08" +dependencies = [ + "libc", + "rustix", +] + [[package]] name = "yaml-rust2" version = "0.10.4" diff --git a/workflows-cli/src/login/mod.rs b/workflows-cli/src/login/mod.rs new file mode 100644 index 000000000..ca22f1710 --- /dev/null +++ b/workflows-cli/src/login/mod.rs @@ -0,0 +1,57 @@ +use clap::Parser; +use reqwest::{Client, Url}; +use serde::{Deserialize, Serialize}; +use serde_json::json; + +use crate::{AUTH_URL, auth::get_auth_token}; + +#[derive(Debug, Parser, Serialize, Deserialize)] +pub struct ServiceLoginArgs { + /// The Keycloak client ID + #[arg(long)] + client_id: String, + + /// The secret associated with the Keycloak client + #[arg(long)] + client_secret: String, +} + +pub async fn service_account_login(args: ServiceLoginArgs) { + let url = Url::parse(AUTH_URL).expect("Should be able to parse auth URL"); + + let token = match get_auth_token().await { + Ok(token) => token, + Err(e) => { + eprintln!("Failed to obtain authentication token: {e}"); + return; + } + }; + + let response = match Client::new() + .post(url) + .bearer_auth(token.secret()) + .json(&json!({ + "client_id": args.client_id, + "client_secret": args.client_secret, + })) + .send() + .await + { + Ok(response) => response, + Err(e) => { + eprintln!("Request failed: {e}"); + return; + } + }; + + let status = response.status(); + let body = response.text().await.unwrap_or_default(); + + if status.is_success() { + println!("Successfully authenticated as service account"); + } else if body.is_empty() { + eprintln!("Authentication failed: {status}"); + } else { + eprintln!("Authentication failed ({status}): {body}"); + } +} diff --git a/workflows-cli/src/main.rs b/workflows-cli/src/main.rs index 0b3aaa092..4b6845246 100644 --- a/workflows-cli/src/main.rs +++ b/workflows-cli/src/main.rs @@ -18,12 +18,12 @@ use helm_integration::ManifestType; /// Create repository for workflow templates mod create; +mod auth; +/// Log in as a service account +mod login; mod triggers; - mod visit; -mod auth; - use clap::Parser; use gql_client::{Client, GraphQLError}; use serde::Serialize; @@ -35,6 +35,8 @@ use std::process::Command; /// Address for making queries to the GraphQL API const GRAPH_URL: &str = "https://workflows.diamond.ac.uk/graphql"; +const AUTH_URL: &str = "http://workflows.diamond.ac.uk/auth/client-creds"; + /// The path to the ClusterTriggerTemplate custom resource definition const TRIGGER_TEMPLATE_CRD: &str = include_str!("../../charts/events/crds/ctt-crd.yaml"); @@ -52,6 +54,8 @@ enum Cli { Create(CreateArgs), /// Create a new workflow trigger from a trigger template TriggerCreate(triggers::TriggerCreateArgs), + /// Login as a service/machine account using client credentials and store its refresh token in Workflows + ServiceLogin(login::ServiceLoginArgs), } /// Arguments for linting from a configfile @@ -136,6 +140,9 @@ async fn main() { Cli::TriggerCreate(args) => { triggers::create_trigger(args).await; } + Cli::ServiceLogin(args) => { + login::service_account_login(args).await; + } } }