kanidm_unix_resolver/idprovider/
system.rs

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