Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
b712b59
Add permissions settings tab
j-chmielewski Aug 12, 2024
04ea23c
Model, migration & working edit device creation settings form
j-chmielewski Aug 19, 2024
30c3405
Lint permissions form
j-chmielewski Aug 19, 2024
b926b93
Block device creation on backend
j-chmielewski Aug 19, 2024
9a57369
Permission check for device modification and deletion
j-chmielewski Aug 20, 2024
72b5e48
Block device management on frontend
j-chmielewski Aug 20, 2024
1c8fa42
Enterprise-only behaviour tab
j-chmielewski Aug 20, 2024
783295e
EnterpriseSettings model, migration, default
j-chmielewski Aug 20, 2024
d1b5e20
Enterprise settings routes, useApi hook
j-chmielewski Aug 20, 2024
c2b0641
Fix compilation
Aug 20, 2024
84a492e
Use RwLock for License, fix patch_enterprise_settings endpoint
j-chmielewski Aug 20, 2024
86c5901
Move enterprise settings entpoints, update frontend
j-chmielewski Aug 20, 2024
985d48b
Remove unnecessary dereferences
j-chmielewski Aug 21, 2024
8e16a1d
Don't derive default enterprise settings
j-chmielewski Aug 21, 2024
80b6f30
Rename disable_device_management -> admin_device_management
j-chmielewski Aug 21, 2024
584fb10
Rename permissions -> behaviour
j-chmielewski Aug 21, 2024
96d1de3
Comments
j-chmielewski Aug 21, 2024
b0a78fb
Update sqlx offline queries
j-chmielewski Aug 21, 2024
aa7cb70
Move the check to middleware
j-chmielewski Aug 21, 2024
05a0de1
Comments
j-chmielewski Aug 21, 2024
dc77da2
Format
j-chmielewski Aug 21, 2024
94525ec
Test sqlx fixtures
j-chmielewski Aug 21, 2024
0a855ad
Behaviour -> behavior
j-chmielewski Aug 21, 2024
048aeb5
Tests
j-chmielewski Aug 21, 2024
5b637d1
Add helper msg
j-chmielewski Aug 21, 2024
ed6f2f0
Frontend lint
j-chmielewski Aug 21, 2024
2a9095a
Merge branch 'dev' into only-admin-adds-devices
j-chmielewski Aug 21, 2024
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.

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