use crate::cache::file::{hash_license_key, FileCache};
use crate::cache::format::CacheRecord;
use crate::client::http::KeygenClient;
use crate::clock::{Clock, SystemClock};
use crate::config::GatewardenConfig;
use crate::crypto::pipeline::verify_response;
use crate::policy::access::{check_access_with_usage, UsageCaps};
use crate::policy::fse::compiler::CompiledPlan;
use crate::policy::fse::defaults::compile_default_plan;
use crate::policy::fse::runtime::execute;
use crate::policy::fse::{GatewardenEvalInput, RuleDecision};
use crate::protocol::models::{KeygenValidateResponse, LicenseState};
use crate::GatewardenError;
use std::sync::Arc;
#[derive(Debug, Clone)]
pub struct ValidationResult {
pub valid: bool,
pub state: LicenseState,
pub caps: UsageCaps,
pub from_cache: bool,
pub selectors_scanned: usize,
}
pub struct LicenseManager {
config: GatewardenConfig,
clock: Arc<dyn Clock>,
client: KeygenClient,
cache: FileCache,
fse_plan: CompiledPlan,
}
impl LicenseManager {
pub fn new(config: GatewardenConfig) -> Result<Self, GatewardenError> {
config.validate()?;
Self::with_clock(config, Arc::new(SystemClock))
}
#[cfg(any(test, feature = "test-seams"))]
pub fn new_with_clock(
config: GatewardenConfig,
clock: Arc<dyn Clock>,
) -> Result<Self, GatewardenError> {
config.validate()?;
Self::with_clock(config, clock)
}
fn with_clock(
config: GatewardenConfig,
clock: Arc<dyn Clock>,
) -> Result<Self, GatewardenError> {
let client = KeygenClient::new(&config)?;
let cache = FileCache::new(&config.cache_namespace)?;
let fse_plan = compile_default_plan(&config)?;
Ok(Self {
config,
clock,
client,
cache,
fse_plan,
})
}
pub fn validate_key(&self, license_key: &str) -> Result<ValidationResult, GatewardenError> {
if license_key.is_empty() {
return Err(GatewardenError::MissingLicense);
}
let key_hash = hash_license_key(license_key);
match self.validate_online(license_key, &key_hash) {
Ok(result) => Ok(result),
Err(online_error) => {
self.validate_offline(&key_hash, online_error)
}
}
}
pub fn check_access(&self, license_key: &str) -> Result<ValidationResult, GatewardenError> {
if license_key.is_empty() {
return Err(GatewardenError::MissingLicense);
}
let key_hash = hash_license_key(license_key);
let record = self
.cache
.load(&key_hash)?
.ok_or(GatewardenError::InvalidLicense)?;
record.verify(
&self.config.public_key_hex,
self.config.offline_grace,
self.clock.as_ref(),
)?;
let response: KeygenValidateResponse = serde_json::from_str(record.body())
.map_err(|e| GatewardenError::ProtocolError(format!("Cache parse error: {}", e)))?;
let state = LicenseState::from_keygen_response(&response)?;
let input = GatewardenEvalInput::from_validated_response(state.clone(), true);
let fse_result = execute(&self.fse_plan, &input);
if !fse_result.allow {
for outcome in &fse_result.outcomes {
if outcome.decision == RuleDecision::False {
tracing::warn!("FSE rule failed (check_access): {}", outcome.rule_id);
}
}
return Err(GatewardenError::InvalidLicense);
}
let entitlements: Vec<&str> = self
.config
.required_entitlements
.iter()
.map(|s| s.as_str())
.collect();
let caps = check_access_with_usage(
&state,
&entitlements,
0, )?;
Ok(ValidationResult {
valid: state.valid,
state,
caps,
from_cache: true,
selectors_scanned: fse_result.selectors_scanned,
})
}
fn validate_online(
&self,
license_key: &str,
key_hash: &str,
) -> Result<ValidationResult, GatewardenError> {
let entitlements: Vec<&str> = self
.config
.required_entitlements
.iter()
.map(|s| s.as_str())
.collect();
let response = self.client.validate_key(license_key, &entitlements)?;
verify_response(&response, &self.config.public_key_hex, self.clock.as_ref())?;
let date = response.date.clone().unwrap_or_default();
let signature = response.signature.clone().unwrap_or_default();
let digest = response.digest.clone();
let request_path = response.request_path.clone();
let host = response.host.clone();
let body_str = response.body_str()?;
let keygen_response: KeygenValidateResponse = serde_json::from_str(body_str)
.map_err(|e| GatewardenError::ProtocolError(format!("Parse error: {}", e)))?;
let state = LicenseState::from_keygen_response(&keygen_response)?;
let input = GatewardenEvalInput::from_validated_response(state.clone(), true);
let fse_result = execute(&self.fse_plan, &input);
if !fse_result.allow {
for outcome in &fse_result.outcomes {
if outcome.decision == RuleDecision::False {
tracing::warn!("FSE rule failed: {}", outcome.rule_id);
}
}
return Err(GatewardenError::InvalidLicense);
}
let entitlements: Vec<&str> = self
.config
.required_entitlements
.iter()
.map(|s| s.as_str())
.collect();
let caps = check_access_with_usage(
&state,
&entitlements,
0, )?;
let cache_record = CacheRecord::new(
date,
signature,
digest,
body_str.to_string(),
request_path,
host,
self.clock.as_ref(),
);
self.cache.save(key_hash, &cache_record)?;
Ok(ValidationResult {
valid: state.valid,
state,
caps,
from_cache: false,
selectors_scanned: fse_result.selectors_scanned,
})
}
fn validate_offline(
&self,
key_hash: &str,
online_error: GatewardenError,
) -> Result<ValidationResult, GatewardenError> {
if !matches!(online_error, GatewardenError::KeygenTransport(_)) {
return Err(online_error);
}
let record = self.cache.load(key_hash)?.ok_or(online_error)?;
record.verify(
&self.config.public_key_hex,
self.config.offline_grace,
self.clock.as_ref(),
)?;
let response: KeygenValidateResponse = serde_json::from_str(record.body())
.map_err(|e| GatewardenError::ProtocolError(format!("Cache parse error: {}", e)))?;
let state = LicenseState::from_keygen_response(&response)?;
let input = GatewardenEvalInput::from_validated_response(state.clone(), true);
let fse_result = execute(&self.fse_plan, &input);
if !fse_result.allow {
for outcome in &fse_result.outcomes {
if outcome.decision == RuleDecision::False {
tracing::warn!("FSE rule failed (cached): {}", outcome.rule_id);
}
}
return Err(GatewardenError::InvalidLicense);
}
let entitlements: Vec<&str> = self
.config
.required_entitlements
.iter()
.map(|s| s.as_str())
.collect();
let caps = check_access_with_usage(&state, &entitlements, 0)?;
Ok(ValidationResult {
valid: state.valid,
state,
caps,
from_cache: true,
selectors_scanned: fse_result.selectors_scanned,
})
}
pub fn config(&self) -> &GatewardenConfig {
&self.config
}
pub fn fse_plan(&self) -> &CompiledPlan {
&self.fse_plan
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
fn test_config() -> GatewardenConfig {
GatewardenConfig {
app_name: "test-app".to_string(),
feature_name: "test".to_string(),
account_id: "test-account".to_string(),
public_key_hex: "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a"
.to_string(),
required_entitlements: vec![],
user_agent_product: "test-product".to_string(),
cache_namespace: "gatewarden-test".to_string(),
offline_grace: Duration::from_secs(86400),
}
}
#[test]
fn test_license_manager_creation() {
let config = test_config();
let manager = LicenseManager::new(config);
assert!(manager.is_ok());
}
#[test]
fn test_validate_key_empty() {
let config = test_config();
let manager = LicenseManager::new(config).unwrap();
let result = manager.validate_key("");
assert!(matches!(result, Err(GatewardenError::MissingLicense)));
}
#[test]
fn test_check_access_empty() {
let config = test_config();
let manager = LicenseManager::new(config).unwrap();
let result = manager.check_access("");
assert!(matches!(result, Err(GatewardenError::MissingLicense)));
}
#[test]
fn test_config_accessor() {
let config = test_config();
let manager = LicenseManager::new(config).unwrap();
assert_eq!(manager.config().app_name, "test-app");
}
}