pam_kanidm/
core.rs

1use crate::constants::PamResultCode;
2use crate::module::PamResult;
3use crate::pam::ModuleOptions;
4use kanidm_unix_common::client_sync::DaemonClientBlocking;
5use kanidm_unix_common::unix_config::PamNssConfig;
6use kanidm_unix_common::unix_passwd::{
7    read_etc_passwd_file, read_etc_shadow_file, EtcShadow, EtcUser,
8};
9use kanidm_unix_common::unix_proto::{ClientRequest, ClientResponse};
10use kanidm_unix_common::unix_proto::{
11    DeviceAuthorizationResponse, PamAuthRequest, PamAuthResponse, PamServiceInfo,
12};
13use std::time::Duration;
14use time::OffsetDateTime;
15
16use tracing::{debug, error};
17
18#[cfg(test)]
19use kanidm_unix_common::client_sync::UnixStream;
20
21pub enum RequestOptions {
22    Main {
23        config_path: &'static str,
24    },
25    #[cfg(test)]
26    Test {
27        socket: Option<UnixStream>,
28        users: Vec<EtcUser>,
29        // groups: Vec<EtcGroup>,
30        shadow: Vec<EtcShadow>,
31    },
32}
33
34enum Source {
35    Daemon(DaemonClientBlocking),
36    Fallback {
37        users: Vec<EtcUser>,
38        // groups: Vec<EtcGroup>,
39        shadow: Vec<EtcShadow>,
40    },
41}
42
43impl RequestOptions {
44    fn connect_to_daemon(self) -> Source {
45        match self {
46            RequestOptions::Main { config_path } => {
47                let maybe_client = PamNssConfig::new()
48                    .read_options_from_optional_config(config_path)
49                    .ok()
50                    .and_then(|cfg| {
51                        DaemonClientBlocking::new(cfg.sock_path.as_str(), cfg.unix_sock_timeout)
52                            .ok()
53                    });
54
55                if let Some(client) = maybe_client {
56                    Source::Daemon(client)
57                } else {
58                    let users = read_etc_passwd_file("/etc/passwd").unwrap_or_default();
59                    // let groups = read_etc_group_file("/etc/group").unwrap_or_default();
60                    let shadow = read_etc_shadow_file("/etc/shadow").unwrap_or_default();
61                    Source::Fallback {
62                        users,
63                        // groups,
64                        shadow,
65                    }
66                }
67            }
68            #[cfg(test)]
69            RequestOptions::Test {
70                socket,
71                users,
72                // groups,
73                shadow,
74            } => {
75                if let Some(socket) = socket {
76                    Source::Daemon(DaemonClientBlocking::from(socket))
77                } else {
78                    Source::Fallback { users, shadow }
79                }
80            }
81        }
82    }
83}
84
85pub trait PamHandler {
86    fn account_id(&self) -> PamResult<String>;
87
88    fn service_info(&self) -> PamResult<PamServiceInfo>;
89
90    fn authtok(&self) -> PamResult<Option<String>>;
91
92    /// Display a message to the user.
93    fn message(&self, prompt: &str) -> PamResult<()>;
94
95    /// Display a device grant request to the user.
96    fn message_device_grant(&self, data: &DeviceAuthorizationResponse) -> PamResult<()>;
97
98    /// Request a password from the user.
99    fn prompt_for_password(&self) -> PamResult<Option<String>>;
100
101    fn prompt_for_pin(&self, msg: Option<&str>) -> PamResult<Option<String>>;
102
103    fn prompt_for_mfacode(&self) -> PamResult<Option<String>>;
104}
105
106pub fn sm_authenticate_connected<P: PamHandler>(
107    pamh: &P,
108    opts: &ModuleOptions,
109    _current_time: OffsetDateTime,
110    mut daemon_client: DaemonClientBlocking,
111) -> PamResultCode {
112    let info = match pamh.service_info() {
113        Ok(info) => info,
114        Err(e) => {
115            error!(err = ?e, "get_pam_info");
116            return e;
117        }
118    };
119
120    let account_id = match pamh.account_id() {
121        Ok(acc) => acc,
122        Err(err) => return err,
123    };
124
125    let mut timeout: Option<u64> = None;
126    let mut active_polling_interval = Duration::from_secs(1);
127
128    let mut stacked_authtok = if opts.use_first_pass {
129        match pamh.authtok() {
130            Ok(authtok) => authtok,
131            Err(err) => return err,
132        }
133    } else {
134        None
135    };
136
137    let mut req = ClientRequest::PamAuthenticateInit { account_id, info };
138
139    loop {
140        let client_response = match daemon_client.call_and_wait(&req, timeout) {
141            Ok(r) => r,
142            Err(err) => {
143                // Something unrecoverable occurred, bail and stop everything
144                error!(?err, "PAM_AUTH_ERR");
145                return PamResultCode::PAM_AUTH_ERR;
146            }
147        };
148
149        match client_response {
150            ClientResponse::PamAuthenticateStepResponse(PamAuthResponse::Success) => {
151                return PamResultCode::PAM_SUCCESS;
152            }
153            ClientResponse::PamAuthenticateStepResponse(PamAuthResponse::Denied) => {
154                return PamResultCode::PAM_AUTH_ERR;
155            }
156            ClientResponse::PamAuthenticateStepResponse(PamAuthResponse::Unknown) => {
157                if opts.ignore_unknown_user {
158                    return PamResultCode::PAM_IGNORE;
159                } else {
160                    return PamResultCode::PAM_USER_UNKNOWN;
161                }
162            }
163            ClientResponse::PamAuthenticateStepResponse(PamAuthResponse::Password) => {
164                let mut authtok = None;
165                std::mem::swap(&mut authtok, &mut stacked_authtok);
166
167                let cred = if let Some(cred) = authtok {
168                    cred
169                } else {
170                    match pamh.prompt_for_password() {
171                        Ok(Some(cred)) => cred,
172                        Ok(None) => return PamResultCode::PAM_CRED_INSUFFICIENT,
173                        Err(err) => return err,
174                    }
175                };
176
177                // Now setup the request for the next loop.
178                timeout = None;
179                req = ClientRequest::PamAuthenticateStep(PamAuthRequest::Password { cred });
180                continue;
181            }
182            ClientResponse::PamAuthenticateStepResponse(
183                PamAuthResponse::DeviceAuthorizationGrant { data },
184            ) => {
185                if let Err(err) = pamh.message_device_grant(&data) {
186                    return err;
187                };
188
189                timeout = Some(u64::from(data.expires_in));
190                req =
191                    ClientRequest::PamAuthenticateStep(PamAuthRequest::DeviceAuthorizationGrant {
192                        data,
193                    });
194                continue;
195            }
196            ClientResponse::PamAuthenticateStepResponse(PamAuthResponse::MFACode { msg: _ }) => {
197                let cred = match pamh.prompt_for_mfacode() {
198                    Ok(Some(cred)) => cred,
199                    Ok(None) => return PamResultCode::PAM_CRED_INSUFFICIENT,
200                    Err(err) => return err,
201                };
202
203                // Now setup the request for the next loop.
204                timeout = None;
205                req = ClientRequest::PamAuthenticateStep(PamAuthRequest::MFACode { cred });
206                continue;
207            }
208            ClientResponse::PamAuthenticateStepResponse(PamAuthResponse::MFAPoll {
209                msg,
210                polling_interval,
211            }) => {
212                if let Err(err) = pamh.message(msg.as_str()) {
213                    if opts.debug {
214                        println!("Message prompt failed");
215                    }
216                    return err;
217                }
218
219                active_polling_interval = Duration::from_secs(polling_interval.into());
220
221                timeout = None;
222                req = ClientRequest::PamAuthenticateStep(PamAuthRequest::MFAPoll);
223                // We don't need to actually sleep here as we immediately will poll and then go
224                // into the MFAPollWait response below.
225            }
226            ClientResponse::PamAuthenticateStepResponse(PamAuthResponse::MFAPollWait) => {
227                // Counter intuitive, but we don't need a max poll attempts here because
228                // if the resolver goes away, then this will error on the sock and
229                // will shutdown. This allows the resolver to dynamically extend the
230                // timeout if needed, and removes logic from the front end.
231                std::thread::sleep(active_polling_interval);
232                timeout = None;
233                req = ClientRequest::PamAuthenticateStep(PamAuthRequest::MFAPoll);
234            }
235
236            ClientResponse::PamAuthenticateStepResponse(PamAuthResponse::SetupPin { msg }) => {
237                if let Err(err) = pamh.message(msg.as_str()) {
238                    return err;
239                }
240
241                let mut pin;
242                let mut confirm;
243
244                loop {
245                    pin = match pamh.prompt_for_pin(Some("New PIN: ")) {
246                        Ok(Some(p)) => p,
247                        Ok(None) => {
248                            debug!("no pin");
249                            return PamResultCode::PAM_CRED_INSUFFICIENT;
250                        }
251                        Err(err) => {
252                            debug!("unable to get pin");
253                            return err;
254                        }
255                    };
256
257                    confirm = match pamh.prompt_for_pin(Some("Confirm PIN: ")) {
258                        Ok(Some(p)) => p,
259                        Ok(None) => {
260                            debug!("no pin");
261                            return PamResultCode::PAM_CRED_INSUFFICIENT;
262                        }
263                        Err(err) => {
264                            debug!("unable to get pin");
265                            return err;
266                        }
267                    };
268
269                    if pin == confirm {
270                        break;
271                    } else if let Err(err) = pamh.message("Inputs did not match. Try again.") {
272                        return err;
273                    }
274                }
275
276                // Now setup the request for the next loop.
277                timeout = None;
278                req = ClientRequest::PamAuthenticateStep(PamAuthRequest::SetupPin { pin });
279                continue;
280            }
281            ClientResponse::PamAuthenticateStepResponse(PamAuthResponse::Pin) => {
282                let mut authtok = None;
283                std::mem::swap(&mut authtok, &mut stacked_authtok);
284
285                let cred = if let Some(cred) = authtok {
286                    cred
287                } else {
288                    match pamh.prompt_for_pin(None) {
289                        Ok(Some(cred)) => cred,
290                        Ok(None) => return PamResultCode::PAM_CRED_INSUFFICIENT,
291                        Err(err) => return err,
292                    }
293                };
294
295                // Now setup the request for the next loop.
296                timeout = None;
297                req = ClientRequest::PamAuthenticateStep(PamAuthRequest::Pin { cred });
298                continue;
299            }
300
301            ClientResponse::Error(err) => {
302                error!("Error from kanidm-unixd: {}", err);
303                return PamResultCode::PAM_AUTH_ERR;
304            }
305            ClientResponse::Ok
306            | ClientResponse::SshKeys(_)
307            | ClientResponse::NssAccounts(_)
308            | ClientResponse::NssAccount(_)
309            | ClientResponse::NssGroups(_)
310            | ClientResponse::PamStatus(_)
311            | ClientResponse::ProviderStatus(_)
312            | ClientResponse::NssGroup(_) => {
313                debug!("PamResultCode::PAM_AUTH_ERR");
314                return PamResultCode::PAM_AUTH_ERR;
315            }
316        }
317    } // while true, continue calling PamAuthenticateStep until we get a decision.
318}
319
320pub fn sm_authenticate_fallback<P: PamHandler>(
321    pamh: &P,
322    opts: &ModuleOptions,
323    current_time: OffsetDateTime,
324    users: Vec<EtcUser>,
325    shadow: Vec<EtcShadow>,
326) -> PamResultCode {
327    let account_id = match pamh.account_id() {
328        Ok(acc) => acc,
329        Err(err) => return err,
330    };
331
332    let user = users.into_iter().find(|etcuser| etcuser.name == account_id);
333
334    let shadow = shadow
335        .into_iter()
336        .find(|etcshadow| etcshadow.name == account_id);
337
338    let (_user, shadow) = match (user, shadow) {
339        (Some(user), Some(shadow)) => (user, shadow),
340        _ => {
341            if opts.ignore_unknown_user {
342                debug!("PamResultCode::PAM_IGNORE");
343                return PamResultCode::PAM_IGNORE;
344            } else {
345                debug!("PamResultCode::PAM_USER_UNKNOWN");
346                return PamResultCode::PAM_USER_UNKNOWN;
347            }
348        }
349    };
350
351    let expiration_date = shadow
352        .epoch_expire_date
353        .map(|expire| OffsetDateTime::UNIX_EPOCH + time::Duration::days(expire));
354
355    if let Some(expire) = expiration_date {
356        if current_time >= expire {
357            debug!("PamResultCode::PAM_ACCT_EXPIRED");
358            return PamResultCode::PAM_ACCT_EXPIRED;
359        }
360    };
361
362    // All checks passed! We can now proceed to authenticate the account.
363    let mut stacked_authtok = if opts.use_first_pass {
364        match pamh.authtok() {
365            Ok(authtok) => authtok,
366            Err(err) => return err,
367        }
368    } else {
369        None
370    };
371
372    let mut authtok = None;
373    std::mem::swap(&mut authtok, &mut stacked_authtok);
374
375    let cred = if let Some(cred) = authtok {
376        cred
377    } else {
378        match pamh.prompt_for_password() {
379            Ok(Some(cred)) => cred,
380            Ok(None) => return PamResultCode::PAM_CRED_INSUFFICIENT,
381            Err(err) => return err,
382        }
383    };
384
385    if shadow.password.check_pw(cred.as_str()) {
386        PamResultCode::PAM_SUCCESS
387    } else {
388        PamResultCode::PAM_AUTH_ERR
389    }
390}
391
392pub fn sm_authenticate<P: PamHandler>(
393    pamh: &P,
394    opts: &ModuleOptions,
395    req_opt: RequestOptions,
396    current_time: OffsetDateTime,
397) -> PamResultCode {
398    match req_opt.connect_to_daemon() {
399        Source::Daemon(daemon_client) => {
400            sm_authenticate_connected(pamh, opts, current_time, daemon_client)
401        }
402        Source::Fallback { users, shadow } => {
403            sm_authenticate_fallback(pamh, opts, current_time, users, shadow)
404        }
405    }
406}
407
408pub fn acct_mgmt<P: PamHandler>(
409    pamh: &P,
410    opts: &ModuleOptions,
411    req_opt: RequestOptions,
412    current_time: OffsetDateTime,
413) -> PamResultCode {
414    let account_id = match pamh.account_id() {
415        Ok(acc) => acc,
416        Err(err) => return err,
417    };
418
419    match req_opt.connect_to_daemon() {
420        Source::Daemon(mut daemon_client) => {
421            let req = ClientRequest::PamAccountAllowed(account_id);
422            match daemon_client.call_and_wait(&req, None) {
423                Ok(r) => match r {
424                    ClientResponse::PamStatus(Some(true)) => {
425                        debug!("PamResultCode::PAM_SUCCESS");
426                        PamResultCode::PAM_SUCCESS
427                    }
428                    ClientResponse::PamStatus(Some(false)) => {
429                        debug!("PamResultCode::PAM_AUTH_ERR");
430                        PamResultCode::PAM_AUTH_ERR
431                    }
432                    ClientResponse::PamStatus(None) => {
433                        if opts.ignore_unknown_user {
434                            debug!("PamResultCode::PAM_IGNORE");
435                            PamResultCode::PAM_IGNORE
436                        } else {
437                            debug!("PamResultCode::PAM_USER_UNKNOWN");
438                            PamResultCode::PAM_USER_UNKNOWN
439                        }
440                    }
441                    _ => {
442                        // unexpected response.
443                        error!(err = ?r, "PAM_IGNORE, unexpected resolver response");
444                        PamResultCode::PAM_IGNORE
445                    }
446                },
447                Err(e) => {
448                    error!(err = ?e, "PamResultCode::PAM_IGNORE");
449                    PamResultCode::PAM_IGNORE
450                }
451            }
452        }
453        Source::Fallback { users, shadow } => {
454            let user = users.into_iter().find(|etcuser| etcuser.name == account_id);
455
456            let shadow = shadow
457                .into_iter()
458                .find(|etcshadow| etcshadow.name == account_id);
459
460            let (_user, shadow) = match (user, shadow) {
461                (Some(user), Some(shadow)) => (user, shadow),
462                _ => {
463                    if opts.ignore_unknown_user {
464                        debug!("PamResultCode::PAM_IGNORE");
465                        return PamResultCode::PAM_IGNORE;
466                    } else {
467                        debug!("PamResultCode::PAM_USER_UNKNOWN");
468                        return PamResultCode::PAM_USER_UNKNOWN;
469                    }
470                }
471            };
472
473            let expiration_date = shadow
474                .epoch_expire_date
475                .map(|expire| OffsetDateTime::UNIX_EPOCH + time::Duration::days(expire));
476
477            if let Some(expire) = expiration_date {
478                if current_time >= expire {
479                    debug!("PamResultCode::PAM_ACCT_EXPIRED");
480                    return PamResultCode::PAM_ACCT_EXPIRED;
481                }
482            };
483
484            // All checks passed!
485
486            debug!("PAM_SUCCESS");
487            PamResultCode::PAM_SUCCESS
488        }
489    }
490}
491
492pub fn sm_open_session<P: PamHandler>(
493    pamh: &P,
494    _opts: &ModuleOptions,
495    req_opt: RequestOptions,
496) -> PamResultCode {
497    let account_id = match pamh.account_id() {
498        Ok(acc) => acc,
499        Err(err) => return err,
500    };
501
502    match req_opt.connect_to_daemon() {
503        Source::Daemon(mut daemon_client) => {
504            let req = ClientRequest::PamAccountBeginSession(account_id);
505
506            match daemon_client.call_and_wait(&req, None) {
507                Ok(ClientResponse::Ok) => {
508                    debug!("PAM_SUCCESS");
509                    PamResultCode::PAM_SUCCESS
510                }
511                other => {
512                    debug!(err = ?other, "PAM_IGNORE");
513                    PamResultCode::PAM_IGNORE
514                }
515            }
516        }
517        Source::Fallback {
518            users: _,
519            shadow: _,
520        } => {
521            debug!("PAM_SUCCESS");
522            PamResultCode::PAM_SUCCESS
523        }
524    }
525}
526
527pub fn sm_close_session<P: PamHandler>(_pamh: &P, _opts: &ModuleOptions) -> PamResultCode {
528    PamResultCode::PAM_SUCCESS
529}
530
531pub fn sm_chauthtok<P: PamHandler>(_pamh: &P, _opts: &ModuleOptions) -> PamResultCode {
532    PamResultCode::PAM_IGNORE
533}
534
535pub fn sm_setcred<P: PamHandler>(_pamh: &P, _opts: &ModuleOptions) -> PamResultCode {
536    PamResultCode::PAM_SUCCESS
537}