From fd3736252d312cf667d9463f37a1340362d149b3 Mon Sep 17 00:00:00 2001 From: mdecimus <11444311+mdecimus@users.noreply.github.com> Date: Mon, 15 Dec 2025 16:30:12 +0100 Subject: [PATCH] Enterprise: remove tenant admin permissions when license is invalid --- crates/common/src/auth/access_token.rs | 51 +++++++------- crates/common/src/enterprise/config.rs | 21 +++--- crates/common/src/enterprise/license.rs | 81 ++-------------------- crates/common/src/enterprise/mod.rs | 29 +++++++- crates/http/src/management/principal.rs | 90 +++++++++++++++---------- 5 files changed, 125 insertions(+), 147 deletions(-) diff --git a/crates/common/src/auth/access_token.rs b/crates/common/src/auth/access_token.rs index 786edb12..dd23790a 100644 --- a/crates/common/src/auth/access_token.rs +++ b/crates/common/src/auth/access_token.rs @@ -97,32 +97,37 @@ impl Server { // SPDX-License-Identifier: LicenseRef-SEL #[cfg(feature = "enterprise")] - if self.is_enterprise_edition() - && let Some(tenant_id) = tenant_id { - // Limit tenant permissions + use directory::{QueryParams, ROLE_USER}; - use directory::QueryParams; - permissions.intersection(&self.get_role_permissions(tenant_id).await?.enabled); + if let Some(tenant_id) = tenant_id { + if self.is_enterprise_edition() { + // Limit tenant permissions + permissions.intersection(&self.get_role_permissions(tenant_id).await?.enabled); - // Obtain tenant quota - tenant = Some(TenantInfo { - id: tenant_id, - quota: self - .store() - .query(QueryParams::id(tenant_id).with_return_member_of(false)) - .await - .caused_by(trc::location!())? - .ok_or_else(|| { - trc::SecurityEvent::Unauthorized - .into_err() - .details("Tenant not found") - .id(tenant_id) - .caused_by(trc::location!()) - })? - .quota() - .unwrap_or_default(), - }); + // Obtain tenant quota + tenant = Some(TenantInfo { + id: tenant_id, + quota: self + .store() + .query(QueryParams::id(tenant_id).with_return_member_of(false)) + .await + .caused_by(trc::location!())? + .ok_or_else(|| { + trc::SecurityEvent::Unauthorized + .into_err() + .details("Tenant not found") + .id(tenant_id) + .caused_by(trc::location!()) + })? + .quota() + .unwrap_or_default(), + }); + } else { + // Enterprise edition downgrade, remove any tenant administrator permissions + permissions.intersection(&self.get_role_permissions(ROLE_USER).await?.enabled); + } + } } // SPDX-SnippetEnd diff --git a/crates/common/src/enterprise/config.rs b/crates/common/src/enterprise/config.rs index 1cf7db93..2768344e 100644 --- a/crates/common/src/enterprise/config.rs +++ b/crates/common/src/enterprise/config.rs @@ -8,10 +8,17 @@ * */ -use std::{sync::Arc, time::Duration}; - +use super::{ + AlertContent, AlertContentToken, AlertMethod, Enterprise, MetricAlert, MetricStore, + SpamFilterLlmConfig, TraceStore, Undelete, license::LicenseKey, llm::AiApiConfig, +}; +use crate::{ + expr::{Expression, tokenizer::TokenMap}, + manager::config::ConfigManager, +}; use ahash::AHashMap; use directory::{Type, backend::internal::manage::ManageDirectory}; +use std::{sync::Arc, time::Duration}; use store::{Store, Stores}; use trc::{EventType, MetricType, TOTAL_EVENT_COUNT}; use utils::{ @@ -23,16 +30,6 @@ use utils::{ template::Template, }; -use crate::{ - expr::{Expression, tokenizer::TokenMap}, - manager::config::ConfigManager, -}; - -use super::{ - AlertContent, AlertContentToken, AlertMethod, Enterprise, MetricAlert, MetricStore, - SpamFilterLlmConfig, TraceStore, Undelete, license::LicenseKey, llm::AiApiConfig, -}; - impl Enterprise { pub async fn parse( config: &mut Config, diff --git a/crates/common/src/enterprise/license.rs b/crates/common/src/enterprise/license.rs index 79fb8ea2..29c026af 100644 --- a/crates/common/src/enterprise/license.rs +++ b/crates/common/src/enterprise/license.rs @@ -19,20 +19,17 @@ * for copyright infringement, breach of contract, and fraud. */ +use crate::manager::fetch_resource; +use base64::{Engine, engine::general_purpose::STANDARD}; +use hyper::{HeaderMap, header::AUTHORIZATION}; +use ring::signature::{ED25519, UnparsedPublicKey}; use std::{ fmt::{Display, Formatter}, time::Duration, }; - -use hyper::{HeaderMap, header::AUTHORIZATION}; -use ring::signature::{ED25519, UnparsedPublicKey}; - -use base64::{Engine, engine::general_purpose::STANDARD}; use store::write::now; use trc::ServerEvent; -use crate::manager::fetch_resource; - //const LICENSING_API: &str = "https://localhost:444/api/license/"; const LICENSING_API: &str = "https://license.stalw.art/api/license/"; const RENEW_THRESHOLD: u64 = 60 * 60 * 24 * 4; // 4 days @@ -281,73 +278,3 @@ impl Display for LicenseError { } } } - -/* - -use rustls::sign::CertifiedKey; -use webpki::TrustAnchor; -use x509_parser::{certificate::X509Certificate, prelude::FromDer}; - - -fn validate_certificate(key: &CertifiedKey) -> Result<(), Box> { - let cert_der = key.end_entity_cert()?.as_ref(); - - webpki::EndEntityCert::try_from(cert_der)?.verify_is_valid_tls_server_cert( - &[ - &webpki::ECDSA_P256_SHA256, - &webpki::ECDSA_P256_SHA384, - &webpki::ECDSA_P384_SHA256, - &webpki::ECDSA_P384_SHA384, - &webpki::ED25519, - &webpki::RSA_PKCS1_2048_8192_SHA256, - &webpki::RSA_PKCS1_2048_8192_SHA384, - &webpki::RSA_PKCS1_2048_8192_SHA512, - &webpki::RSA_PKCS1_3072_8192_SHA384, - &webpki::RSA_PSS_2048_8192_SHA256_LEGACY_KEY, - &webpki::RSA_PSS_2048_8192_SHA384_LEGACY_KEY, - &webpki::RSA_PSS_2048_8192_SHA512_LEGACY_KEY, - ], - &webpki::TlsServerTrustAnchors( - webpki_roots::TLS_SERVER_ROOTS - .iter() - .map(|ta| TrustAnchor { - subject: ta.subject.as_ref(), - spki: ta.subject_public_key_info.as_ref(), - name_constraints: ta.name_constraints.as_ref().map(|nc| nc.as_ref()), - }) - .collect::>() - .as_slice(), - ), - &key.cert - .iter() - .skip(1) - .map(|der| der.as_ref()) - .collect::>(), - webpki::Time::try_from(SystemTime::now())?, - )?; - - // Additional checks - let x509 = X509Certificate::from_der(cert_der)?.1; - - // Check if self-signed - if x509.issuer() == x509.subject() { - return Err("Certificate is self-signed".into()); - } - - // Check expiration - let not_before = x509.validity().not_before.timestamp(); - let not_after = x509.validity().not_after.timestamp(); - let now = SystemTime::UNIX_EPOCH - .elapsed() - .unwrap_or_default() - .as_secs() as i64; - - if now < not_before || now > not_after { - Err("Certificate is expired or not yet valid".into()) - } else { - Ok(()) - } -} - - -*/ diff --git a/crates/common/src/enterprise/mod.rs b/crates/common/src/enterprise/mod.rs index 20e735b6..68df4ff6 100644 --- a/crates/common/src/enterprise/mod.rs +++ b/crates/common/src/enterprise/mod.rs @@ -15,7 +15,10 @@ pub mod llm; pub mod undelete; use ahash::{AHashMap, AHashSet}; -use directory::{QueryParams, Type, backend::internal::lookup::DirectoryStore}; +use directory::{ + QueryParams, Type, + backend::internal::{lookup::DirectoryStore, manage::ManageDirectory}, +}; use license::LicenseKey; use llm::AiApiConfig; use mail_parser::DateTime; @@ -150,6 +153,30 @@ impl Server { } } + pub async fn can_create_account(&self) -> trc::Result { + if let Some(enterprise) = &self.core.enterprise { + let total_accounts = self + .store() + .count_principals(None, Type::Individual.into(), None) + .await + .caused_by(trc::location!())?; + + if total_accounts + 1 > enterprise.license.accounts as u64 { + trc::event!( + Server(trc::ServerEvent::Licensing), + Details = "Account creation not possible: license key account limit reached", + Domain = enterprise.license.domain.clone(), + Total = total_accounts, + Limit = enterprise.license.accounts, + ); + + return Ok(false); + } + } + + Ok(true) + } + pub async fn logo_resource(&self, domain: &str) -> trc::Result>>> { const MAX_IMAGE_SIZE: usize = 1024 * 1024; diff --git a/crates/http/src/management/principal.rs b/crates/http/src/management/principal.rs index d081698e..5986be70 100644 --- a/crates/http/src/management/principal.rs +++ b/crates/http/src/management/principal.rs @@ -103,11 +103,27 @@ impl PrincipalManager for Server { // SPDX-License-Identifier: LicenseRef-SEL #[cfg(feature = "enterprise")] - if (matches!(principal.typ(), Type::Tenant) - || principal.has_field(PrincipalField::Tenant)) - && !self.core.is_enterprise_edition() { - return Err(manage::enterprise()); + if (matches!(principal.typ(), Type::Tenant) + || principal.has_field(PrincipalField::Tenant)) + && !self.core.is_enterprise_edition() + { + return Err(manage::enterprise()); + } + + if matches!(principal.typ(), Type::Individual) + && self.core.is_enterprise_edition() + && !self.can_create_account().await? + { + return Err(manage::error( + "License account limit reached", + format!( + "Enterprise licensed account limit reached: {} accounts licensed.", + self.licensed_accounts() + ) + .into(), + )); + } } // SPDX-SnippetEnd @@ -257,22 +273,24 @@ impl PrincipalManager for Server { // SPDX-FileCopyrightText: 2020 Stalwart Labs LLC // SPDX-License-Identifier: LicenseRef-SEL #[cfg(feature = "enterprise")] - if self.core.is_enterprise_edition() { - if tenant.is_none() { - // Limit search to current tenant - if let Some(tenant_name) = params.get("tenant") { - tenant = self - .core - .storage - .data - .get_principal_info(tenant_name) - .await? - .filter(|p| p.typ == Type::Tenant) - .map(|p| p.id); + { + if self.core.is_enterprise_edition() { + if tenant.is_none() { + // Limit search to current tenant + if let Some(tenant_name) = params.get("tenant") { + tenant = self + .core + .storage + .data + .get_principal_info(tenant_name) + .await? + .filter(|p| p.typ == Type::Tenant) + .map(|p| p.id); + } } + } else if types.contains(&Type::Tenant) { + return Err(manage::enterprise()); } - } else if types.contains(&Type::Tenant) { - return Err(manage::enterprise()); } // SPDX-SnippetEnd @@ -348,22 +366,24 @@ impl PrincipalManager for Server { // SPDX-FileCopyrightText: 2020 Stalwart Labs LLC // SPDX-License-Identifier: LicenseRef-SEL #[cfg(feature = "enterprise")] - if self.core.is_enterprise_edition() { - if tenant.is_none() { - // Limit search to current tenant - if let Some(tenant_name) = params.get("tenant") { - tenant = self - .core - .storage - .data - .get_principal_info(tenant_name) - .await? - .filter(|p| p.typ == Type::Tenant) - .map(|p| p.id); + { + if self.core.is_enterprise_edition() { + if tenant.is_none() { + // Limit search to current tenant + if let Some(tenant_name) = params.get("tenant") { + tenant = self + .core + .storage + .data + .get_principal_info(tenant_name) + .await? + .filter(|p| p.typ == Type::Tenant) + .map(|p| p.id); + } } + } else if typ == Type::Tenant { + return Err(manage::enterprise()); } - } else if typ == Type::Tenant { - return Err(manage::enterprise()); } // SPDX-SnippetEnd @@ -429,8 +449,10 @@ impl PrincipalManager for Server { // SPDX-License-Identifier: LicenseRef-SEL #[cfg(feature = "enterprise")] - if matches!(typ, Type::Tenant) && !self.core.is_enterprise_edition() { - return Err(manage::enterprise()); + { + if matches!(typ, Type::Tenant) && !self.core.is_enterprise_edition() { + return Err(manage::enterprise()); + } } // SPDX-SnippetEnd