1#![deny(missing_docs)]
16
17mod error;
20pub mod layered_authorization;
22pub mod maintenance;
23pub mod resource_pattern;
25pub mod resource_type;
27pub mod secret_provider;
28pub mod secure_deployment;
29
30pub use layered_authorization::combine_layered_authorization;
31pub use layered_authorization::AuthorizationDecision;
32pub use layered_authorization::AuthorizationDenial;
33pub use layered_authorization::DetailedDecision;
34pub use layered_authorization::IngressDecision;
35pub use layered_authorization::IngressPolicy;
36pub use layered_authorization::LayerEvaluation;
37pub use layered_authorization::LayerFailureKind;
38pub use layered_authorization::LayerRequirement;
39pub use maintenance::MaintenanceAuthorizationContext;
40pub use maintenance::MaintenanceAuthorizationDenial;
41pub use maintenance::MaintenanceAuthorizationGrant;
42pub use maintenance::MaintenanceAuthorizer;
43pub use maintenance::MaintenanceCapability;
44pub use maintenance::MaintenancePolicy;
45pub use maintenance::MaintenancePrincipalBinding;
46pub use maintenance::MaintenanceRequestClass;
47pub use maintenance::MaintenanceResourceBudget;
48pub use maintenance::MaintenanceRole;
49pub use maintenance::MaintenanceRoleGrant;
50pub use maintenance::ValidatedMaintenancePolicy;
51pub use maintenance::MAINTENANCE_POLICY_SCHEMA_VERSION;
52pub use resource_pattern::ResourcePattern;
53pub use resource_type::ResourceType;
54
55pub use error::MaintenancePolicyRule;
56pub use error::SecurityContractViolation;
57pub use error::SecurityIdentifierRule;
58pub use error::SecurityOperation;
59pub use error::SecurityProviderError;
60pub use error::SecurityProviderFailure;
61
62pub use secret_provider::SecretAccess;
63pub use secret_provider::SecretMaterial;
64pub use secret_provider::SecretName;
65pub use secret_provider::SecretPersistence;
66pub use secret_provider::SecretProvider;
67pub use secret_provider::SecretProviderCapabilities;
68pub use secret_provider::SecretProviderId;
69pub use secret_provider::SecretVersion;
70pub use secret_provider::SecretVersioning;
71pub use secret_provider::VersionedSecret;
72pub use secure_deployment::validate_deployment_security;
73pub use secure_deployment::validate_security_bootstrap_from_env;
74pub use secure_deployment::BootstrapReadinessView;
75pub use secure_deployment::DeploymentOrigin;
76pub use secure_deployment::DeploymentSecurityConfigView;
77pub use secure_deployment::DeploymentSecurityFailure;
78pub use secure_deployment::DeploymentSecurityReport;
79pub use secure_deployment::SecurityBootstrap;
80pub use secure_deployment::SecurityBootstrapConfig;
81pub use secure_deployment::SecurityBootstrapMaterial;
82pub use secure_deployment::SecurityBootstrapOutcome;
83pub use secure_deployment::SecurityBootstrapProfile;
84pub use secure_deployment::SecurityMigrationStatus;
85pub use secure_deployment::SecurityProfileResolution;
86pub use secure_deployment::SecurityProfileSelection;
87pub use secure_deployment::ValidatedSecurityBootstrap;
88pub use secure_deployment::MOUNTED_FILES_SECRET_PROVIDER;
89pub use secure_deployment::SECURITY_ADMIN_IDENTITY_ENV;
90pub use secure_deployment::SECURITY_PROFILE_ENV;
91pub use secure_deployment::SECURITY_REQUEST_POLICY_ENV;
92pub use secure_deployment::SECURITY_SECRET_PROVIDER_ENV;
93pub use secure_deployment::SECURITY_TLS_CERT_ENV;
94pub use secure_deployment::SECURITY_TLS_KEY_ENV;
95pub use secure_deployment::SECURITY_TRUST_ANCHOR_ENV;
96
97use std::collections::HashMap;
98use std::fmt;
99use std::fs::File;
100use std::io::Read;
101use std::net::SocketAddr;
102use std::path::Path;
103use std::str::FromStr;
104use std::time::SystemTime;
105
106use cheetah_string::CheetahString;
107
108#[derive(Clone, PartialEq, Eq)]
110pub struct PeerInfo {
111 address: SocketAddr,
112 tls: bool,
113 certificate_subject: Option<CheetahString>,
114}
115
116impl PeerInfo {
117 pub fn new(address: SocketAddr, tls: bool) -> Self {
119 Self {
120 address,
121 tls,
122 certificate_subject: None,
123 }
124 }
125
126 pub fn with_certificate_subject(mut self, subject: impl Into<CheetahString>) -> Self {
128 self.certificate_subject = Some(subject.into());
129 self
130 }
131
132 pub fn address(&self) -> SocketAddr {
134 self.address
135 }
136
137 pub fn is_tls(&self) -> bool {
139 self.tls
140 }
141
142 pub fn certificate_subject(&self) -> Option<&str> {
144 self.certificate_subject.as_deref()
145 }
146}
147
148impl fmt::Debug for PeerInfo {
149 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150 f.debug_struct("PeerInfo")
151 .field("address", &self.address)
152 .field("tls", &self.tls)
153 .field(
154 "certificate_subject",
155 &self.certificate_subject.as_ref().map(|_| "[REDACTED]"),
156 )
157 .finish()
158 }
159}
160
161#[derive(Clone, Copy)]
163pub struct SecurityRequestView<'a> {
164 code: i32,
165 version: i32,
166 fields: &'a HashMap<CheetahString, CheetahString>,
167 body: Option<&'a [u8]>,
168 peer: Option<&'a PeerInfo>,
169}
170
171impl<'a> SecurityRequestView<'a> {
172 pub fn new(
174 code: i32,
175 version: i32,
176 fields: &'a HashMap<CheetahString, CheetahString>,
177 body: Option<&'a [u8]>,
178 peer: Option<&'a PeerInfo>,
179 ) -> Self {
180 Self {
181 code,
182 version,
183 fields,
184 body,
185 peer,
186 }
187 }
188
189 pub fn code(&self) -> i32 {
191 self.code
192 }
193
194 pub fn version(&self) -> i32 {
196 self.version
197 }
198
199 pub fn fields(&self) -> &'a HashMap<CheetahString, CheetahString> {
201 self.fields
202 }
203
204 pub fn body(&self) -> Option<&'a [u8]> {
206 self.body
207 }
208
209 pub fn peer(&self) -> Option<&'a PeerInfo> {
211 self.peer
212 }
213}
214
215impl fmt::Debug for SecurityRequestView<'_> {
216 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
217 f.debug_struct("SecurityRequestView")
218 .field("code", &self.code)
219 .field("version", &self.version)
220 .field("field_count", &self.fields.len())
221 .field("body_len", &self.body.map(<[u8]>::len))
222 .field("peer", &self.peer)
223 .finish()
224 }
225}
226
227#[derive(Debug, Clone, PartialEq, Eq, Hash)]
229pub struct Principal {
230 id: CheetahString,
231}
232
233impl Principal {
234 pub fn new(id: impl Into<CheetahString>) -> Self {
236 Self { id: id.into() }
237 }
238
239 pub fn id(&self) -> &str {
241 &self.id
242 }
243}
244
245#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
247pub enum ResourceKind {
248 Topic,
250 ConsumerGroup,
252 Cluster,
254 Broker,
256 Other,
258}
259
260#[derive(Debug, Clone, PartialEq, Eq, Hash)]
262pub struct Resource {
263 kind: ResourceKind,
264 name: CheetahString,
265}
266
267impl Resource {
268 pub fn new(kind: ResourceKind, name: impl Into<CheetahString>) -> Self {
270 Self {
271 kind,
272 name: name.into(),
273 }
274 }
275
276 pub fn topic(name: impl Into<CheetahString>) -> Self {
278 Self::new(ResourceKind::Topic, name)
279 }
280
281 pub fn kind(&self) -> ResourceKind {
283 self.kind
284 }
285
286 pub fn name(&self) -> &str {
288 &self.name
289 }
290}
291
292#[repr(u8)]
298#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
299pub enum Action {
300 Unknown = 0,
302 All = 1,
304 Any = 2,
306 Pub = 3,
308 Sub = 4,
310 Create = 5,
312 Update = 6,
314 Delete = 7,
316 Get = 8,
318 List = 9,
320 Publish = 10,
322 Subscribe = 11,
324 Describe = 12,
326 Manage = 13,
328}
329
330impl Action {
331 pub const fn code(self) -> u8 {
333 self as u8
334 }
335
336 pub const fn name(self) -> &'static str {
338 match self {
339 Self::Unknown => "Unknown",
340 Self::All => "All",
341 Self::Any => "Any",
342 Self::Pub => "Pub",
343 Self::Sub => "Sub",
344 Self::Create => "Create",
345 Self::Update => "Update",
346 Self::Delete => "Delete",
347 Self::Get => "Get",
348 Self::List => "List",
349 Self::Publish => "Publish",
350 Self::Subscribe => "Subscribe",
351 Self::Describe => "Describe",
352 Self::Manage => "Manage",
353 }
354 }
355
356 pub fn get_by_name(name: &str) -> Option<Self> {
358 match name.trim().to_ascii_lowercase().as_str() {
359 "unknown" => Some(Self::Unknown),
360 "all" => Some(Self::All),
361 "any" => Some(Self::Any),
362 "pub" => Some(Self::Pub),
363 "sub" => Some(Self::Sub),
364 "create" => Some(Self::Create),
365 "update" => Some(Self::Update),
366 "delete" => Some(Self::Delete),
367 "get" => Some(Self::Get),
368 "list" => Some(Self::List),
369 "publish" => Some(Self::Publish),
370 "subscribe" => Some(Self::Subscribe),
371 "describe" => Some(Self::Describe),
372 "manage" => Some(Self::Manage),
373 _ => None,
374 }
375 }
376}
377
378impl TryFrom<u8> for Action {
379 type Error = ();
380
381 fn try_from(value: u8) -> Result<Self, Self::Error> {
382 match value {
383 0 => Ok(Self::Unknown),
384 1 => Ok(Self::All),
385 2 => Ok(Self::Any),
386 3 => Ok(Self::Pub),
387 4 => Ok(Self::Sub),
388 5 => Ok(Self::Create),
389 6 => Ok(Self::Update),
390 7 => Ok(Self::Delete),
391 8 => Ok(Self::Get),
392 9 => Ok(Self::List),
393 10 => Ok(Self::Publish),
394 11 => Ok(Self::Subscribe),
395 12 => Ok(Self::Describe),
396 13 => Ok(Self::Manage),
397 _ => Err(()),
398 }
399 }
400}
401
402impl fmt::Display for Action {
403 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
404 formatter.write_str(self.name())
405 }
406}
407
408pub struct RequestContext<'a> {
410 request: SecurityRequestView<'a>,
411 principal: Option<&'a Principal>,
412 resource: Resource,
413 action: Action,
414}
415
416impl<'a> RequestContext<'a> {
417 pub fn new(
419 request: SecurityRequestView<'a>,
420 principal: Option<&'a Principal>,
421 resource: Resource,
422 action: Action,
423 ) -> Self {
424 Self {
425 request,
426 principal,
427 resource,
428 action,
429 }
430 }
431
432 pub fn principal(&self) -> Option<&Principal> {
434 self.principal
435 }
436}
437
438#[derive(Clone, Copy)]
440pub struct AuthenticatedRequestContext<'a> {
441 request: SecurityRequestView<'a>,
442 principal: &'a Principal,
443 resource: &'a Resource,
444 action: Action,
445}
446
447impl<'a> AuthenticatedRequestContext<'a> {
448 pub fn request(&self) -> SecurityRequestView<'a> {
450 self.request
451 }
452
453 pub fn principal(&self) -> &'a Principal {
455 self.principal
456 }
457
458 pub fn resource(&self) -> &'a Resource {
460 self.resource
461 }
462
463 pub fn action(&self) -> Action {
465 self.action
466 }
467}
468
469pub trait RequestPolicy: Send + Sync {
471 fn evaluate_authenticated(&self, context: AuthenticatedRequestContext<'_>) -> AuthorizationDecision;
473}
474
475pub fn evaluate_request(policy: &dyn RequestPolicy, context: &RequestContext<'_>) -> AuthorizationDecision {
480 let Some(principal) = context.principal else {
481 return AuthorizationDecision::Deny(AuthorizationDenial::SubjectUnknown);
482 };
483 policy.evaluate_authenticated(AuthenticatedRequestContext {
484 request: context.request,
485 principal,
486 resource: &context.resource,
487 action: context.action,
488 })
489}
490
491pub struct Signature {
493 fields: Vec<(CheetahString, Secret<CheetahString>)>,
494}
495
496impl Signature {
497 pub fn new(fields: Vec<(CheetahString, Secret<CheetahString>)>) -> Self {
499 Self { fields }
500 }
501
502 pub fn fields(&self) -> &[(CheetahString, Secret<CheetahString>)] {
504 &self.fields
505 }
506}
507
508impl fmt::Debug for Signature {
509 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
510 f.debug_struct("Signature")
511 .field("field_count", &self.fields.len())
512 .finish()
513 }
514}
515
516pub trait OutboundSigner: Send + Sync {
518 fn sign(&self, request: SecurityRequestView<'_>) -> Result<Signature, SecurityProviderError>;
525}
526
527pub struct Secret<T>(T);
529
530impl<T> Secret<T> {
531 pub fn new(value: T) -> Self {
533 Self(value)
534 }
535
536 pub fn expose_secret(&self) -> &T {
538 &self.0
539 }
540
541 pub fn into_inner(self) -> T {
543 self.0
544 }
545}
546
547impl<T> fmt::Debug for Secret<T> {
548 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
549 f.write_str("[REDACTED]")
550 }
551}
552
553#[derive(Debug, Clone, Copy, PartialEq, Eq)]
555pub enum DeploymentProfile {
556 Development,
558 Compatibility,
560 Secure,
562}
563
564impl FromStr for DeploymentProfile {
565 type Err = SecurityContractViolation;
566
567 fn from_str(value: &str) -> Result<Self, Self::Err> {
568 match value.to_ascii_lowercase().as_str() {
569 "development" => Ok(Self::Development),
570 "compatibility" => Ok(Self::Compatibility),
571 "secure" => Ok(Self::Secure),
572 _ => Err(SecurityContractViolation::UnknownSecurityProfile),
573 }
574 }
575}
576
577#[derive(Debug, Clone, Copy)]
579pub struct SecurityConfigView<'a> {
580 profile: &'a str,
581 trust_anchor: Option<&'a Path>,
582 secret_file: Option<&'a Path>,
583 bootstrap_expires_at: Option<SystemTime>,
584 insecure_downgrade: bool,
585}
586
587impl<'a> SecurityConfigView<'a> {
588 pub fn new(profile: &'a str) -> Self {
590 Self {
591 profile,
592 trust_anchor: None,
593 secret_file: None,
594 bootstrap_expires_at: None,
595 insecure_downgrade: false,
596 }
597 }
598
599 pub fn with_trust_anchor(mut self, path: &'a Path) -> Self {
601 self.trust_anchor = Some(path);
602 self
603 }
604
605 pub fn with_secret_file(mut self, path: &'a Path) -> Self {
607 self.secret_file = Some(path);
608 self
609 }
610
611 pub fn with_bootstrap_expiry(mut self, expires_at: SystemTime) -> Self {
613 self.bootstrap_expires_at = Some(expires_at);
614 self
615 }
616
617 pub fn with_insecure_downgrade(mut self, enabled: bool) -> Self {
619 self.insecure_downgrade = enabled;
620 self
621 }
622}
623
624#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
626pub enum SecurityReadinessFailure {
627 UnknownProfile,
629 MissingTrustAnchor,
631 TrustAnchorUnavailable,
633 TrustAnchorNotRegularFile,
635 MissingSecretFile,
637 SecretFileUnavailable,
639 SecretFileNotRegularFile,
641 InsecureSecretFilePermissions,
643 MissingBootstrapExpiry,
645 ExpiredBootstrap,
647 InsecureDowngrade,
649}
650
651#[derive(Debug, Clone, PartialEq, Eq)]
653pub struct SecurityReadinessReport {
654 profile: Option<DeploymentProfile>,
655 failures: Vec<SecurityReadinessFailure>,
656}
657
658impl SecurityReadinessReport {
659 pub fn is_ready(&self) -> bool {
661 self.failures.is_empty()
662 }
663
664 pub fn profile(&self) -> Option<DeploymentProfile> {
666 self.profile
667 }
668
669 pub fn failures(&self) -> &[SecurityReadinessFailure] {
671 &self.failures
672 }
673}
674
675pub fn validate_security_config(view: SecurityConfigView<'_>, now: SystemTime) -> SecurityReadinessReport {
677 let Ok(profile) = DeploymentProfile::from_str(view.profile) else {
678 return SecurityReadinessReport {
679 profile: None,
680 failures: vec![SecurityReadinessFailure::UnknownProfile],
681 };
682 };
683 let mut failures = Vec::new();
684 if profile == DeploymentProfile::Secure {
685 match view.trust_anchor {
686 None => failures.push(SecurityReadinessFailure::MissingTrustAnchor),
687 Some(path) => inspect_readable_file(
688 path,
689 SecurityReadinessFailure::TrustAnchorUnavailable,
690 SecurityReadinessFailure::TrustAnchorNotRegularFile,
691 false,
692 &mut failures,
693 ),
694 }
695 match view.secret_file {
696 None => failures.push(SecurityReadinessFailure::MissingSecretFile),
697 Some(path) => inspect_readable_file(
698 path,
699 SecurityReadinessFailure::SecretFileUnavailable,
700 SecurityReadinessFailure::SecretFileNotRegularFile,
701 true,
702 &mut failures,
703 ),
704 }
705 match view.bootstrap_expires_at {
706 None => failures.push(SecurityReadinessFailure::MissingBootstrapExpiry),
707 Some(expiry) if expiry <= now => failures.push(SecurityReadinessFailure::ExpiredBootstrap),
708 Some(_) => {}
709 }
710 if view.insecure_downgrade {
711 failures.push(SecurityReadinessFailure::InsecureDowngrade);
712 }
713 }
714 SecurityReadinessReport {
715 profile: Some(profile),
716 failures,
717 }
718}
719
720fn inspect_readable_file(
721 path: &Path,
722 unavailable: SecurityReadinessFailure,
723 not_regular_file: SecurityReadinessFailure,
724 require_owner_only_permissions: bool,
725 failures: &mut Vec<SecurityReadinessFailure>,
726) {
727 let Ok(path_metadata) = path.metadata() else {
728 failures.push(unavailable);
729 return;
730 };
731 if !path_metadata.is_file() {
732 failures.push(not_regular_file);
733 return;
734 }
735 let Ok(file) = File::open(path) else {
736 failures.push(unavailable);
737 return;
738 };
739 let Ok(metadata) = file.metadata() else {
740 failures.push(unavailable);
741 return;
742 };
743 if !metadata.is_file() {
744 failures.push(not_regular_file);
745 return;
746 }
747 if require_owner_only_permissions && !platform_secret_file_permissions(&metadata).is_owner_only() {
748 failures.push(SecurityReadinessFailure::InsecureSecretFilePermissions);
749 }
750}
751
752#[derive(Debug, Clone, Copy, PartialEq, Eq)]
754pub struct SecretFilePermissions {
755 pub group_access: bool,
757 pub other_access: bool,
759}
760
761impl SecretFilePermissions {
762 pub fn is_owner_only(self) -> bool {
764 !self.group_access && !self.other_access
765 }
766}
767
768pub fn read_secret_file(path: &Path) -> Result<Secret<Vec<u8>>, SecurityProviderError> {
775 let mut file = File::open(path).map_err(|source| {
776 SecurityProviderError::caused_by(
777 SecurityProviderFailure::Unavailable,
778 SecurityOperation::ReadSecret,
779 source,
780 )
781 })?;
782 let metadata = file.metadata().map_err(|source| {
783 SecurityProviderError::caused_by(
784 SecurityProviderFailure::OperationFailed,
785 SecurityOperation::InspectSecret,
786 source,
787 )
788 })?;
789 if !platform_secret_file_permissions(&metadata).is_owner_only() {
790 return Err(SecurityProviderError::contract(
791 SecurityOperation::InspectSecret,
792 SecurityContractViolation::SecretStoragePermissionsInsecure,
793 ));
794 }
795 let mut value = Vec::new();
796 file.read_to_end(&mut value).map_err(|source| {
797 SecurityProviderError::caused_by(
798 SecurityProviderFailure::OperationFailed,
799 SecurityOperation::ReadSecret,
800 source,
801 )
802 })?;
803 Ok(Secret::new(value))
804}
805
806#[cfg(unix)]
807fn platform_secret_file_permissions(metadata: &std::fs::Metadata) -> SecretFilePermissions {
808 use std::os::unix::fs::PermissionsExt;
809
810 let mode = metadata.permissions().mode();
811 SecretFilePermissions {
812 group_access: mode & 0o070 != 0,
813 other_access: mode & 0o007 != 0,
814 }
815}
816
817#[cfg(windows)]
818fn platform_secret_file_permissions(_metadata: &std::fs::Metadata) -> SecretFilePermissions {
819 SecretFilePermissions {
822 group_access: true,
823 other_access: true,
824 }
825}