rocketmq_auth/
maintenance.rs1use std::error::Error;
23use std::fmt;
24use std::fs;
25use std::path::Component;
26use std::path::Path;
27use std::path::PathBuf;
28
29use rocketmq_security_api::maintenance::MaintenancePolicy;
30use rocketmq_security_api::ValidatedMaintenancePolicy;
31use serde::Deserialize;
32use serde::Serialize;
33use sha2::Digest;
34use sha2::Sha256;
35
36use crate::AuthFailureKind;
37use crate::AuthOperation;
38use crate::AuthServiceError;
39use crate::AuthServiceResult;
40
41#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
43#[serde(deny_unknown_fields, rename_all = "snake_case")]
44pub struct MaintenancePolicyReference {
45 pub path: PathBuf,
47 pub version: u64,
49 pub sha256: String,
51}
52
53impl MaintenancePolicyReference {
54 pub fn load_from(&self, configuration_root: impl AsRef<Path>) -> AuthServiceResult<LoadedMaintenancePolicy> {
62 self.load_from_inner(configuration_root)
63 .map_err(|source| AuthServiceError::with_source(AuthOperation::MaintainService, source.kind(), source))
64 }
65
66 fn load_from_inner(
67 &self,
68 configuration_root: impl AsRef<Path>,
69 ) -> Result<LoadedMaintenancePolicy, MaintenancePolicyLoadError> {
70 validate_sha256(&self.sha256)?;
71 if self.version == 0 {
72 return Err(MaintenancePolicyLoadError::InvalidReference(
73 "policy reference version must be greater than zero".to_string(),
74 ));
75 }
76 if self.path.as_os_str().is_empty() {
77 return Err(MaintenancePolicyLoadError::InvalidReference(
78 "policy reference path is empty".to_string(),
79 ));
80 }
81 if self.path.is_relative()
82 && self.path.components().any(|component| {
83 matches!(
84 component,
85 Component::ParentDir | Component::RootDir | Component::Prefix(_)
86 )
87 })
88 {
89 return Err(MaintenancePolicyLoadError::InvalidReference(
90 "relative policy path cannot escape the configuration root".to_string(),
91 ));
92 }
93
94 let resolved_path = if self.path.is_absolute() {
95 self.path.clone()
96 } else {
97 configuration_root.as_ref().join(&self.path)
98 };
99 let bytes = fs::read(&resolved_path).map_err(|source| MaintenancePolicyLoadError::Read {
100 path: resolved_path.clone(),
101 source,
102 })?;
103 let actual_sha256 = hex::encode(Sha256::digest(&bytes));
104 if actual_sha256 != self.sha256 {
105 return Err(MaintenancePolicyLoadError::DigestMismatch {
106 expected: self.sha256.clone(),
107 actual: actual_sha256,
108 });
109 }
110
111 let policy: MaintenancePolicy =
112 serde_json::from_slice(&bytes).map_err(|source| MaintenancePolicyLoadError::Decode {
113 path: resolved_path.clone(),
114 source,
115 })?;
116 let policy = policy
117 .into_validated()
118 .map_err(|source| MaintenancePolicyLoadError::Contract(Box::new(source)))?;
119 if policy.policy().policy_version != self.version {
120 return Err(MaintenancePolicyLoadError::VersionMismatch {
121 expected: self.version,
122 actual: policy.policy().policy_version,
123 });
124 }
125
126 Ok(LoadedMaintenancePolicy {
127 policy,
128 reference: self.clone(),
129 resolved_path,
130 })
131 }
132}
133
134#[derive(Clone, Debug)]
136pub struct LoadedMaintenancePolicy {
137 policy: ValidatedMaintenancePolicy,
138 reference: MaintenancePolicyReference,
139 resolved_path: PathBuf,
140}
141
142impl LoadedMaintenancePolicy {
143 pub const fn policy(&self) -> &MaintenancePolicy {
145 self.policy.policy()
146 }
147
148 pub const fn reference(&self) -> &MaintenancePolicyReference {
150 &self.reference
151 }
152
153 pub fn resolved_path(&self) -> &Path {
155 &self.resolved_path
156 }
157}
158
159impl From<LoadedMaintenancePolicy> for ValidatedMaintenancePolicy {
160 fn from(loaded: LoadedMaintenancePolicy) -> Self {
161 loaded.policy
162 }
163}
164
165#[derive(Debug)]
166enum MaintenancePolicyLoadError {
167 InvalidReference(String),
168 Read { path: PathBuf, source: std::io::Error },
169 Decode { path: PathBuf, source: serde_json::Error },
170 DigestMismatch { expected: String, actual: String },
171 VersionMismatch { expected: u64, actual: u64 },
172 Contract(Box<dyn Error + Send + Sync + 'static>),
173}
174
175impl MaintenancePolicyLoadError {
176 const fn kind(&self) -> AuthFailureKind {
177 match self {
178 Self::InvalidReference(_) | Self::Contract(_) => AuthFailureKind::InvalidConfiguration,
179 Self::Read { .. } => AuthFailureKind::Unavailable,
180 Self::Decode { .. } | Self::DigestMismatch { .. } | Self::VersionMismatch { .. } => {
181 AuthFailureKind::InvalidData
182 }
183 }
184 }
185}
186
187impl fmt::Display for MaintenancePolicyLoadError {
188 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
189 match self {
190 Self::InvalidReference(reason) => write!(formatter, "invalid maintenance policy reference: {reason}"),
191 Self::Read { path, source } => write!(formatter, "failed to read maintenance policy {path:?}: {source}"),
192 Self::Decode { path, source } => {
193 write!(formatter, "failed to decode maintenance policy {path:?}: {source}")
194 }
195 Self::DigestMismatch { expected, actual } => {
196 write!(
197 formatter,
198 "maintenance policy digest mismatch: expected {expected}, actual {actual}"
199 )
200 }
201 Self::VersionMismatch { expected, actual } => {
202 write!(
203 formatter,
204 "maintenance policy version mismatch: expected {expected}, actual {actual}"
205 )
206 }
207 Self::Contract(source) => write!(formatter, "maintenance policy contract violation: {source}"),
208 }
209 }
210}
211
212impl Error for MaintenancePolicyLoadError {
213 fn source(&self) -> Option<&(dyn Error + 'static)> {
214 match self {
215 Self::Read { source, .. } => Some(source),
216 Self::Decode { source, .. } => Some(source),
217 Self::Contract(source) => Some(source.as_ref()),
218 Self::InvalidReference(_) | Self::DigestMismatch { .. } | Self::VersionMismatch { .. } => None,
219 }
220 }
221}
222
223fn validate_sha256(value: &str) -> Result<(), MaintenancePolicyLoadError> {
224 if value.len() == 64
225 && value
226 .bytes()
227 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
228 {
229 Ok(())
230 } else {
231 Err(MaintenancePolicyLoadError::InvalidReference(
232 "policy SHA-256 must be 64 lowercase hexadecimal characters".to_string(),
233 ))
234 }
235}