From 6ff016200ef695ffed439db1837384e05d380a42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Ciarcin=CC=81ski?= Date: Thu, 15 Jan 2026 13:27:19 +0100 Subject: [PATCH] Extend OpenAPI docs with OpenID providers --- Cargo.lock | 45 ++--- .../enterprise/handlers/openid_providers.rs | 9 +- crates/defguard_core/src/handlers/auth.rs | 21 ++- crates/defguard_core/src/handlers/mod.rs | 2 +- crates/defguard_core/src/lib.rs | 149 +---------------- crates/defguard_core/src/openapi.rs | 155 ++++++++++++++++++ 6 files changed, 206 insertions(+), 175 deletions(-) create mode 100644 crates/defguard_core/src/openapi.rs diff --git a/Cargo.lock b/Cargo.lock index 8969cce534..535698a6ec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -629,9 +629,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" -version = "0.4.42" +version = "0.4.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" dependencies = [ "iana-time-zone", "js-sys", @@ -2668,9 +2668,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.83" +version = "0.3.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" dependencies = [ "once_cell", "wasm-bindgen", @@ -4441,9 +4441,9 @@ dependencies = [ [[package]] name = "rust-embed" -version = "8.10.0" +version = "8.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f783a9e226b5319beefe29d45941f559ace8b56801bb8355be17eea277fc8272" +checksum = "04113cb9355a377d83f06ef1f0a45b8ab8cd7d8b1288160717d66df5c7988d27" dependencies = [ "rust-embed-impl", "rust-embed-utils", @@ -4452,9 +4452,9 @@ dependencies = [ [[package]] name = "rust-embed-impl" -version = "8.10.0" +version = "8.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "303d4e979140595f1d824b3dd53a32684835fa32425542056826521ac279f538" +checksum = "da0902e4c7c8e997159ab384e6d0fc91c221375f6894346ae107f47dd0f3ccaa" dependencies = [ "proc-macro2", "quote", @@ -4465,9 +4465,9 @@ dependencies = [ [[package]] name = "rust-embed-utils" -version = "8.10.0" +version = "8.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f6b4ab509cae251bd524d2425d746b0af0018f5a81fc1eaecdd4e661c8ab3a0" +checksum = "5bcdef0be6fe7f6fa333b1073c949729274b05f123a0ad7efcb8efd878e5c3b1" dependencies = [ "globset", "sha2", @@ -6288,9 +6288,9 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.106" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" dependencies = [ "cfg-if", "once_cell", @@ -6301,11 +6301,12 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.56" +version = "0.4.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" +checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" dependencies = [ "cfg-if", + "futures-util", "js-sys", "once_cell", "wasm-bindgen", @@ -6314,9 +6315,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.106" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -6324,9 +6325,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.106" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" dependencies = [ "bumpalo", "proc-macro2", @@ -6337,9 +6338,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.106" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" dependencies = [ "unicode-ident", ] @@ -6359,9 +6360,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.83" +version = "0.3.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" +checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" dependencies = [ "js-sys", "wasm-bindgen", diff --git a/crates/defguard_core/src/enterprise/handlers/openid_providers.rs b/crates/defguard_core/src/enterprise/handlers/openid_providers.rs index 13519dfe48..e559418c39 100644 --- a/crates/defguard_core/src/enterprise/handlers/openid_providers.rs +++ b/crates/defguard_core/src/enterprise/handlers/openid_providers.rs @@ -54,8 +54,11 @@ pub struct AddProviderData { #[utoipa::path( post, path = "/api/v1/openid/provider", + params( + ("data" = AddProviderData, Path, description = "OpenID provider data",) + ), responses( - (status = CREATED, description = "Add OpenID provider", body = [AddProviderData]), + (status = CREATED, description = "Add OpenID provider"), ), )] pub(crate) async fn add_openid_provider( @@ -194,7 +197,7 @@ pub(crate) async fn add_openid_provider( /// # Returns /// - HTTP Status "OK" on success. #[utoipa::path( - put, + get, path = "/api/v1/openid/provider/{name}", responses( (status = OK, description = "Get OpenID provider"), @@ -240,7 +243,7 @@ pub(crate) async fn get_openid_provider( /// # Returns /// - HTTP Status "OK" on success. #[utoipa::path( - get, + delete, path = "/api/v1/openid/provider/{name}", responses( (status = OK, description = "Delete OpenID provider"), diff --git a/crates/defguard_core/src/handlers/auth.rs b/crates/defguard_core/src/handlers/auth.rs index 8b3f213f69..9e01c18243 100644 --- a/crates/defguard_core/src/handlers/auth.rs +++ b/crates/defguard_core/src/handlers/auth.rs @@ -130,9 +130,19 @@ pub(crate) async fn create_session( } } -/// For successful login, return: +/// Authenticate a user. +/// +/// # For successful login, returns: /// * 200 with MFA disabled /// * 201 with MFA enabled when additional authentication factor is required +#[utoipa::path( + post, + path = "/api/v1/auth", + responses( + (status = OK, description = "User authenticated"), + (status = CREATED, description = "User authenticated, but an additional authentication factor is required"), + ), +)] pub(crate) async fn authenticate( cookies: CookieJar, mut private_cookies: PrivateCookieJar, @@ -298,7 +308,14 @@ pub(crate) async fn authenticate( } /// Logout - forget the session cookie. -pub async fn logout( +#[utoipa::path( + post, + path = "/api/v1/auth/logout", + responses( + (status = OK, description = "User logged out"), + ), +)] +pub(crate) async fn logout( cookies: CookieJar, SessionExtractor(session): SessionExtractor, user_agent: TypedHeader, diff --git a/crates/defguard_core/src/handlers/mod.rs b/crates/defguard_core/src/handlers/mod.rs index c18017ba92..1002d5b363 100644 --- a/crates/defguard_core/src/handlers/mod.rs +++ b/crates/defguard_core/src/handlers/mod.rs @@ -221,7 +221,7 @@ impl IntoResponse for ApiResponse { pub type ApiResult = Result; -#[derive(Deserialize, Serialize)] +#[derive(Deserialize, Serialize, ToSchema)] pub struct Auth { username: String, password: String, diff --git a/crates/defguard_core/src/lib.rs b/crates/defguard_core/src/lib.rs index 3024643b57..eb622875ab 100644 --- a/crates/defguard_core/src/lib.rs +++ b/crates/defguard_core/src/lib.rs @@ -67,10 +67,7 @@ use tower_http::{ trace::{DefaultOnResponse, TraceLayer}, }; use tracing::Level; -use utoipa::{ - Modify, OpenApi, - openapi::security::{ApiKey, ApiKeyValue, SecurityScheme}, -}; +use utoipa::OpenApi; use utoipa_swagger_ui::SwaggerUi; use self::{ @@ -188,149 +185,7 @@ static PHONE_NUMBER_REGEX: LazyLock = LazyLock::new(|| { .expect("Failed to parse phone number regex") }); -mod openapi { - use defguard_common::{ - db::models::{ - Device, - device::{AddDevice, ModifyDevice, UserDevice}, - }, - types::user_info::UserInfo, - }; - use handlers::{ - ApiResponse, EditGroupInfo, GroupInfo, PasswordChange, PasswordChangeSelf, - SESSION_COOKIE_NAME, StartEnrollmentRequest, Username, - group::{self, BulkAssignToGroupsRequest, Groups}, - user, wireguard as device, wireguard as network, - wireguard::AddDeviceResult, - }; - use utoipa::{ - OpenApi, - openapi::security::{HttpAuthScheme, HttpBuilder}, - }; - - use super::*; - use crate::{enterprise::snat::handlers as snat, error::WebError, handlers::user::UserDetails}; - - #[derive(OpenApi)] - #[openapi( - modifiers(&SecurityAddon), - paths( - // /user - user::list_users, - user::get_user, - user::add_user, - user::start_enrollment, - user::start_remote_desktop_configuration, - user::username_available, - user::modify_user, - user::delete_user, - user::change_self_password, - user::change_password, - user::reset_password, - user::delete_security_key, - user::me, - user::delete_authorized_app, - // /group - group::bulk_assign_to_groups, - group::list_groups_info, - group::list_groups, - group::get_group, - group::create_group, - group::modify_group, - group::delete_group, - group::add_group_member, - group::remove_group_member, - // /device - device::add_device, - device::modify_device, - device::get_device, - device::delete_device, - device::list_devices, - device::list_user_devices, - // /network - network::create_network, - network::modify_network, - network::delete_network, - network::list_networks, - network::network_details, - // /network/{location_id}/snat - snat::list_snat_bindings, - snat::create_snat_binding, - snat::modify_snat_binding, - snat::delete_snat_binding, - ), - components( - schemas( - ApiResponse, UserInfo, UserDetails, UserDevice, Groups, Username, StartEnrollmentRequest, PasswordChangeSelf, PasswordChange, AddDevice, AddDeviceResult, Device, ModifyDevice, BulkAssignToGroupsRequest, GroupInfo, EditGroupInfo, WebError - ), - ), - tags( - (name = "user", description = " -### Endpoints for managing users -Available actions: -- list all users -- disable/enable user -- CRUD mechanism for handling users -- operations on security key and authorized app -- change user password. -- start remote desktop configuratiion -- trigger enrollment process - "), - (name = "group", description = " -### Endpoints for managing groups -Available actions: -- list all groups -- CRUD mechanism for handling groups -- add or delete a group member -- remove group -- bulk assign users to groups - "), - (name = "device", description = " -### Endpoints for managing devices - -Available actions: -- list all devices or user devices -- CRUD mechanism for handling devices. - "), - (name = "network", description = " -### Endpoints that allow to control your networks. - -Available actions: -- list all wireguard networks -- CRUD mechanism for handling devices. - "), - (name = "SNAT", description = " -### Endpoints that allow you to control user SNAT bindings for your locations. - -Available actions: -- list all SNAT bindings -- create new SNAT binding -- modify SNAT binding -- delete SNAT binding - "), - ) - )] - pub struct ApiDoc; - - struct SecurityAddon; - - impl Modify for SecurityAddon { - fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) { - if let Some(components) = openapi.components.as_mut() { - // session cookie auth - components.add_security_scheme( - "cookie", - SecurityScheme::ApiKey(ApiKey::Cookie(ApiKeyValue::new(SESSION_COOKIE_NAME))), - ); - // API token auth - components.add_security_scheme( - "api_token", - SecurityScheme::Http(HttpBuilder::new().scheme(HttpAuthScheme::Bearer).build()), - ); - } - } - } -} +mod openapi; /// Simple health-check. async fn health_check() -> &'static str { diff --git a/crates/defguard_core/src/openapi.rs b/crates/defguard_core/src/openapi.rs new file mode 100644 index 0000000000..498a8ffca9 --- /dev/null +++ b/crates/defguard_core/src/openapi.rs @@ -0,0 +1,155 @@ +use defguard_common::{ + db::models::{ + Device, + device::{AddDevice, ModifyDevice, UserDevice}, + }, + types::user_info::UserInfo, +}; +use utoipa::{ + Modify, OpenApi, + openapi::security::{ApiKey, ApiKeyValue, HttpAuthScheme, HttpBuilder, SecurityScheme}, +}; + +use super::{ + enterprise::{handlers::openid_providers, snat::handlers as snat}, + error::WebError, + handlers::{ + ApiResponse, EditGroupInfo, GroupInfo, PasswordChange, PasswordChangeSelf, + SESSION_COOKIE_NAME, StartEnrollmentRequest, Username, auth, + group::{self, BulkAssignToGroupsRequest, Groups}, + user::{self, UserDetails}, + wireguard as device, wireguard as network, + wireguard::AddDeviceResult, + }, +}; + +#[derive(OpenApi)] +#[openapi( + modifiers(&SecurityAddon), + paths( + // /auth + auth::authenticate, + auth::logout, + // /user + user::list_users, + user::get_user, + user::add_user, + user::start_enrollment, + user::start_remote_desktop_configuration, + user::username_available, + user::modify_user, + user::delete_user, + user::change_self_password, + user::change_password, + user::reset_password, + user::delete_security_key, + user::me, + user::delete_authorized_app, + // /group + group::bulk_assign_to_groups, + group::list_groups_info, + group::list_groups, + group::get_group, + group::create_group, + group::modify_group, + group::delete_group, + group::add_group_member, + group::remove_group_member, + // /device + device::add_device, + device::modify_device, + device::get_device, + device::delete_device, + device::list_devices, + device::list_user_devices, + // /network + network::create_network, + network::modify_network, + network::delete_network, + network::list_networks, + network::network_details, + // /network/{location_id}/snat + snat::list_snat_bindings, + snat::create_snat_binding, + snat::modify_snat_binding, + snat::delete_snat_binding, + // /openid + openid_providers::add_openid_provider, + openid_providers::get_openid_provider, + openid_providers::delete_openid_provider, + openid_providers::modify_openid_provider, + openid_providers::list_openid_providers, + ), + components( + schemas( + ApiResponse, UserInfo, UserDetails, UserDevice, Groups, Username, + StartEnrollmentRequest, PasswordChangeSelf, PasswordChange, AddDevice, AddDeviceResult, + Device, ModifyDevice, BulkAssignToGroupsRequest, GroupInfo, EditGroupInfo, WebError + ), + ), + tags( + (name = "user", description = " +### Endpoints for managing users +Available actions: +- list all users +- disable/enable user +- CRUD mechanism for handling users +- operations on security key and authorized app +- change user password. +- start remote desktop configuratiion +- trigger enrollment process + "), + (name = "group", description = " +### Endpoints for managing groups +Available actions: +- list all groups +- CRUD mechanism for handling groups +- add or delete a group member +- remove group +- bulk assign users to groups + "), + (name = "device", description = " +### Endpoints for managing devices + +Available actions: +- list all devices or user devices +- CRUD mechanism for handling devices. + "), + (name = "network", description = " +### Endpoints that allow to control your networks. + +Available actions: +- list all wireguard networks +- CRUD mechanism for handling devices. + "), + (name = "SNAT", description = " +### Endpoints that allow you to control user SNAT bindings for your locations. + +Available actions: +- list all SNAT bindings +- create new SNAT binding +- modify SNAT binding +- delete SNAT binding + "), + ) +)] +pub struct ApiDoc; + +struct SecurityAddon; + +impl Modify for SecurityAddon { + fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) { + if let Some(components) = openapi.components.as_mut() { + // session cookie auth + components.add_security_scheme( + "cookie", + SecurityScheme::ApiKey(ApiKey::Cookie(ApiKeyValue::new(SESSION_COOKIE_NAME))), + ); + // API token auth + components.add_security_scheme( + "api_token", + SecurityScheme::Http(HttpBuilder::new().scheme(HttpAuthScheme::Bearer).build()), + ); + } + } +}