use std::backtrace::Backtrace;
use std::error::Error as StdError;
use std::fmt;
use std::panic::Location;
use rocketmq_error::fields;
use rocketmq_error::CanonicalCondition;
use rocketmq_error::DiagnosticView;
use rocketmq_error::Error as CanonicalError;
use rocketmq_error::ErrorCode;
use rocketmq_error::ErrorContext;
use rocketmq_error::ErrorDescriptor;
use rocketmq_error::ErrorSeverity;
use rocketmq_error::PublicErrorView;
use rocketmq_error::RecoveryHint;
use rocketmq_error::SharedError;
use rocketmq_error::ViewContextViolation;
use crate::SecurityBootstrapMaterial;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum SecurityIdentifierRule {
NotEmpty,
MaximumLength,
CanonicalCharacters,
}
impl fmt::Display for SecurityIdentifierRule {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(match self {
Self::NotEmpty => "identifier-not-empty",
Self::MaximumLength => "identifier-maximum-length",
Self::CanonicalCharacters => "identifier-canonical-characters",
})
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum MaintenancePolicyRule {
SchemaVersion,
PolicyIdentifier,
PolicyVersion,
AuthenticationRequired,
AuthorizationRequired,
FencingTokenRequired,
RequestLifetime,
ResourceLimits,
PrincipalIdentifier,
UniquePrincipalBinding,
PrincipalRoleRequired,
ReleaseOperatorRequired,
UniqueRoleGrant,
RoleCapabilityRequired,
AdministratorCheckpointForbidden,
ReleaseCheckpointRequired,
}
impl fmt::Display for MaintenancePolicyRule {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(match self {
Self::SchemaVersion => "maintenance-schema-version",
Self::PolicyIdentifier => "maintenance-policy-identifier",
Self::PolicyVersion => "maintenance-policy-version",
Self::AuthenticationRequired => "maintenance-authentication-required",
Self::AuthorizationRequired => "maintenance-authorization-required",
Self::FencingTokenRequired => "maintenance-fencing-token-required",
Self::RequestLifetime => "maintenance-request-lifetime",
Self::ResourceLimits => "maintenance-resource-limits",
Self::PrincipalIdentifier => "maintenance-principal-identifier",
Self::UniquePrincipalBinding => "maintenance-unique-principal-binding",
Self::PrincipalRoleRequired => "maintenance-principal-role-required",
Self::ReleaseOperatorRequired => "maintenance-release-operator-required",
Self::UniqueRoleGrant => "maintenance-unique-role-grant",
Self::RoleCapabilityRequired => "maintenance-role-capability-required",
Self::AdministratorCheckpointForbidden => "maintenance-administrator-checkpoint-forbidden",
Self::ReleaseCheckpointRequired => "maintenance-release-checkpoint-required",
})
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
pub enum SecurityContractViolation {
#[error("security identifier violates {rule}")]
InvalidIdentifier {
rule: SecurityIdentifierRule,
},
#[error("secret material must not be empty")]
EmptySecretMaterial,
#[error("security bootstrap profile is required")]
BootstrapProfileRequired,
#[error("security profile is unknown")]
UnknownSecurityProfile,
#[error("security bootstrap environment value is not valid Unicode")]
BootstrapEnvironmentEncoding,
#[error("security bootstrap is missing {material}")]
BootstrapMaterialRequired {
material: SecurityBootstrapMaterial,
},
#[error("security bootstrap {material} is not a regular file")]
BootstrapMaterialNotRegularFile {
material: SecurityBootstrapMaterial,
},
#[error("security bootstrap {material} is empty")]
BootstrapMaterialEmpty {
material: SecurityBootstrapMaterial,
},
#[error("secure bootstrap requires a secret provider")]
SecretProviderRequired,
#[error("configured secret provider is unsupported")]
SecretProviderUnsupported,
#[error("development-insecure profile requires loopback listeners")]
DevelopmentListenerNotLoopback,
#[error("secret provider configuration is invalid")]
ProviderConfigurationInvalid,
#[error("secret storage permissions are not owner-only")]
SecretStoragePermissionsInsecure,
#[error("secure transport requires a signing provider")]
SigningProviderRequired,
#[error("maintenance policy violates {rule}")]
MaintenancePolicy {
rule: MaintenancePolicyRule,
},
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum SecurityProviderFailure {
NotFound,
Conflict,
Unsupported,
InvalidData,
ContractViolation,
Unavailable,
OperationFailed,
}
impl fmt::Display for SecurityProviderFailure {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(match self {
Self::NotFound => "not_found",
Self::Conflict => "conflict",
Self::Unsupported => "unsupported",
Self::InvalidData => "invalid_data",
Self::ContractViolation => "contract_violation",
Self::Unavailable => "unavailable",
Self::OperationFailed => "operation_failed",
})
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum SecurityOperation {
Validate,
ResolveProvider,
RegisterProvider,
InspectBootstrapMaterial,
ReadSecret,
WriteSecret,
InspectSecret,
EncryptSecret,
DecryptSecret,
SynchronizeSecret,
SignRequest,
LoadPolicy,
RecordAudit,
RotateCredentials,
}
impl SecurityOperation {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Validate => "validate",
Self::ResolveProvider => "resolve_provider",
Self::RegisterProvider => "register_provider",
Self::InspectBootstrapMaterial => "inspect_bootstrap_material",
Self::ReadSecret => "read_secret",
Self::WriteSecret => "write_secret",
Self::InspectSecret => "inspect_secret",
Self::EncryptSecret => "encrypt_secret",
Self::DecryptSecret => "decrypt_secret",
Self::SynchronizeSecret => "synchronize_secret",
Self::SignRequest => "sign_request",
Self::LoadPolicy => "load_policy",
Self::RecordAudit => "record_audit",
Self::RotateCredentials => "rotate_credentials",
}
}
}
impl fmt::Display for SecurityOperation {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
#[derive(Clone)]
pub struct SecurityProviderError {
error: SharedError,
kind: SecurityProviderFailure,
operation: SecurityOperation,
}
impl SecurityProviderError {
#[must_use]
#[track_caller]
pub fn new(kind: SecurityProviderFailure, operation: SecurityOperation) -> Self {
let error = CanonicalError::new(provider_descriptor(kind)).with_context(provider_context(operation, false));
Self {
error: std::sync::Arc::new(error),
kind,
operation,
}
}
#[must_use]
#[track_caller]
pub fn caused_by(
kind: SecurityProviderFailure,
operation: SecurityOperation,
source: impl StdError + Send + Sync + 'static,
) -> Self {
let error = CanonicalError::caused_by(provider_descriptor(kind), source)
.with_context(provider_context(operation, true));
Self {
error: std::sync::Arc::new(error),
kind,
operation,
}
}
#[must_use]
pub fn contract(operation: SecurityOperation, violation: SecurityContractViolation) -> Self {
Self::caused_by(SecurityProviderFailure::ContractViolation, operation, violation)
}
#[must_use]
pub const fn kind(&self) -> SecurityProviderFailure {
self.kind
}
#[must_use]
pub const fn operation(&self) -> SecurityOperation {
self.operation
}
#[must_use]
pub fn shared_error(&self) -> &SharedError {
&self.error
}
#[must_use]
pub fn into_shared_error(self) -> SharedError {
self.error
}
#[must_use]
pub fn descriptor(&self) -> &'static ErrorDescriptor {
self.error.descriptor()
}
#[must_use]
pub fn code(&self) -> ErrorCode {
self.error.code()
}
#[must_use]
pub fn condition(&self) -> CanonicalCondition {
self.error.condition()
}
#[must_use]
pub fn severity(&self) -> ErrorSeverity {
self.error.severity()
}
#[must_use]
pub fn recovery_hint(&self) -> RecoveryHint {
self.error.recovery_hint()
}
#[must_use]
pub fn context(&self) -> &ErrorContext {
self.error.context()
}
#[must_use]
pub fn location(&self) -> &'static Location<'static> {
self.error.location()
}
#[must_use]
pub fn backtrace(&self) -> Option<&Backtrace> {
self.error.backtrace()
}
pub fn public_view(&self) -> Result<PublicErrorView<'_>, ViewContextViolation> {
self.error.public_view()
}
pub fn diagnostic_view(&self) -> Result<DiagnosticView<'_>, ViewContextViolation> {
self.error.diagnostic_view()
}
#[must_use]
pub fn source(&self) -> Option<&(dyn StdError + 'static)> {
self.error.source()
}
}
fn provider_descriptor(kind: SecurityProviderFailure) -> &'static ErrorDescriptor {
match kind {
SecurityProviderFailure::NotFound => &rocketmq_error::SECURITY_PROVIDER_NOT_FOUND,
SecurityProviderFailure::Conflict => &rocketmq_error::SECURITY_PROVIDER_CONFLICT,
SecurityProviderFailure::Unsupported => &rocketmq_error::SECURITY_PROVIDER_UNSUPPORTED,
SecurityProviderFailure::InvalidData => &rocketmq_error::SECURITY_PROVIDER_INVALID_DATA,
SecurityProviderFailure::ContractViolation => &rocketmq_error::SECURITY_PROVIDER_CONTRACT_VIOLATION,
SecurityProviderFailure::Unavailable => &rocketmq_error::SECURITY_PROVIDER_UNAVAILABLE,
SecurityProviderFailure::OperationFailed => &rocketmq_error::SECURITY_PROVIDER_OPERATION_FAILED,
}
}
fn provider_context(operation: SecurityOperation, source_present: bool) -> ErrorContext {
let mut context = ErrorContext::new().with_text(fields::OPERATION_DIAGNOSTIC, operation.as_str());
if source_present {
context = context.with_secret_presence(fields::SOURCE_PRESENT);
}
context
}
impl From<SecurityContractViolation> for SecurityProviderError {
fn from(violation: SecurityContractViolation) -> Self {
Self::contract(SecurityOperation::Validate, violation)
}
}
impl fmt::Display for SecurityProviderError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(self.error.as_ref(), formatter)
}
}
impl fmt::Debug for SecurityProviderError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("SecurityProviderError")
.field("code", &self.code())
.field("condition", &self.condition())
.field("kind", &self.kind)
.field("operation", &self.operation)
.field("source_present", &self.error.source().is_some())
.finish()
}
}
impl StdError for SecurityProviderError {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
self.error.source()
}
}
#[cfg(test)]
mod tests {
use std::error::Error;
use std::io;
use std::mem::size_of;
use super::*;
#[test]
fn provider_error_redacts_and_retains_typed_source() {
let sentinel = "secret-provider-sentinel";
let error = SecurityProviderError::caused_by(
SecurityProviderFailure::Unavailable,
SecurityOperation::ReadSecret,
io::Error::other(sentinel),
);
assert!(!error.to_string().contains(sentinel));
assert!(!format!("{error:?}").contains(sentinel));
assert_eq!(error.kind(), SecurityProviderFailure::Unavailable);
assert_eq!(error.operation(), SecurityOperation::ReadSecret);
assert_eq!(error.descriptor(), &rocketmq_error::SECURITY_PROVIDER_UNAVAILABLE);
assert_eq!(error.condition(), CanonicalCondition::Unavailable);
assert_eq!(error.context().len(), 2);
assert!(error.public_view().is_ok());
assert!(error.diagnostic_view().is_ok());
assert!(Error::source(&error).unwrap().downcast_ref::<io::Error>().is_some());
}
#[test]
fn contract_conversion_is_value_free_and_small() {
let error = SecurityProviderError::from(SecurityContractViolation::EmptySecretMaterial);
assert_eq!(error.kind(), SecurityProviderFailure::ContractViolation);
assert_eq!(error.operation(), SecurityOperation::Validate);
assert_eq!(
error.descriptor(),
&rocketmq_error::SECURITY_PROVIDER_CONTRACT_VIOLATION
);
assert!(error
.source()
.unwrap()
.downcast_ref::<SecurityContractViolation>()
.is_some());
assert!(size_of::<SecurityProviderError>() <= 4 * size_of::<usize>());
}
#[test]
fn every_provider_failure_has_one_catalog_identity() {
let cases = [
(
SecurityProviderFailure::NotFound,
&rocketmq_error::SECURITY_PROVIDER_NOT_FOUND,
),
(
SecurityProviderFailure::Conflict,
&rocketmq_error::SECURITY_PROVIDER_CONFLICT,
),
(
SecurityProviderFailure::Unsupported,
&rocketmq_error::SECURITY_PROVIDER_UNSUPPORTED,
),
(
SecurityProviderFailure::InvalidData,
&rocketmq_error::SECURITY_PROVIDER_INVALID_DATA,
),
(
SecurityProviderFailure::ContractViolation,
&rocketmq_error::SECURITY_PROVIDER_CONTRACT_VIOLATION,
),
(
SecurityProviderFailure::Unavailable,
&rocketmq_error::SECURITY_PROVIDER_UNAVAILABLE,
),
(
SecurityProviderFailure::OperationFailed,
&rocketmq_error::SECURITY_PROVIDER_OPERATION_FAILED,
),
];
for (kind, descriptor) in cases {
let error = SecurityProviderError::new(kind, SecurityOperation::ReadSecret);
assert_eq!(error.descriptor(), descriptor);
assert_eq!(error.kind(), kind);
assert_eq!(error.context().len(), 1);
assert!(error.source().is_none());
}
}
}