kanidm_unix_resolver/idprovider/
system.rs

1use hashbrown::HashMap;
2use std::sync::Arc;
3use time::OffsetDateTime;
4use tokio::sync::Mutex;
5
6use super::interface::{AuthCredHandler, AuthRequest, Id, IdpError};
7use kanidm_unix_common::unix_passwd::{CryptPw, EtcGroup, EtcShadow, EtcUser};
8use kanidm_unix_common::unix_proto::PamAuthRequest;
9use kanidm_unix_common::unix_proto::{NssGroup, NssUser};
10
11// The minimum GID that Kanidm will consider for creating a UPG
12const SYSTEM_GID_BOUNDARY: u32 = 1000;
13
14pub struct SystemProviderInternal {
15    users: HashMap<Id, Arc<EtcUser>>,
16    user_list: Vec<Arc<EtcUser>>,
17    groups: HashMap<Id, Arc<EtcGroup>>,
18    group_list: Vec<Arc<EtcGroup>>,
19
20    shadow_enabled: bool,
21    shadow: HashMap<String, Arc<Shadow>>,
22}
23
24pub enum SystemProviderAuthInit {
25    Begin {
26        next_request: AuthRequest,
27        cred_handler: AuthCredHandler,
28        shadow: Arc<Shadow>,
29    },
30    ShadowMissing,
31    CredentialsUnavailable,
32    Expired,
33    Ignore,
34}
35
36pub enum SystemProviderSession {
37    Start,
38    // Not sure that we need this
39    // StartCreateHome(HomeDirectoryInfo),
40    Ignore,
41}
42
43pub enum SystemAuthResult {
44    Denied,
45    Success,
46    Next(AuthRequest),
47}
48
49#[allow(dead_code)]
50struct AgingPolicy {
51    last_change: time::OffsetDateTime,
52    min_password_change: time::OffsetDateTime,
53    max_password_change: Option<time::OffsetDateTime>,
54    warning_period_start: Option<time::OffsetDateTime>,
55    inactivity_period_deadline: Option<time::OffsetDateTime>,
56}
57
58impl AgingPolicy {
59    fn new(
60        last_change: time::OffsetDateTime,
61        days_min_password_age: i64,
62        days_max_password_age: Option<i64>,
63        days_warning_period: i64,
64        days_inactivity_period: Option<i64>,
65    ) -> Self {
66        let min_password_change = last_change + time::Duration::days(days_min_password_age);
67
68        let max_password_change =
69            days_max_password_age.map(|max| last_change + time::Duration::days(max));
70
71        let (warning_period_start, inactivity_period_deadline) =
72            if let Some(expiry) = max_password_change.as_ref() {
73                // Both of these values are relative to the max age, so without a max age
74                // they are meaningless.
75
76                // If the warning isnt 0
77                let warning = if days_warning_period != 0 {
78                    // This is a subtract
79                    Some(*expiry - time::Duration::days(days_warning_period))
80                } else {
81                    None
82                };
83
84                let inactive =
85                    days_inactivity_period.map(|inactive| *expiry + time::Duration::days(inactive));
86
87                (warning, inactive)
88            } else {
89                (None, None)
90            };
91
92        AgingPolicy {
93            last_change,
94            min_password_change,
95            max_password_change,
96            warning_period_start,
97            inactivity_period_deadline,
98        }
99    }
100}
101
102pub struct Shadow {
103    crypt_pw: CryptPw,
104    #[allow(dead_code)]
105    aging_policy: Option<AgingPolicy>,
106    expiration_date: Option<time::OffsetDateTime>,
107}
108
109impl Shadow {
110    pub fn auth_step(
111        &self,
112        cred_handler: &mut AuthCredHandler,
113        pam_next_req: PamAuthRequest,
114    ) -> SystemAuthResult {
115        match (cred_handler, pam_next_req) {
116            (AuthCredHandler::Password, PamAuthRequest::Password { cred }) => {
117                if self.crypt_pw.check_pw(&cred) {
118                    SystemAuthResult::Success
119                } else {
120                    SystemAuthResult::Denied
121                }
122            }
123            _ => SystemAuthResult::Denied,
124        }
125    }
126}
127
128pub struct SystemProvider {
129    inner: Mutex<SystemProviderInternal>,
130}
131
132impl SystemProvider {
133    pub fn new() -> Result<Self, IdpError> {
134        Ok(SystemProvider {
135            inner: Mutex::new(SystemProviderInternal {
136                users: Default::default(),
137                user_list: Default::default(),
138                groups: Default::default(),
139                group_list: Default::default(),
140                shadow_enabled: Default::default(),
141                shadow: Default::default(),
142            }),
143        })
144    }
145
146    pub async fn reload(&self, users: Vec<EtcUser>, shadow: Vec<EtcShadow>, groups: Vec<EtcGroup>) {
147        let mut system_ids_txn = self.inner.lock().await;
148        system_ids_txn.users.clear();
149        system_ids_txn.user_list.clear();
150        system_ids_txn.groups.clear();
151        system_ids_txn.group_list.clear();
152        system_ids_txn.shadow.clear();
153
154        system_ids_txn.shadow_enabled = !shadow.is_empty();
155
156        let s_iter = shadow.into_iter().filter_map(|shadow_entry| {
157            let EtcShadow {
158                name,
159                password,
160                epoch_change_seconds,
161                days_min_password_age,
162                days_max_password_age,
163                days_warning_period,
164                days_inactivity_period,
165                epoch_expire_seconds,
166                flag_reserved: _,
167            } = shadow_entry;
168
169            if password.is_valid() {
170                let aging_policy = epoch_change_seconds.map(|change_seconds| {
171                    AgingPolicy::new(
172                        change_seconds,
173                        days_min_password_age,
174                        days_max_password_age,
175                        days_warning_period,
176                        days_inactivity_period,
177                    )
178                });
179
180                let expiration_date = epoch_expire_seconds;
181
182                Some((
183                    name,
184                    Arc::new(Shadow {
185                        crypt_pw: password,
186                        aging_policy,
187                        expiration_date,
188                    }),
189                ))
190            } else {
191                // Invalid password, skip the account
192                debug!(?name, "account password is invalid.");
193                None
194            }
195        });
196
197        system_ids_txn.shadow.extend(s_iter);
198
199        for group in groups {
200            let name = Id::Name(group.name.clone());
201            let gid = Id::Gid(group.gid);
202            let group = Arc::new(group);
203
204            if system_ids_txn.groups.insert(name, group.clone()).is_some() {
205                error!(name = %group.name, gid = %group.gid, "group name conflict");
206            };
207            if system_ids_txn.groups.insert(gid, group.clone()).is_some() {
208                error!(name = %group.name, gid = %group.gid, "group id conflict");
209            }
210            system_ids_txn.group_list.push(group);
211        }
212
213        for user in users {
214            let name = Id::Name(user.name.clone());
215            let uid = Id::Gid(user.uid);
216            let gid = Id::Gid(user.gid);
217
218            // Security checks.
219            if user.uid != user.gid {
220                warn!(name = %user.name, uid = %user.uid, gid = %user.gid, "user uid and gid are not the same, this may be a security risk!");
221            } else if let Some(group) = system_ids_txn.groups.get(&gid) {
222                if group.name != user.name {
223                    warn!(name = %user.name, uid = %user.uid, gid = %user.gid, "user private group does not appear to have the same name as the user, this may be a security risk!");
224                }
225                if !(group.members.is_empty()
226                    || (group.members.len() == 1 && group.members.first() == Some(&user.name)))
227                {
228                    warn!(name = %user.name, uid = %user.uid, gid = %user.gid, members = ?group.members, "user private group must not have members, THIS IS A SECURITY RISK!");
229                }
230            } else if user.uid < SYSTEM_GID_BOUNDARY {
231                warn!(name = %user.name, uid = %user.uid, gid = %user.gid, "user private group is not present on system, ignoring as this is a system account.");
232            } else {
233                info!(name = %user.name, uid = %user.uid, gid = %user.gid, "user private group is not present on system, synthesising it.");
234                let group = Arc::new(EtcGroup {
235                    name: user.name.clone(),
236                    password: String::new(),
237                    gid: user.gid,
238                    members: vec![user.name.clone()],
239                });
240
241                system_ids_txn.groups.insert(name.clone(), group.clone());
242                system_ids_txn.groups.insert(gid.clone(), group.clone());
243                system_ids_txn.group_list.push(group);
244            }
245
246            let user = Arc::new(user);
247            if system_ids_txn.users.insert(name, user.clone()).is_some() {
248                error!(name = %user.name, uid = %user.uid, "user name conflict");
249            }
250            if system_ids_txn.users.insert(uid, user.clone()).is_some() {
251                error!(name = %user.name, uid = %user.uid, "user id conflict");
252            }
253            system_ids_txn.user_list.push(user);
254        }
255    }
256
257    pub async fn auth_init(
258        &self,
259        account_id: &Id,
260        current_time: OffsetDateTime,
261    ) -> SystemProviderAuthInit {
262        let inner = self.inner.lock().await;
263
264        let Some(user) = inner.users.get(account_id) else {
265            // Not for us, not a system user.
266            return SystemProviderAuthInit::Ignore;
267        };
268
269        if !inner.shadow_enabled {
270            // We were unable to read shadow, so we can't proceed. Return that we don't know
271            // the user.
272            return SystemProviderAuthInit::ShadowMissing;
273        }
274
275        // Does the user have a related shadow entry?
276        let Some(shadow) = inner.shadow.get(user.name.as_str()) else {
277            return SystemProviderAuthInit::CredentialsUnavailable;
278        };
279
280        // If they do, is there a unix style auth policy attached?
281        if let Some(expire) = shadow.expiration_date.as_ref() {
282            if current_time >= *expire {
283                return SystemProviderAuthInit::Expired;
284            }
285        }
286
287        // Good to go, lets try to auth them.
288        // Today, we only support password, but we can support more in future.
289        let cred_handler = AuthCredHandler::Password;
290
291        let next_request = AuthRequest::Password;
292
293        SystemProviderAuthInit::Begin {
294            next_request,
295            cred_handler,
296            shadow: shadow.clone(),
297        }
298    }
299
300    pub async fn authorise(&self, account_id: &Id) -> Option<bool> {
301        let inner = self.inner.lock().await;
302        if inner.users.contains_key(account_id) {
303            Some(true)
304        } else {
305            None
306        }
307    }
308
309    pub async fn begin_session(&self, account_id: &Id) -> SystemProviderSession {
310        let inner = self.inner.lock().await;
311        if inner.users.contains_key(account_id) {
312            SystemProviderSession::Start
313        } else {
314            SystemProviderSession::Ignore
315        }
316    }
317
318    pub async fn contains_group(&self, account_id: &Id) -> bool {
319        let inner = self.inner.lock().await;
320        inner.groups.contains_key(account_id)
321    }
322
323    pub async fn get_nssaccount(&self, account_id: &Id) -> Option<NssUser> {
324        let inner = self.inner.lock().await;
325        inner.users.get(account_id).map(NssUser::from)
326    }
327
328    pub async fn get_nssaccounts(&self) -> Vec<NssUser> {
329        let inner = self.inner.lock().await;
330        inner.user_list.iter().map(NssUser::from).collect()
331    }
332
333    pub async fn get_nssgroup(&self, grp_id: &Id) -> Option<NssGroup> {
334        let inner = self.inner.lock().await;
335        inner.groups.get(grp_id).map(NssGroup::from)
336    }
337
338    pub async fn get_nssgroups(&self) -> Vec<NssGroup> {
339        let inner = self.inner.lock().await;
340        inner.group_list.iter().map(NssGroup::from).collect()
341    }
342}