Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
f969ba7
update dependencies
wojcik91 Mar 5, 2026
783da8d
add test for destination delete API
wojcik91 Mar 5, 2026
affbdb2
split acl API tests into separate modules
wojcik91 Mar 5, 2026
6663e84
add missing destination API tests to mirror aliases
wojcik91 Mar 5, 2026
ec4e6a4
validate aliases in API
wojcik91 Mar 5, 2026
47c27ad
formatting
wojcik91 Mar 5, 2026
bac1a0f
add dedicated destination apply endpoint
wojcik91 Mar 5, 2026
4fa6118
update frontend API call
wojcik91 Mar 5, 2026
4671589
add dedicated Destination errors
wojcik91 Mar 5, 2026
c49abb6
add a deletion blocked modal
wojcik91 Mar 6, 2026
5a31df2
display delete blocked modal
wojcik91 Mar 6, 2026
06bc273
add skeleton when loading rules
wojcik91 Mar 6, 2026
3795987
fix close button type
wojcik91 Mar 6, 2026
1b12e11
add a delete confirmation modal
wojcik91 Mar 6, 2026
1d2c6fb
fix rule name display on pending tabs
wojcik91 Mar 6, 2026
df59fcd
Merge branch 'dev' into block_used_alias_delete
wojcik91 Mar 6, 2026
c24f1d0
post-merge fix
wojcik91 Mar 6, 2026
6d9d59a
review fixes
wojcik91 Mar 6, 2026
6c8415f
refactor new delete confirmation to follow recommendations
wojcik91 Mar 6, 2026
9b4ff86
align deletion blocked modal with requirements
wojcik91 Mar 6, 2026
1818cbc
use scoped styles
wojcik91 Mar 6, 2026
c4c4a69
use the existing suspense mechanism
wojcik91 Mar 6, 2026
b972bf7
Merge branch 'dev' into block_used_alias_delete
wojcik91 Mar 6, 2026
878879e
update dependencies
wojcik91 Mar 6, 2026
78bd5d9
more review fixes
wojcik91 Mar 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

