async_user_lookup/
lib.rs

1// Copyright 2022 Mattias Eriksson
2//
3// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
4// https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
5// <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
6// option. This file may not be copied, modified, or distributed
7// except according to those terms.
8
9//! `async_user_lookup` provides an easy way to lookup Linux/Unix user and group information
10//! from /etc/passwd and /etc/group. It uses tokio async and will cache the information for a
11//! duration specified by the user. If no caching is desired, a Duration of 0.0 can be used.
12//!
13//!```rust,ignore
14//!use async_user_lookup::PasswdReader;
15//!use std::time::Duration;
16//!
17//!#[tokio::main]
18//!async fn main() {
19//!   let mut reader = PasswdReader::new(Duration::new(0,0));
20//!
21//!   println!("User with uid 1000 is: {}",
22//!   reader.get_username_by_uid(1000).await.unwrap().unwrap());
23//!}
24//!
25//!```
26use std::time::Duration;
27
28use tokio::time::Instant;
29
30/// A passwd entry, representing one row in
31/// `/etc/passwd`
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct PasswdEntry {
34    /// Username
35    pub username: String,
36    /// User password
37    pub passwd: String,
38    /// User ID
39    pub uid: u32,
40    /// Group ID
41    pub gid: u32,
42    /// User full name or comment
43    pub gecos: String,
44    /// Home directory
45    pub home_dir: String,
46    /// Shell
47    pub shell: String,
48}
49
50impl PasswdEntry {
51    ///Create a PasswdEntry from &str.
52    pub fn parse(s: &str) -> Option<PasswdEntry> {
53        let mut entries = s.splitn(7, ':');
54        Some(PasswdEntry {
55            username: match entries.next() {
56                None => return None,
57                Some(s) => s.to_string(),
58            },
59            passwd: match entries.next() {
60                None => return None,
61                Some(s) => s.to_string(),
62            },
63            uid: match entries.next().and_then(|s| s.parse().ok()) {
64                None => return None,
65                Some(s) => s,
66            },
67            gid: match entries.next().and_then(|s| s.parse().ok()) {
68                None => return None,
69                Some(s) => s,
70            },
71            gecos: match entries.next() {
72                None => return None,
73                Some(s) => s.to_string(),
74            },
75            home_dir: match entries.next() {
76                None => return None,
77                Some(s) => s.to_string(),
78            },
79            shell: match entries.next() {
80                None => return None,
81                Some(s) => s.to_string(),
82            },
83        })
84    }
85}
86
87/// A group entry, representing one row in
88/// ```/etc/group```
89#[derive(Debug, Clone, PartialEq, Eq)]
90pub struct GroupEntry {
91    //Username
92    pub name: String,
93    //Password
94    pub passwd: String,
95    //Group ID
96    pub gid: u32,
97    //List of users
98    pub users: Vec<String>,
99}
100
101impl GroupEntry {
102    ///Create a GroupEntry from &str.
103    pub fn parse(s: &str) -> Option<GroupEntry> {
104        let mut entries = s.splitn(4, ':');
105        Some(GroupEntry {
106            name: match entries.next() {
107                None => return None,
108                Some(s) => s.to_string(),
109            },
110            passwd: match entries.next() {
111                None => return None,
112                Some(s) => s.to_string(),
113            },
114            gid: match entries.next().and_then(|s| s.parse().ok()) {
115                None => return None,
116                Some(s) => s,
117            },
118            users: match entries.next() {
119                None => return None,
120                Some(s) => s.split(',').map(|p| p.to_string()).collect(),
121            },
122        })
123    }
124}
125
126///The main entity to reaad and lookup user information. It
127///supports caching the information to avoid having to read
128///the information from disk more than needed.
129/// ```
130/// use async_user_lookup::PasswdReader;
131/// use std::time::Duration;
132///
133/// #[tokio::main]
134/// async fn main() {
135///    let mut reader = PasswdReader::new_at("test_files/passwd",Duration::new(0,0));
136///    let entries = reader.get_entries().await.unwrap();
137///
138///    assert_eq!(3, entries.len());
139///    assert_eq!(Some("user1".to_string()), reader.get_username_by_uid(1000).await.unwrap());
140///    assert_eq!(Some("user2".to_string()), reader.get_username_by_uid(1001).await.unwrap());
141/// }
142/// ```
143pub struct PasswdReader {
144    file: Option<String>,
145    cache_time: Duration,
146    last_check: Instant,
147    passwd: Vec<PasswdEntry>,
148}
149
150impl PasswdReader {
151    ///Creates a new PasswdReader for `/etc/passwd` with a
152    ///specified cache_time in seconds.
153    ///
154    ///Use cache_time with a Duration of 0 to disable caching.
155    pub fn new(cache_time: Duration) -> Self {
156        let last_check = Instant::now() - (cache_time);
157        Self {
158            file: None,
159            cache_time,
160            last_check,
161            passwd: vec![],
162        }
163    }
164
165    ///Creates a new PasswdReader with the
166    /// passwd file at an specified alternative
167    /// location. Uses the specified cache_time in seconds.
168    ///
169    ///Use cache_time with a Duration of 0 to disable caching.
170    pub fn new_at(file: &str, cache_time: Duration) -> Self {
171        let last_check = Instant::now() - (cache_time);
172        Self {
173            file: Some(file.to_string()),
174            cache_time,
175            last_check,
176            passwd: vec![],
177        }
178    }
179
180    async fn refresh_if_needed(&mut self) -> Result<(), std::io::Error> {
181        if Instant::now() < (self.last_check + self.cache_time) {
182            return Ok(());
183        }
184        let contents =
185            tokio::fs::read_to_string(self.file.as_ref().unwrap_or(&"/etc/passwd".to_string()))
186                .await?;
187        self.passwd = contents.lines().filter_map(PasswdEntry::parse).collect();
188        Ok(())
189    }
190
191    ///Get all the entire list of passwd entries
192    pub async fn get_entries(&mut self) -> Result<&Vec<PasswdEntry>, std::io::Error> {
193        self.refresh_if_needed().await?;
194        Ok(&self.passwd)
195    }
196
197    ///Will return an IntoIter to iterate over PasswdEntry
198    pub async fn to_iter(mut self) -> Result<std::vec::IntoIter<PasswdEntry>, std::io::Error> {
199        self.refresh_if_needed().await?;
200        Ok(self.passwd.into_iter())
201    }
202
203    ///Look up a PasswdEntry by username
204    pub async fn get_by_username(
205        &mut self,
206        username: &str,
207    ) -> Result<Option<PasswdEntry>, std::io::Error> {
208        self.refresh_if_needed().await?;
209        Ok(self
210            .passwd
211            .iter()
212            .find(|e| e.username == username)
213            .map(|e| e.to_owned()))
214    }
215
216    ///Look up a PasswdEntry by uid
217    pub async fn get_by_uid(&mut self, uid: u32) -> Result<Option<PasswdEntry>, std::io::Error> {
218        self.refresh_if_needed().await?;
219        Ok(self
220            .passwd
221            .iter()
222            .find(|e| e.uid == uid)
223            .map(|e| e.to_owned()))
224    }
225
226    ///Look up a username by uid
227    pub async fn get_username_by_uid(
228        &mut self,
229        uid: u32,
230    ) -> Result<Option<String>, std::io::Error> {
231        self.refresh_if_needed().await?;
232        Ok(self
233            .passwd
234            .iter()
235            .find(|e| e.uid == uid)
236            .map(|e| e.username.to_owned()))
237    }
238
239    ///Look up a user ID by username
240    pub async fn get_uid_by_username(
241        &mut self,
242        username: &str,
243    ) -> Result<Option<u32>, std::io::Error> {
244        self.refresh_if_needed().await?;
245        Ok(self
246            .passwd
247            .iter()
248            .find(|e| e.username == username)
249            .map(|e| e.uid))
250    }
251}
252
253///The main entity to reaad and lookup groups information. It
254///supports caching the information to avoid having to read
255///the information from disk more than needed.
256/// ```
257/// use async_user_lookup::GroupReader;
258/// use std::time::Duration;
259///
260/// #[tokio::main]
261/// async fn main() {
262///    let mut reader = GroupReader::new_at("test_files/group",Duration::new(0,0));
263///    let groups = reader.get_groups().await.unwrap();
264///
265///    assert_eq!(3, groups.len());
266///    assert_eq!(Some("users".to_string()), reader.get_name_by_gid(100).await.unwrap());
267/// }
268/// ```
269pub struct GroupReader {
270    file: Option<String>,
271    cache_time: Duration,
272    last_check: Instant,
273    groups: Vec<GroupEntry>,
274}
275
276impl GroupReader {
277    ///Creates a new GroupReader for `/etc/group` with a
278    ///specified cache_time in seconds.
279    ///
280    ///Use cache_time with a duration of 0 to disable caching.
281    pub fn new(cache_time: Duration) -> Self {
282        let last_check = Instant::now() - (cache_time);
283        Self {
284            file: None,
285            cache_time,
286            last_check,
287            groups: vec![],
288        }
289    }
290
291    ///Creates a new GroupReader which reads
292    ///the group file at a specific path, and
293    ///uses the specified cache_time in seconds.
294    ///
295    ///Use cache_time with a duration of 0 to disable caching.
296    pub fn new_at(file: &str, cache_time: Duration) -> Self {
297        let last_check = Instant::now() - (cache_time);
298        Self {
299            file: Some(file.to_string()),
300            cache_time,
301            last_check,
302            groups: vec![],
303        }
304    }
305
306    async fn refresh_if_needed(&mut self) -> Result<(), std::io::Error> {
307        if Instant::now() < (self.last_check + self.cache_time) {
308            return Ok(());
309        }
310        let contents =
311            tokio::fs::read_to_string(self.file.as_ref().unwrap_or(&"/etc/group".to_string()))
312                .await?;
313        self.groups = contents.lines().filter_map(GroupEntry::parse).collect();
314        Ok(())
315    }
316
317    ///Get the entire list of group entries
318    pub async fn get_groups(&mut self) -> Result<&Vec<GroupEntry>, std::io::Error> {
319        self.refresh_if_needed().await?;
320        Ok(&self.groups)
321    }
322
323    ///Will return an IntoIter to iterate over GroupEntry
324    pub async fn to_iter(mut self) -> Result<std::vec::IntoIter<GroupEntry>, std::io::Error> {
325        self.refresh_if_needed().await?;
326        Ok(self.groups.into_iter())
327    }
328
329    ///Look up a GroupEntry by the group name
330    pub async fn get_by_name(&mut self, name: &str) -> Result<Option<GroupEntry>, std::io::Error> {
331        self.refresh_if_needed().await?;
332        Ok(self
333            .groups
334            .iter()
335            .find(|e| e.name == name)
336            .map(|e| e.to_owned()))
337    }
338
339    ///Look up a GroupEntry by gid
340    pub async fn get_by_gid(&mut self, gid: u32) -> Result<Option<GroupEntry>, std::io::Error> {
341        self.refresh_if_needed().await?;
342        Ok(self
343            .groups
344            .iter()
345            .find(|e| e.gid == gid)
346            .map(|e| e.to_owned()))
347    }
348
349    ///Look up a group name by gid
350    pub async fn get_name_by_gid(&mut self, gid: u32) -> Result<Option<String>, std::io::Error> {
351        self.refresh_if_needed().await?;
352        Ok(self
353            .groups
354            .iter()
355            .find(|e| e.gid == gid)
356            .map(|e| e.name.to_owned()))
357    }
358
359    ///Look up a group ID by the group name
360    pub async fn get_gid_by_name(&mut self, name: &str) -> Result<Option<u32>, std::io::Error> {
361        self.refresh_if_needed().await?;
362        Ok(self.groups.iter().find(|e| e.name == name).map(|e| e.gid))
363    }
364}