Skip to main content

rocketmq_security_api/
error.rs

1// Copyright 2023 The RocketMQ Rust Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Security contract and provider failure types.
16
17use std::backtrace::Backtrace;
18use std::error::Error as StdError;
19use std::fmt;
20use std::panic::Location;
21
22use rocketmq_error::fields;
23use rocketmq_error::CanonicalCondition;
24use rocketmq_error::DiagnosticView;
25use rocketmq_error::Error as CanonicalError;
26use rocketmq_error::ErrorCode;
27use rocketmq_error::ErrorContext;
28use rocketmq_error::ErrorDescriptor;
29use rocketmq_error::ErrorSeverity;
30use rocketmq_error::PublicErrorView;
31use rocketmq_error::RecoveryHint;
32use rocketmq_error::SharedError;
33use rocketmq_error::ViewContextViolation;
34
35use crate::SecurityBootstrapMaterial;
36
37/// Closed validation rules for provider identifiers and logical secret names.
38#[derive(Clone, Copy, Debug, Eq, PartialEq)]
39pub enum SecurityIdentifierRule {
40    /// An identifier must contain at least one byte.
41    NotEmpty,
42    /// An identifier must not exceed the documented length bound.
43    MaximumLength,
44    /// An identifier must use only the documented canonical character set.
45    CanonicalCharacters,
46}
47
48impl fmt::Display for SecurityIdentifierRule {
49    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
50        formatter.write_str(match self {
51            Self::NotEmpty => "identifier-not-empty",
52            Self::MaximumLength => "identifier-maximum-length",
53            Self::CanonicalCharacters => "identifier-canonical-characters",
54        })
55    }
56}
57
58/// Closed semantic rules for maintenance policies.
59#[derive(Clone, Copy, Debug, Eq, PartialEq)]
60pub enum MaintenancePolicyRule {
61    /// The schema version must be supported.
62    SchemaVersion,
63    /// The policy identifier must be canonical.
64    PolicyIdentifier,
65    /// The policy version must be non-zero.
66    PolicyVersion,
67    /// Authentication must be required.
68    AuthenticationRequired,
69    /// Authorization must be required.
70    AuthorizationRequired,
71    /// Fencing tokens must be required.
72    FencingTokenRequired,
73    /// Request lifetime must be within the supported range.
74    RequestLifetime,
75    /// Every resource limit must be non-zero.
76    ResourceLimits,
77    /// Every bound principal must be canonical.
78    PrincipalIdentifier,
79    /// A principal may be bound only once.
80    UniquePrincipalBinding,
81    /// Every bound principal must have at least one role.
82    PrincipalRoleRequired,
83    /// At least one release operator must be bound.
84    ReleaseOperatorRequired,
85    /// A role may have only one grant entry.
86    UniqueRoleGrant,
87    /// Every role grant must contain at least one capability.
88    RoleCapabilityRequired,
89    /// Administrators must not receive the release-checkpoint capability.
90    AdministratorCheckpointForbidden,
91    /// Release operators must receive the release-checkpoint capability.
92    ReleaseCheckpointRequired,
93}
94
95impl fmt::Display for MaintenancePolicyRule {
96    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
97        formatter.write_str(match self {
98            Self::SchemaVersion => "maintenance-schema-version",
99            Self::PolicyIdentifier => "maintenance-policy-identifier",
100            Self::PolicyVersion => "maintenance-policy-version",
101            Self::AuthenticationRequired => "maintenance-authentication-required",
102            Self::AuthorizationRequired => "maintenance-authorization-required",
103            Self::FencingTokenRequired => "maintenance-fencing-token-required",
104            Self::RequestLifetime => "maintenance-request-lifetime",
105            Self::ResourceLimits => "maintenance-resource-limits",
106            Self::PrincipalIdentifier => "maintenance-principal-identifier",
107            Self::UniquePrincipalBinding => "maintenance-unique-principal-binding",
108            Self::PrincipalRoleRequired => "maintenance-principal-role-required",
109            Self::ReleaseOperatorRequired => "maintenance-release-operator-required",
110            Self::UniqueRoleGrant => "maintenance-unique-role-grant",
111            Self::RoleCapabilityRequired => "maintenance-role-capability-required",
112            Self::AdministratorCheckpointForbidden => "maintenance-administrator-checkpoint-forbidden",
113            Self::ReleaseCheckpointRequired => "maintenance-release-checkpoint-required",
114        })
115    }
116}
117
118/// Deterministic violation of a security input, configuration, or policy contract.
119///
120/// Variants contain only closed identifiers. Secret values, paths, principals,
121/// provider identifiers, and arbitrary reason strings are deliberately absent.
122#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
123pub enum SecurityContractViolation {
124    /// A provider identifier or logical secret name is invalid.
125    #[error("security identifier violates {rule}")]
126    InvalidIdentifier {
127        /// The violated identifier rule.
128        rule: SecurityIdentifierRule,
129    },
130    /// Secret material is empty.
131    #[error("secret material must not be empty")]
132    EmptySecretMaterial,
133    /// Bootstrap fields were configured without selecting a profile.
134    #[error("security bootstrap profile is required")]
135    BootstrapProfileRequired,
136    /// A configured security profile is unknown.
137    #[error("security profile is unknown")]
138    UnknownSecurityProfile,
139    /// A bootstrap environment value is not valid Unicode.
140    #[error("security bootstrap environment value is not valid Unicode")]
141    BootstrapEnvironmentEncoding,
142    /// Required bootstrap material was not configured.
143    #[error("security bootstrap is missing {material}")]
144    BootstrapMaterialRequired {
145        /// The missing material class.
146        material: SecurityBootstrapMaterial,
147    },
148    /// Configured bootstrap material is not a regular file.
149    #[error("security bootstrap {material} is not a regular file")]
150    BootstrapMaterialNotRegularFile {
151        /// The invalid material class.
152        material: SecurityBootstrapMaterial,
153    },
154    /// Configured bootstrap material is empty.
155    #[error("security bootstrap {material} is empty")]
156    BootstrapMaterialEmpty {
157        /// The empty material class.
158        material: SecurityBootstrapMaterial,
159    },
160    /// Secure bootstrap has no configured secret provider.
161    #[error("secure bootstrap requires a secret provider")]
162    SecretProviderRequired,
163    /// The configured secret-provider capability is unsupported.
164    #[error("configured secret provider is unsupported")]
165    SecretProviderUnsupported,
166    /// An insecure development profile was configured with a non-loopback listener.
167    #[error("development-insecure profile requires loopback listeners")]
168    DevelopmentListenerNotLoopback,
169    /// Provider configuration violates a closed contract.
170    #[error("secret provider configuration is invalid")]
171    ProviderConfigurationInvalid,
172    /// Secret storage permissions are not owner-only.
173    #[error("secret storage permissions are not owner-only")]
174    SecretStoragePermissionsInsecure,
175    /// Secure transport was configured without a signing provider.
176    #[error("secure transport requires a signing provider")]
177    SigningProviderRequired,
178    /// A maintenance policy violates a closed semantic rule.
179    #[error("maintenance policy violates {rule}")]
180    MaintenancePolicy {
181        /// The violated maintenance-policy rule.
182        rule: MaintenancePolicyRule,
183    },
184}
185
186/// Stable category of a security provider failure.
187#[derive(Clone, Copy, Debug, Eq, PartialEq)]
188pub enum SecurityProviderFailure {
189    /// Requested secret material does not exist.
190    NotFound,
191    /// Provider state conflicts with the requested update.
192    Conflict,
193    /// The requested provider capability is unsupported.
194    Unsupported,
195    /// Provider data is malformed, corrupted, or cannot be authenticated.
196    InvalidData,
197    /// A deterministic security contract was violated.
198    ContractViolation,
199    /// The provider or a required dependency is unavailable.
200    Unavailable,
201    /// The provider failed for another operational reason.
202    OperationFailed,
203}
204
205impl fmt::Display for SecurityProviderFailure {
206    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
207        formatter.write_str(match self {
208            Self::NotFound => "not_found",
209            Self::Conflict => "conflict",
210            Self::Unsupported => "unsupported",
211            Self::InvalidData => "invalid_data",
212            Self::ContractViolation => "contract_violation",
213            Self::Unavailable => "unavailable",
214            Self::OperationFailed => "operation_failed",
215        })
216    }
217}
218
219/// Closed operation performed at a security-provider boundary.
220#[derive(Clone, Copy, Debug, Eq, PartialEq)]
221pub enum SecurityOperation {
222    /// Validate deterministic security input or configuration.
223    Validate,
224    /// Resolve or select a provider.
225    ResolveProvider,
226    /// Register a provider.
227    RegisterProvider,
228    /// Inspect bootstrap material.
229    InspectBootstrapMaterial,
230    /// Read secret material.
231    ReadSecret,
232    /// Write secret material.
233    WriteSecret,
234    /// Inspect secret storage metadata.
235    InspectSecret,
236    /// Encrypt secret material.
237    EncryptSecret,
238    /// Decrypt secret material.
239    DecryptSecret,
240    /// Synchronize durable secret-provider state.
241    SynchronizeSecret,
242    /// Sign an outbound request.
243    SignRequest,
244    /// Load a security or maintenance policy.
245    LoadPolicy,
246    /// Record a security audit event.
247    RecordAudit,
248    /// Rotate credentials.
249    RotateCredentials,
250}
251
252impl SecurityOperation {
253    /// Returns the stable diagnostic label for this operation.
254    #[must_use]
255    pub const fn as_str(self) -> &'static str {
256        match self {
257            Self::Validate => "validate",
258            Self::ResolveProvider => "resolve_provider",
259            Self::RegisterProvider => "register_provider",
260            Self::InspectBootstrapMaterial => "inspect_bootstrap_material",
261            Self::ReadSecret => "read_secret",
262            Self::WriteSecret => "write_secret",
263            Self::InspectSecret => "inspect_secret",
264            Self::EncryptSecret => "encrypt_secret",
265            Self::DecryptSecret => "decrypt_secret",
266            Self::SynchronizeSecret => "synchronize_secret",
267            Self::SignRequest => "sign_request",
268            Self::LoadPolicy => "load_policy",
269            Self::RecordAudit => "record_audit",
270            Self::RotateCredentials => "rotate_credentials",
271        }
272    }
273}
274
275impl fmt::Display for SecurityOperation {
276    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
277        formatter.write_str(self.as_str())
278    }
279}
280
281/// Operational failure at a security-provider boundary.
282///
283/// Formatting is intentionally fixed and never renders the typed source. The
284/// source remains available for internal downcasting through [`Self::source`]
285/// and [`std::error::Error::source`].
286#[derive(Clone)]
287pub struct SecurityProviderError {
288    error: SharedError,
289    kind: SecurityProviderFailure,
290    operation: SecurityOperation,
291}
292
293impl SecurityProviderError {
294    /// Creates a source-free provider failure.
295    #[must_use]
296    #[track_caller]
297    pub fn new(kind: SecurityProviderFailure, operation: SecurityOperation) -> Self {
298        let error = CanonicalError::new(provider_descriptor(kind)).with_context(provider_context(operation, false));
299        Self {
300            error: std::sync::Arc::new(error),
301            kind,
302            operation,
303        }
304    }
305
306    /// Creates a provider failure while retaining its typed source.
307    #[must_use]
308    #[track_caller]
309    pub fn caused_by(
310        kind: SecurityProviderFailure,
311        operation: SecurityOperation,
312        source: impl StdError + Send + Sync + 'static,
313    ) -> Self {
314        let error = CanonicalError::caused_by(provider_descriptor(kind), source)
315            .with_context(provider_context(operation, true));
316        Self {
317            error: std::sync::Arc::new(error),
318            kind,
319            operation,
320        }
321    }
322
323    /// Wraps a deterministic contract violation for a specific operation.
324    #[must_use]
325    pub fn contract(operation: SecurityOperation, violation: SecurityContractViolation) -> Self {
326        Self::caused_by(SecurityProviderFailure::ContractViolation, operation, violation)
327    }
328
329    /// Returns the stable failure category.
330    #[must_use]
331    pub const fn kind(&self) -> SecurityProviderFailure {
332        self.kind
333    }
334
335    /// Returns the closed operation identifier.
336    #[must_use]
337    pub const fn operation(&self) -> SecurityOperation {
338        self.operation
339    }
340
341    /// Borrows the canonical error retained by this provider failure.
342    #[must_use]
343    pub fn shared_error(&self) -> &SharedError {
344        &self.error
345    }
346
347    /// Consumes this facade and returns its canonical error allocation.
348    #[must_use]
349    pub fn into_shared_error(self) -> SharedError {
350        self.error
351    }
352
353    /// Returns the catalog descriptor that owns this failure's identity.
354    #[must_use]
355    pub fn descriptor(&self) -> &'static ErrorDescriptor {
356        self.error.descriptor()
357    }
358
359    /// Returns the stable dotted catalog code.
360    #[must_use]
361    pub fn code(&self) -> ErrorCode {
362        self.error.code()
363    }
364
365    /// Returns the descriptor-owned canonical condition.
366    #[must_use]
367    pub fn condition(&self) -> CanonicalCondition {
368        self.error.condition()
369    }
370
371    /// Returns the descriptor-owned severity.
372    #[must_use]
373    pub fn severity(&self) -> ErrorSeverity {
374        self.error.severity()
375    }
376
377    /// Returns the descriptor-owned recovery hint.
378    #[must_use]
379    pub fn recovery_hint(&self) -> RecoveryHint {
380        self.error.recovery_hint()
381    }
382
383    /// Returns the bounded diagnostic context.
384    #[must_use]
385    pub fn context(&self) -> &ErrorContext {
386        self.error.context()
387    }
388
389    /// Returns the first-promotion caller location.
390    #[must_use]
391    pub fn location(&self) -> &'static Location<'static> {
392        self.error.location()
393    }
394
395    /// Returns the catalog-controlled captured backtrace, when enabled.
396    #[must_use]
397    pub fn backtrace(&self) -> Option<&Backtrace> {
398        self.error.backtrace()
399    }
400
401    /// Creates the descriptor-validated public projection.
402    ///
403    /// # Errors
404    ///
405    /// Returns a schema violation only if the owner-generated context no longer
406    /// matches the selected provider descriptor.
407    pub fn public_view(&self) -> Result<PublicErrorView<'_>, ViewContextViolation> {
408        self.error.public_view()
409    }
410
411    /// Creates the descriptor-validated controlled diagnostic projection.
412    ///
413    /// # Errors
414    ///
415    /// Returns a schema violation only if the owner-generated context no longer
416    /// matches the selected provider descriptor.
417    pub fn diagnostic_view(&self) -> Result<DiagnosticView<'_>, ViewContextViolation> {
418        self.error.diagnostic_view()
419    }
420
421    /// Returns the typed source without formatting it.
422    #[must_use]
423    pub fn source(&self) -> Option<&(dyn StdError + 'static)> {
424        self.error.source()
425    }
426}
427
428fn provider_descriptor(kind: SecurityProviderFailure) -> &'static ErrorDescriptor {
429    match kind {
430        SecurityProviderFailure::NotFound => &rocketmq_error::SECURITY_PROVIDER_NOT_FOUND,
431        SecurityProviderFailure::Conflict => &rocketmq_error::SECURITY_PROVIDER_CONFLICT,
432        SecurityProviderFailure::Unsupported => &rocketmq_error::SECURITY_PROVIDER_UNSUPPORTED,
433        SecurityProviderFailure::InvalidData => &rocketmq_error::SECURITY_PROVIDER_INVALID_DATA,
434        SecurityProviderFailure::ContractViolation => &rocketmq_error::SECURITY_PROVIDER_CONTRACT_VIOLATION,
435        SecurityProviderFailure::Unavailable => &rocketmq_error::SECURITY_PROVIDER_UNAVAILABLE,
436        SecurityProviderFailure::OperationFailed => &rocketmq_error::SECURITY_PROVIDER_OPERATION_FAILED,
437    }
438}
439
440fn provider_context(operation: SecurityOperation, source_present: bool) -> ErrorContext {
441    let mut context = ErrorContext::new().with_text(fields::OPERATION_DIAGNOSTIC, operation.as_str());
442    if source_present {
443        context = context.with_secret_presence(fields::SOURCE_PRESENT);
444    }
445    context
446}
447
448impl From<SecurityContractViolation> for SecurityProviderError {
449    fn from(violation: SecurityContractViolation) -> Self {
450        Self::contract(SecurityOperation::Validate, violation)
451    }
452}
453
454impl fmt::Display for SecurityProviderError {
455    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
456        fmt::Display::fmt(self.error.as_ref(), formatter)
457    }
458}
459
460impl fmt::Debug for SecurityProviderError {
461    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
462        formatter
463            .debug_struct("SecurityProviderError")
464            .field("code", &self.code())
465            .field("condition", &self.condition())
466            .field("kind", &self.kind)
467            .field("operation", &self.operation)
468            .field("source_present", &self.error.source().is_some())
469            .finish()
470    }
471}
472
473impl StdError for SecurityProviderError {
474    fn source(&self) -> Option<&(dyn StdError + 'static)> {
475        self.error.source()
476    }
477}
478
479#[cfg(test)]
480mod tests {
481    use std::error::Error;
482    use std::io;
483    use std::mem::size_of;
484
485    use super::*;
486
487    #[test]
488    fn provider_error_redacts_and_retains_typed_source() {
489        let sentinel = "secret-provider-sentinel";
490        let error = SecurityProviderError::caused_by(
491            SecurityProviderFailure::Unavailable,
492            SecurityOperation::ReadSecret,
493            io::Error::other(sentinel),
494        );
495
496        assert!(!error.to_string().contains(sentinel));
497        assert!(!format!("{error:?}").contains(sentinel));
498        assert_eq!(error.kind(), SecurityProviderFailure::Unavailable);
499        assert_eq!(error.operation(), SecurityOperation::ReadSecret);
500        assert_eq!(error.descriptor(), &rocketmq_error::SECURITY_PROVIDER_UNAVAILABLE);
501        assert_eq!(error.condition(), CanonicalCondition::Unavailable);
502        assert_eq!(error.context().len(), 2);
503        assert!(error.public_view().is_ok());
504        assert!(error.diagnostic_view().is_ok());
505        assert!(Error::source(&error).unwrap().downcast_ref::<io::Error>().is_some());
506    }
507
508    #[test]
509    fn contract_conversion_is_value_free_and_small() {
510        let error = SecurityProviderError::from(SecurityContractViolation::EmptySecretMaterial);
511
512        assert_eq!(error.kind(), SecurityProviderFailure::ContractViolation);
513        assert_eq!(error.operation(), SecurityOperation::Validate);
514        assert_eq!(
515            error.descriptor(),
516            &rocketmq_error::SECURITY_PROVIDER_CONTRACT_VIOLATION
517        );
518        assert!(error
519            .source()
520            .unwrap()
521            .downcast_ref::<SecurityContractViolation>()
522            .is_some());
523        assert!(size_of::<SecurityProviderError>() <= 4 * size_of::<usize>());
524    }
525
526    #[test]
527    fn every_provider_failure_has_one_catalog_identity() {
528        let cases = [
529            (
530                SecurityProviderFailure::NotFound,
531                &rocketmq_error::SECURITY_PROVIDER_NOT_FOUND,
532            ),
533            (
534                SecurityProviderFailure::Conflict,
535                &rocketmq_error::SECURITY_PROVIDER_CONFLICT,
536            ),
537            (
538                SecurityProviderFailure::Unsupported,
539                &rocketmq_error::SECURITY_PROVIDER_UNSUPPORTED,
540            ),
541            (
542                SecurityProviderFailure::InvalidData,
543                &rocketmq_error::SECURITY_PROVIDER_INVALID_DATA,
544            ),
545            (
546                SecurityProviderFailure::ContractViolation,
547                &rocketmq_error::SECURITY_PROVIDER_CONTRACT_VIOLATION,
548            ),
549            (
550                SecurityProviderFailure::Unavailable,
551                &rocketmq_error::SECURITY_PROVIDER_UNAVAILABLE,
552            ),
553            (
554                SecurityProviderFailure::OperationFailed,
555                &rocketmq_error::SECURITY_PROVIDER_OPERATION_FAILED,
556            ),
557        ];
558
559        for (kind, descriptor) in cases {
560            let error = SecurityProviderError::new(kind, SecurityOperation::ReadSecret);
561            assert_eq!(error.descriptor(), descriptor);
562            assert_eq!(error.kind(), kind);
563            assert_eq!(error.context().len(), 1);
564            assert!(error.source().is_none());
565        }
566    }
567}