#![deny(missing_docs)]
mod error;
pub mod layered_authorization;
pub mod maintenance;
pub mod resource_pattern;
pub mod resource_type;
pub mod secret_provider;
pub mod secure_deployment;
pub use layered_authorization::combine_layered_authorization;
pub use layered_authorization::AuthorizationDecision;
pub use layered_authorization::AuthorizationDenial;
pub use layered_authorization::DetailedDecision;
pub use layered_authorization::IngressDecision;
pub use layered_authorization::IngressPolicy;
pub use layered_authorization::LayerEvaluation;
pub use layered_authorization::LayerFailureKind;
pub use layered_authorization::LayerRequirement;
pub use maintenance::MaintenanceAuthorizationContext;
pub use maintenance::MaintenanceAuthorizationDenial;
pub use maintenance::MaintenanceAuthorizationGrant;
pub use maintenance::MaintenanceAuthorizer;
pub use maintenance::MaintenanceCapability;
pub use maintenance::MaintenancePolicy;
pub use maintenance::MaintenancePrincipalBinding;
pub use maintenance::MaintenanceRequestClass;
pub use maintenance::MaintenanceResourceBudget;
pub use maintenance::MaintenanceRole;
pub use maintenance::MaintenanceRoleGrant;
pub use maintenance::ValidatedMaintenancePolicy;
pub use maintenance::MAINTENANCE_POLICY_SCHEMA_VERSION;
pub use resource_pattern::ResourcePattern;
pub use resource_type::ResourceType;
pub use error::MaintenancePolicyRule;
pub use error::SecurityContractViolation;
pub use error::SecurityIdentifierRule;
pub use error::SecurityOperation;
pub use error::SecurityProviderError;
pub use error::SecurityProviderFailure;
pub use secret_provider::SecretAccess;
pub use secret_provider::SecretMaterial;
pub use secret_provider::SecretName;
pub use secret_provider::SecretPersistence;
pub use secret_provider::SecretProvider;
pub use secret_provider::SecretProviderCapabilities;
pub use secret_provider::SecretProviderId;
pub use secret_provider::SecretVersion;
pub use secret_provider::SecretVersioning;
pub use secret_provider::VersionedSecret;
pub use secure_deployment::validate_deployment_security;
pub use secure_deployment::validate_security_bootstrap_from_env;
pub use secure_deployment::BootstrapReadinessView;
pub use secure_deployment::DeploymentOrigin;
pub use secure_deployment::DeploymentSecurityConfigView;
pub use secure_deployment::DeploymentSecurityFailure;
pub use secure_deployment::DeploymentSecurityReport;
pub use secure_deployment::SecurityBootstrap;
pub use secure_deployment::SecurityBootstrapConfig;
pub use secure_deployment::SecurityBootstrapMaterial;
pub use secure_deployment::SecurityBootstrapOutcome;
pub use secure_deployment::SecurityBootstrapProfile;
pub use secure_deployment::SecurityMigrationStatus;
pub use secure_deployment::SecurityProfileResolution;
pub use secure_deployment::SecurityProfileSelection;
pub use secure_deployment::ValidatedSecurityBootstrap;
pub use secure_deployment::MOUNTED_FILES_SECRET_PROVIDER;
pub use secure_deployment::SECURITY_ADMIN_IDENTITY_ENV;
pub use secure_deployment::SECURITY_PROFILE_ENV;
pub use secure_deployment::SECURITY_REQUEST_POLICY_ENV;
pub use secure_deployment::SECURITY_SECRET_PROVIDER_ENV;
pub use secure_deployment::SECURITY_TLS_CERT_ENV;
pub use secure_deployment::SECURITY_TLS_KEY_ENV;
pub use secure_deployment::SECURITY_TRUST_ANCHOR_ENV;
use std::collections::HashMap;
use std::fmt;
use std::fs::File;
use std::io::Read;
use std::net::SocketAddr;
use std::path::Path;
use std::str::FromStr;
use std::time::SystemTime;
use cheetah_string::CheetahString;
#[derive(Clone, PartialEq, Eq)]
pub struct PeerInfo {
address: SocketAddr,
tls: bool,
certificate_subject: Option<CheetahString>,
}
impl PeerInfo {
pub fn new(address: SocketAddr, tls: bool) -> Self {
Self {
address,
tls,
certificate_subject: None,
}
}
pub fn with_certificate_subject(mut self, subject: impl Into<CheetahString>) -> Self {
self.certificate_subject = Some(subject.into());
self
}
pub fn address(&self) -> SocketAddr {
self.address
}
pub fn is_tls(&self) -> bool {
self.tls
}
pub fn certificate_subject(&self) -> Option<&str> {
self.certificate_subject.as_deref()
}
}
impl fmt::Debug for PeerInfo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("PeerInfo")
.field("address", &self.address)
.field("tls", &self.tls)
.field(
"certificate_subject",
&self.certificate_subject.as_ref().map(|_| "[REDACTED]"),
)
.finish()
}
}
#[derive(Clone, Copy)]
pub struct SecurityRequestView<'a> {
code: i32,
version: i32,
fields: &'a HashMap<CheetahString, CheetahString>,
body: Option<&'a [u8]>,
peer: Option<&'a PeerInfo>,
}
impl<'a> SecurityRequestView<'a> {
pub fn new(
code: i32,
version: i32,
fields: &'a HashMap<CheetahString, CheetahString>,
body: Option<&'a [u8]>,
peer: Option<&'a PeerInfo>,
) -> Self {
Self {
code,
version,
fields,
body,
peer,
}
}
pub fn code(&self) -> i32 {
self.code
}
pub fn version(&self) -> i32 {
self.version
}
pub fn fields(&self) -> &'a HashMap<CheetahString, CheetahString> {
self.fields
}
pub fn body(&self) -> Option<&'a [u8]> {
self.body
}
pub fn peer(&self) -> Option<&'a PeerInfo> {
self.peer
}
}
impl fmt::Debug for SecurityRequestView<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SecurityRequestView")
.field("code", &self.code)
.field("version", &self.version)
.field("field_count", &self.fields.len())
.field("body_len", &self.body.map(<[u8]>::len))
.field("peer", &self.peer)
.finish()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Principal {
id: CheetahString,
}
impl Principal {
pub fn new(id: impl Into<CheetahString>) -> Self {
Self { id: id.into() }
}
pub fn id(&self) -> &str {
&self.id
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ResourceKind {
Topic,
ConsumerGroup,
Cluster,
Broker,
Other,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Resource {
kind: ResourceKind,
name: CheetahString,
}
impl Resource {
pub fn new(kind: ResourceKind, name: impl Into<CheetahString>) -> Self {
Self {
kind,
name: name.into(),
}
}
pub fn topic(name: impl Into<CheetahString>) -> Self {
Self::new(ResourceKind::Topic, name)
}
pub fn kind(&self) -> ResourceKind {
self.kind
}
pub fn name(&self) -> &str {
&self.name
}
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Action {
Unknown = 0,
All = 1,
Any = 2,
Pub = 3,
Sub = 4,
Create = 5,
Update = 6,
Delete = 7,
Get = 8,
List = 9,
Publish = 10,
Subscribe = 11,
Describe = 12,
Manage = 13,
}
impl Action {
pub const fn code(self) -> u8 {
self as u8
}
pub const fn name(self) -> &'static str {
match self {
Self::Unknown => "Unknown",
Self::All => "All",
Self::Any => "Any",
Self::Pub => "Pub",
Self::Sub => "Sub",
Self::Create => "Create",
Self::Update => "Update",
Self::Delete => "Delete",
Self::Get => "Get",
Self::List => "List",
Self::Publish => "Publish",
Self::Subscribe => "Subscribe",
Self::Describe => "Describe",
Self::Manage => "Manage",
}
}
pub fn get_by_name(name: &str) -> Option<Self> {
match name.trim().to_ascii_lowercase().as_str() {
"unknown" => Some(Self::Unknown),
"all" => Some(Self::All),
"any" => Some(Self::Any),
"pub" => Some(Self::Pub),
"sub" => Some(Self::Sub),
"create" => Some(Self::Create),
"update" => Some(Self::Update),
"delete" => Some(Self::Delete),
"get" => Some(Self::Get),
"list" => Some(Self::List),
"publish" => Some(Self::Publish),
"subscribe" => Some(Self::Subscribe),
"describe" => Some(Self::Describe),
"manage" => Some(Self::Manage),
_ => None,
}
}
}
impl TryFrom<u8> for Action {
type Error = ();
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
0 => Ok(Self::Unknown),
1 => Ok(Self::All),
2 => Ok(Self::Any),
3 => Ok(Self::Pub),
4 => Ok(Self::Sub),
5 => Ok(Self::Create),
6 => Ok(Self::Update),
7 => Ok(Self::Delete),
8 => Ok(Self::Get),
9 => Ok(Self::List),
10 => Ok(Self::Publish),
11 => Ok(Self::Subscribe),
12 => Ok(Self::Describe),
13 => Ok(Self::Manage),
_ => Err(()),
}
}
}
impl fmt::Display for Action {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.name())
}
}
pub struct RequestContext<'a> {
request: SecurityRequestView<'a>,
principal: Option<&'a Principal>,
resource: Resource,
action: Action,
}
impl<'a> RequestContext<'a> {
pub fn new(
request: SecurityRequestView<'a>,
principal: Option<&'a Principal>,
resource: Resource,
action: Action,
) -> Self {
Self {
request,
principal,
resource,
action,
}
}
pub fn principal(&self) -> Option<&Principal> {
self.principal
}
}
#[derive(Clone, Copy)]
pub struct AuthenticatedRequestContext<'a> {
request: SecurityRequestView<'a>,
principal: &'a Principal,
resource: &'a Resource,
action: Action,
}
impl<'a> AuthenticatedRequestContext<'a> {
pub fn request(&self) -> SecurityRequestView<'a> {
self.request
}
pub fn principal(&self) -> &'a Principal {
self.principal
}
pub fn resource(&self) -> &'a Resource {
self.resource
}
pub fn action(&self) -> Action {
self.action
}
}
pub trait RequestPolicy: Send + Sync {
fn evaluate_authenticated(&self, context: AuthenticatedRequestContext<'_>) -> AuthorizationDecision;
}
pub fn evaluate_request(policy: &dyn RequestPolicy, context: &RequestContext<'_>) -> AuthorizationDecision {
let Some(principal) = context.principal else {
return AuthorizationDecision::Deny(AuthorizationDenial::SubjectUnknown);
};
policy.evaluate_authenticated(AuthenticatedRequestContext {
request: context.request,
principal,
resource: &context.resource,
action: context.action,
})
}
pub struct Signature {
fields: Vec<(CheetahString, Secret<CheetahString>)>,
}
impl Signature {
pub fn new(fields: Vec<(CheetahString, Secret<CheetahString>)>) -> Self {
Self { fields }
}
pub fn fields(&self) -> &[(CheetahString, Secret<CheetahString>)] {
&self.fields
}
}
impl fmt::Debug for Signature {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Signature")
.field("field_count", &self.fields.len())
.finish()
}
}
pub trait OutboundSigner: Send + Sync {
fn sign(&self, request: SecurityRequestView<'_>) -> Result<Signature, SecurityProviderError>;
}
pub struct Secret<T>(T);
impl<T> Secret<T> {
pub fn new(value: T) -> Self {
Self(value)
}
pub fn expose_secret(&self) -> &T {
&self.0
}
pub fn into_inner(self) -> T {
self.0
}
}
impl<T> fmt::Debug for Secret<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("[REDACTED]")
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DeploymentProfile {
Development,
Compatibility,
Secure,
}
impl FromStr for DeploymentProfile {
type Err = SecurityContractViolation;
fn from_str(value: &str) -> Result<Self, Self::Err> {
match value.to_ascii_lowercase().as_str() {
"development" => Ok(Self::Development),
"compatibility" => Ok(Self::Compatibility),
"secure" => Ok(Self::Secure),
_ => Err(SecurityContractViolation::UnknownSecurityProfile),
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct SecurityConfigView<'a> {
profile: &'a str,
trust_anchor: Option<&'a Path>,
secret_file: Option<&'a Path>,
bootstrap_expires_at: Option<SystemTime>,
insecure_downgrade: bool,
}
impl<'a> SecurityConfigView<'a> {
pub fn new(profile: &'a str) -> Self {
Self {
profile,
trust_anchor: None,
secret_file: None,
bootstrap_expires_at: None,
insecure_downgrade: false,
}
}
pub fn with_trust_anchor(mut self, path: &'a Path) -> Self {
self.trust_anchor = Some(path);
self
}
pub fn with_secret_file(mut self, path: &'a Path) -> Self {
self.secret_file = Some(path);
self
}
pub fn with_bootstrap_expiry(mut self, expires_at: SystemTime) -> Self {
self.bootstrap_expires_at = Some(expires_at);
self
}
pub fn with_insecure_downgrade(mut self, enabled: bool) -> Self {
self.insecure_downgrade = enabled;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SecurityReadinessFailure {
UnknownProfile,
MissingTrustAnchor,
TrustAnchorUnavailable,
TrustAnchorNotRegularFile,
MissingSecretFile,
SecretFileUnavailable,
SecretFileNotRegularFile,
InsecureSecretFilePermissions,
MissingBootstrapExpiry,
ExpiredBootstrap,
InsecureDowngrade,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SecurityReadinessReport {
profile: Option<DeploymentProfile>,
failures: Vec<SecurityReadinessFailure>,
}
impl SecurityReadinessReport {
pub fn is_ready(&self) -> bool {
self.failures.is_empty()
}
pub fn profile(&self) -> Option<DeploymentProfile> {
self.profile
}
pub fn failures(&self) -> &[SecurityReadinessFailure] {
&self.failures
}
}
pub fn validate_security_config(view: SecurityConfigView<'_>, now: SystemTime) -> SecurityReadinessReport {
let Ok(profile) = DeploymentProfile::from_str(view.profile) else {
return SecurityReadinessReport {
profile: None,
failures: vec![SecurityReadinessFailure::UnknownProfile],
};
};
let mut failures = Vec::new();
if profile == DeploymentProfile::Secure {
match view.trust_anchor {
None => failures.push(SecurityReadinessFailure::MissingTrustAnchor),
Some(path) => inspect_readable_file(
path,
SecurityReadinessFailure::TrustAnchorUnavailable,
SecurityReadinessFailure::TrustAnchorNotRegularFile,
false,
&mut failures,
),
}
match view.secret_file {
None => failures.push(SecurityReadinessFailure::MissingSecretFile),
Some(path) => inspect_readable_file(
path,
SecurityReadinessFailure::SecretFileUnavailable,
SecurityReadinessFailure::SecretFileNotRegularFile,
true,
&mut failures,
),
}
match view.bootstrap_expires_at {
None => failures.push(SecurityReadinessFailure::MissingBootstrapExpiry),
Some(expiry) if expiry <= now => failures.push(SecurityReadinessFailure::ExpiredBootstrap),
Some(_) => {}
}
if view.insecure_downgrade {
failures.push(SecurityReadinessFailure::InsecureDowngrade);
}
}
SecurityReadinessReport {
profile: Some(profile),
failures,
}
}
fn inspect_readable_file(
path: &Path,
unavailable: SecurityReadinessFailure,
not_regular_file: SecurityReadinessFailure,
require_owner_only_permissions: bool,
failures: &mut Vec<SecurityReadinessFailure>,
) {
let Ok(path_metadata) = path.metadata() else {
failures.push(unavailable);
return;
};
if !path_metadata.is_file() {
failures.push(not_regular_file);
return;
}
let Ok(file) = File::open(path) else {
failures.push(unavailable);
return;
};
let Ok(metadata) = file.metadata() else {
failures.push(unavailable);
return;
};
if !metadata.is_file() {
failures.push(not_regular_file);
return;
}
if require_owner_only_permissions && !platform_secret_file_permissions(&metadata).is_owner_only() {
failures.push(SecurityReadinessFailure::InsecureSecretFilePermissions);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SecretFilePermissions {
pub group_access: bool,
pub other_access: bool,
}
impl SecretFilePermissions {
pub fn is_owner_only(self) -> bool {
!self.group_access && !self.other_access
}
}
pub fn read_secret_file(path: &Path) -> Result<Secret<Vec<u8>>, SecurityProviderError> {
let mut file = File::open(path).map_err(|source| {
SecurityProviderError::caused_by(
SecurityProviderFailure::Unavailable,
SecurityOperation::ReadSecret,
source,
)
})?;
let metadata = file.metadata().map_err(|source| {
SecurityProviderError::caused_by(
SecurityProviderFailure::OperationFailed,
SecurityOperation::InspectSecret,
source,
)
})?;
if !platform_secret_file_permissions(&metadata).is_owner_only() {
return Err(SecurityProviderError::contract(
SecurityOperation::InspectSecret,
SecurityContractViolation::SecretStoragePermissionsInsecure,
));
}
let mut value = Vec::new();
file.read_to_end(&mut value).map_err(|source| {
SecurityProviderError::caused_by(
SecurityProviderFailure::OperationFailed,
SecurityOperation::ReadSecret,
source,
)
})?;
Ok(Secret::new(value))
}
#[cfg(unix)]
fn platform_secret_file_permissions(metadata: &std::fs::Metadata) -> SecretFilePermissions {
use std::os::unix::fs::PermissionsExt;
let mode = metadata.permissions().mode();
SecretFilePermissions {
group_access: mode & 0o070 != 0,
other_access: mode & 0o007 != 0,
}
}
#[cfg(windows)]
fn platform_secret_file_permissions(_metadata: &std::fs::Metadata) -> SecretFilePermissions {
SecretFilePermissions {
group_access: true,
other_access: true,
}
}