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