Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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

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

2 changes: 1 addition & 1 deletion crates/defguard_core/src/enterprise/limits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ pub async fn update_counts<'e, E: sqlx::PgExecutor<'e>>(executor: E) -> sqlx::Re
debug!("Updating device, user, and wireguard network counts.");
let result = query!(
"SELECT \
(SELECT count(*) FROM \"user\") \"users!\", \
(SELECT count(*) FROM \"user\" WHERE is_active) \"users!\", \
(SELECT count(*) FROM device WHERE device_type = 'user') \"user_devices!\", \
(SELECT count(*) FROM device WHERE device_type = 'network') \"network_devices!\",
(SELECT count(*) FROM wireguard_network) \"wireguard_networks!\"
Expand Down
38 changes: 38 additions & 0 deletions crates/defguard_core/src/handlers/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -973,6 +973,20 @@ pub(crate) async fn modify_user(
return Ok(ApiResponse::with_status(StatusCode::BAD_REQUEST));
}

// check if re-enabling a disabled user will go over license limits
if !user.is_active && user_info.is_active {
let user_count = get_counts().user();

if get_cached_license()
.as_ref()
.and_then(|l| l.limits.as_ref())
.is_some_and(|l| user_count >= l.users)
{
error!("Enabling user {username} blocked! License limit reached.");
return Ok(WebError::Forbidden("License limit reached").into());
}
}

// update VPN gateway config if user status or groups have changed
group_diff = user_info
.handle_user_groups(&mut transaction, &mut user)
Expand Down Expand Up @@ -1003,6 +1017,9 @@ pub(crate) async fn modify_user(

user.save(&mut *transaction).await?;
transaction.commit().await?;
if status_changing {
update_counts(&appstate.pool).await?;
}
let user_info = UserInfo::from_user(
&appstate.pool,
user.clone(),
Expand Down Expand Up @@ -1770,6 +1787,24 @@ pub(crate) async fn bulk_enable_users(
));
}

// check if enabling the requested users will go over license limits
let to_enable_count = users.iter().filter(|user| !user.is_active).count() as u32;
if to_enable_count > 0 {
let user_count = get_counts().user();

if get_cached_license()
.as_ref()
.and_then(|l| l.limits.as_ref())
.is_some_and(|l| user_count + to_enable_count > l.users)
{
error!(
"User {} bulk-enabling users blocked! License limit reached.",
session.user.username
);
return Ok(WebError::Forbidden("License limit reached").into());
}
}

let mut events = Vec::with_capacity(users.len());
let mut transaction = appstate.pool.begin().await?;
for user in users {
Expand All @@ -1784,6 +1819,9 @@ pub(crate) async fn bulk_enable_users(
events.push((before, user_to_enable));
}
transaction.commit().await?;
if to_enable_count > 0 {
update_counts(&appstate.pool).await?;
}

for (_, user) in &mut events {
Box::pin(ldap_update_user_state(
Expand Down
1 change: 1 addition & 0 deletions crates/defguard_core/src/user_management.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ pub async fn disable_user(
) -> Result<(), UserManagementError> {
user.is_active = false;
user.save(&mut *conn).await?;
update_counts(&mut *conn).await?;
user.logout_all_sessions(&mut *conn).await?;
sync_allowed_user_devices(user, conn, gateway_tx).await?;
Ok(())
Expand Down
216 changes: 216 additions & 0 deletions crates/defguard_core/tests/integration/api/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1046,6 +1046,222 @@ async fn test_add_user_blocked_when_user_count_exceeds_license_limit(
client.assert_event_queue_is_empty();
}

#[sqlx::test]
async fn test_disabled_users_not_counted_towards_license_limit(
_: PgPoolOptions,
options: PgConnectOptions,
) {
let pool = setup_pool(options).await;

let (mut client, pool) = make_client_with_db(pool).await;

client.login_user("admin", "pass123").await;

// baseline is admin + hpotter, both active
let hpotter = get_db_user(&pool, "hpotter").await;
let response = client
.post("/api/v1/user/bulk-disable")
.json(&serde_json::json!({ "users": [hpotter.id] }))
.send()
.await;
assert_eq!(response.status(), StatusCode::OK);

let license = get_cached_license().clone();
set_cached_license(Some(License::new(
"test_customer".to_owned(),
false,
None,
Some(LicenseLimits {
users: 2,
devices: 100,
locations: 100,
network_devices: Some(100),
}),
None,
LicenseTier::Business,
SupportType::Basic,
vec![],
)));

// only admin is active, so there is still room under the limit of 2
let new_user = AddUserData {
username: "adumbledore".into(),
last_name: "Dumbledore".into(),
first_name: "Albus".into(),
email: "a.dumbledore@hogwart.edu.uk".into(),
phone: Some("1234".into()),
password: Some("Password1234543$!".into()),
};
let response = client.post("/api/v1/user").json(&new_user).send().await;
assert_eq!(response.status(), StatusCode::CREATED);

set_cached_license(license);
}

#[sqlx::test]
async fn test_modify_user_enable_blocked_when_it_would_exceed_license_limit(
_: PgPoolOptions,
options: PgConnectOptions,
) {
let pool = setup_pool(options).await;

let (mut client, pool) = make_client_with_db(pool).await;

client.login_user("admin", "pass123").await;

let new_user = AddUserData {
username: "adumbledore".into(),
last_name: "Dumbledore".into(),
first_name: "Albus".into(),
email: "a.dumbledore@hogwart.edu.uk".into(),
phone: Some("1234".into()),
password: Some("Password1234543$!".into()),
};
let response = client.post("/api/v1/user").json(&new_user).send().await;
assert_eq!(response.status(), StatusCode::CREATED);
let added_dumbledore = get_db_user(&pool, "adumbledore").await;

// disable the new user so there is something to re-enable; active count drops back to 2
let response = client
.post("/api/v1/user/bulk-disable")
.json(&serde_json::json!({ "users": [added_dumbledore.id] }))
.send()
.await;
assert_eq!(response.status(), StatusCode::OK);
let disabled_dumbledore = get_db_user(&pool, "adumbledore").await;
assert!(!disabled_dumbledore.is_active);

let license = get_cached_license().clone();
set_cached_license(Some(License::new(
"test_customer".to_owned(),
false,
None,
Some(LicenseLimits {
users: 2,
devices: 100,
locations: 100,
network_devices: Some(100),
}),
None,
LicenseTier::Business,
SupportType::Basic,
vec![],
)));

// active count is already at the limit of 2 (admin, hpotter), so re-enabling must be blocked
let mut user_details = fetch_user_details(&client, "adumbledore").await;
user_details.user.is_active = true;
let response = client
.put("/api/v1/user/adumbledore")
.json(&user_details.user)
.send()
.await;
assert_eq!(response.status(), StatusCode::FORBIDDEN);

let still_disabled_dumbledore = get_db_user(&pool, "adumbledore").await;
assert!(!still_disabled_dumbledore.is_active);

set_cached_license(license);
client.verify_api_events(&[
ApiEventType::UserAdded {
user: added_dumbledore.clone(),
},
ApiEventType::UserModified {
before: added_dumbledore,
after: disabled_dumbledore,
},
]);
}

#[sqlx::test]
async fn test_bulk_enable_users_blocked_when_it_would_exceed_license_limit(
_: PgPoolOptions,
options: PgConnectOptions,
) {
let pool = setup_pool(options).await;

let (mut client, pool) = make_client_with_db(pool).await;

client.login_user("admin", "pass123").await;

for (username, email) in [
("adumbledore", "a.dumbledore@hogwart.edu.uk"),
("mmcgonagall", "m.mcgonagall@hogwart.edu.uk"),
] {
let new_user = AddUserData {
username: username.into(),
last_name: format!("{username}-last"),
first_name: format!("{username}-first"),
email: email.into(),
phone: Some("1234".into()),
password: Some("Password1234543$!".into()),
};
let response = client.post("/api/v1/user").json(&new_user).send().await;
assert_eq!(response.status(), StatusCode::CREATED);
}

let added_dumbledore = get_db_user(&pool, "adumbledore").await;
let added_mcgonagall = get_db_user(&pool, "mmcgonagall").await;

// disable both new users; active count drops back to 2 (admin, hpotter)
let response = client
.post("/api/v1/user/bulk-disable")
.json(&serde_json::json!({ "users": [added_dumbledore.id, added_mcgonagall.id] }))
.send()
.await;
assert_eq!(response.status(), StatusCode::OK);
let disabled_dumbledore = get_db_user(&pool, "adumbledore").await;
let disabled_mcgonagall = get_db_user(&pool, "mmcgonagall").await;

let license = get_cached_license().clone();
set_cached_license(Some(License::new(
"test_customer".to_owned(),
false,
None,
Some(LicenseLimits {
users: 3,
devices: 100,
locations: 100,
network_devices: Some(100),
}),
None,
LicenseTier::Business,
SupportType::Basic,
vec![],
)));

// active count is 2 (admin, hpotter); re-enabling both would bring it to 4, over the limit of 3
let response = client
.post("/api/v1/user/bulk-enable")
.json(&serde_json::json!({ "users": [disabled_dumbledore.id, disabled_mcgonagall.id] }))
.send()
.await;
assert_eq!(response.status(), StatusCode::FORBIDDEN);

let still_disabled_dumbledore = get_db_user(&pool, "adumbledore").await;
let still_disabled_mcgonagall = get_db_user(&pool, "mmcgonagall").await;
assert!(!still_disabled_dumbledore.is_active);
assert!(!still_disabled_mcgonagall.is_active);

set_cached_license(license);
client.verify_api_events(&[
ApiEventType::UserAdded {
user: added_dumbledore.clone(),
},
ApiEventType::UserAdded {
user: added_mcgonagall.clone(),
},
ApiEventType::UserModified {
before: added_dumbledore,
after: disabled_dumbledore,
},
ApiEventType::UserModified {
before: added_mcgonagall,
after: disabled_mcgonagall,
},
]);
}

#[sqlx::test]
async fn test_check_username(_: PgPoolOptions, options: PgConnectOptions) {
let pool = setup_pool(options).await;
Expand Down
Loading