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 shadow: Vec<EtcShadow>,
31 },
32}
33
34enum Source {
35 Daemon(DaemonClientBlocking),
36 Fallback {
37 users: Vec<EtcUser>,
38 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 shadow = read_etc_shadow_file("/etc/shadow").unwrap_or_default();
61 Source::Fallback {
62 users,
63 shadow,
65 }
66 }
67 }
68 #[cfg(test)]
69 RequestOptions::Test {
70 socket,
71 users,
72 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 fn message(&self, prompt: &str) -> PamResult<()>;
94
95 fn message_device_grant(&self, data: &DeviceAuthorizationResponse) -> PamResult<()>;
97
98 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 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 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 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 }
226 ClientResponse::PamAuthenticateStepResponse(PamAuthResponse::MFAPollWait) => {
227 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 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 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 } }
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 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 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 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}