1use std::fs::{self, DirBuilder, Permissions};
2use std::os::unix::fs::{DirBuilderExt, OpenOptionsExt, PermissionsExt};
3use std::path::{Component, Path, PathBuf};
4
5use crate::error::{Error, Result};
6use crate::exec::{self, Tools};
7use crate::identity::MachineIdentity;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11#[cfg_attr(feature = "serde", derive(serde::Serialize))]
12pub enum Freshness {
13 Renewed,
16 Minted,
20}
21
22#[derive(Debug, Clone)]
24pub struct ArmorCache {
25 path: PathBuf,
26}
27
28impl ArmorCache {
29 pub fn new(path: impl Into<PathBuf>) -> Self {
30 Self { path: path.into() }
31 }
32
33 pub fn path(&self) -> &Path {
34 &self.path
35 }
36
37 pub fn cache_name(&self) -> String {
39 format!("FILE:{}", self.path.display())
40 }
41
42 pub fn is_valid(&self, tools: &Tools) -> bool {
44 exec::succeeds(&tools.klist, ["-s", &self.cache_name()])
45 }
46
47 pub fn renew(&self, tools: &Tools, identity: &MachineIdentity) -> Result<()> {
60 exec::run_ok(
63 &tools.kinit,
64 ["-R", "-c", &self.cache_name(), "--", &identity.principal],
65 )
66 .map(drop)
67 }
68
69 pub fn mint(&self, tools: &Tools, identity: &MachineIdentity) -> Result<()> {
72 exec::run_ok(
73 &tools.kinit,
74 [
75 "-k".as_ref(),
76 "-t".as_ref(),
77 identity.keytab.as_os_str(),
78 "-c".as_ref(),
79 self.cache_name().as_ref(),
80 "--".as_ref(), identity.principal.as_ref(),
82 ],
83 )
84 .map(drop)
85 }
86
87 pub fn ensure(&self, tools: &Tools, identity: &MachineIdentity) -> Result<Freshness> {
93 if self.renew(tools, identity).is_ok() && self.is_valid(tools) {
94 return Ok(Freshness::Renewed);
95 }
96 self.mint(tools, identity)?;
97 if self.is_valid(tools) {
98 Ok(Freshness::Minted)
99 } else {
100 Err(Error::CacheStillInvalid(self.path.clone()))
101 }
102 }
103
104 pub fn set_access(&self, gid: u32) -> Result<()> {
114 let file = fs::OpenOptions::new()
115 .read(true)
116 .custom_flags(libc::O_NOFOLLOW)
117 .open(&self.path)
118 .map_err(|e| {
119 if e.raw_os_error() == Some(libc::ELOOP) {
122 Error::RefusingSymlink(self.path.clone())
123 } else {
124 e.into()
125 }
126 })?;
127 std::os::unix::fs::fchown(&file, None, Some(gid))?;
128 file.set_permissions(Permissions::from_mode(0o640))?;
129 Ok(())
130 }
131
132 pub fn klist_text(&self, tools: &Tools) -> Result<String> {
134 exec::run_ok(&tools.klist, [self.cache_name()])
135 }
136
137 pub fn destroy(&self, tools: &Tools) {
139 let _ = exec::run(&tools.kdestroy, ["-c", &self.cache_name()]);
140 let _ = fs::remove_file(&self.path);
141 }
142}
143
144pub fn prepare_dir(dir: &Path, gid: Option<u32>) -> Result<()> {
150 if !dir.is_absolute()
151 || dir.components().any(|c| c == Component::ParentDir)
152 || is_shared_dir(dir)
153 {
154 return Err(Error::RefusingSharedDir(dir.to_path_buf()));
155 }
156 match fs::symlink_metadata(dir) {
157 Ok(meta) if meta.file_type().is_symlink() => {
158 return Err(Error::RefusingSymlink(dir.to_path_buf()));
159 }
160 Ok(_) => {}
161 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
162 DirBuilder::new().recursive(true).mode(0o750).create(dir)?;
163 }
164 Err(e) => return Err(e.into()),
165 }
166 let real = fs::canonicalize(dir)?;
169 if is_shared_dir(&real) {
170 return Err(Error::RefusingSharedDir(dir.to_path_buf()));
171 }
172 if let Some(gid) = gid {
173 std::os::unix::fs::chown(&real, None, Some(gid))?;
174 }
175 fs::set_permissions(&real, Permissions::from_mode(0o750))?;
176 Ok(())
177}
178
179fn is_shared_dir(canonical: &Path) -> bool {
181 const SHARED: &[&str] = &[
182 "/",
183 "/run",
184 "/var/run",
185 "/run/user",
186 "/tmp",
187 "/var/tmp",
188 "/var",
189 "/etc",
190 "/home",
191 "/root",
192 "/usr",
193 "/usr/local",
194 "/usr/local/bin",
195 "/var/usrlocal",
196 "/var/lib",
197 "/opt",
198 "/srv",
199 "/boot",
200 "/dev",
201 "/dev/shm",
202 ];
203 SHARED.iter().any(|s| Path::new(s) == canonical)
206}
207
208pub fn lookup_gid(tools: &Tools, group: &str) -> Result<u32> {
211 let line = exec::run(&tools.getent, ["--", "group", group])?;
213 if !line.status.success() {
214 return Err(Error::GroupNotFound(group.to_string()));
215 }
216 parse_getent_gid(&String::from_utf8_lossy(&line.stdout))
217 .ok_or_else(|| Error::GroupNotFound(group.to_string()))
218}
219
220pub(crate) fn parse_getent_gid(line: &str) -> Option<u32> {
222 line.trim().split(':').nth(2)?.parse().ok()
223}
224
225#[cfg(test)]
226mod tests {
227 use super::*;
228
229 #[test]
230 fn getent_gid_parses() {
231 assert_eq!(parse_getent_gid("machine-krb:x:973:dylan\n"), Some(973));
232 assert_eq!(parse_getent_gid("wheel:x:10:"), Some(10));
233 assert_eq!(parse_getent_gid(""), None);
234 assert_eq!(parse_getent_gid("garbage"), None);
235 assert_eq!(parse_getent_gid("a:b"), None);
236 }
237
238 #[test]
239 fn prepare_dir_refuses_shared_dirs() {
240 for d in ["/run", "/tmp", "/", "/var", "/etc", "/usr/local"] {
241 assert!(matches!(
242 prepare_dir(Path::new(d), None),
243 Err(Error::RefusingSharedDir(_))
244 ));
245 }
246 }
247
248 #[test]
249 fn prepare_dir_refuses_dotdot_and_relative() {
250 for d in [
251 "/run/machine-krb/..",
252 "/run/../run",
253 "run/machine-krb",
254 "../x",
255 ] {
256 assert!(matches!(
257 prepare_dir(Path::new(d), None),
258 Err(Error::RefusingSharedDir(_))
259 ));
260 }
261 }
262
263 #[test]
264 fn shared_dir_list_hits_canonical_forms() {
265 for d in ["/var/usrlocal", "/run/user", "/var/lib", "/opt"] {
266 assert!(is_shared_dir(Path::new(d)));
267 }
268 assert!(!is_shared_dir(Path::new("/run/machine-krb")));
269 }
270
271 #[test]
272 fn cache_name_is_file_prefixed() {
273 let c = ArmorCache::new("/run/machine-krb/armor.ccache");
274 assert_eq!(c.cache_name(), "FILE:/run/machine-krb/armor.ccache");
275 }
276}