feat(rustfs): add optional license gating feature (#2197)

This commit is contained in:
安正超
2026-03-18 08:45:05 +08:00
committed by GitHub
parent bddb0d0a05
commit c9a2fd756c
4 changed files with 273 additions and 44 deletions
+1
View File
@@ -36,6 +36,7 @@ metrics = []
ftps = ["rustfs-protocols/ftps"]
swift = ["rustfs-protocols/swift"]
webdav = ["rustfs-protocols/webdav"]
license = []
full = ["metrics", "ftps", "swift", "webdav"]
[lints]
+259 -37
View File
@@ -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<Token> = OnceLock::new();
pub type LicenseResult<T> = std::result::Result<T, LicenseError>;
pub type SharedLicenseVerifier = Arc<dyn LicenseVerifier>;
/// Initialize the license
pub fn init_license(license: Option<String>) {
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<Token> {
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<Token>,
status: LicenseStatus,
}
/// Verifier for parsing and validating raw license materials.
pub trait LicenseVerifier: Send + Sync {
fn validate(&self, raw_license: &str, now: u64) -> LicenseResult<Token>;
}
#[derive(Debug, Default)]
struct AppAuthLicenseVerifier;
impl LicenseVerifier for AppAuthLicenseVerifier {
fn validate(&self, raw_license: &str, _now: u64) -> LicenseResult<Token> {
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<RwLock<LicenseState>> = OnceLock::new();
static LICENSE_VERIFIER: OnceLock<SharedLicenseVerifier> = OnceLock::new();
fn license_state() -> &'static RwLock<LicenseState> {
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<u64> {
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<String>) -> Option<String> {
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<String>) {
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<String>) -> 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<String>) {
initialize_license(license);
}
/// Return the current license information.
pub fn get_license() -> Option<Token> {
license_state().read().ok().and_then(|state| state.token.clone())
}
/// New name for compatibility with external integrations.
pub fn current_license() -> Option<Token> {
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)
}
+6 -1
View File
@@ -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);
+7 -6
View File
@@ -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<CreateBucketInput>) -> 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<CreateMultipartUploadInput>) -> 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<PutObjectInput>) -> 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());