diff --git a/Cargo.lock b/Cargo.lock index 5a465bf7ba..7bab554b59 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -217,7 +217,7 @@ dependencies = [ "nom 7.1.3", "num-traits", "rusticata-macros", - "thiserror 2.0.17", + "thiserror 2.0.18", "time", ] @@ -459,7 +459,7 @@ dependencies = [ "proc-macro2", "quote", "syn", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -596,9 +596,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.52" +version = "1.2.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd4932aefd12402b36c60956a4fe0035421f544799057659ff86f923657aada3" +checksum = "755d2fce177175ffca841e9a06afdb2c4ab0f593d53b4dee48147dfaade85932" dependencies = [ "find-msvc-tools", "jobserver", @@ -1149,7 +1149,7 @@ dependencies = [ "rustls-pki-types", "serde", "sqlx", - "thiserror 2.0.17", + "thiserror 2.0.18", "time", "x509-parser 0.18.0", ] @@ -1180,7 +1180,7 @@ dependencies = [ "serde_cbor_2", "sqlx", "struct-patch", - "thiserror 2.0.17", + "thiserror 2.0.18", "tonic", "totp-lite", "tracing", @@ -1245,7 +1245,7 @@ dependencies = [ "strum", "strum_macros", "tera", - "thiserror 2.0.17", + "thiserror 2.0.18", "time", "tokio", "tokio-stream", @@ -1279,7 +1279,7 @@ dependencies = [ "defguard_core", "serde_json", "sqlx", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tracing", ] @@ -1291,7 +1291,7 @@ dependencies = [ "defguard_core", "defguard_event_logger", "defguard_mail", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tracing", ] @@ -1310,7 +1310,7 @@ dependencies = [ "serde_json", "sqlx", "tera", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tracing", ] @@ -1344,7 +1344,7 @@ dependencies = [ "secrecy", "semver", "sqlx", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tokio-stream", "tonic", @@ -1369,7 +1369,7 @@ dependencies = [ "os_info", "semver", "serde", - "thiserror 2.0.17", + "thiserror 2.0.18", "tonic", "tower", "tracing", @@ -1795,9 +1795,9 @@ checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "find-msvc-tools" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f449e6c6c08c865631d4890cfacf252b3d396c9bcc83adb6623cdb02a8336c41" +checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" [[package]] name = "fixedbitset" @@ -2687,7 +2687,7 @@ dependencies = [ "num-bigint", "serde", "serde_json", - "thiserror 2.0.17", + "thiserror 2.0.18", "yasna", "zeroize", ] @@ -2764,7 +2764,7 @@ dependencies = [ "native-tls", "nom 7.1.3", "percent-encoding", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tokio-native-tls", "tokio-stream", @@ -3508,9 +3508,9 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-probe" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f50d9b3dabb09ecd771ad0aa242ca6894994c130308ca3d7684634df8037391" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "openssl-sys" @@ -4127,7 +4127,7 @@ dependencies = [ "rustc-hash", "rustls", "socket2", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tracing", "web-time", @@ -4148,7 +4148,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.17", + "thiserror 2.0.18", "tinyvec", "tracing", "web-time", @@ -4256,9 +4256,9 @@ dependencies = [ [[package]] name = "rcgen" -version = "0.14.6" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ec0a99f2de91c3cddc84b37e7db80e4d96b743e05607f647eb236fc0455907f" +checksum = "10b99e0098aa4082912d4c649628623db6aba77335e4f4569ff5083a6448b32e" dependencies = [ "pem", "ring", @@ -4542,7 +4542,7 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" dependencies = [ - "openssl-probe 0.2.0", + "openssl-probe 0.2.1", "rustls-pki-types", "schannel", "security-framework 3.5.1", @@ -4550,9 +4550,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.13.3" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4910321ebe4151be888e35fe062169554e74aad01beafed60410131420ceffbc" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" dependencies = [ "web-time", "zeroize", @@ -4560,9 +4560,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.8" +version = "0.103.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" dependencies = [ "ring", "rustls-pki-types", @@ -4814,7 +4814,7 @@ checksum = "f3faaf9e727533a19351a43cc5a8de957372163c7d35cc48c90b75cdda13c352" dependencies = [ "percent-encoding", "serde", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -4999,7 +4999,7 @@ checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" dependencies = [ "num-bigint", "num-traits", - "thiserror 2.0.17", + "thiserror 2.0.18", "time", ] @@ -5127,7 +5127,7 @@ dependencies = [ "serde_json", "sha2", "smallvec", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tokio-stream", "tracing", @@ -5211,7 +5211,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.17", + "thiserror 2.0.18", "tracing", "uuid", "whoami", @@ -5251,7 +5251,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.17", + "thiserror 2.0.18", "tracing", "uuid", "whoami", @@ -5277,7 +5277,7 @@ dependencies = [ "serde", "serde_urlencoded", "sqlx-core", - "thiserror 2.0.17", + "thiserror 2.0.18", "tracing", "url", "uuid", @@ -5547,11 +5547,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.17", + "thiserror-impl 2.0.18", ] [[package]] @@ -5567,9 +5567,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", @@ -6904,7 +6904,7 @@ dependencies = [ "oid-registry 0.8.1", "ring", "rusticata-macros", - "thiserror 2.0.17", + "thiserror 2.0.18", "time", ] @@ -7057,9 +7057,9 @@ checksum = "40990edd51aae2c2b6907af74ffb635029d5788228222c4bb811e9351c0caad3" [[package]] name = "zmij" -version = "1.0.14" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd8f3f50b848df28f887acb68e41201b5aea6bc8a8dacc00fb40635ff9a72fea" +checksum = "dfcd145825aace48cff44a8844de64bf75feec3080e0aa5cdbde72961ae51a65" [[package]] name = "zopfli" diff --git a/crates/defguard_common/src/db/models/user.rs b/crates/defguard_common/src/db/models/user.rs index 7557aedd6e..2dc5a5a133 100644 --- a/crates/defguard_common/src/db/models/user.rs +++ b/crates/defguard_common/src/db/models/user.rs @@ -616,6 +616,24 @@ impl User { Ok(res) } + /// Return all active users. + pub async fn all_active<'e, E>(executor: E) -> Result, SqlxError> + where + E: PgExecutor<'e>, + { + query_as!( + User, + "SELECT id, username, password_hash, last_name, first_name, email, phone, mfa_enabled, \ + totp_enabled, totp_secret, email_mfa_enabled, email_mfa_secret, \ + mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub, from_ldap, \ + ldap_pass_randomized, ldap_rdn, ldap_user_path, enrollment_pending \ + FROM \"user\" \ + WHERE is_active = true" + ) + .fetch_all(executor) + .await + } + /// Return all members of group pub async fn find_by_group_name( pool: &PgPool, diff --git a/crates/defguard_core/src/enterprise/db/models/acl.rs b/crates/defguard_core/src/enterprise/db/models/acl.rs index dbff930223..39ba6c96ea 100644 --- a/crates/defguard_core/src/enterprise/db/models/acl.rs +++ b/crates/defguard_core/src/enterprise/db/models/acl.rs @@ -22,6 +22,7 @@ use sqlx::{ postgres::types::PgRange, query, query_as, query_scalar, }; use thiserror::Error; +use utoipa::ToSchema; use crate::{ appstate::AppState, @@ -676,7 +677,8 @@ impl AclRule { .await?; if !invalid_alias_ids.is_empty() { error!( - "Cannot use aliases which have not been applied in an ACL rule. Invalid aliases: {invalid_alias_ids:?}" + "Cannot use aliases which have not been applied in an ACL rule. Invalid aliases: \ + {invalid_alias_ids:?}" ); return Err(AclError::CannotUseModifiedAliasInRuleError( invalid_alias_ids, @@ -1029,10 +1031,8 @@ impl AclRule { Group, "SELECT g.id, name, is_admin \ FROM aclrulegroup r \ - JOIN \"group\" g \ - ON g.id = r.group_id \ - WHERE r.rule_id = $1 \ - AND r.allow = $2", + JOIN \"group\" g ON g.id = r.group_id \ + WHERE r.rule_id = $1 AND r.allow = $2", self.id, allowed, ) @@ -1068,10 +1068,8 @@ impl AclRule { "SELECT d.id, name, wireguard_pubkey, user_id, created, description, \ device_type \"device_type: DeviceType\", configured \ FROM aclruledevice r \ - JOIN device d \ - ON d.id = r.device_id \ - WHERE r.rule_id = $1 \ - AND r.allow = true AND d.configured = true", + JOIN device d ON d.id = r.device_id \ + WHERE r.rule_id = $1 AND r.allow = true AND d.configured = true", self.id, ) .fetch_all(executor) @@ -1090,10 +1088,8 @@ impl AclRule { "SELECT d.id, name, wireguard_pubkey, user_id, created, description, \ device_type \"device_type: DeviceType\", configured \ FROM aclruledevice r \ - JOIN device d \ - ON d.id = r.device_id \ - WHERE r.rule_id = $1 \ - AND r.allow = false AND d.configured = true", + JOIN device d ON d.id = r.device_id \ + WHERE r.rule_id = $1 AND r.allow = false AND d.configured = true", self.id, ) .fetch_all(executor) @@ -1178,30 +1174,21 @@ impl AclRuleInfo { "allow_all_users flag is enabled for ACL rule {}. Fetching all active users", self.id ); - let all_active_users = query_as!( - User, - "SELECT id, username, password_hash, last_name, first_name, email, \ - phone, mfa_enabled, totp_enabled, totp_secret, \ - email_mfa_enabled, email_mfa_secret, \ - mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub, from_ldap, \ - ldap_pass_randomized, ldap_rdn, ldap_user_path, enrollment_pending \ - FROM \"user\" \ - WHERE is_active = true" - ) - .fetch_all(executor) - .await; - - return all_active_users; + return User::::all_active(executor).await; } // get explicitly allowed users let mut allowed_users = self.allowed_users.clone(); // get allowed groups IDs - let allowed_group_ids: Vec = self.allowed_groups.iter().map(|group| group.id).collect(); + let allowed_group_ids = self + .allowed_groups + .iter() + .map(|group| group.id) + .collect::>(); // fetch all active members of allowed groups - let allowed_groups_users: Vec> = query_as!( + let allowed_groups_users = query_as!( User, "SELECT id, username, password_hash, last_name, first_name, email, phone, mfa_enabled, \ totp_enabled, totp_secret, email_mfa_enabled, email_mfa_secret, \ @@ -1239,20 +1226,7 @@ impl AclRuleInfo { "deny_all_users flag is enabled for ACL rule {}. Fetching all active users", self.id ); - let all_denied_users = query_as!( - User, - "SELECT id, username, password_hash, last_name, first_name, email, \ - phone, mfa_enabled, totp_enabled, totp_secret, \ - email_mfa_enabled, email_mfa_secret, \ - mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub, from_ldap, \ - ldap_pass_randomized, ldap_rdn, ldap_user_path, enrollment_pending \ - FROM \"user\" \ - WHERE is_active = true" - ) - .fetch_all(executor) - .await; - - return all_denied_users; + return User::::all_active(executor).await; } // get explicitly denied users @@ -1262,7 +1236,7 @@ impl AclRuleInfo { let denied_group_ids: Vec = self.denied_groups.iter().map(|group| group.id).collect(); // fetch all active members of denied groups - let denied_groups_users: Vec> = query_as!( + let denied_groups_users = query_as!( User, "SELECT id, username, password_hash, last_name, first_name, email, \ phone, mfa_enabled, totp_enabled, totp_secret, \ @@ -1353,7 +1327,7 @@ impl AclRuleInfo { /// Helper struct combining all DB objects related to given [`AclAlias`]. /// All related objects are stored in vectors. #[derive(Clone, Debug)] -pub struct AclAliasInfo { +pub(crate) struct AclAliasInfo { pub id: I, pub parent_id: Option, pub name: String, @@ -1368,7 +1342,7 @@ pub struct AclAliasInfo { impl AclAliasInfo { /// Constructs a [`String`] of comma-separated addresses and address ranges - pub fn format_destination(&self) -> String { + pub(crate) fn format_destination(&self) -> String { // process single addresses let addrs = match &self.destination { d if d.is_empty() => String::new(), @@ -1383,6 +1357,7 @@ impl AclAliasInfo { }; // remove full mask from resulting string + // FIXME: This mask shouldn't be removed for IP v6 addresses. let destination = (addrs + &ranges).replace("/32", ""); if destination.is_empty() { destination @@ -1393,7 +1368,7 @@ impl AclAliasInfo { } /// Constructs a [`String`] of comma-separated ports and port ranges - pub fn format_ports(&self) -> String { + pub(crate) fn format_ports(&self) -> String { self.ports .iter() .map(ToString::to_string) @@ -1402,26 +1377,6 @@ impl AclAliasInfo { } } -impl TryFrom for AclAlias { - type Error = AclError; - - fn try_from(alias: EditAclAlias) -> Result { - Ok(Self { - destination: parse_destination(&alias.destination)?.addrs, - ports: parse_ports(&alias.ports)? - .into_iter() - .map(Into::into) - .collect(), - id: NoId, - parent_id: None, - name: alias.name, - kind: alias.kind, - state: AliasState::Applied, - protocols: alias.protocols, - }) - } -} - /// ACL alias can be in one of the following states: /// - Applied: the alias can be used in ACL rules /// - Modified: the alias has been modified and the changes have not yet been applied @@ -1441,7 +1396,7 @@ pub enum AliasState { /// ACL alias can be of one of the following types: /// - Destination: the alias defines a complete destination that an ACL rule applies to /// - Component: the alias defines parts of a destination and will be combined with other parts manually defined in an ACL rule -#[derive(Clone, Debug, Default, Deserialize, Serialize, Type, PartialEq, Eq)] +#[derive(Clone, Debug, Default, Deserialize, Eq, Serialize, PartialEq, ToSchema, Type)] #[sqlx(type_name = "aclalias_kind", rename_all = "lowercase")] pub enum AliasKind { #[default] @@ -1494,22 +1449,41 @@ impl AclAlias { } } - /// Creates new [`AclAlias`] with all related objects based on [`AclAliasInfo`] + /// Try to convert alias from API. + pub(crate) fn try_from(alias: &EditAclAlias, kind: AliasKind) -> Result { + Ok(Self { + destination: parse_destination(&alias.destination)?.addrs, + ports: parse_ports(&alias.ports)? + .into_iter() + .map(Into::into) + .collect(), + id: NoId, + parent_id: None, + name: alias.name.clone(), + kind, + state: AliasState::Applied, + protocols: alias.protocols.clone(), + }) + } + + /// Creates new [`AclAlias`] with all related objects based on [`AclAliasInfo`]. pub(crate) async fn create_from_api( pool: &PgPool, api_alias: &EditAclAlias, + kind: AliasKind, ) -> Result { let mut transaction = pool.begin().await?; // save the alias - let alias: AclAlias = api_alias.clone().try_into()?; - let alias = alias.save(&mut *transaction).await?; + let alias = AclAlias::try_from(api_alias, kind)? + .save(&mut *transaction) + .await?; // create related objects Self::create_related_objects(&mut transaction, alias.id, api_alias).await?; transaction.commit().await?; - let result: ApiAclAlias = alias.to_info(pool).await?.into(); + let result = ApiAclAlias::from(alias.to_info(pool).await?); Ok(result) } @@ -1518,6 +1492,7 @@ impl AclAlias { pool: &PgPool, id: Id, api_alias: &EditAclAlias, + kind: AliasKind, ) -> Result { let mut transaction = pool.begin().await?; @@ -1529,8 +1504,8 @@ impl AclAlias { AclError::AliasNotFoundError(id) })?; - // convert API alias to model - let mut alias: AclAlias = api_alias.clone().try_into()?; + // Convert alias from API to model. + let mut alias = AclAlias::try_from(api_alias, kind)?; // perform appropriate updates depending on existing alias' state let alias = match existing_alias.state { @@ -1937,8 +1912,8 @@ impl From<&AclRuleDestinationRange> for RangeInclusive { } } -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct AclAliasDestinationRange { +#[derive(Clone, Debug, Deserialize, PartialEq)] +pub(crate) struct AclAliasDestinationRange { pub id: I, pub alias_id: Id, pub start: IpAddr, diff --git a/crates/defguard_core/src/enterprise/handlers/acl.rs b/crates/defguard_core/src/enterprise/handlers/acl.rs index 953550e1cb..b341029b16 100644 --- a/crates/defguard_core/src/enterprise/handlers/acl.rs +++ b/crates/defguard_core/src/enterprise/handlers/acl.rs @@ -1,3 +1,5 @@ +pub(crate) mod destination; + use axum::{ Json, extract::{Path, State}, @@ -6,6 +8,7 @@ use axum::{ use chrono::NaiveDateTime; use defguard_common::db::Id; use serde_json::{Value, json}; +use utoipa::ToSchema; use super::LicenseInfo; use crate::{ @@ -18,8 +21,8 @@ use crate::{ handlers::{ApiResponse, ApiResult}, }; -/// API representation of [`AclRule`] used in API responses -/// All relations represented as arrays of ids. +/// API representation of [`AclRule`] used in API responses. +/// All relations represented as arrays of IDs. #[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] pub struct ApiAclRule { pub id: Id, @@ -78,7 +81,7 @@ impl From> for ApiAclRule { } /// API representation of [`AclRule`] used in API requests for modification operations -#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, ToSchema)] pub struct EditAclRule { pub name: String, pub all_networks: bool, @@ -180,26 +183,34 @@ impl From> for ApiAclAlias { } /// API representation of [`AclAlias`] used in API requests for modification operations -#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, ToSchema)] pub struct EditAclAlias { pub name: String, - pub kind: AliasKind, pub destination: String, pub ports: String, pub protocols: Vec, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, ToSchema)] pub struct ApplyAclRulesData { rules: Vec, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, ToSchema)] pub struct ApplyAclAliasesData { aliases: Vec, } -pub async fn list_acl_rules( +/// List all ACL rules. +#[utoipa::path( + get, + path = "/api/v1/acl/rule", + tag = "ACL", + responses( + (status = OK, description = "ACL rules"), + ), +)] +pub(crate) async fn list_acl_rules( _license: LicenseInfo, _admin: AdminRole, State(appstate): State, @@ -208,11 +219,11 @@ pub async fn list_acl_rules( debug!("User {} listing ACL rules", session.user.username); let mut conn = appstate.pool.acquire().await?; let rules = AclRule::all(&mut *conn).await?; - let mut api_rules: Vec = Vec::with_capacity(rules.len()); - for r in &rules { + let mut api_rules = Vec::::with_capacity(rules.len()); + for rule in &rules { // TODO: may require optimisation wrt. sql queries - let info = r.to_info(&mut conn).await.map_err(|err| { - error!("Error retrieving ACL rule {r:?}: {err}"); + let info = rule.to_info(&mut conn).await.map_err(|err| { + error!("Error retrieving ACL rule {rule:?}: {err}"); err })?; api_rules.push(info.into()); @@ -224,7 +235,19 @@ pub async fn list_acl_rules( }) } -pub async fn get_acl_rule( +/// Get ACL rule. +#[utoipa::path( + get, + path = "/api/v1/acl/rule/{id}", + tag = "ACL", + params( + ("id" = Id, Path, description = "ID of ACL rule",) + ), + responses( + (status = OK, description = "ACL rule"), + ) +)] +pub(crate) async fn get_acl_rule( _license: LicenseInfo, _admin: AdminRole, State(appstate): State, @@ -235,12 +258,12 @@ pub async fn get_acl_rule( let mut conn = appstate.pool.acquire().await?; let (rule, status) = match AclRule::find_by_id(&mut *conn, id).await? { Some(rule) => ( - json!(Into::::into( - rule.to_info(&mut conn).await.map_err(|err| { + json!(ApiAclRule::from(rule.to_info(&mut conn).await.map_err( + |err| { error!("Error retrieving ACL rule {rule:?}: {err}"); err - })? - )), + } + )?)), StatusCode::OK, ), None => (Value::Null, StatusCode::NOT_FOUND), @@ -250,7 +273,17 @@ pub async fn get_acl_rule( Ok(ApiResponse::new(rule, status)) } -pub async fn create_acl_rule( +/// Create ACL rule. +#[utoipa::path( + post, + path = "/api/v1/acl/rule", + tag = "ACL", + request_body = EditAclRule, + responses( + (status = OK, description = "ACL rule"), + ) +)] +pub(crate) async fn create_acl_rule( _license: LicenseInfo, _admin: AdminRole, State(appstate): State, @@ -278,7 +311,20 @@ pub async fn create_acl_rule( }) } -pub async fn update_acl_rule( +/// Update ACL rule. +#[utoipa::path( + put, + path = "/api/v1/acl/rule/{id}", + tag = "ACL", + params( + ("id" = Id, Path, description = "ID of ACL rule",) + ), + request_body = EditAclRule, + responses( + (status = OK, description = "ACL rule"), + ) +)] +pub(crate) async fn update_acl_rule( _license: LicenseInfo, _admin: AdminRole, State(appstate): State, @@ -304,12 +350,24 @@ pub async fn update_acl_rule( }) } -pub async fn delete_acl_rule( +/// Delete ACL rule. +#[utoipa::path( + delete, + path = "/api/v1/acl/rule/{id}", + tag = "ACL", + params( + ("id" = Id, Path, description = "ID of ACL rule",) + ), + responses( + (status = OK, description = "ACL rule"), + ) +)] +pub(crate) async fn delete_acl_rule( _license: LicenseInfo, _admin: AdminRole, State(appstate): State, session: SessionInfo, - Path(id): Path, + Path(id): Path, ) -> ApiResult { debug!("User {} deleting ACL rule {id}", session.user.username); AclRule::delete_from_api(&appstate.pool, id) @@ -322,7 +380,16 @@ pub async fn delete_acl_rule( Ok(ApiResponse::default()) } -pub async fn list_acl_aliases( +/// List all ACL aliases. +#[utoipa::path( + get, + path = "/api/v1/acl/alias", + tag = "ACL", + responses( + (status = OK, description = "ACL alias"), + ), +)] +pub(crate) async fn list_acl_aliases( _license: LicenseInfo, _admin: AdminRole, State(appstate): State, @@ -331,10 +398,10 @@ pub async fn list_acl_aliases( debug!("User {} listing ACL aliases", session.user.username); let aliases = AclAlias::all(&appstate.pool).await?; let mut api_aliases: Vec = Vec::with_capacity(aliases.len()); - for a in &aliases { + for alias in &aliases { // TODO: may require optimisation wrt. sql queries - let info = a.to_info(&appstate.pool).await.map_err(|err| { - error!("Error retrieving ACL alias {a:?}: {err}"); + let info = alias.to_info(&appstate.pool).await.map_err(|err| { + error!("Error retrieving ACL alias {alias:?}: {err}"); err })?; api_aliases.push(info.into()); @@ -346,7 +413,19 @@ pub async fn list_acl_aliases( }) } -pub async fn get_acl_alias( +/// Get ACL alias. +#[utoipa::path( + get, + path = "/api/v1/acl/alias/{id}", + tag = "ACL", + params( + ("id" = Id, Path, description = "ID of ACL alias",) + ), + responses( + (status = OK, description = "ACL alias"), + ) +)] +pub(crate) async fn get_acl_alias( _license: LicenseInfo, _admin: AdminRole, State(appstate): State, @@ -356,7 +435,7 @@ pub async fn get_acl_alias( debug!("User {} retrieving ACL alias {id}", session.user.username); let (alias, status) = match AclAlias::find_by_id(&appstate.pool, id).await? { Some(alias) => ( - json!(Into::::into( + json!(ApiAclAlias::from( alias.to_info(&appstate.pool).await.map_err(|err| { error!("Error retrieving ACL alias {alias:?}: {err}"); err @@ -374,7 +453,17 @@ pub async fn get_acl_alias( }) } -pub async fn create_acl_alias( +/// Create ACL alias. +#[utoipa::path( + post, + path = "/api/v1/acl/alias", + tag = "ACL", + request_body = EditAclAlias, + responses( + (status = CREATED, description = "ACL alias"), + ) +)] +pub(crate) async fn create_acl_alias( _license: LicenseInfo, _admin: AdminRole, State(appstate): State, @@ -382,7 +471,7 @@ pub async fn create_acl_alias( Json(data): Json, ) -> ApiResult { debug!("User {} creating ACL alias {data:?}", session.user.username); - let alias = AclAlias::create_from_api(&appstate.pool, &data) + let alias = AclAlias::create_from_api(&appstate.pool, &data, AliasKind::Component) .await .map_err(|err| { error!("Error creating ACL alias {data:?}: {err}"); @@ -398,7 +487,20 @@ pub async fn create_acl_alias( }) } -pub async fn update_acl_alias( +/// Update ACL alias. +#[utoipa::path( + put, + path = "/api/v1/acl/alias/{id}", + tag = "ACL", + params( + ("id" = Id, Path, description = "ID of ACL alias",) + ), + request_body = EditAclAlias, + responses( + (status = OK, description = "ACL alias"), + ) +)] +pub(crate) async fn update_acl_alias( _license: LicenseInfo, _admin: AdminRole, State(appstate): State, @@ -407,7 +509,7 @@ pub async fn update_acl_alias( Json(data): Json, ) -> ApiResult { debug!("User {} updating ACL alias {data:?}", session.user.username); - let alias = AclAlias::update_from_api(&appstate.pool, id, &data) + let alias = AclAlias::update_from_api(&appstate.pool, id, &data, AliasKind::Component) .await .map_err(|err| { error!("Error updating ACL alias {data:?}: {err}"); @@ -420,7 +522,18 @@ pub async fn update_acl_alias( }) } -pub async fn delete_acl_alias( +/// Delete ACL alias. +#[utoipa::path( + delete, + path = "/api/v1/acl/alias/{id}", + params( + ("id" = Id, Path, description = "ID of ACL alias",) + ), + responses( + (status = OK, description = "ACL alias"), + ) +)] +pub(crate) async fn delete_acl_alias( _license: LicenseInfo, _admin: AdminRole, State(appstate): State, @@ -438,7 +551,16 @@ pub async fn delete_acl_alias( Ok(ApiResponse::default()) } -pub async fn apply_acl_rules( +/// Apply ACL alias. +#[utoipa::path( + put, + path = "/api/v1/acl/rule/apply", + request_body = ApplyAclRulesData, + responses( + (status = OK, description = "ACL alias"), + ) +)] +pub(crate) async fn apply_acl_rules( _license: LicenseInfo, _admin: AdminRole, State(appstate): State, @@ -462,7 +584,16 @@ pub async fn apply_acl_rules( Ok(ApiResponse::default()) } -pub async fn apply_acl_aliases( +/// Apply ACL aliases. +#[utoipa::path( + put, + path = "/api/v1/acl/alias/apply", + request_body = ApplyAclAliasesData, + responses( + (status = OK, description = "ACL alias"), + ) +)] +pub(crate) async fn apply_acl_aliases( _license: LicenseInfo, _admin: AdminRole, State(appstate): State, diff --git a/crates/defguard_core/src/enterprise/handlers/acl/destination.rs b/crates/defguard_core/src/enterprise/handlers/acl/destination.rs new file mode 100644 index 0000000000..2df0c168f2 --- /dev/null +++ b/crates/defguard_core/src/enterprise/handlers/acl/destination.rs @@ -0,0 +1,209 @@ +use axum::{ + Json, + extract::{Path, State}, +}; +use defguard_common::db::Id; +use reqwest::StatusCode; +use serde_json::{Value, json}; + +use crate::{ + appstate::AppState, + auth::{AdminRole, SessionInfo}, + enterprise::{ + db::models::acl::{AclAlias, AliasKind}, + handlers::{ + LicenseInfo, + acl::{ApiAclAlias, EditAclAlias}, + }, + }, + handlers::{ApiResponse, ApiResult}, +}; + +/// List ACL destinations. +#[utoipa::path( + get, + path = "/api/v1/acl/destination", + tag = "ACL", + responses( + (status = OK, description = "ACL destination"), + ) +)] +pub(crate) async fn list_acl_destinations( + _license: LicenseInfo, + _admin: AdminRole, + State(appstate): State, + session: SessionInfo, +) -> ApiResult { + debug!("User {} listing ACL destinations", session.user.username); + let aliases = AclAlias::all(&appstate.pool).await?; + let mut api_aliases: Vec = Vec::with_capacity(aliases.len()); + for alias in &aliases { + // TODO: may require optimisation wrt. sql queries + let info = alias.to_info(&appstate.pool).await.map_err(|err| { + error!("Error retrieving ACL destination {alias:?}: {err}"); + err + })?; + api_aliases.push(info.into()); + } + info!("User {} listed ACL destinations", session.user.username); + Ok(ApiResponse { + json: json!(api_aliases), + status: StatusCode::OK, + }) +} + +/// Get ACL destination. +#[utoipa::path( + get, + path = "/api/v1/acl/destination/{id}", + tag = "ACL", + params( + ("id" = Id, Path, description = "ID of ACL destination",) + ), + responses( + (status = OK, description = "ACL destination"), + ) +)] +pub(crate) async fn get_acl_destination( + _license: LicenseInfo, + _admin: AdminRole, + State(appstate): State, + session: SessionInfo, + Path(id): Path, +) -> ApiResult { + debug!( + "User {} retrieving ACL destination {id}", + session.user.username + ); + let (alias, status) = match AclAlias::find_by_id(&appstate.pool, id).await? { + Some(alias) => ( + json!(ApiAclAlias::from( + alias.to_info(&appstate.pool).await.map_err(|err| { + error!("Error retrieving ACL destination {alias:?}: {err}"); + err + })? + )), + StatusCode::OK, + ), + None => (Value::Null, StatusCode::NOT_FOUND), + }; + + info!( + "User {} retrieved ACL destination {id}", + session.user.username + ); + Ok(ApiResponse { + json: alias, + status, + }) +} + +/// Create ACL destination. +#[utoipa::path( + post, + path = "/api/v1/acl/destination", + tag = "ACL", + request_body = EditAclAlias, + responses( + (status = CREATED, description = "ACL destination"), + ) +)] +pub(crate) async fn create_acl_destination( + _license: LicenseInfo, + _admin: AdminRole, + State(appstate): State, + session: SessionInfo, + Json(data): Json, +) -> ApiResult { + debug!( + "User {} creating ACL destination {data:?}", + session.user.username + ); + let alias = AclAlias::create_from_api(&appstate.pool, &data, AliasKind::Destination) + .await + .map_err(|err| { + error!("Error creating ACL destination {data:?}: {err}"); + err + })?; + info!( + "User {} created ACL destination {}", + session.user.username, alias.id + ); + Ok(ApiResponse { + json: json!(alias), + status: StatusCode::CREATED, + }) +} + +/// Update ACL destination. +#[utoipa::path( + put, + path = "/api/v1/acl/destination/{id}", + tag = "ACL", + params( + ("id" = Id, Path, description = "ID of ACL destination",) + ), + responses( + (status = OK, description = "ACL destination"), + ) +)] +pub(crate) async fn update_acl_destination( + _license: LicenseInfo, + _admin: AdminRole, + State(appstate): State, + session: SessionInfo, + Path(id): Path, + Json(data): Json, +) -> ApiResult { + debug!( + "User {} updating ACL destination {data:?}", + session.user.username + ); + let alias = AclAlias::update_from_api(&appstate.pool, id, &data, AliasKind::Destination) + .await + .map_err(|err| { + error!("Error updating ACL destination {data:?}: {err}"); + err + })?; + info!("User {} updated ACL destination", session.user.username); + Ok(ApiResponse { + json: json!(alias), + status: StatusCode::OK, + }) +} + +/// Delete ACL destination. +#[utoipa::path( + delete, + path = "/api/v1/acl/destination/{id}", + tag = "ACL", + params( + ("id" = Id, Path, description = "ID of ACL destination",) + ), + responses( + (status = OK, description = "ACL destination"), + ) +)] +pub(crate) async fn delete_acl_destination( + _license: LicenseInfo, + _admin: AdminRole, + State(appstate): State, + session: SessionInfo, + Path(id): Path, +) -> ApiResult { + debug!( + "User {} deleting ACL destination {id}", + session.user.username + ); + AclAlias::delete_from_api(&appstate.pool, id) + .await + .map_err(|err| { + error!("Error deleting ACL destination {id}: {err}"); + err + })?; + info!( + "User {} deleted ACL destination {id}", + session.user.username + ); + Ok(ApiResponse::default()) +} diff --git a/crates/defguard_core/src/enterprise/handlers/openid_providers.rs b/crates/defguard_core/src/enterprise/handlers/openid_providers.rs index 8524173b1d..60ae0b0132 100644 --- a/crates/defguard_core/src/enterprise/handlers/openid_providers.rs +++ b/crates/defguard_core/src/enterprise/handlers/openid_providers.rs @@ -57,6 +57,7 @@ pub struct AddProviderData { #[utoipa::path( post, path = "/api/v1/openid/provider", + tag = "OpenID", params( ("data" = AddProviderData, Path, description = "OpenID provider data",) ), @@ -203,6 +204,7 @@ pub(crate) async fn add_openid_provider( #[utoipa::path( get, path = "/api/v1/openid/provider/{name}", + tag = "OpenID", responses( (status = OK, description = "Get OpenID provider"), ), @@ -249,6 +251,7 @@ pub(crate) async fn get_openid_provider( #[utoipa::path( delete, path = "/api/v1/openid/provider/{name}", + tag = "OpenID", responses( (status = OK, description = "Delete OpenID provider"), ), @@ -318,6 +321,7 @@ pub(crate) async fn delete_openid_provider( #[utoipa::path( put, path = "/api/v1/openid/provider/{name}", + tag = "OpenID", responses( (status = OK, description = "Modify OpenID provider"), ), @@ -377,8 +381,9 @@ pub(crate) async fn modify_openid_provider( #[utoipa::path( get, path = "/api/v1/openid/provider", + tag = "OpenID", responses( - (status = OK, description = "List all OpenID provider"), + (status = OK, description = "List of OpenID providers"), ), )] pub(crate) async fn list_openid_providers( diff --git a/crates/defguard_core/src/handlers/openid_flow.rs b/crates/defguard_core/src/handlers/openid_flow.rs index ab3f87e9a6..2f12e57fcc 100644 --- a/crates/defguard_core/src/handlers/openid_flow.rs +++ b/crates/defguard_core/src/handlers/openid_flow.rs @@ -139,7 +139,7 @@ where return Ok(Self( OAuth2Client::find_by_auth(&appstate.pool, client_id, client_secret) .await - .map_err(Into::::into)?, + .map_err(WebError::from)?, )); } } diff --git a/crates/defguard_core/src/handlers/yubikey.rs b/crates/defguard_core/src/handlers/yubikey.rs index b8a83b0c0c..23a1d65ce6 100644 --- a/crates/defguard_core/src/handlers/yubikey.rs +++ b/crates/defguard_core/src/handlers/yubikey.rs @@ -3,16 +3,16 @@ use axum::{ extract::{Path, State}, http::StatusCode, }; -use defguard_common::db::models::YubiKey; +use defguard_common::db::{Id, models::YubiKey}; use serde_json::json; use super::{ApiResponse, ApiResult, user_for_admin_or_self}; use crate::{appstate::AppState, auth::SessionInfo, error::WebError}; -pub async fn delete_yubikey( +pub(crate) async fn delete_yubikey( State(appstate): State, session: SessionInfo, - Path((username, key_id)): Path<(String, i64)>, + Path((username, key_id)): Path<(String, Id)>, ) -> ApiResult { debug!("Deleting yubikey {key_id} by {:?}", &session.user.id); let user = user_for_admin_or_self(&appstate.pool, &session, &username).await?; @@ -35,19 +35,19 @@ pub async fn delete_yubikey( }) } -#[derive(Debug, Deserialize, Clone)] -pub struct RenameRequest { +#[derive(Deserialize)] +pub(crate) struct RenameRequest { name: String, } -pub async fn rename_yubikey( +pub(crate) async fn rename_yubikey( State(appstate): State, session: SessionInfo, - Path((username, key_id)): Path<(String, i64)>, + Path((username, key_id)): Path<(String, Id)>, Json(data): Json, ) -> ApiResult { let user = user_for_admin_or_self(&appstate.pool, &session, &username).await?; - debug!("User {} attempts to rename yubikey {}", user.id, key_id); + debug!("User {} attempts to rename yubikey {key_id}", user.id); let Some(mut yubikey) = YubiKey::find_by_id(&appstate.pool, key_id).await? else { error!("Yubikey with id {key_id} not found"); return Err(WebError::ObjectNotFound("YubiKey not found".into())); diff --git a/crates/defguard_core/src/lib.rs b/crates/defguard_core/src/lib.rs index eb622875ab..cedb4e4fbd 100644 --- a/crates/defguard_core/src/lib.rs +++ b/crates/defguard_core/src/lib.rs @@ -70,7 +70,7 @@ use tracing::Level; use utoipa::OpenApi; use utoipa_swagger_ui::SwaggerUi; -use self::{ +use crate::{ appstate::AppState, auth::failed_login::FailedLoginMap, db::AppEvent, @@ -78,8 +78,13 @@ use self::{ handlers::{ acl::{ apply_acl_aliases, apply_acl_rules, create_acl_alias, create_acl_rule, - delete_acl_alias, delete_acl_rule, get_acl_alias, get_acl_rule, list_acl_aliases, - list_acl_rules, update_acl_alias, update_acl_rule, + delete_acl_alias, delete_acl_rule, + destination::{ + create_acl_destination, delete_acl_destination, get_acl_destination, + list_acl_destinations, update_acl_destination, + }, + get_acl_alias, get_acl_rule, list_acl_aliases, list_acl_rules, update_acl_alias, + update_acl_rule, }, activity_log_stream::{ create_activity_log_stream, delete_activity_log_stream, get_activity_log_stream, @@ -91,14 +96,14 @@ use self::{ openid_login::{auth_callback, get_auth_info}, openid_providers::{ add_openid_provider, delete_openid_provider, get_openid_provider, - modify_openid_provider, test_dirsync_connection, + list_openid_providers, modify_openid_provider, test_dirsync_connection, }, }, snat::handlers::{ create_snat_binding, delete_snat_binding, list_snat_bindings, modify_snat_binding, }, }, - grpc::WorkerState, + grpc::{WorkerState, gateway::events::GatewayEvent}, handlers::{ app_info::get_app_info, auth::{ @@ -138,18 +143,14 @@ use self::{ add_webhook, change_enabled, change_webhook, delete_webhook, get_webhook, list_webhooks, }, wireguard::{ - add_device, add_user_devices, create_network, create_network_token, delete_device, - delete_network, devices_stats, download_config, gateway_status, get_device, - import_network, list_devices, list_networks, list_user_devices, modify_device, - modify_network, network_details, network_stats, remove_gateway, + add_device, add_gateway, add_user_devices, change_gateway, create_network, + create_network_token, delete_device, delete_network, devices_stats, download_config, + gateway_status, get_device, import_network, list_devices, list_networks, + list_user_devices, modify_device, modify_network, network_details, network_stats, + remove_gateway, }, worker::{create_job, create_worker_token, job_status, list_workers, remove_worker}, }, -}; -use crate::{ - enterprise::handlers::openid_providers::list_openid_providers, - grpc::gateway::events::GatewayEvent, - handlers::wireguard::{add_gateway, change_gateway}, location_management::sync_location_allowed_devices, version::IncompatibleComponents, }; @@ -431,7 +432,17 @@ pub fn build_webapp( .put(update_acl_alias) .delete(delete_acl_alias), ) - .route("/alias/apply", put(apply_acl_aliases)), + .route("/alias/apply", put(apply_acl_aliases)) + .route( + "/destination", + get(list_acl_destinations).post(create_acl_destination), + ) + .route( + "/destination/{id}", + get(get_acl_destination) + .put(update_acl_destination) + .delete(delete_acl_destination), + ), ); let webapp = webapp.nest( diff --git a/crates/defguard_core/src/openapi.rs b/crates/defguard_core/src/openapi.rs index 498a8ffca9..3d792b4449 100644 --- a/crates/defguard_core/src/openapi.rs +++ b/crates/defguard_core/src/openapi.rs @@ -11,7 +11,10 @@ use utoipa::{ }; use super::{ - enterprise::{handlers::openid_providers, snat::handlers as snat}, + enterprise::{ + handlers::{acl, openid_providers}, + snat::handlers as snat, + }, error::WebError, handlers::{ ApiResponse, EditGroupInfo, GroupInfo, PasswordChange, PasswordChangeSelf, @@ -79,6 +82,26 @@ use super::{ openid_providers::delete_openid_provider, openid_providers::modify_openid_provider, openid_providers::list_openid_providers, + // /acl/rule + acl::list_acl_rules, + acl::create_acl_rule, + acl::apply_acl_rules, + acl::get_acl_rule, + acl::update_acl_rule, + acl::delete_acl_rule, + // /acl/alias + acl::list_acl_aliases, + acl::create_acl_alias, + acl::get_acl_alias, + acl::update_acl_alias, + acl::delete_acl_alias, + acl::apply_acl_aliases, + // /acl/destination + acl::destination::list_acl_destinations, + acl::destination::create_acl_destination, + acl::destination::get_acl_destination, + acl::destination::update_acl_destination, + acl::destination::delete_acl_destination, ), components( schemas( @@ -131,6 +154,8 @@ Available actions: - modify SNAT binding - delete SNAT binding "), + (name = "ACL", description = "Access Control Lists (ACL)"), + (name = "OpenID", description = "OpenID providers"), ) )] pub struct ApiDoc; diff --git a/crates/defguard_core/tests/integration/api/acl.rs b/crates/defguard_core/tests/integration/api/acl.rs index bb51bf7ce4..79d14a1558 100644 --- a/crates/defguard_core/tests/integration/api/acl.rs +++ b/crates/defguard_core/tests/integration/api/acl.rs @@ -48,20 +48,20 @@ fn make_rule() -> EditAclRule { EditAclRule { name: "rule".to_string(), all_networks: false, - networks: vec![], + networks: Vec::new(), expires: None, allow_all_users: false, deny_all_users: false, allow_all_network_devices: false, deny_all_network_devices: false, allowed_users: vec![1], - denied_users: vec![], - allowed_groups: vec![], - denied_groups: vec![], - allowed_devices: vec![], - denied_devices: vec![], + denied_users: Vec::new(), + allowed_groups: Vec::new(), + denied_groups: Vec::new(), + allowed_devices: Vec::new(), + denied_devices: Vec::new(), destination: "10.2.2.2, 10.0.0.1/24, 10.0.10.1-10.0.20.1".to_string(), - aliases: vec![], + aliases: Vec::new(), enabled: true, protocols: vec![6, 17], ports: "1, 2, 3, 10-20, 30-40".to_string(), @@ -78,7 +78,6 @@ async fn set_rule_state(pool: &PgPool, id: Id, state: RuleState, parent_id: Opti fn make_alias() -> EditAclAlias { EditAclAlias { name: "alias".to_string(), - kind: AliasKind::Destination, destination: "10.2.2.2, 10.0.0.1/24, 10.0.10.1-10.0.20.1".to_string(), protocols: vec![6, 17], ports: "1, 2, 3, 10-20, 30-40".to_string(), @@ -122,6 +121,7 @@ fn edit_alias_data_into_api_response( id: Id, parent_id: Option, state: AliasState, + kind: AliasKind, rules: Vec, ) -> ApiAclAlias { ApiAclAlias { @@ -129,7 +129,7 @@ fn edit_alias_data_into_api_response( parent_id, state, name: data.name, - kind: data.kind, + kind, destination: data.destination, ports: data.ports, protocols: data.protocols, @@ -248,6 +248,7 @@ async fn test_alias_crud(_: PgPoolOptions, options: PgConnectOptions) { response_alias.id, None, AliasState::Applied, + AliasKind::Component, Vec::new(), ); assert_eq!(response_alias, expected_response); @@ -360,6 +361,7 @@ async fn test_empty_strings(_: PgPoolOptions, options: PgConnectOptions) { response_alias.id, None, AliasState::Applied, + AliasKind::Component, Vec::new(), ); assert_eq!(response_alias, expected_response);