Skip to content

Commit

Permalink
feat: enterprise behavior settings (#715)
Browse files Browse the repository at this point in the history
  • Loading branch information
j-chmielewski authored Aug 21, 2024
1 parent 5e69037 commit 35f0fa1
Show file tree
Hide file tree
Showing 35 changed files with 747 additions and 38 deletions.

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

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

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

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

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

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.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ sqlx = { version = "0.7", features = [
"uuid",
] }
ssh-key = "0.6"
struct-patch = "0.7"
struct-patch = "0.8"
tera = "1.20"
thiserror = "1.0"
# match axum-extra -> cookies
Expand Down
1 change: 1 addition & 0 deletions migrations/20240819134151_enterprise_settings.down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DROP TABLE enterprisesettings;
6 changes: 6 additions & 0 deletions migrations/20240819134151_enterprise_settings.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
CREATE TABLE enterprisesettings (
id bigserial PRIMARY KEY,
admin_device_management BOOLEAN NOT NULL DEFAULT false
);

INSERT INTO enterprisesettings (admin_device_management) values (false);
47 changes: 47 additions & 0 deletions src/enterprise/db/models/enterprise_settings.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
use model_derive::Model;
use sqlx::PgExecutor;
use struct_patch::Patch;

use crate::enterprise::license::{get_cached_license, validate_license};

#[derive(Model, Deserialize, Serialize, Patch)]
#[patch(attribute(derive(Serialize, Deserialize)))]
pub struct EnterpriseSettings {
#[serde(skip)]
pub id: Option<i64>,
// If true, only admins can manage devices
pub admin_device_management: bool,
}

// We want to be conscious of what the defaults are here
#[allow(clippy::derivable_impls)]
impl Default for EnterpriseSettings {
fn default() -> Self {
Self {
id: None,
admin_device_management: false,
}
}
}

impl EnterpriseSettings {
/// If license is valid returns current [`EnterpriseSettings`] object.
/// Otherwise returns [`EnterpriseSettings::default()`].
pub async fn get<'e, E>(executor: E) -> Result<Self, sqlx::Error>
where
E: PgExecutor<'e>,
{
// avoid holding the rwlock across await, makes the future !Send
// and therefore unusable in axum handlers
let is_valid = {
let license = get_cached_license();
validate_license(license.as_ref()).is_ok()
};
if is_valid {
let settings = Self::find_by_id(executor, 1).await?;
Ok(settings.expect("EnterpriseSettings not found"))
} else {
Ok(EnterpriseSettings::default())
}
}
}
1 change: 1 addition & 0 deletions src/enterprise/db/models/mod.rs
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
pub mod enterprise_settings;
pub mod openid_provider;
49 changes: 49 additions & 0 deletions src/enterprise/handlers/enterprise_settings.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
use axum::{extract::State, http::StatusCode, Json};
use serde_json::json;
use struct_patch::Patch;

use super::LicenseInfo;
use crate::{
appstate::AppState,
auth::{AdminRole, SessionInfo},
enterprise::db::models::enterprise_settings::{EnterpriseSettings, EnterpriseSettingsPatch},
handlers::{ApiResponse, ApiResult},
};

pub async fn get_enterprise_settings(
session: SessionInfo,
State(appstate): State<AppState>,
) -> ApiResult {
debug!(
"User {} retrieving enterprise settings",
session.user.username
);
let settings = EnterpriseSettings::get(&appstate.pool).await?;
info!(
"User {} retrieved enterprise settings",
session.user.username
);
Ok(ApiResponse {
json: json!(settings),
status: StatusCode::OK,
})
}

pub async fn patch_enterprise_settings(
_license: LicenseInfo,
_admin: AdminRole,
State(appstate): State<AppState>,
session: SessionInfo,
Json(data): Json<EnterpriseSettingsPatch>,
) -> ApiResult {
debug!(
"Admin {} patching enterprise settings.",
session.user.username,
);
let mut settings = EnterpriseSettings::get(&appstate.pool).await?;

settings.apply(data);
settings.save(&appstate.pool).await?;
info!("Admin {} patched settings.", session.user.username);
Ok(ApiResponse::default())
}
35 changes: 32 additions & 3 deletions src/enterprise/handlers/mod.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
use crate::{
auth::SessionInfo,
enterprise::license::validate_license,
handlers::{ApiResponse, ApiResult},
};

pub mod enterprise_settings;
pub mod openid_login;
pub mod openid_providers;

Expand All @@ -12,13 +14,16 @@ use axum::{
http::{request::Parts, StatusCode},
};

use super::license::get_cached_license;
use super::{db::models::enterprise_settings::EnterpriseSettings, license::get_cached_license};
use crate::{appstate::AppState, error::WebError};

pub struct LicenseInfo {
pub valid: bool,
}

/// Used to check if user is allowed to manage his devices.
pub struct CanManageDevices;

#[async_trait]
impl<S> FromRequestParts<S> for LicenseInfo
where
Expand All @@ -30,7 +35,7 @@ where
async fn from_request_parts(_parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
let license = get_cached_license();

match validate_license((*license).as_ref()) {
match validate_license(license.as_ref()) {
// Useless struct, but may come in handy later
Ok(_) => Ok(LicenseInfo { valid: true }),
Err(e) => Err(WebError::Forbidden(e.to_string())),
Expand All @@ -41,10 +46,34 @@ where
pub async fn check_enterprise_status() -> ApiResult {
let license = get_cached_license();

let valid = validate_license((*license).as_ref()).is_ok();
let valid = validate_license((license).as_ref()).is_ok();

Ok(ApiResponse {
json: serde_json::json!({ "enabled": valid }),
status: StatusCode::OK,
})
}

#[async_trait]
impl<S> FromRequestParts<S> for CanManageDevices
where
S: Send + Sync,
AppState: FromRef<S>,
{
type Rejection = WebError;

/// Returns an error if current session user is not allowed to manage devices.
/// The permission is defined by [`EnterpriseSettings::admin_device_management`] setting.
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
let appstate = AppState::from_ref(state);
let session = SessionInfo::from_request_parts(parts, state).await?;
let settings = EnterpriseSettings::get(&appstate.pool).await?;
if settings.admin_device_management && !session.is_admin {
Err(WebError::Forbidden(
"Only admin users can manage devices".into(),
))
} else {
Ok(Self)
}
}
}
10 changes: 5 additions & 5 deletions src/enterprise/license.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::sync::{Mutex, MutexGuard};
use std::sync::{RwLock, RwLockReadGuard};

use anyhow::Result;
use base64::prelude::*;
Expand All @@ -14,17 +14,17 @@ use crate::{
server_config,
};

static LICENSE: Mutex<Option<License>> = Mutex::new(None);
static LICENSE: RwLock<Option<License>> = RwLock::new(None);

pub fn set_cached_license(license: Option<License>) {
*LICENSE
.lock()
.write()
.expect("Failed to acquire lock on the license mutex.") = license;
}

pub fn get_cached_license() -> MutexGuard<'static, Option<License>> {
pub fn get_cached_license() -> RwLockReadGuard<'static, Option<License>> {
LICENSE
.lock()
.read()
.expect("Failed to acquire lock on the license mutex.")
}

Expand Down
2 changes: 1 addition & 1 deletion src/handlers/app_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ pub(crate) async fn get_app_info(
let networks = WireguardNetwork::all(&appstate.pool).await?;
let settings = Settings::get_settings(&appstate.pool).await?;
let license = get_cached_license();
let enterprise = validate_license((*license).as_ref()).is_ok();
let enterprise = validate_license((license).as_ref()).is_ok();
let res = AppInfo {
network_present: !networks.is_empty(),
smtp_enabled: settings.smtp_configured(),
Expand Down
4 changes: 2 additions & 2 deletions src/handlers/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ pub async fn patch_settings(
session: SessionInfo,
Json(data): Json<SettingsPatch>,
) -> ApiResult {
debug!("Admin {} patching settings.", &session.user.username);
debug!("Admin {} patching settings.", session.user.username);
let mut settings = Settings::get_settings(&appstate.pool).await?;

// Handle updating the cached license
Expand All @@ -119,7 +119,7 @@ pub async fn patch_settings(

settings.apply(data);
settings.save(&appstate.pool).await?;
info!("Admin {} patched settings.", &session.user.username);
info!("Admin {} patched settings.", session.user.username);
Ok(ApiResponse::default())
}

Expand Down
Loading

0 comments on commit 35f0fa1

Please sign in to comment.