kanidm_lib_file_permissions/
unix.rs
1use std::fs::Metadata;
2
3#[cfg(target_os = "freebsd")]
4use std::os::freebsd::fs::MetadataExt;
5
6#[cfg(target_os = "openbsd")]
7use std::os::openbsd::fs::MetadataExt;
8
9#[cfg(target_os = "linux")]
10use std::os::linux::fs::MetadataExt;
11
12#[cfg(target_os = "macos")]
13use std::os::macos::fs::MetadataExt;
14
15#[cfg(target_os = "illumos")]
16use std::os::illumos::fs::MetadataExt;
17
18#[cfg(target_os = "android")]
19use std::os::android::fs::MetadataExt;
20
21use kanidm_utils_users::{get_current_gid, get_current_uid};
22
23use std::fmt;
24use std::path::{Path, PathBuf};
25
26pub fn readonly(meta: &Metadata) -> bool {
28 let cuid = get_current_uid();
30 let cgid = get_current_gid();
31
32 let f_gid = meta.st_gid();
35 let f_uid = meta.st_uid();
36
37 let f_mode = meta.st_mode();
38
39 !(
40 cuid == f_uid ||
42 (cgid == f_gid && (f_mode & 0o0020) != 0) ||
44 ((f_mode & 0o0002) != 0)
46 )
47}
48
49#[derive(Debug)]
50pub enum PathStatus {
51 Dir {
52 f_gid: u32,
53 f_uid: u32,
54 f_mode: u32,
55 access: bool,
56 },
57 Link {
58 f_gid: u32,
59 f_uid: u32,
60 f_mode: u32,
61 access: bool,
62 },
63 File {
64 f_gid: u32,
65 f_uid: u32,
66 f_mode: u32,
67 access: bool,
68 },
69 Error(std::io::Error),
70}
71
72#[derive(Debug)]
73pub struct Diagnosis {
74 cuid: u32,
75 cgid: u32,
76 path: PathBuf,
77 abs_path: Result<PathBuf, std::io::Error>,
78 ancestors: Vec<(PathBuf, PathStatus)>,
79}
80
81impl fmt::Display for Diagnosis {
82 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
83 writeln!(f, "-- diagnosis for path: {}", self.path.to_string_lossy())?;
84 let indent = match &self.abs_path {
85 Ok(abs) => {
86 let abs_str = abs.to_string_lossy();
87 writeln!(f, "canonicalised to: {}", abs_str)?;
88 abs_str.len() + 1
89 }
90 Err(_) => {
91 writeln!(f, "unable to canonicalise path")?;
92 self.path.to_string_lossy().len() + 1
93 }
94 };
95
96 writeln!(f, "running as: {}:{}", self.cuid, self.cgid)?;
97
98 writeln!(f, "path permissions\n")?;
99 for (anc, status) in &self.ancestors {
100 match &status {
101 PathStatus::Dir {
102 f_gid,
103 f_uid,
104 f_mode,
105 access,
106 } => {
107 writeln!(
108 f,
109 " {:indent$}: DIR access: {} owner: {} group: {} mode: {}",
110 anc.to_string_lossy(),
111 access,
112 f_uid,
113 f_gid,
114 mode_to_string(*f_mode)
115 )?;
116 }
117 PathStatus::Link {
118 f_gid,
119 f_uid,
120 f_mode,
121 access,
122 } => {
123 writeln!(
124 f,
125 " {:indent$}: LINK access: {} owner: {} group: {} mode: {}",
126 anc.to_string_lossy(),
127 access,
128 f_uid,
129 f_gid,
130 mode_to_string(*f_mode)
131 )?;
132 }
133 PathStatus::File {
134 f_gid,
135 f_uid,
136 f_mode,
137 access,
138 } => {
139 writeln!(
140 f,
141 " {:indent$}: FILE access: {} owner: {} group: {} mode: {}",
142 anc.to_string_lossy(),
143 access,
144 f_uid,
145 f_gid,
146 mode_to_string(*f_mode)
147 )?;
148 }
149 PathStatus::Error(err) => {
150 writeln!(f, " {:indent$}: ERROR: {:?}", anc.to_string_lossy(), err)?;
151 }
152 }
153 }
154
155 writeln!(
156 f,
157 "\n note that accessibility does not account for ACL's or MAC"
158 )?;
159 writeln!(f, "-- end diagnosis")
160 }
161}
162
163pub fn diagnose_path(path: &Path) -> Diagnosis {
164 let cuid = get_current_uid();
166 let cgid = get_current_gid();
167
168 let path: PathBuf = path.into();
170
171 let abs_path = path.canonicalize();
173
174 let mut all_ancestors: Vec<_> = match &abs_path {
178 Ok(ap) => ap.ancestors().collect(),
179 Err(_) => path.ancestors().collect(),
180 };
181
182 let mut ancestors = Vec::with_capacity(all_ancestors.len());
183
184 while let Some(anc) = all_ancestors.pop() {
186 let status = match anc.metadata() {
187 Ok(meta) => {
188 let f_gid = meta.st_gid();
189 let f_uid = meta.st_uid();
190 let f_mode = meta.st_mode();
191 if meta.is_dir() {
192 let access = x_accessible(cuid, cgid, f_uid, f_gid, f_mode);
193
194 PathStatus::Dir {
195 f_gid,
196 f_uid,
197 f_mode,
198 access,
199 }
200 } else if meta.is_symlink() {
201 let access = x_accessible(cuid, cgid, f_uid, f_gid, f_mode);
202
203 PathStatus::Link {
204 f_gid,
205 f_uid,
206 f_mode,
207 access,
208 }
209 } else {
210 let access = accessible(cuid, cgid, f_uid, f_gid, f_mode);
211
212 PathStatus::File {
213 f_gid,
214 f_uid,
215 f_mode,
216 access,
217 }
218 }
219 }
220 Err(e) => PathStatus::Error(e),
221 };
222
223 ancestors.push((anc.into(), status))
224 }
225
226 Diagnosis {
227 cuid,
228 cgid,
229 path,
230 abs_path,
231 ancestors,
232 }
233}
234
235fn x_accessible(cuid: u32, cgid: u32, f_uid: u32, f_gid: u32, f_mode: u32) -> bool {
236 (cuid == f_uid && f_mode & 0o500 == 0o500)
237 || (cgid == f_gid && f_mode & 0o050 == 0o050)
238 || f_mode & 0o005 == 0o005
239}
240
241fn accessible(cuid: u32, cgid: u32, f_uid: u32, f_gid: u32, f_mode: u32) -> bool {
242 (cuid == f_uid && f_mode & 0o400 == 0o400)
243 || (cgid == f_gid && f_mode & 0o040 == 0o040)
244 || f_mode & 0o004 == 0o004
245}
246
247fn mode_to_string(mode: u32) -> String {
248 let mut mode_str = String::with_capacity(9);
249 if mode & 0o400 != 0 {
250 mode_str.push('r')
251 } else {
252 mode_str.push('-')
253 }
254
255 if mode & 0o200 != 0 {
256 mode_str.push('w')
257 } else {
258 mode_str.push('-')
259 }
260
261 if mode & 0o100 != 0 {
262 mode_str.push('x')
263 } else {
264 mode_str.push('-')
265 }
266
267 if mode & 0o040 != 0 {
268 mode_str.push('r')
269 } else {
270 mode_str.push('-')
271 }
272
273 if mode & 0o020 != 0 {
274 mode_str.push('w')
275 } else {
276 mode_str.push('-')
277 }
278
279 if mode & 0o010 != 0 {
280 mode_str.push('x')
281 } else {
282 mode_str.push('-')
283 }
284
285 if mode & 0o004 != 0 {
286 mode_str.push('r')
287 } else {
288 mode_str.push('-')
289 }
290
291 if mode & 0o002 != 0 {
292 mode_str.push('w')
293 } else {
294 mode_str.push('-')
295 }
296
297 if mode & 0o001 != 0 {
298 mode_str.push('x')
299 } else {
300 mode_str.push('-')
301 }
302
303 mode_str
304}
305
306#[test]
307fn test_readonly() {
308 let meta = std::fs::metadata("Cargo.toml").expect("Can't find Cargo.toml");
309 println!("meta={:?} -> readonly={:?}", meta, readonly(&meta));
310 assert!(!readonly(&meta));
311}