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
26/// Check a given file's metadata is read-only for the current user (true = read-only)
27pub fn readonly(meta: &Metadata) -> bool {
28    // Who are we running as?
29    let cuid = get_current_uid();
30    let cgid = get_current_gid();
31
32    // Who owns the file?
33    // Who is the group owner of the file?
34    let f_gid = meta.st_gid();
35    let f_uid = meta.st_uid();
36
37    let f_mode = meta.st_mode();
38
39    !(
40        // If we are the owner, we have write perms as we can alter the DAC rights
41        cuid == f_uid ||
42        // If we are the group owner, check the mode bits do not have write.
43        (cgid == f_gid && (f_mode & 0o0020) != 0) ||
44        // Finally, check that everyone bits don't have write.
45        ((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    // Who are we?
165    let cuid = get_current_uid();
166    let cgid = get_current_gid();
167
168    // clone the path
169    let path: PathBuf = path.into();
170
171    // Display the abs/resolved path.
172    let abs_path = path.canonicalize();
173
174    // For each segment, from the root inc root
175    // show the path -> owner/group mode
176    //      or show that we have permission denied.
177    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    // Now pop from the right to start from the root.
185    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}