diff --git a/Cargo.lock b/Cargo.lock index f0321c206..6e66f7ab7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6918,6 +6918,8 @@ dependencies = [ "tracing", "trustification-common", "url", + "utoipa", + "utoipa-swagger-ui", ] [[package]] diff --git a/auth/Cargo.toml b/auth/Cargo.toml index 70dede15b..389ed5935 100644 --- a/auth/Cargo.toml +++ b/auth/Cargo.toml @@ -10,7 +10,7 @@ async-trait = "0.1" anyhow = "1" biscuit = "0.6" chrono = { version = "0.4.26", default-features = false } -clap = { version = "4", features = ["derive", "env"]} +clap = { version = "4", features = ["derive", "env"] } futures-util = "0.3" humantime = "2" log = "0.4" @@ -31,7 +31,10 @@ actix-http = { version = "3.3.1", optional = true } actix-web-httpauth = { version = "0.8", optional = true } actix-web-extras = { version = "0.1", optional = true } +utoipa = { version = "3", features = ["actix_extras"], optional = true } +utoipa-swagger-ui = { version = "3", features = ["actix-web"], optional = true } + [features] default = [] actix = ["actix-web", "actix-http", "actix-web-httpauth", "actix-web-extras"] - +swagger = ["utoipa", "utoipa-swagger-ui", "actix"] diff --git a/auth/src/authenticator/config.rs b/auth/src/authenticator/config.rs index c1b287367..41784bc21 100644 --- a/auth/src/authenticator/config.rs +++ b/auth/src/authenticator/config.rs @@ -1,3 +1,4 @@ +use crate::devmode; use clap::ArgAction; use serde::Deserialize; use std::path::PathBuf; @@ -24,9 +25,8 @@ impl AuthenticatorConfig { AuthenticatorConfig { disabled: false, clients: SingleAuthenticatorClientConfig { - client_ids: vec!["frontend".to_string(), "walker".to_string()], - issuer_url: std::env::var("ISSUER_URL") - .unwrap_or_else(|_| "http://localhost:8090/realms/chicken".to_string()), + client_ids: devmode::CLIENT_IDS.iter().map(|s| s.to_string()).collect(), + issuer_url: devmode::issuer_url(), ..Default::default() }, } diff --git a/auth/src/authenticator/mod.rs b/auth/src/authenticator/mod.rs index b549c7885..64c04d8e6 100644 --- a/auth/src/authenticator/mod.rs +++ b/auth/src/authenticator/mod.rs @@ -21,7 +21,7 @@ use trustification_common::reqwest::ClientFactory; /// An authenticator to authenticate incoming requests. #[derive(Clone)] pub struct Authenticator { - clients: Vec>, + pub clients: Vec>, } impl Authenticator { @@ -158,7 +158,7 @@ async fn create_client( } #[derive(Clone)] -struct AuthenticatorClient

+pub struct AuthenticatorClient

where P: CompactJson + Claims, { diff --git a/auth/src/devmode.rs b/auth/src/devmode.rs new file mode 100644 index 000000000..118711db1 --- /dev/null +++ b/auth/src/devmode.rs @@ -0,0 +1,7 @@ +const ISSUER_URL: &str = "http://localhost:8090/realms/chicken"; +pub const CLIENT_IDS: &[&str] = &["frontend", "walker"]; +pub const SWAGGER_UI_CLIENT_ID: &str = "frontend"; + +pub fn issuer_url() -> String { + std::env::var("ISSUER_URL").unwrap_or_else(|_| ISSUER_URL.to_string()) +} diff --git a/auth/src/lib.rs b/auth/src/lib.rs index 84b33e47c..1eb0d9b4b 100644 --- a/auth/src/lib.rs +++ b/auth/src/lib.rs @@ -1,5 +1,9 @@ pub mod authenticator; pub mod client; +pub mod devmode; + +#[cfg(feature = "swagger")] +pub mod swagger_ui; /// A registered user pub const ROLE_USER: &str = "chicken-user"; diff --git a/auth/src/swagger_ui.rs b/auth/src/swagger_ui.rs new file mode 100644 index 000000000..b972a709f --- /dev/null +++ b/auth/src/swagger_ui.rs @@ -0,0 +1,95 @@ +use crate::devmode::{self, SWAGGER_UI_CLIENT_ID}; +use openid::{Client, Discovered, Provider, StandardClaims}; +use url::Url; +use utoipa::openapi::{ + security::{AuthorizationCode, Flow, OAuth2, Scopes, SecurityScheme}, + OpenApi, SecurityRequirement, +}; +use utoipa_swagger_ui::{oauth, SwaggerUi}; + +#[derive(Clone, Debug, Default, clap::Args)] +#[command(rename_all_env = "SCREAMING_SNAKE_CASE", next_help_heading = "Swagger UI OIDC")] +pub struct SwaggerUiOidcConfig { + /// The issuer URL used by the Swagger UI, disabled if none. + #[arg(long, env)] + pub swagger_ui_oidc_issuer_url: Option, + /// The client ID use by the swagger UI frontend + #[arg(long, env, default_value = "frontend")] + pub swagger_ui_oidc_client_id: String, +} + +impl SwaggerUiOidcConfig { + pub fn devmode() -> Self { + Self { + swagger_ui_oidc_issuer_url: Some(devmode::issuer_url()), + swagger_ui_oidc_client_id: SWAGGER_UI_CLIENT_ID.to_string(), + } + } +} + +pub struct SwaggerUiOidc { + pub client_id: String, + pub auth_url: String, + pub token_url: String, +} + +impl SwaggerUiOidc { + pub async fn new(config: SwaggerUiOidcConfig) -> anyhow::Result> { + let issuer_url = match config.swagger_ui_oidc_issuer_url { + None => return Ok(None), + Some(issuer_url) => issuer_url, + }; + + let client: Client = openid::Client::discover( + config.swagger_ui_oidc_client_id.clone(), + None, + None, + Url::parse(&issuer_url)?, + ) + .await?; + + Ok(Some(Self { + token_url: client.provider.token_uri().to_string(), + auth_url: client.provider.auth_uri().to_string(), + client_id: client.client_id, + })) + } + + pub async fn from_devmode_or_config(devmode: bool, config: SwaggerUiOidcConfig) -> anyhow::Result> { + let config = match devmode { + true => SwaggerUiOidcConfig::devmode(), + false => config, + }; + + Self::new(config).await + } + + pub fn apply(&self, swagger: SwaggerUi, openapi: &mut OpenApi) -> SwaggerUi { + if let Some(components) = &mut openapi.components { + // the swagger UI expects the full "well known" endpoint + // let url = format!("{}/.well-known/openid-configuration", self.issuer_url); + //components.add_security_scheme("oidc", SecurityScheme::OpenIdConnect(OpenIdConnect::new(url))); + + // The swagger UI OIDC client still is weird, let's use OAuth2 + + components.add_security_scheme( + "oidc", + SecurityScheme::OAuth2(OAuth2::new([Flow::AuthorizationCode(AuthorizationCode::new( + &self.auth_url, + &self.token_url, + Scopes::one("oidc", "OpenID Connect"), + ))])), + ); + } + + openapi.security = Some(vec![SecurityRequirement::new::<_, _, String>("oidc", [])]); + + swagger.oauth( + oauth::Config::new() + .client_id(&self.client_id) + .app_name("Trustification") + .scopes(vec!["openid".to_string()]) + .use_pkce_with_authorization_code_grant(true), + ) + } +} diff --git a/bombastic/api/Cargo.toml b/bombastic/api/Cargo.toml index b9d0980c7..457826edb 100644 --- a/bombastic/api/Cargo.toml +++ b/bombastic/api/Cargo.toml @@ -15,7 +15,7 @@ log = "0.4" bombastic-index = { path = "../index" } bombastic-model = { path = "../model" } trustification-api = { path = "../../api" } -trustification-auth = { path = "../../auth", features = ["actix"] } +trustification-auth = { path = "../../auth", features = ["actix", "swagger"] } trustification-infrastructure = { path = "../../infrastructure" } trustification-storage = { path = "../../storage" } trustification-index = { path = "../../index" } diff --git a/bombastic/api/src/lib.rs b/bombastic/api/src/lib.rs index eba3eecb5..892c528f0 100644 --- a/bombastic/api/src/lib.rs +++ b/bombastic/api/src/lib.rs @@ -14,6 +14,7 @@ use prometheus::Registry; use tokio::sync::RwLock; use trustification_auth::authenticator::config::AuthenticatorConfig; use trustification_auth::authenticator::Authenticator; +use trustification_auth::swagger_ui::{SwaggerUiOidc, SwaggerUiOidcConfig}; use trustification_index::{IndexConfig, IndexStore}; use trustification_infrastructure::app::{new_app, AppOptions}; use trustification_infrastructure::{Infrastructure, InfrastructureConfig}; @@ -45,6 +46,9 @@ pub struct Run { #[command(flatten)] pub oidc: AuthenticatorConfig, + + #[command(flatten)] + pub swagger_ui_oidc: SwaggerUiOidcConfig, } impl Run { @@ -56,6 +60,11 @@ impl Run { .await? .map(Arc::new); + let swagger_oidc: Option> = + SwaggerUiOidc::from_devmode_or_config(self.devmode, self.swagger_ui_oidc) + .await? + .map(Arc::new); + if authenticator.is_none() { log::warn!("Authentication is disabled"); } @@ -72,6 +81,7 @@ impl Run { let http_metrics = http_metrics.clone(); let cors = Cors::permissive(); let authenticator = authenticator.clone(); + let swagger_oidc = swagger_oidc.clone(); new_app(AppOptions { cors: Some(cors), @@ -79,7 +89,7 @@ impl Run { authenticator: None, }) .app_data(web::Data::new(state.clone())) - .configure(move |svc| server::config(svc, authenticator.clone())) + .configure(move |svc| server::config(svc, authenticator.clone(), swagger_oidc.clone())) }); srv = match listener { Some(v) => srv.listen(v)?, diff --git a/bombastic/api/src/server.rs b/bombastic/api/src/server.rs index a266c6af3..90630a5e9 100644 --- a/bombastic/api/src/server.rs +++ b/bombastic/api/src/server.rs @@ -20,6 +20,7 @@ use serde::Deserialize; use trustification_api::search::SearchOptions; use trustification_auth::{ authenticator::{user::UserDetails, Authenticator}, + swagger_ui::SwaggerUiOidc, ROLE_MANAGER, }; use trustification_index::Error as IndexError; @@ -35,7 +36,11 @@ use utoipa_swagger_ui::SwaggerUi; )] pub struct ApiDoc; -pub fn config(cfg: &mut web::ServiceConfig, auth: Option>) { +pub fn config( + cfg: &mut web::ServiceConfig, + auth: Option>, + swagger_ui_oidc: Option>, +) { cfg.service( web::scope("/api/v1") .wrap(new_auth!(auth)) @@ -45,7 +50,16 @@ pub fn config(cfg: &mut web::ServiceConfig, auth: Option>) { .service(publish_sbom) .service(delete_sbom), ) - .service(SwaggerUi::new("/swagger-ui/{_:.*}").url("/openapi.json", ApiDoc::openapi())); + .service({ + let mut openapi = ApiDoc::openapi(); + let mut swagger = SwaggerUi::new("/swagger-ui/{_:.*}"); + + if let Some(swagger_ui_oidc) = &swagger_ui_oidc { + swagger = swagger_ui_oidc.apply(swagger, &mut openapi); + } + + swagger.url("/openapi.json", openapi) + }); } const ACCEPT_ENCODINGS: [&str; 2] = ["bzip2", "zstd"]; @@ -193,6 +207,7 @@ impl From<&SearchParams> for SearchOptions { responses( (status = 200, description = "Search completed"), (status = BAD_REQUEST, description = "Bad query"), + (status = 401, description = "Not authenticated"), ), params( ("q" = String, Query, description = "Search query"), diff --git a/integration-tests/src/lib.rs b/integration-tests/src/lib.rs index bbf4e033c..37518eb7e 100644 --- a/integration-tests/src/lib.rs +++ b/integration-tests/src/lib.rs @@ -19,6 +19,7 @@ use tokio::{select, time::timeout}; use trustification_auth::{ authenticator::config::{AuthenticatorConfig, SingleAuthenticatorClientConfig}, client::TokenInjector, + swagger_ui::SwaggerUiOidcConfig, }; use trustification_event_bus::{EventBusConfig, EventBusType}; use trustification_index::IndexConfig; @@ -94,6 +95,7 @@ fn bombastic_api() -> bombastic_api::Run { enable_tracing: false, }, oidc: testing_oidc(), + swagger_ui_oidc: testing_swagger_ui_oidc(), } } @@ -151,6 +153,7 @@ fn vexination_api() -> vexination_api::Run { enable_tracing: false, }, oidc: testing_oidc(), + swagger_ui_oidc: testing_swagger_ui_oidc(), } } @@ -174,6 +177,7 @@ fn spog_api(bport: u16, vport: u16) -> spog_api::Run { enable_tracing: false, }, oidc: testing_oidc(), + swagger_ui_oidc: testing_swagger_ui_oidc(), } } @@ -192,6 +196,13 @@ fn testing_oidc() -> AuthenticatorConfig { } } +fn testing_swagger_ui_oidc() -> SwaggerUiOidcConfig { + SwaggerUiOidcConfig { + swagger_ui_oidc_issuer_url: Some(SSO_ENDPOINT.to_string()), + swagger_ui_oidc_client_id: "frontend".to_string(), + } +} + pub async fn get_response( port: u16, api_endpoint: &str, diff --git a/spog/api/Cargo.toml b/spog/api/Cargo.toml index c74d1941d..32ad3dd5f 100644 --- a/spog/api/Cargo.toml +++ b/spog/api/Cargo.toml @@ -52,7 +52,7 @@ trustification-api = { path = "../../api" } trustification-common = { path = "../../common" } trustification-infrastructure = { path = "../../infrastructure" } trustification-version = { path = "../../version", features = ["actix-web"] } -trustification-auth = { path = "../../auth", features = ["actix"] } +trustification-auth = { path = "../../auth", features = ["actix", "swagger"] } [build-dependencies] trustification-version = { path = "../../version", features = ["build"] } diff --git a/spog/api/src/lib.rs b/spog/api/src/lib.rs index 646673336..bb89719f3 100644 --- a/spog/api/src/lib.rs +++ b/spog/api/src/lib.rs @@ -1,7 +1,7 @@ use std::process::ExitCode; use std::{net::TcpListener, path::PathBuf}; use trustification_auth::authenticator::config::AuthenticatorConfig; - +use trustification_auth::swagger_ui::SwaggerUiOidcConfig; use trustification_infrastructure::{Infrastructure, InfrastructureConfig}; mod advisory; @@ -62,6 +62,9 @@ pub struct Run { #[command(flatten)] pub oidc: AuthenticatorConfig, + + #[command(flatten)] + pub swagger_ui_oidc: SwaggerUiOidcConfig, } impl Run { diff --git a/spog/api/src/server.rs b/spog/api/src/server.rs index f87644c4f..a6f2768f7 100644 --- a/spog/api/src/server.rs +++ b/spog/api/src/server.rs @@ -11,6 +11,7 @@ use trustification_api::{search::SearchOptions, Apply}; use trustification_auth::{ authenticator::Authenticator, client::{TokenInjector, TokenProvider}, + swagger_ui::SwaggerUiOidc, }; use trustification_infrastructure::app::{new_app, AppOptions}; use trustification_version::version; @@ -59,8 +60,6 @@ impl Server { } pub async fn run(self, registry: &Registry, listener: Option) -> anyhow::Result<()> { - let openapi = ApiDoc::openapi(); - let state = configure(&self.run)?; let http_metrics = PrometheusMetricsBuilder::new("spog_api") @@ -75,6 +74,11 @@ impl Server { .await? .map(Arc::new); + let swagger_oidc: Option> = + SwaggerUiOidc::from_devmode_or_config(self.run.devmode, self.run.swagger_ui_oidc) + .await? + .map(Arc::new); + if authenticator.is_none() { log::warn!("Authentication is disabled"); } @@ -88,6 +92,7 @@ impl Server { let http_metrics = http_metrics.clone(); let cors = Cors::permissive(); let authenticator = authenticator.clone(); + let swagger_oidc = swagger_oidc.clone(); let mut app = new_app(AppOptions { cors: Some(cors), @@ -100,7 +105,16 @@ impl Server { .configure(sbom::configure(authenticator.clone())) .configure(advisory::configure(authenticator.clone())) .configure(config_configurator.clone()) - .service(SwaggerUi::new("/swagger-ui/{_:.*}").url("/openapi.json", openapi.clone())); + .service({ + let mut openapi = ApiDoc::openapi(); + let mut swagger = SwaggerUi::new("/swagger-ui/{_:.*}"); + + if let Some(swagger_ui_oidc) = &swagger_oidc { + swagger = swagger_ui_oidc.apply(swagger, &mut openapi); + } + + swagger.url("/openapi.json", openapi) + }); if let Some(crda) = &crda { app = app diff --git a/spog/ui/src/console/mod.rs b/spog/ui/src/console/mod.rs index 488e3da13..bd159165e 100644 --- a/spog/ui/src/console/mod.rs +++ b/spog/ui/src/console/mod.rs @@ -58,9 +58,6 @@ fn authenticated_page(props: &ChildrenProperties) -> Html { to={AppRoute::Scanner}>{ "Scanner" }> - if let Ok(url) = backend.join(Endpoint::Api, "/swagger-ui/") { - { "API" } - } if let Ok(url) = backend.join(Endpoint::Bombastic, "/swagger-ui/") { { "SBOM API" } } diff --git a/vexination/api/Cargo.toml b/vexination/api/Cargo.toml index 166bf0033..1a83ff6af 100644 --- a/vexination/api/Cargo.toml +++ b/vexination/api/Cargo.toml @@ -13,7 +13,7 @@ serde_json = "1.0.68" tokio = { version = "1.0", features = ["full"] } log = "0.4" trustification-api = { path = "../../api" } -trustification-auth = { path = "../../auth", features = ["actix"] } +trustification-auth = { path = "../../auth", features = ["actix", "swagger"] } trustification-infrastructure = { path = "../../infrastructure" } trustification-storage = { path = "../../storage" } trustification-index = { path = "../../index" } diff --git a/vexination/api/src/lib.rs b/vexination/api/src/lib.rs index a830ae6f6..693f4ec06 100644 --- a/vexination/api/src/lib.rs +++ b/vexination/api/src/lib.rs @@ -12,11 +12,15 @@ use actix_web_prom::PrometheusMetricsBuilder; use anyhow::anyhow; use prometheus::Registry; use tokio::sync::RwLock; -use trustification_auth::authenticator::config::AuthenticatorConfig; -use trustification_auth::authenticator::Authenticator; +use trustification_auth::{ + authenticator::{config::AuthenticatorConfig, Authenticator}, + swagger_ui::{SwaggerUiOidc, SwaggerUiOidcConfig}, +}; use trustification_index::{IndexConfig, IndexStore}; -use trustification_infrastructure::app::{new_app, AppOptions}; -use trustification_infrastructure::{Infrastructure, InfrastructureConfig}; +use trustification_infrastructure::{ + app::{new_app, AppOptions}, + Infrastructure, InfrastructureConfig, +}; use trustification_storage::{Storage, StorageConfig}; mod server; @@ -44,6 +48,9 @@ pub struct Run { #[command(flatten)] pub oidc: AuthenticatorConfig, + + #[command(flatten)] + pub swagger_ui_oidc: SwaggerUiOidcConfig, } impl Run { @@ -55,6 +62,11 @@ impl Run { .await? .map(Arc::new); + let swagger_oidc: Option> = + SwaggerUiOidc::from_devmode_or_config(self.devmode, self.swagger_ui_oidc) + .await? + .map(Arc::new); + if authenticator.is_none() { log::warn!("Authentication is disabled"); } @@ -70,6 +82,7 @@ impl Run { let http_metrics = http_metrics.clone(); let cors = Cors::permissive(); let authenticator = authenticator.clone(); + let swagger_oidc = swagger_oidc.clone(); new_app(AppOptions { cors: Some(cors), @@ -77,7 +90,7 @@ impl Run { authenticator: None, }) .app_data(web::Data::new(state.clone())) - .configure(move |svc| server::config(svc, authenticator.clone())) + .configure(move |svc| server::config(svc, authenticator.clone(), swagger_oidc.clone())) }); srv = match listener { Some(v) => srv.listen(v)?, diff --git a/vexination/api/src/server.rs b/vexination/api/src/server.rs index e638bae67..410845065 100644 --- a/vexination/api/src/server.rs +++ b/vexination/api/src/server.rs @@ -10,6 +10,7 @@ use std::sync::Arc; use trustification_api::search::SearchOptions; use trustification_auth::{ authenticator::{user::UserDetails, Authenticator}, + swagger_ui::SwaggerUiOidc, ROLE_MANAGER, }; use trustification_infrastructure::new_auth; @@ -27,7 +28,11 @@ use crate::SharedState; )] pub struct ApiDoc; -pub fn config(cfg: &mut web::ServiceConfig, auth: Option>) { +pub fn config( + cfg: &mut web::ServiceConfig, + auth: Option>, + swagger_ui_oidc: Option>, +) { cfg.service( web::scope("/api/v1") .wrap(new_auth!(auth)) @@ -36,7 +41,16 @@ pub fn config(cfg: &mut web::ServiceConfig, auth: Option>) { .service(publish_vex) .service(search_vex), ) - .service(SwaggerUi::new("/swagger-ui/{_:.*}").url("/openapi.json", ApiDoc::openapi())); + .service({ + let mut openapi = ApiDoc::openapi(); + let mut swagger = SwaggerUi::new("/swagger-ui/{_:.*}"); + + if let Some(swagger_ui_oidc) = &swagger_ui_oidc { + swagger = swagger_ui_oidc.apply(swagger, &mut openapi); + } + + swagger.url("/openapi.json", openapi) + }); } async fn fetch_object(storage: &Storage, key: &str) -> HttpResponse {