Skip to main content

rocketmq_security_api/
lib.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#![deny(missing_docs)]
16
17//! Runtime-neutral security contracts.
18
19mod error;
20/// Layered ingress and detailed authorization contracts.
21pub mod layered_authorization;
22pub mod maintenance;
23/// Resource pattern types and operations.
24pub mod resource_pattern;
25/// Resource type types and operations.
26pub 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/// Read-only peer metadata made available to security policies.
109#[derive(Clone, PartialEq, Eq)]
110pub struct PeerInfo {
111    address: SocketAddr,
112    tls: bool,
113    certificate_subject: Option<CheetahString>,
114}
115
116impl PeerInfo {
117    /// Creates a new `PeerInfo`.
118    pub fn new(address: SocketAddr, tls: bool) -> Self {
119        Self {
120            address,
121            tls,
122            certificate_subject: None,
123        }
124    }
125
126    /// Sets certificate subject and returns the updated value.
127    pub fn with_certificate_subject(mut self, subject: impl Into<CheetahString>) -> Self {
128        self.certificate_subject = Some(subject.into());
129        self
130    }
131
132    /// Returns the address.
133    pub fn address(&self) -> SocketAddr {
134        self.address
135    }
136
137    /// Returns whether tls.
138    pub fn is_tls(&self) -> bool {
139        self.tls
140    }
141
142    /// Returns the certificate subject.
143    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/// Minimal borrowed projection of an inbound or outbound request.
162#[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    /// Creates a new `SecurityRequestView`.
173    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    /// Returns the code.
190    pub fn code(&self) -> i32 {
191        self.code
192    }
193
194    /// Returns the version.
195    pub fn version(&self) -> i32 {
196        self.version
197    }
198
199    /// Returns the fields.
200    pub fn fields(&self) -> &'a HashMap<CheetahString, CheetahString> {
201        self.fields
202    }
203
204    /// Returns the body.
205    pub fn body(&self) -> Option<&'a [u8]> {
206        self.body
207    }
208
209    /// Returns the peer.
210    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/// Authenticated identity without credential material.
228#[derive(Debug, Clone, PartialEq, Eq, Hash)]
229pub struct Principal {
230    id: CheetahString,
231}
232
233impl Principal {
234    /// Creates a new `Principal`.
235    pub fn new(id: impl Into<CheetahString>) -> Self {
236        Self { id: id.into() }
237    }
238
239    /// Returns the id.
240    pub fn id(&self) -> &str {
241        &self.id
242    }
243}
244
245/// Protected resource category.
246#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
247pub enum ResourceKind {
248    /// Represents the topic case.
249    Topic,
250    /// Represents the consumer group case.
251    ConsumerGroup,
252    /// Represents the cluster case.
253    Cluster,
254    /// Represents the broker case.
255    Broker,
256    /// Represents the other case.
257    Other,
258}
259
260/// Resource selected for a policy decision.
261#[derive(Debug, Clone, PartialEq, Eq, Hash)]
262pub struct Resource {
263    kind: ResourceKind,
264    name: CheetahString,
265}
266
267impl Resource {
268    /// Creates a new `Resource`.
269    pub fn new(kind: ResourceKind, name: impl Into<CheetahString>) -> Self {
270        Self {
271            kind,
272            name: name.into(),
273        }
274    }
275
276    /// Creates the topic value.
277    pub fn topic(name: impl Into<CheetahString>) -> Self {
278        Self::new(ResourceKind::Topic, name)
279    }
280
281    /// Returns the kind.
282    pub fn kind(&self) -> ResourceKind {
283        self.kind
284    }
285
286    /// Returns the name.
287    pub fn name(&self) -> &str {
288        &self.name
289    }
290}
291
292/// Security-relevant operation requested on a resource.
293///
294/// Values `0..=9` preserve the RocketMQ ACL wire codes. The semantic
295/// `Publish`, `Subscribe`, `Describe`, and `Manage` variants are policy-level
296/// operations and intentionally have distinct codes.
297#[repr(u8)]
298#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
299pub enum Action {
300    /// Represents the unknown case.
301    Unknown = 0,
302    /// Represents the all case.
303    All = 1,
304    /// Represents the any case.
305    Any = 2,
306    /// Represents the pub case.
307    Pub = 3,
308    /// Represents the sub case.
309    Sub = 4,
310    /// Represents the create case.
311    Create = 5,
312    /// Represents the update case.
313    Update = 6,
314    /// Represents the delete case.
315    Delete = 7,
316    /// Represents the get case.
317    Get = 8,
318    /// Represents the list case.
319    List = 9,
320    /// Represents the publish case.
321    Publish = 10,
322    /// Represents the subscribe case.
323    Subscribe = 11,
324    /// Represents the describe case.
325    Describe = 12,
326    /// Represents the manage case.
327    Manage = 13,
328}
329
330impl Action {
331    /// Returns the code.
332    pub const fn code(self) -> u8 {
333        self as u8
334    }
335
336    /// Returns the name.
337    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    /// Returns by name.
357    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
408/// Complete security input before authentication has been required.
409pub 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    /// Creates a new `RequestContext`.
418    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    /// Returns the principal.
433    pub fn principal(&self) -> Option<&Principal> {
434        self.principal
435    }
436}
437
438/// Policy input for which the principal invariant has been established.
439#[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    /// Returns the request.
449    pub fn request(&self) -> SecurityRequestView<'a> {
450        self.request
451    }
452
453    /// Returns the principal.
454    pub fn principal(&self) -> &'a Principal {
455        self.principal
456    }
457
458    /// Returns the resource.
459    pub fn resource(&self) -> &'a Resource {
460        self.resource
461    }
462
463    /// Returns the action.
464    pub fn action(&self) -> Action {
465        self.action
466    }
467}
468
469/// Authorization policy that can only evaluate authenticated requests.
470pub trait RequestPolicy: Send + Sync {
471    /// Returns the final decision for an authenticated request.
472    fn evaluate_authenticated(&self, context: AuthenticatedRequestContext<'_>) -> AuthorizationDecision;
473}
474
475/// Applies the mandatory fail-closed identity gate before invoking a policy.
476///
477/// A missing principal resolves to [`AuthorizationDenial::SubjectUnknown`]
478/// without invoking the policy.
479pub 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
491/// Redacted signature fields produced by an outbound signer.
492pub struct Signature {
493    fields: Vec<(CheetahString, Secret<CheetahString>)>,
494}
495
496impl Signature {
497    /// Creates a new `Signature`.
498    pub fn new(fields: Vec<(CheetahString, Secret<CheetahString>)>) -> Self {
499        Self { fields }
500    }
501
502    /// Returns the fields.
503    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
516/// Contract implemented by transport-specific outbound signers in `rocketmq-auth`.
517pub trait OutboundSigner: Send + Sync {
518    /// Signs an outbound security request.
519    ///
520    /// # Errors
521    ///
522    /// Returns a redacted [`SecurityProviderError`] when the signing provider
523    /// cannot produce a signature safely.
524    fn sign(&self, request: SecurityRequestView<'_>) -> Result<Signature, SecurityProviderError>;
525}
526
527/// Sensitive value whose debug representation is always redacted.
528pub struct Secret<T>(T);
529
530impl<T> Secret<T> {
531    /// Creates a new `Secret`.
532    pub fn new(value: T) -> Self {
533        Self(value)
534    }
535
536    /// Returns the expose secret.
537    pub fn expose_secret(&self) -> &T {
538        &self.0
539    }
540
541    /// Converts this value into inner.
542    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/// Deployment security profile selected by configuration.
554#[derive(Debug, Clone, Copy, PartialEq, Eq)]
555pub enum DeploymentProfile {
556    /// Represents the development case.
557    Development,
558    /// Represents the compatibility case.
559    Compatibility,
560    /// Represents the secure case.
561    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/// Borrowed configuration input for a side-effect-free readiness check.
578#[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    /// Creates a new `SecurityConfigView`.
589    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    /// Sets trust anchor and returns the updated value.
600    pub fn with_trust_anchor(mut self, path: &'a Path) -> Self {
601        self.trust_anchor = Some(path);
602        self
603    }
604
605    /// Sets secret file and returns the updated value.
606    pub fn with_secret_file(mut self, path: &'a Path) -> Self {
607        self.secret_file = Some(path);
608        self
609    }
610
611    /// Sets bootstrap expiry and returns the updated value.
612    pub fn with_bootstrap_expiry(mut self, expires_at: SystemTime) -> Self {
613        self.bootstrap_expires_at = Some(expires_at);
614        self
615    }
616
617    /// Sets insecure downgrade and returns the updated value.
618    pub fn with_insecure_downgrade(mut self, enabled: bool) -> Self {
619        self.insecure_downgrade = enabled;
620        self
621    }
622}
623
624/// Stable reasons why a profile is not ready.
625#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
626pub enum SecurityReadinessFailure {
627    /// Represents the unknown profile case.
628    UnknownProfile,
629    /// Represents the missing trust anchor case.
630    MissingTrustAnchor,
631    /// Represents the trust anchor unavailable case.
632    TrustAnchorUnavailable,
633    /// Represents the trust anchor not regular file case.
634    TrustAnchorNotRegularFile,
635    /// Represents the missing secret file case.
636    MissingSecretFile,
637    /// Represents the secret file unavailable case.
638    SecretFileUnavailable,
639    /// Represents the secret file not regular file case.
640    SecretFileNotRegularFile,
641    /// Represents the insecure secret file permissions case.
642    InsecureSecretFilePermissions,
643    /// Represents the missing bootstrap expiry case.
644    MissingBootstrapExpiry,
645    /// Represents the expired bootstrap case.
646    ExpiredBootstrap,
647    /// Represents the insecure downgrade case.
648    InsecureDowngrade,
649}
650
651/// Result of a side-effect-free security configuration dry run.
652#[derive(Debug, Clone, PartialEq, Eq)]
653pub struct SecurityReadinessReport {
654    profile: Option<DeploymentProfile>,
655    failures: Vec<SecurityReadinessFailure>,
656}
657
658impl SecurityReadinessReport {
659    /// Returns whether ready.
660    pub fn is_ready(&self) -> bool {
661        self.failures.is_empty()
662    }
663
664    /// Returns the profile.
665    pub fn profile(&self) -> Option<DeploymentProfile> {
666        self.profile
667    }
668
669    /// Returns the failures.
670    pub fn failures(&self) -> &[SecurityReadinessFailure] {
671        &self.failures
672    }
673}
674
675/// Validates a configuration snapshot without reading file contents or writing external state.
676pub 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/// Portable projection of secret-file permissions.
753#[derive(Debug, Clone, Copy, PartialEq, Eq)]
754pub struct SecretFilePermissions {
755    /// Whether group access.
756    pub group_access: bool,
757    /// Whether other access.
758    pub other_access: bool,
759}
760
761impl SecretFilePermissions {
762    /// Returns whether owner only.
763    pub fn is_owner_only(self) -> bool {
764        !self.group_access && !self.other_access
765    }
766}
767
768/// Opens, permission-checks and reads a secret using the same file handle.
769///
770/// # Errors
771///
772/// Returns a redacted [`SecurityProviderError`] while retaining safe typed I/O
773/// sources. Insecure permissions are retained as a closed contract source.
774pub 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    // Windows ACL inspection is supplied by the deployment adapter. The portable contract stays
820    // fail-closed until that adapter provides an owner-only projection.
821    SecretFilePermissions {
822        group_access: true,
823        other_access: true,
824    }
825}