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