rocketmq-security-api 1.0.0

Runtime-neutral RocketMQ security contracts
Documentation
// Copyright 2023 The RocketMQ Rust Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! Runtime-neutral contracts for injected secret providers.

use std::fmt;

use cheetah_string::CheetahString;
use zeroize::Zeroize;

use crate::SecurityContractViolation;
use crate::SecurityIdentifierRule;
use crate::SecurityProviderError;

/// Stable identifier for an explicitly registered secret provider.
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct SecretProviderId(CheetahString);

impl SecretProviderId {
    /// Validates and creates a provider identifier.
    ///
    /// # Errors
    ///
    /// Returns [`SecurityContractViolation::InvalidIdentifier`] when the value
    /// violates the closed identifier grammar.
    pub fn new(value: impl Into<CheetahString>) -> Result<Self, SecurityContractViolation> {
        let value = value.into();
        validate_identifier(&value)?;
        Ok(Self(value))
    }

    /// Borrows this value as str.
    pub fn as_str(&self) -> &str {
        &self.0
    }
}

impl fmt::Debug for SecretProviderId {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_tuple("SecretProviderId").field(&self.0).finish()
    }
}

/// Validated logical secret name. The name cannot contain path separators or traversal segments.
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct SecretName(CheetahString);

impl SecretName {
    /// Validates and creates a secret name.
    ///
    /// # Errors
    ///
    /// Returns [`SecurityContractViolation::InvalidIdentifier`] when the value
    /// violates the closed identifier grammar.
    pub fn new(value: impl Into<CheetahString>) -> Result<Self, SecurityContractViolation> {
        let value = value.into();
        validate_identifier(&value)?;
        Ok(Self(value))
    }

    /// Borrows this value as str.
    pub fn as_str(&self) -> &str {
        &self.0
    }
}

impl fmt::Debug for SecretName {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str("SecretName([REDACTED])")
    }
}

fn validate_identifier(value: &str) -> Result<(), SecurityContractViolation> {
    if value.is_empty() {
        return Err(SecurityContractViolation::InvalidIdentifier {
            rule: SecurityIdentifierRule::NotEmpty,
        });
    }
    if value.len() > 128 {
        return Err(SecurityContractViolation::InvalidIdentifier {
            rule: SecurityIdentifierRule::MaximumLength,
        });
    }
    if !value
        .bytes()
        .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.'))
        || value == "."
        || value == ".."
        || value.starts_with('.')
    {
        return Err(SecurityContractViolation::InvalidIdentifier {
            rule: SecurityIdentifierRule::CanonicalCharacters,
        });
    }
    Ok(())
}

/// Sensitive byte material that is redacted and zeroized when dropped.
pub struct SecretMaterial(Vec<u8>);

impl SecretMaterial {
    /// Creates non-empty secret material.
    ///
    /// # Errors
    ///
    /// Returns [`SecurityContractViolation::EmptySecretMaterial`] when the
    /// supplied material is empty.
    pub fn new(value: Vec<u8>) -> Result<Self, SecurityContractViolation> {
        if value.is_empty() {
            return Err(SecurityContractViolation::EmptySecretMaterial);
        }
        Ok(Self(value))
    }

    /// Borrows the material for the shortest practical operation scope.
    pub fn expose_secret(&self) -> &[u8] {
        &self.0
    }

    /// Returns the len.
    pub fn len(&self) -> usize {
        self.0.len()
    }

    /// Returns whether empty.
    pub fn is_empty(&self) -> bool {
        self.0.is_empty()
    }
}

impl fmt::Debug for SecretMaterial {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str("[REDACTED]")
    }
}

impl Zeroize for SecretMaterial {
    fn zeroize(&mut self) {
        self.0.zeroize();
    }
}

impl Drop for SecretMaterial {
    fn drop(&mut self) {
        self.zeroize();
    }
}

/// Version assigned by a provider supporting optimistic updates.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct SecretVersion(u64);

impl SecretVersion {
    /// Creates a new `SecretVersion`.
    pub const fn new(value: u64) -> Self {
        Self(value)
    }

    /// Returns the get.
    pub const fn get(self) -> u64 {
        self.0
    }
}

/// Secret value returned together with its optional provider version.
pub struct VersionedSecret {
    material: SecretMaterial,
    version: Option<SecretVersion>,
}

