use std::env;
use std::fmt;
use std::fs::File;
use std::net::SocketAddr;
use std::path::Path;
use std::path::PathBuf;
use std::str::FromStr;
use std::time::SystemTime;
use crate::DeploymentProfile;
use crate::SecurityContractViolation;
use crate::SecurityOperation;
use crate::SecurityProviderError;
use crate::SecurityProviderFailure;
pub const SECURITY_PROFILE_ENV: &str = "ROCKETMQ_SECURITY_PROFILE";
pub const SECURITY_TRUST_ANCHOR_ENV: &str = "ROCKETMQ_SECURITY_TRUST_ANCHOR";
pub const SECURITY_TLS_CERT_ENV: &str = "ROCKETMQ_SECURITY_TLS_CERT";
pub const SECURITY_TLS_KEY_ENV: &str = "ROCKETMQ_SECURITY_TLS_KEY";
pub const SECURITY_SECRET_PROVIDER_ENV: &str = "ROCKETMQ_SECURITY_SECRET_PROVIDER";
pub const SECURITY_ADMIN_IDENTITY_ENV: &str = "ROCKETMQ_SECURITY_ADMIN_IDENTITY";
pub const SECURITY_REQUEST_POLICY_ENV: &str = "ROCKETMQ_SECURITY_REQUEST_POLICY";
pub const MOUNTED_FILES_SECRET_PROVIDER: &str = "mounted-files";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SecurityBootstrapProfile {
DevelopmentInsecureLoopback,
SecureEnforced,
}
impl SecurityBootstrapProfile {
pub const fn as_str(self) -> &'static str {
match self {
Self::DevelopmentInsecureLoopback => "development-insecure-loopback",
Self::SecureEnforced => "secure-enforced",
}
}
}
impl FromStr for SecurityBootstrapProfile {
type Err = SecurityContractViolation;
fn from_str(value: &str) -> Result<Self, Self::Err> {
match value.trim().to_ascii_lowercase().as_str() {
"development-insecure-loopback" => Ok(Self::DevelopmentInsecureLoopback),
"secure-enforced" => Ok(Self::SecureEnforced),
_ => Err(SecurityContractViolation::UnknownSecurityProfile),
}
}
}
#[derive(Clone)]
pub struct SecurityBootstrapConfig {
profile: SecurityBootstrapProfile,
trust_anchor: Option<PathBuf>,
tls_certificate: Option<PathBuf>,
tls_private_key: Option<PathBuf>,
secret_provider: Option<String>,
admin_identity: Option<PathBuf>,
request_policy: Option<PathBuf>,
}
#[derive(Debug, Clone)]
pub enum SecurityBootstrap {
Disabled,
Enabled(SecurityBootstrapConfig),
}
impl SecurityBootstrap {
pub const fn is_enabled(&self) -> bool {
matches!(self, Self::Enabled(_))
}
pub const fn requires_authentication(&self) -> bool {
matches!(self, Self::Enabled(config) if matches!(config.profile, SecurityBootstrapProfile::SecureEnforced))
}
pub fn validate(
&self,
listener_addresses: &[SocketAddr],
) -> Result<SecurityBootstrapOutcome, SecurityProviderError> {
match self {
Self::Disabled => Ok(SecurityBootstrapOutcome::Disabled),
Self::Enabled(config) => config
.validate(listener_addresses)
.map(SecurityBootstrapOutcome::Validated),
}
}
}
#[derive(Default)]
struct SecurityBootstrapEnvironment {
profile: Option<String>,
trust_anchor: Option<PathBuf>,
tls_certificate: Option<PathBuf>,
tls_private_key: Option<PathBuf>,
secret_provider: Option<String>,
admin_identity: Option<PathBuf>,
request_policy: Option<PathBuf>,
}
impl SecurityBootstrapEnvironment {
fn from_env() -> Result<Self, SecurityContractViolation> {
Ok(Self {
profile: optional_env(SECURITY_PROFILE_ENV)?,
trust_anchor: optional_path_env(SECURITY_TRUST_ANCHOR_ENV)?,
tls_certificate: optional_path_env(SECURITY_TLS_CERT_ENV)?,
tls_private_key: optional_path_env(SECURITY_TLS_KEY_ENV)?,
secret_provider: optional_env(SECURITY_SECRET_PROVIDER_ENV)?,
admin_identity: optional_path_env(SECURITY_ADMIN_IDENTITY_ENV)?,
request_policy: optional_path_env(SECURITY_REQUEST_POLICY_ENV)?,
})
}
fn resolve(self) -> Result<SecurityBootstrap, SecurityContractViolation> {
let Self {
profile,
trust_anchor,
tls_certificate,
tls_private_key,
secret_provider,
admin_identity,
request_policy,
} = self;
let Some(profile) = profile else {
if trust_anchor.is_some()
|| tls_certificate.is_some()
|| tls_private_key.is_some()
|| secret_provider.is_some()
|| admin_identity.is_some()
|| request_policy.is_some()
{
return Err(SecurityContractViolation::BootstrapProfileRequired);
}
return Ok(SecurityBootstrap::Disabled);
};
let profile = profile.parse::<SecurityBootstrapProfile>()?;
Ok(SecurityBootstrap::Enabled(SecurityBootstrapConfig {
profile,
trust_anchor,
tls_certificate,
tls_private_key,
secret_provider,
admin_identity,
request_policy,
}))
}
}
impl fmt::Debug for SecurityBootstrapConfig {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SecurityBootstrapConfig")
.field("profile", &self.profile)
.field("trust_anchor_configured", &self.trust_anchor.is_some())
.field("tls_certificate_configured", &self.tls_certificate.is_some())
.field("tls_private_key_configured", &self.tls_private_key.is_some())
.field("secret_provider_configured", &self.secret_provider.is_some())
.field("admin_identity_configured", &self.admin_identity.is_some())
.field("request_policy_configured", &self.request_policy.is_some())
.finish()
}
}
impl SecurityBootstrapConfig {
pub const fn new(profile: SecurityBootstrapProfile) -> Self {
Self {
profile,
trust_anchor: None,
tls_certificate: None,
tls_private_key: None,
secret_provider: None,
admin_identity: None,
request_policy: None,
}
}
pub fn from_env() -> Result<SecurityBootstrap, SecurityContractViolation> {
SecurityBootstrapEnvironment::from_env()?.resolve()
}
pub fn with_trust_anchor(mut self, path: impl Into<PathBuf>) -> Self {
self.trust_anchor = Some(path.into());
self
}
pub fn with_tls_identity(mut self, certificate: impl Into<PathBuf>, private_key: impl Into<PathBuf>) -> Self {
self.tls_certificate = Some(certificate.into());
self.tls_private_key = Some(private_key.into());
self
}
pub fn with_secret_provider(mut self, provider: impl Into<String>) -> Self {
self.secret_provider = Some(provider.into());
self
}
pub fn with_admin_identity(mut self, path: impl Into<PathBuf>) -> Self {
self.admin_identity = Some(path.into());
self
}
pub fn with_request_policy(mut self, path: impl Into<PathBuf>) -> Self {
self.request_policy = Some(path.into());
self
}
pub fn validate(
&self,
listener_addresses: &[SocketAddr],
) -> Result<ValidatedSecurityBootstrap, SecurityProviderError> {
match self.profile {
SecurityBootstrapProfile::DevelopmentInsecureLoopback => {
if listener_addresses.iter().any(|address| !address.ip().is_loopback()) {
return Err(SecurityProviderError::contract(
SecurityOperation::Validate,
SecurityContractViolation::DevelopmentListenerNotLoopback,
));
}
}
SecurityBootstrapProfile::SecureEnforced => {
inspect_bootstrap_file(self.trust_anchor.as_deref(), SecurityBootstrapMaterial::TrustAnchor)?;
inspect_bootstrap_file(
self.tls_certificate.as_deref(),
SecurityBootstrapMaterial::TlsCertificate,
)?;
inspect_bootstrap_file(
self.tls_private_key.as_deref(),
SecurityBootstrapMaterial::TlsPrivateKey,
)?;
let provider = self
.secret_provider
.as_deref()
.map(str::trim)
.filter(|provider| !provider.is_empty())
.ok_or(SecurityContractViolation::SecretProviderRequired)
.map_err(|violation| SecurityProviderError::contract(SecurityOperation::Validate, violation))?;
if provider != MOUNTED_FILES_SECRET_PROVIDER {
return Err(SecurityProviderError::contract(
SecurityOperation::Validate,
SecurityContractViolation::SecretProviderUnsupported,
));
}
inspect_bootstrap_file(self.admin_identity.as_deref(), SecurityBootstrapMaterial::AdminIdentity)?;
inspect_bootstrap_file(self.request_policy.as_deref(), SecurityBootstrapMaterial::RequestPolicy)?;
}
}
Ok(ValidatedSecurityBootstrap {
profile: self.profile,
listener_count: listener_addresses.len(),
})
}
}
pub fn validate_security_bootstrap_from_env(
listener_addresses: &[SocketAddr],
) -> Result<SecurityBootstrapOutcome, SecurityProviderError> {
SecurityBootstrapConfig::from_env()
.map_err(SecurityProviderError::from)?
.validate(listener_addresses)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SecurityBootstrapOutcome {
Disabled,
Validated(ValidatedSecurityBootstrap),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ValidatedSecurityBootstrap {
profile: SecurityBootstrapProfile,
listener_count: usize,
}
impl ValidatedSecurityBootstrap {
pub const fn profile(self) -> SecurityBootstrapProfile {
self.profile
}
pub const fn listener_count(self) -> usize {
self.listener_count
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SecurityBootstrapMaterial {
TrustAnchor,
TlsCertificate,
TlsPrivateKey,
AdminIdentity,
RequestPolicy,
}
impl fmt::Display for SecurityBootstrapMaterial {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::TrustAnchor => "trust anchor",
Self::TlsCertificate => "TLS certificate",
Self::TlsPrivateKey => "TLS private key",
Self::AdminIdentity => "administrator identity",
Self::RequestPolicy => "request policy",
})
}
}
fn optional_path_env(name: &'static str) -> Result<Option<PathBuf>, SecurityContractViolation> {
optional_env(name).map(|value| value.map(PathBuf::from))
}
fn optional_env(name: &'static str) -> Result<Option<String>, SecurityContractViolation> {
let Some(value) = env::var_os(name) else {
return Ok(None);
};
let value = value
.into_string()
.map_err(|_| SecurityContractViolation::BootstrapEnvironmentEncoding)?;
let value = value.trim();
Ok((!value.is_empty()).then(|| value.to_string()))
}
fn inspect_bootstrap_file(
path: Option<&Path>,
material: SecurityBootstrapMaterial,
) -> Result<(), SecurityProviderError> {
let path = path
.ok_or(SecurityContractViolation::BootstrapMaterialRequired { material })
.map_err(|violation| SecurityProviderError::contract(SecurityOperation::Validate, violation))?;
let metadata = path.metadata().map_err(|source| {
SecurityProviderError::caused_by(
SecurityProviderFailure::Unavailable,
SecurityOperation::InspectBootstrapMaterial,
source,
)
})?;
if !metadata.is_file() {
return Err(SecurityProviderError::contract(
SecurityOperation::InspectBootstrapMaterial,
SecurityContractViolation::BootstrapMaterialNotRegularFile { material },
));
}
let file = File::open(path).map_err(|source| {
SecurityProviderError::caused_by(
SecurityProviderFailure::Unavailable,
SecurityOperation::InspectBootstrapMaterial,
source,
)
})?;
let metadata = file.metadata().map_err(|source| {
SecurityProviderError::caused_by(
SecurityProviderFailure::OperationFailed,
SecurityOperation::InspectBootstrapMaterial,
source,
)
})?;
if !metadata.is_file() {
return Err(SecurityProviderError::contract(
SecurityOperation::InspectBootstrapMaterial,
SecurityContractViolation::BootstrapMaterialNotRegularFile { material },
));
}
if metadata.len() == 0 {
return Err(SecurityProviderError::contract(
SecurityOperation::InspectBootstrapMaterial,
SecurityContractViolation::BootstrapMaterialEmpty { material },
));
}
Ok(())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DeploymentOrigin {
New,
Existing,
}
#[derive(Debug, Clone, Copy)]
pub struct SecurityProfileSelection<'a> {
origin: DeploymentOrigin,
configured_profile: Option<&'a str>,
}
impl<'a> SecurityProfileSelection<'a> {
pub const fn new(origin: DeploymentOrigin) -> Self {
Self {
origin,
configured_profile: None,
}
}
pub const fn with_configured_profile(mut self, profile: &'a str) -> Self {
self.configured_profile = Some(profile);
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SecurityMigrationStatus {
NotRequired,
CompatibilityProfileMustBePersisted,
MigrationToSecurePending,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SecurityProfileResolution {
profile: DeploymentProfile,
migration_status: SecurityMigrationStatus,
}
impl SecurityProfileResolution {
pub const fn profile(self) -> DeploymentProfile {
self.profile
}
pub const fn migration_status(self) -> SecurityMigrationStatus {
self.migration_status
}
}
pub fn resolve_security_profile(
selection: SecurityProfileSelection<'_>,
) -> Result<SecurityProfileResolution, SecurityContractViolation> {
let configured = selection
.configured_profile
.map(str::trim)
.filter(|profile| !profile.is_empty());
let profile = match configured {
Some(profile) => DeploymentProfile::from_str(profile)?,
None if selection.origin == DeploymentOrigin::New => DeploymentProfile::Secure,
None => DeploymentProfile::Compatibility,
};
let migration_status = match (selection.origin, configured, profile) {
(DeploymentOrigin::Existing, None, DeploymentProfile::Compatibility) => {
SecurityMigrationStatus::CompatibilityProfileMustBePersisted
}
(_, _, DeploymentProfile::Compatibility | DeploymentProfile::Development) => {
SecurityMigrationStatus::MigrationToSecurePending
}
_ => SecurityMigrationStatus::NotRequired,
};
Ok(SecurityProfileResolution {
profile,
migration_status,
})
}
#[derive(Debug, Clone, Copy)]
pub struct BootstrapReadinessView {
expires_at: SystemTime,
material_available: bool,
verified_tls_listener: bool,
}
impl BootstrapReadinessView {
pub const fn new(expires_at: SystemTime) -> Self {
Self {
expires_at,
material_available: false,
verified_tls_listener: false,
}
}
pub const fn with_available_material(mut self) -> Self {
self.material_available = true;
self
}
pub const fn with_verified_tls_listener(mut self) -> Self {
self.verified_tls_listener = true;
self
}
}
#[derive(Debug, Clone, Copy)]
pub struct DeploymentSecurityConfigView<'a> {
profile: SecurityProfileSelection<'a>,
trust_anchor: Option<&'a Path>,
secret_provider_registered: bool,
provisioned_admin_identity: bool,
one_time_bootstrap: Option<BootstrapReadinessView>,
insecure_downgrade: bool,
}
impl<'a> DeploymentSecurityConfigView<'a> {
pub const fn new(profile: SecurityProfileSelection<'a>) -> Self {
Self {
profile,
trust_anchor: None,
secret_provider_registered: false,
provisioned_admin_identity: false,
one_time_bootstrap: None,
insecure_downgrade: false,
}
}
pub const fn with_trust_anchor(mut self, path: &'a Path) -> Self {
self.trust_anchor = Some(path);
self
}
pub const fn with_registered_secret_provider(mut self) -> Self {
self.secret_provider_registered = true;
self
}
pub const fn with_provisioned_admin_identity(mut self) -> Self {
self.provisioned_admin_identity = true;
self
}
pub const fn with_one_time_bootstrap(mut self, bootstrap: BootstrapReadinessView) -> Self {
self.one_time_bootstrap = Some(bootstrap);
self
}
pub const fn with_insecure_downgrade(mut self) -> Self {
self.insecure_downgrade = true;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DeploymentSecurityFailure {
MissingTrustAnchor,
TrustAnchorUnavailable,
TrustAnchorNotRegularFile,
MissingSecretProvider,
MissingIdentityBootstrap,
MultipleIdentityBootstrapSources,
MissingBootstrapMaterial,
ExpiredBootstrap,
BootstrapListenerNotTls,
InsecureDowngrade,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DeploymentSecurityReport {
resolution: SecurityProfileResolution,
failures: Vec<DeploymentSecurityFailure>,
}
impl DeploymentSecurityReport {
pub fn is_ready(&self) -> bool {
self.failures.is_empty()
}
pub const fn resolution(&self) -> SecurityProfileResolution {
self.resolution
}
pub fn failures(&self) -> &[DeploymentSecurityFailure] {
&self.failures
}
}
pub fn validate_deployment_security(
view: DeploymentSecurityConfigView<'_>,
now: SystemTime,
) -> Result<DeploymentSecurityReport, SecurityContractViolation> {
let resolution = resolve_security_profile(view.profile)?;
let mut failures = Vec::new();
if resolution.profile == DeploymentProfile::Secure {
inspect_trust_anchor(view.trust_anchor, &mut failures);
if !view.secret_provider_registered {
failures.push(DeploymentSecurityFailure::MissingSecretProvider);
}
match (view.provisioned_admin_identity, view.one_time_bootstrap) {
(false, None) => failures.push(DeploymentSecurityFailure::MissingIdentityBootstrap),
(true, Some(_)) => failures.push(DeploymentSecurityFailure::MultipleIdentityBootstrapSources),
_ => {}
}
if let Some(bootstrap) = view.one_time_bootstrap {
if !bootstrap.material_available {
failures.push(DeploymentSecurityFailure::MissingBootstrapMaterial);
}
if bootstrap.expires_at <= now {
failures.push(DeploymentSecurityFailure::ExpiredBootstrap);
}
if !bootstrap.verified_tls_listener {
failures.push(DeploymentSecurityFailure::BootstrapListenerNotTls);
}
}
if view.insecure_downgrade {
failures.push(DeploymentSecurityFailure::InsecureDowngrade);
}
}
Ok(DeploymentSecurityReport { resolution, failures })
}
fn inspect_trust_anchor(path: Option<&Path>, failures: &mut Vec<DeploymentSecurityFailure>) {
let Some(path) = path else {
failures.push(DeploymentSecurityFailure::MissingTrustAnchor);
return;
};
let Ok(path_metadata) = path.metadata() else {
failures.push(DeploymentSecurityFailure::TrustAnchorUnavailable);
return;
};
if !path_metadata.is_file() {
failures.push(DeploymentSecurityFailure::TrustAnchorNotRegularFile);
return;
}
let Ok(file) = File::open(path) else {
failures.push(DeploymentSecurityFailure::TrustAnchorUnavailable);
return;
};
match file.metadata() {
Ok(metadata) if metadata.is_file() => {}
Ok(_) => failures.push(DeploymentSecurityFailure::TrustAnchorNotRegularFile),
Err(_) => failures.push(DeploymentSecurityFailure::TrustAnchorUnavailable),
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use tempfile::NamedTempFile;
use super::*;
fn contract_source(error: &SecurityProviderError) -> SecurityContractViolation {
*error
.source()
.and_then(|source| source.downcast_ref::<SecurityContractViolation>())
.expect("provider error should retain its contract source")
}
#[test]
fn security_bootstrap_environment_without_profile_or_fields_is_disabled() {
let security_bootstrap = SecurityBootstrapEnvironment::default()
.resolve()
.expect("empty security bootstrap environment should resolve");
assert!(matches!(security_bootstrap, SecurityBootstrap::Disabled));
}
#[test]
fn security_bootstrap_environment_rejects_every_field_without_profile() {
let configured_environments = [
SecurityBootstrapEnvironment {
trust_anchor: Some(PathBuf::from("ca.crt")),
..SecurityBootstrapEnvironment::default()
},
SecurityBootstrapEnvironment {
tls_certificate: Some(PathBuf::from("tls.crt")),
..SecurityBootstrapEnvironment::default()
},
SecurityBootstrapEnvironment {
tls_private_key: Some(PathBuf::from("tls.key")),
..SecurityBootstrapEnvironment::default()
},
SecurityBootstrapEnvironment {
secret_provider: Some(MOUNTED_FILES_SECRET_PROVIDER.to_string()),
..SecurityBootstrapEnvironment::default()
},
SecurityBootstrapEnvironment {
admin_identity: Some(PathBuf::from("admin.identity")),
..SecurityBootstrapEnvironment::default()
},
SecurityBootstrapEnvironment {
request_policy: Some(PathBuf::from("request-policy.json")),
..SecurityBootstrapEnvironment::default()
},
];
for environment in configured_environments {
assert_eq!(
environment
.resolve()
.expect_err("security fields without a profile must fail"),
SecurityContractViolation::BootstrapProfileRequired
);
}
}
#[test]
fn security_bootstrap_environment_enables_known_profiles_and_rejects_unknown_profile() {
let development = SecurityBootstrapEnvironment {
profile: Some("development-insecure-loopback".to_string()),
..SecurityBootstrapEnvironment::default()
}
.resolve()
.expect("development profile should resolve");
let SecurityBootstrap::Enabled(development) = development else {
panic!("development profile must enable security bootstrap");
};
assert_eq!(
development
.validate(&[])
.expect("development profile without listeners should validate")
.profile(),
SecurityBootstrapProfile::DevelopmentInsecureLoopback
);
let secure = SecurityBootstrapEnvironment {
profile: Some("secure-enforced".to_string()),
..SecurityBootstrapEnvironment::default()
}
.resolve()
.expect("secure profile should resolve");
let SecurityBootstrap::Enabled(secure) = secure else {
panic!("secure profile must enable security bootstrap");
};
let error = secure
.validate(&[])
.expect_err("secure profile without material must fail closed");
assert_eq!(
contract_source(&error),
SecurityContractViolation::BootstrapMaterialRequired {
material: SecurityBootstrapMaterial::TrustAnchor,
}
);
assert_eq!(
SecurityBootstrapEnvironment {
profile: Some("unknown".to_string()),
..SecurityBootstrapEnvironment::default()
}
.resolve()
.expect_err("unknown profile must fail closed"),
SecurityContractViolation::UnknownSecurityProfile
);
}
#[test]
fn new_defaults_secure_while_existing_defaults_compatibility_with_report() {
let new = resolve_security_profile(SecurityProfileSelection::new(DeploymentOrigin::New)).unwrap();
assert_eq!(new.profile(), DeploymentProfile::Secure);
assert_eq!(new.migration_status(), SecurityMigrationStatus::NotRequired);
let existing = resolve_security_profile(SecurityProfileSelection::new(DeploymentOrigin::Existing)).unwrap();
assert_eq!(existing.profile(), DeploymentProfile::Compatibility);
assert_eq!(
existing.migration_status(),
SecurityMigrationStatus::CompatibilityProfileMustBePersisted
);
}
#[test]
fn unknown_profile_never_downgrades() {
let selection = SecurityProfileSelection::new(DeploymentOrigin::Existing).with_configured_profile("unknown");
assert_eq!(
resolve_security_profile(selection).unwrap_err(),
SecurityContractViolation::UnknownSecurityProfile
);
}
#[test]
fn secure_provisioned_identity_requires_trust_and_provider() {
let now = SystemTime::now();
let missing = validate_deployment_security(
DeploymentSecurityConfigView::new(SecurityProfileSelection::new(DeploymentOrigin::New))
.with_provisioned_admin_identity(),
now,
)
.unwrap();
assert_eq!(
missing.failures(),
&[
DeploymentSecurityFailure::MissingTrustAnchor,
DeploymentSecurityFailure::MissingSecretProvider,
]
);
let trust_anchor = NamedTempFile::new().unwrap();
let ready = validate_deployment_security(
DeploymentSecurityConfigView::new(SecurityProfileSelection::new(DeploymentOrigin::New))
.with_trust_anchor(trust_anchor.path())
.with_registered_secret_provider()
.with_provisioned_admin_identity(),
now,
)
.unwrap();
assert!(ready.is_ready());
}
#[test]
fn token_bootstrap_requires_one_source_material_expiry_and_tls() {
let now = SystemTime::now();
let trust_anchor = NamedTempFile::new().unwrap();
let invalid = validate_deployment_security(
DeploymentSecurityConfigView::new(SecurityProfileSelection::new(DeploymentOrigin::New))
.with_trust_anchor(trust_anchor.path())
.with_registered_secret_provider()
.with_provisioned_admin_identity()
.with_one_time_bootstrap(BootstrapReadinessView::new(now - Duration::from_secs(1))),
now,
)
.unwrap();
assert_eq!(
invalid.failures(),
&[
DeploymentSecurityFailure::MultipleIdentityBootstrapSources,
DeploymentSecurityFailure::MissingBootstrapMaterial,
DeploymentSecurityFailure::ExpiredBootstrap,
DeploymentSecurityFailure::BootstrapListenerNotTls,
]
);
let ready = validate_deployment_security(
DeploymentSecurityConfigView::new(SecurityProfileSelection::new(DeploymentOrigin::New))
.with_trust_anchor(trust_anchor.path())
.with_registered_secret_provider()
.with_one_time_bootstrap(
BootstrapReadinessView::new(now + Duration::from_secs(60))
.with_available_material()
.with_verified_tls_listener(),
),
now,
)
.unwrap();
assert!(ready.is_ready());
}
#[test]
fn compatibility_is_ready_without_secure_material_but_reports_migration() {
let report = validate_deployment_security(
DeploymentSecurityConfigView::new(SecurityProfileSelection::new(DeploymentOrigin::Existing)),
SystemTime::now(),
)
.unwrap();
assert!(report.is_ready());
assert_eq!(
report.resolution().migration_status(),
SecurityMigrationStatus::CompatibilityProfileMustBePersisted
);
}
}