56 changes: 41 additions & 15 deletions crates/defguard_core/src/enterprise/db/models/acl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,18 @@ pub enum AclError {
RuleNotFoundError(Id),
#[error("AliasNotFoundError: {0}")]
AliasNotFoundError(Id),
#[error("DestinationNotFoundError: {0}")]
DestinationNotFoundError(Id),
#[error("RuleAlreadyAppliedError: {0}")]
RuleAlreadyAppliedError(Id),
#[error("AliasAlreadyAppliedError: {0}")]
AliasAlreadyAppliedError(Id),
#[error("DestinationAlreadyAppliedError: {0}")]
DestinationAlreadyAppliedError(Id),
#[error("AliasUsedByRulesError: {0}")]
AliasUsedByRulesError(Id),
#[error("DestinationUsedByRulesError: {0}")]
DestinationUsedByRulesError(Id),
#[error(transparent)]
FirewallError(#[from] FirewallError),
#[error("InvalidIpRangeError: {0}")]
Expand Down Expand Up @@ -1520,16 +1526,23 @@ impl AclAlias {
/// 2. The alias itself is deleted from the database
///
/// Since these aliases were not yet applied, we can safely remove them.
pub(crate) async fn delete_from_api(pool: &PgPool, id: Id) -> Result<(), AclError> {
debug!("Deleting alias {id}");
pub(crate) async fn delete_by_kind(
pool: &PgPool,
id: Id,
kind: AliasKind,
) -> Result<(), AclError> {
debug!("Deleting alias {id} of kind {kind:?}");
let mut transaction = pool.begin().await?;

// find the existing alias
let existing_alias = AclAlias::find_by_id(&mut *transaction, id)
let existing_alias = AclAlias::find_by_id_and_kind(&mut *transaction, id, kind.clone())
.await?
.ok_or_else(|| {
error!("Deletion of nonexistent alias ({id}) failed");
AclError::AliasNotFoundError(id)
match kind {
AliasKind::Component => AclError::AliasNotFoundError(id),
AliasKind::Destination => AclError::DestinationNotFoundError(id),
}
})?;

// check if any rules are using this alias
Expand All @@ -1538,7 +1551,10 @@ impl AclAlias {
error!(
"Deletion of alias ({id}) failed. Alias is currently used by following ACL rules: {rules:?}"
);
return Err(AclError::AliasUsedByRulesError(id));
return Err(match kind {
AliasKind::Component => AclError::AliasUsedByRulesError(id),
AliasKind::Destination => AclError::DestinationUsedByRulesError(id),
});
}

// delete all modifications of this alias if any exist
Expand All @@ -1560,23 +1576,33 @@ impl AclAlias {
Ok(())
}

/// Applies pending changes for all specified aliases
/// Applies pending changes for all specified aliases of a given kind
///
/// # Errors
///
/// - `AclError::AliasNotFoundError`
pub(crate) async fn apply_aliases(aliases: &[Id], appstate: &AppState) -> Result<(), AclError> {
debug!("Applying {} ACL aliases: {aliases:?}", aliases.len());
pub(crate) async fn apply_by_kind(
aliases: &[Id],
kind: AliasKind,
appstate: &AppState,
) -> Result<(), AclError> {
debug!(
"Applying {} ACL aliases of kind {kind:?}: {aliases:?}",
aliases.len(),
);
Comment on lines +1584 to +1592
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In apply_by_kind, the parameter and log message still use the name aliases, but the function is also used for destinations. Renaming the parameter (and related log strings) to something neutral like ids/items would make call sites (e.g. destinations) clearer and avoid confusion.

Copilot uses AI. Check for mistakes.
let mut transaction = appstate.pool.begin().await?;

// prepare variable for collecting affected rules
// we are unable to use `HashSet` because `PgRange` does not implement `Hash` trait
let mut affected_rules = Vec::new();

for id in aliases {
let alias = AclAlias::find_by_id(&mut *transaction, *id)
let alias = AclAlias::find_by_id_and_kind(&mut *transaction, *id, kind.clone())
.await?
.ok_or_else(|| AclError::AliasNotFoundError(*id))?;
.ok_or_else(|| match kind {
AliasKind::Component => AclError::AliasNotFoundError(*id),
AliasKind::Destination => AclError::DestinationNotFoundError(*id),
})?;
// run `apply` before fetching relations, since they'll get updated
alias.clone().apply(&mut transaction).await?;

Expand Down Expand Up @@ -1653,10 +1679,7 @@ impl TryFrom<&EditAclAlias> for AclAlias {

impl AclAlias<Id> {
/// Fetch [`AclAlias`] of a given kind.
pub(crate) async fn all_of_kind<'e, E>(
executor: E,
kind: AliasKind,
) -> Result<Vec<Self>, sqlx::Error>
pub async fn all_of_kind<'e, E>(executor: E, kind: AliasKind) -> Result<Vec<Self>, sqlx::Error>
where
E: PgExecutor<'e>,
{
Expand Down Expand Up @@ -1844,7 +1867,10 @@ impl AclAlias<Id> {
}
AliasState::Applied => {
error!("ACL alias {alias_id} already applied");
return Err(AclError::AliasAlreadyAppliedError(self.id));
return Err(match self.kind {
AliasKind::Component => AclError::AliasAlreadyAppliedError(self.id),
AliasKind::Destination => AclError::DestinationAlreadyAppliedError(self.id),
});
}
}

Expand Down
42 changes: 2 additions & 40 deletions crates/defguard_core/src/enterprise/handlers/acl.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
pub mod alias;
pub(crate) mod destination;
pub mod destination;

use axum::{
Json,
Expand All @@ -16,7 +16,7 @@ use super::LicenseInfo;
use crate::{
appstate::AppState,
auth::{AdminRole, SessionInfo},
enterprise::db::models::acl::{AclAlias, AclRule, AclRuleInfo, Protocol, RuleState},
enterprise::db::models::acl::{AclRule, AclRuleInfo, Protocol, RuleState},
error::WebError,
handlers::{ApiResponse, ApiResult},
};
Expand Down Expand Up @@ -204,11 +204,6 @@ pub(crate) struct ApplyAclRulesData {
rules: Vec<Id>,
}

#[derive(Debug, Deserialize, ToSchema)]
pub(crate) struct ApplyAclAliasesData {
aliases: Vec<Id>,
}

#[derive(Debug, Serialize, ToSchema, sqlx::FromRow)]
pub struct AclStateCount {
pub applied: i64,
Expand Down Expand Up @@ -443,36 +438,3 @@ pub(crate) async fn apply_acl_rules(
);
Ok(ApiResponse::default())
}

/// 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<AppState>,
session: SessionInfo,
Json(data): Json<ApplyAclAliasesData>,
) -> ApiResult {
debug!(
"User {} applying ACL aliases: {:?}",
session.user.username, data.aliases
);
AclAlias::apply_aliases(&data.aliases, &appstate)
.await
.map_err(|err| {
error!("Error applying ACL aliases {data:?}: {err}");
err
})?;
info!(
"User {} applied ACL aliases: {:?}",
session.user.username, data.aliases
);
Ok(ApiResponse::default())
}
55 changes: 54 additions & 1 deletion crates/defguard_core/src/enterprise/handlers/acl/alias.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use crate::{
AclAlias, AclAliasDestinationRange, AclAliasInfo, AclError, AliasKind, AliasState,
Protocol, acl_delete_related_objects, parse_destination_addresses,
},
error::WebError,
handlers::{ApiResponse, ApiResult},
};

Expand All @@ -29,6 +30,18 @@ pub struct EditAclAlias {
}

impl EditAclAlias {
fn validate(&self) -> Result<(), WebError> {
if self.addresses.trim().is_empty()
&& self.ports.trim().is_empty()
&& self.protocols.is_empty()
{
return Err(WebError::BadRequest(
"Must provide alias addresses, ports, or protocols".to_string(),
));
}
Ok(())
}

/// Creates relation objects for a given [`AclAlias`] based on [`AclAliasInfo`] object.
pub(crate) async fn create_related_objects(
&self,
Expand Down Expand Up @@ -69,6 +82,11 @@ pub struct ApiAclAlias {
pub rules: Vec<Id>,
}

#[derive(Debug, Deserialize, ToSchema)]
pub(crate) struct ApplyAclAliasesData {
aliases: Vec<Id>,
}

impl ApiAclAlias {
/// Creates new [`AclAlias`] with all related objects based on [`AclAliasInfo`].
pub(crate) async fn create_from_api(
Expand Down Expand Up @@ -289,6 +307,7 @@ pub(crate) async fn create_acl_alias(
Json(data): Json<EditAclAlias>,
) -> ApiResult {
debug!("User {} creating ACL alias {data:?}", session.user.username);
data.validate()?;
let alias = ApiAclAlias::create_from_api(&appstate.pool, &data)
.await
.map_err(|err| {
Expand Down Expand Up @@ -324,6 +343,7 @@ pub(crate) async fn update_acl_alias(
Json(data): Json<EditAclAlias>,
) -> ApiResult {
debug!("User {} updating ACL alias {data:?}", session.user.username);
data.validate()?;
let alias = ApiAclAlias::update_from_api(&appstate.pool, id, &data)
.await
.map_err(|err| {
Expand Down Expand Up @@ -353,7 +373,7 @@ pub(crate) async fn delete_acl_alias(
Path(id): Path<Id>,
) -> ApiResult {
debug!("User {} deleting ACL alias {id}", session.user.username);
AclAlias::delete_from_api(&appstate.pool, id)
AclAlias::delete_by_kind(&appstate.pool, id, AliasKind::Component)
.await
.map_err(|err| {
error!("Error deleting ACL alias {id}: {err}");
Expand All @@ -362,3 +382,36 @@ pub(crate) async fn delete_acl_alias(
info!("User {} deleted ACL alias {id}", session.user.username);
Ok(ApiResponse::default())
}

/// 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<AppState>,
session: SessionInfo,
Json(data): Json<ApplyAclAliasesData>,
) -> ApiResult {
debug!(
"User {} applying ACL aliases: {:?}",
session.user.username, data.aliases
);
AclAlias::apply_by_kind(&data.aliases, AliasKind::Component, &appstate)
.await
.map_err(|err| {
error!("Error applying ACL aliases {data:?}: {err}");
err
})?;
info!(
"User {} applied ACL aliases: {:?}",
session.user.username, data.aliases
);
Ok(ApiResponse::default())
}
45 changes: 42 additions & 3 deletions crates/defguard_core/src/enterprise/handlers/acl/destination.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ use crate::{

/// API representation of [`AclAlias`] used in API requests for modification operations
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, ToSchema)]
pub(crate) struct EditAclDestination {
pub struct EditAclDestination {
pub name: String,
pub addresses: String,
pub ports: String,
Expand Down Expand Up @@ -80,7 +80,7 @@ impl EditAclDestination {
/// API representation of [`AclAlias`] for "Destination" (not "Alias Component").
/// All relations represented as arrays of IDs.
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, ToSchema)]
pub(crate) struct ApiAclDestination {
pub struct ApiAclDestination {
#[serde(default)]
pub id: Id,
pub parent_id: Option<Id>,
Expand All @@ -96,6 +96,11 @@ pub(crate) struct ApiAclDestination {
pub any_protocol: bool,
}

#[derive(Debug, Deserialize, ToSchema)]
pub(crate) struct ApplyAclDestinationsData {
destinations: Vec<Id>,
}

impl ApiAclDestination {
/// Creates new [`AclAlias`] with all related objects based on [`AclAliasInfo`].
pub(crate) async fn create_from_api(
Expand Down Expand Up @@ -400,7 +405,7 @@ pub(crate) async fn delete_acl_destination(
"User {} deleting ACL destination {id}",
session.user.username
);
AclAlias::delete_from_api(&appstate.pool, id)
AclAlias::delete_by_kind(&appstate.pool, id, AliasKind::Destination)
.await
.map_err(|err| {
error!("Error deleting ACL destination {id}: {err}");
Expand All @@ -412,3 +417,37 @@ pub(crate) async fn delete_acl_destination(
);
Ok(ApiResponse::default())
}

/// Apply ACL destinations.
#[utoipa::path(
put,
path = "/api/v1/acl/destination/apply",
request_body = ApplyAclDestinationsData,
responses(
(status = OK, description = "ACL destination"),
)
)]
pub(crate) async fn apply_acl_destinations(
_license: LicenseInfo,
_admin: AdminRole,
State(appstate): State<AppState>,
session: SessionInfo,
Json(data): Json<ApplyAclDestinationsData>,
) -> ApiResult {
debug!(
"User {} applying ACL destinations: {:?}",
session.user.username, data.destinations
);

AclAlias::apply_by_kind(&data.destinations, AliasKind::Destination, &appstate)
.await
.map_err(|err| {
error!("Error applying ACL destinations {data:?}: {err}");
err
})?;
info!(
"User {} applied ACL destinations: {:?}",
session.user.username, data.destinations
);
Ok(ApiResponse::default())
}
Loading
Loading