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