From c9a2fd756c4cc027b99e9c1102dc43898e7a3367 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AE=89=E6=AD=A3=E8=B6=85?= Date: Wed, 18 Mar 2026 08:45:05 +0800 Subject: [PATCH] feat(rustfs): add optional license gating feature (#2197) --- rustfs/Cargo.toml | 1 + rustfs/src/license.rs | 296 ++++++++++++++++++++++++++++++----- rustfs/src/main.rs | 7 +- rustfs/src/storage/access.rs | 13 +- 4 files changed, 273 insertions(+), 44 deletions(-) diff --git a/rustfs/Cargo.toml b/rustfs/Cargo.toml index a3c44576e..8669373f4 100644 --- a/rustfs/Cargo.toml +++ b/rustfs/Cargo.toml @@ -36,6 +36,7 @@ metrics = [] ftps = ["rustfs-protocols/ftps"] swift = ["rustfs-protocols/swift"] webdav = ["rustfs-protocols/webdav"] +license = [] full = ["metrics", "ftps", "swift", "webdav"] [lints] diff --git a/rustfs/src/license.rs b/rustfs/src/license.rs index 01b42fdb8..d74a507f7 100644 --- a/rustfs/src/license.rs +++ b/rustfs/src/license.rs @@ -13,51 +13,273 @@ // limitations under the License. use rustfs_appauth::token::Token; -use std::io::{Error, Result}; +use rustfs_appauth::token::parse_license; +use std::fmt; +use std::io::{Error, ErrorKind, Result}; +use std::sync::Arc; use std::sync::OnceLock; -use std::time::SystemTime; -use std::time::UNIX_EPOCH; -use tracing::error; -use tracing::info; +use std::sync::RwLock; +use std::time::{SystemTime, UNIX_EPOCH}; +use tracing::{error, info, warn}; -static LICENSE: OnceLock = OnceLock::new(); +pub type LicenseResult = std::result::Result; +pub type SharedLicenseVerifier = Arc; -/// Initialize the license -pub fn init_license(license: Option) { - if license.is_none() { - error!("License is None"); - return; - } - let license = license.unwrap(); - let token = rustfs_appauth::token::parse_license(&license).unwrap_or_default(); - - LICENSE.set(token).unwrap_or_else(|_| { - error!("Failed to set license"); - }); +#[derive(Clone, Debug)] +pub enum LicenseError { + /// Internal license state lock could not be acquired. + StatePoisoned, + /// License is required in licensed builds but not provided. + Missing, + /// Token decoding or signature check failed. + Invalid(String), + /// License expiration check failed. + #[cfg(feature = "license")] + Expired { expired_at: u64, now: u64 }, + /// System time could not be read. + Clock(String), } -/// Get the license -pub fn get_license() -> Option { - LICENSE.get().cloned() -} - -/// Check the license -/// This function checks if the license is valid. -#[allow(unreachable_code)] -pub fn license_check() -> Result<()> { - return Ok(()); - let invalid_license = LICENSE.get().map(|token| { - if token.expired < SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() { - error!("License expired"); - return Err(Error::other("Incorrect license, please contact RustFS.")); +impl fmt::Display for LicenseError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + LicenseError::StatePoisoned => write!(f, "License state is unavailable"), + LicenseError::Missing => write!(f, "License is required when building with feature `license`."), + LicenseError::Invalid(message) => write!(f, "Incorrect license, please contact RustFS. {message}"), + #[cfg(feature = "license")] + LicenseError::Expired { expired_at, now } => { + write!(f, "Incorrect license, please contact RustFS. expired_at={expired_at}, now={now}") + } + LicenseError::Clock(message) => write!(f, "Failed to read system time: {message}"), } - info!("License is valid ! expired at {}", token.expired); - Ok(()) - }); + } +} - if invalid_license.is_none() || invalid_license.is_some_and(|v| v.is_err()) { - return Err(Error::other("Incorrect license, please contact RustFS.")); +impl std::error::Error for LicenseError {} + +impl LicenseError { + fn into_io(self) -> Error { + match self { + LicenseError::StatePoisoned | LicenseError::Clock(_) => Error::other(self.to_string()), + LicenseError::Missing | LicenseError::Invalid(_) => Error::new(ErrorKind::PermissionDenied, self.to_string()), + #[cfg(feature = "license")] + LicenseError::Expired { .. } => Error::new(ErrorKind::PermissionDenied, self.to_string()), + } + } +} + +#[derive(Clone, Debug, Default)] +enum LicenseStatus { + /// Internal state has not been evaluated yet. + #[default] + Uninitialized, + /// License has been validated. + Valid, + /// License is missing for strict license builds. + Missing, + /// License validation failed. + Invalid(String), +} + +impl fmt::Display for LicenseStatus { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Uninitialized => write!(f, "uninitialized"), + Self::Valid => write!(f, "valid"), + Self::Missing => write!(f, "missing"), + Self::Invalid(message) => write!(f, "{message}"), + } + } +} + +#[derive(Clone, Debug, Default)] +struct LicenseState { + token: Option, + status: LicenseStatus, +} + +/// Verifier for parsing and validating raw license materials. +pub trait LicenseVerifier: Send + Sync { + fn validate(&self, raw_license: &str, now: u64) -> LicenseResult; +} + +#[derive(Debug, Default)] +struct AppAuthLicenseVerifier; + +impl LicenseVerifier for AppAuthLicenseVerifier { + fn validate(&self, raw_license: &str, _now: u64) -> LicenseResult { + let token = parse_license(raw_license).map_err(|err| LicenseError::Invalid(err.to_string()))?; + + #[cfg(feature = "license")] + if token.expired <= _now { + return Err(LicenseError::Expired { + expired_at: token.expired, + now: _now, + }); + } + + Ok(token) + } +} + +static LICENSE_STATE: OnceLock> = OnceLock::new(); +static LICENSE_VERIFIER: OnceLock = OnceLock::new(); + +fn license_state() -> &'static RwLock { + LICENSE_STATE.get_or_init(|| RwLock::new(LicenseState::default())) +} + +fn default_license_verifier() -> SharedLicenseVerifier { + Arc::new(AppAuthLicenseVerifier) +} + +fn license_verifier() -> &'static SharedLicenseVerifier { + LICENSE_VERIFIER.get_or_init(default_license_verifier) +} + +fn now_epoch_secs() -> LicenseResult { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map_err(|err| LicenseError::Clock(err.to_string())) + .map(|value| value.as_secs()) +} + +fn normalized_license(raw_license: Option) -> Option { + raw_license.map(|raw| raw.trim().to_string()).filter(|raw| !raw.is_empty()) +} + +fn strict_build_missing_status() -> LicenseStatus { + if cfg!(feature = "license") { + LicenseStatus::Missing + } else { + LicenseStatus::Uninitialized + } +} + +fn apply_missing_status(state: &mut LicenseState) { + state.token = None; + state.status = strict_build_missing_status(); +} + +fn apply_invalid_status(state: &mut LicenseState, err: LicenseError) { + state.token = None; + state.status = LicenseStatus::Invalid(match err { + LicenseError::Invalid(message) => message, + #[cfg(feature = "license")] + LicenseError::Expired { expired_at, now } => format!("expired at {expired_at}, now {now}"), + LicenseError::Clock(message) => format!("system clock error: {message}"), + LicenseError::Missing => "license is required".to_string(), + LicenseError::StatePoisoned => "license state is unavailable".to_string(), + }); +} + +fn apply_valid_status(state: &mut LicenseState, token: Token) { + state.token = Some(token); + state.status = LicenseStatus::Valid; +} + +/// Replace the global license verifier. +/// +/// This is the extension point for OEM/build-time overlays. +/// Returns `false` if the verifier was already initialized. +#[allow(dead_code)] +pub fn set_license_verifier(verifier: SharedLicenseVerifier) -> bool { + LICENSE_VERIFIER.set(verifier).is_ok() +} + +/// Initialize the license in memory. +/// +/// This keeps the default API signature stable and is safe to call multiple times. +pub fn initialize_license(raw_license: Option) { + if let Err(err) = initialize_license_result(raw_license) { + error!("license initialization failed: {err}"); + } +} + +/// Explicit initialization API with typed error return. +pub fn initialize_license_result(raw_license: Option) -> LicenseResult<()> { + let normalized = normalized_license(raw_license); + let mut state = license_state().write().map_err(|_| LicenseError::StatePoisoned)?; + + match normalized { + Some(raw_license) => { + let now = now_epoch_secs()?; + match license_verifier().validate(&raw_license, now) { + Ok(token) => { + apply_valid_status(&mut state, token.clone()); + info!("license loaded, subject: {}", token.name); + Ok(()) + } + Err(err) => { + apply_invalid_status(&mut state, err.clone()); + warn!("license verification failed: {err}"); + Err(err) + } + } + } + None => { + apply_missing_status(&mut state); + if let LicenseStatus::Missing = state.status { + Err(LicenseError::Missing) + } else { + Ok(()) + } + } + } +} + +/// Legacy name kept for existing startup code. +pub fn init_license(license: Option) { + initialize_license(license); +} + +/// Return the current license information. +pub fn get_license() -> Option { + license_state().read().ok().and_then(|state| state.token.clone()) +} + +/// New name for compatibility with external integrations. +pub fn current_license() -> Option { + get_license() +} + +/// Observe the current license status for observability. +pub fn license_status() -> String { + license_state() + .read() + .ok() + .map(|state| state.status.to_string()) + .unwrap_or_else(|| LicenseStatus::Uninitialized.to_string()) +} + +/// Check whether current in-memory license is currently valid. +#[cfg(feature = "license")] +pub fn ensure_license() -> LicenseResult<()> { + let state = license_state().read().map_err(|_| LicenseError::StatePoisoned)?; + match &state.status { + LicenseStatus::Missing => return Err(LicenseError::Missing), + LicenseStatus::Invalid(message) => return Err(LicenseError::Invalid(message.to_string())), + LicenseStatus::Uninitialized | LicenseStatus::Valid => {} + }; + + let token = state.token.as_ref().ok_or(LicenseError::Missing)?; + let now = now_epoch_secs()?; + if token.expired <= now { + return Err(LicenseError::Expired { + expired_at: token.expired, + now, + }); } Ok(()) } + +#[cfg(not(feature = "license"))] +pub fn ensure_license() -> LicenseResult<()> { + Ok(()) +} + +/// Compatibility API for call-sites that still use the legacy name. +pub fn license_check() -> Result<()> { + ensure_license().map_err(LicenseError::into_io) +} diff --git a/rustfs/src/main.rs b/rustfs/src/main.rs index 0f57f29ec..9e9b1f9bb 100644 --- a/rustfs/src/main.rs +++ b/rustfs/src/main.rs @@ -44,7 +44,7 @@ use crate::server::{ SHUTDOWN_TIMEOUT, ServiceState, ServiceStateManager, ShutdownSignal, init_cert, init_event_notifier, shutdown_event_notifier, start_audit_system, start_http_server, stop_audit_system, wait_for_shutdown, }; -use license::init_license; +use license::{current_license, init_license, license_status}; use rustfs_common::{GlobalReadiness, SystemStage, set_global_addr}; use rustfs_credentials::init_global_action_credentials; use rustfs_ecstore::store::init_lock_clients; @@ -136,6 +136,11 @@ async fn async_main() -> Result<()> { } } + info!("license status: {}", license_status()); + if let Some(token) = current_license() { + info!("runtime license loaded: {}", token.name); + } + // print startup logo info!("{}", server::LOGO); diff --git a/rustfs/src/storage/access.rs b/rustfs/src/storage/access.rs index 82845d8dd..eebf27e3c 100644 --- a/rustfs/src/storage/access.rs +++ b/rustfs/src/storage/access.rs @@ -405,6 +405,13 @@ impl S3Access for FS { let ext = cx.extensions_mut(); ext.insert(req_info); + license_check().map_err(|er| match er.kind() { + std::io::ErrorKind::PermissionDenied => s3_error!(AccessDenied, "{er}"), + _ => { + tracing::error!("license check failed due to unexpected error: {er}"); + s3_error!(InternalError, "License validation failed") + } + })?; // Verify uniformly here? Or verify separately below? @@ -415,8 +422,6 @@ impl S3Access for FS { /// /// This method returns `Ok(())` by default. async fn create_bucket(&self, req: &mut S3Request) -> S3Result<()> { - license_check().map_err(|er| s3_error!(AccessDenied, "{:?}", er.to_string()))?; - let req_info = ext_req_info_mut(&mut req.extensions)?; req_info.bucket = Some(req.input.bucket.clone()); @@ -480,8 +485,6 @@ impl S3Access for FS { /// Checks whether the CreateMultipartUpload request has accesses to the resources. async fn create_multipart_upload(&self, req: &mut S3Request) -> S3Result<()> { - license_check().map_err(|er| s3_error!(AccessDenied, "{:?}", er.to_string()))?; - let req_info = ext_req_info_mut(&mut req.extensions)?; req_info.bucket = Some(req.input.bucket.clone()); req_info.object = Some(req.input.key.clone()); @@ -1318,8 +1321,6 @@ impl S3Access for FS { /// /// This method returns `Ok(())` by default. async fn put_object(&self, req: &mut S3Request) -> S3Result<()> { - license_check().map_err(|er| s3_error!(AccessDenied, "{:?}", er.to_string()))?; - let req_info = ext_req_info_mut(&mut req.extensions)?; req_info.bucket = Some(req.input.bucket.clone()); req_info.object = Some(req.input.key.clone());