impl VersionedSecret {
    /// Creates a new `VersionedSecret`.
    pub fn new(material: SecretMaterial, version: Option<SecretVersion>) -> Self {
        Self { material, version }
    }

    /// Returns the material.
    pub fn material(&self) -> &SecretMaterial {
        &self.material
    }

    /// Converts this value into material.
    pub fn into_material(self) -> SecretMaterial {
        self.material
    }

    /// Returns the version.
    pub const fn version(&self) -> Option<SecretVersion> {
        self.version
    }
}

impl fmt::Debug for VersionedSecret {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("VersionedSecret")
            .field("material", &"[REDACTED]")
            .field("version", &self.version)
            .finish()
    }
}

/// Mutation support declared by a provider.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SecretAccess {
    /// Represents the read only case.
    ReadOnly,
    /// Represents the read write case.
    ReadWrite,
}

/// Storage class declared by a provider.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SecretPersistence {
    /// Represents the process environment case.
    ProcessEnvironment,
    /// Represents the encrypted local storage case.
    EncryptedLocalStorage,
    /// Represents the external service case.
    ExternalService,
}

/// Concurrency contract declared by a provider.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SecretVersioning {
    /// Represents the unversioned case.
    Unversioned,
    /// Represents the optimistic case.
    Optimistic,
}

/// Typed capabilities exposed before a provider is selected.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SecretProviderCapabilities {
    access: SecretAccess,
    persistence: SecretPersistence,
    versioning: SecretVersioning,
}

impl SecretProviderCapabilities {
    /// Creates a new `SecretProviderCapabilities`.
    pub const fn new(access: SecretAccess, persistence: SecretPersistence, versioning: SecretVersioning) -> Self {
        Self {
            access,
            persistence,
            versioning,
        }
    }

    /// Returns the access.
    pub const fn access(self) -> SecretAccess {
        self.access
    }

    /// Returns the persistence.
    pub const fn persistence(self) -> SecretPersistence {
        self.persistence
    }

    /// Returns the versioning.
    pub const fn versioning(self) -> SecretVersioning {
        self.versioning
    }
}

/// Synchronous, runtime-neutral boundary implemented by injected providers.
pub trait SecretProvider: Send + Sync {
    /// Returns the id.
    fn id(&self) -> &SecretProviderId;

    /// Returns the capabilities.
    fn capabilities(&self) -> SecretProviderCapabilities;

    /// Reads a secret without exposing provider implementation types.
    ///
    /// # Errors
    ///
    /// Returns a redacted [`SecurityProviderError`] when the material cannot be obtained safely.
    fn read(&self, name: &SecretName) -> Result<VersionedSecret, SecurityProviderError>;

    /// Creates or atomically updates a secret. `None` is create-only; an existing secret requires
    /// its current version to prevent lost updates.
    ///
    /// # Errors
    ///
    /// Returns a redacted [`SecurityProviderError`] when persistence or version validation fails.
    fn write(
        &self,
        name: &SecretName,
        material: SecretMaterial,
        expected_version: Option<SecretVersion>,
    ) -> Result<SecretVersion, SecurityProviderError>;
}

#[cfg(test)]
mod tests {
    use zeroize::Zeroize;

    use super::*;

    #[test]
    fn identifiers_reject_traversal_and_separators() {
        for value in ["", ".", "..", ".hidden", "a/b", "a\\b", "a:b", "a b"] {
            assert!(SecretName::new(value).is_err(), "accepted {value:?}");
        }
        assert_eq!(
            SecretName::new("broker-admin_key.1").unwrap().as_str(),
            "broker-admin_key.1"
        );
    }

    #[test]
    fn material_and_name_debug_are_redacted() {
        let material = SecretMaterial::new(b"visible-only-through-explicit-access".to_vec()).unwrap();
        let name = SecretName::new("cluster-private-key").unwrap();
        assert_eq!(format!("{material:?}"), "[REDACTED]");
        assert_eq!(format!("{name:?}"), "SecretName([REDACTED])");
    }

    #[test]
    fn explicit_zeroize_clears_material() {
        let mut material = SecretMaterial::new(vec![7; 32]).unwrap();
        material.zeroize();
        assert!(material.is_empty());
    }
}