]>
Commit | Line | Data |
---|---|---|
7d817b03 DM |
1 | //! Proxmox Backup Server Authentication |
2 | //! | |
3 | //! This library contains helper to authenticate users. | |
4 | ||
177ee20b | 5 | use std::net::IpAddr; |
19dfcfd8 LW |
6 | use std::path::PathBuf; |
7 | use std::pin::Pin; | |
7d817b03 | 8 | |
d97ff8ae | 9 | use anyhow::{bail, Error}; |
19dfcfd8 | 10 | use futures::Future; |
569324cb | 11 | use once_cell::sync::{Lazy, OnceCell}; |
94d6a65d | 12 | use pbs_config::open_backup_lockfile; |
19dfcfd8 | 13 | use proxmox_router::http_bail; |
7d817b03 DM |
14 | use serde_json::json; |
15 | ||
d97ff8ae WB |
16 | use proxmox_auth_api::api::{Authenticator, LockedTfaConfig}; |
17 | use proxmox_auth_api::ticket::{Empty, Ticket}; | |
18 | use proxmox_auth_api::types::Authid; | |
cf71dc24 | 19 | use proxmox_auth_api::{HMACKey, Keyring}; |
d97ff8ae WB |
20 | use proxmox_ldap::{Config, Connection, ConnectionMode}; |
21 | use proxmox_tfa::api::{OpenUserChallengeData, TfaConfig}; | |
22 | ||
c7051f33 CH |
23 | use pbs_api_types::{ |
24 | AdRealmConfig, LdapMode, LdapRealmConfig, OpenIdRealmConfig, RealmRef, Userid, UsernameRef, | |
25 | }; | |
af06decd | 26 | use pbs_buildcfg::configdir; |
e7cb4dc5 | 27 | |
19dfcfd8 | 28 | use crate::auth_helpers; |
19dfcfd8 | 29 | |
d97ff8ae | 30 | pub const TERM_PREFIX: &str = "PBSTERM"; |
7d817b03 | 31 | |
d97ff8ae | 32 | struct PbsAuthenticator; |
7d817b03 | 33 | |
8e772602 | 34 | pub(crate) const SHADOW_CONFIG_FILENAME: &str = configdir!("/shadow.json"); |
94d6a65d | 35 | pub(crate) const SHADOW_LOCK_FILENAME: &str = configdir!("/shadow.json.lock"); |
7d817b03 | 36 | |
d97ff8ae | 37 | impl Authenticator for PbsAuthenticator { |
19dfcfd8 | 38 | fn authenticate_user<'a>( |
8e772602 | 39 | &'a self, |
19dfcfd8 LW |
40 | username: &'a UsernameRef, |
41 | password: &'a str, | |
8e772602 | 42 | client_ip: Option<&'a IpAddr>, |
19dfcfd8 LW |
43 | ) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'a>> { |
44 | Box::pin(async move { | |
45 | let data = proxmox_sys::fs::file_get_json(SHADOW_CONFIG_FILENAME, Some(json!({})))?; | |
46 | match data[username.as_str()].as_str() { | |
47 | None => bail!("no password set"), | |
8e772602 SS |
48 | Some(enc_password) => { |
49 | proxmox_sys::crypt::verify_crypt_pw(password, enc_password)?; | |
50 | ||
51 | // if the password hash is not based on the current hashing function (as | |
52 | // identified by its prefix), rehash the password. | |
53 | if !enc_password.starts_with(proxmox_sys::crypt::HASH_PREFIX) { | |
54 | // only log that we could not upgrade a password, we already know that the | |
55 | // user has a valid password, no reason the deny to log in attempt. | |
56 | if let Err(e) = self.store_password(username, password, client_ip) { | |
57 | log::warn!("could not upgrade a users password! - {e}"); | |
58 | } | |
59 | } | |
8e772602 | 60 | } |
19dfcfd8 LW |
61 | } |
62 | Ok(()) | |
63 | }) | |
7d817b03 DM |
64 | } |
65 | ||
177ee20b WB |
66 | fn store_password( |
67 | &self, | |
68 | username: &UsernameRef, | |
69 | password: &str, | |
70 | _client_ip: Option<&IpAddr>, | |
71 | ) -> Result<(), Error> { | |
d5790a9f | 72 | let enc_password = proxmox_sys::crypt::encrypt_pw(password)?; |
94d6a65d SS |
73 | |
74 | let _guard = open_backup_lockfile(SHADOW_LOCK_FILENAME, None, true); | |
25877d05 | 75 | let mut data = proxmox_sys::fs::file_get_json(SHADOW_CONFIG_FILENAME, Some(json!({})))?; |
e7cb4dc5 | 76 | data[username.as_str()] = enc_password.into(); |
7d817b03 DM |
77 | |
78 | let mode = nix::sys::stat::Mode::from_bits_truncate(0o0600); | |
9531d2c5 | 79 | let options = proxmox_sys::fs::CreateOptions::new() |
7d817b03 DM |
80 | .perm(mode) |
81 | .owner(nix::unistd::ROOT) | |
82 | .group(nix::unistd::Gid::from_raw(0)); | |
83 | ||
84 | let data = serde_json::to_vec_pretty(&data)?; | |
25877d05 | 85 | proxmox_sys::fs::replace_file(SHADOW_CONFIG_FILENAME, &data, options, true)?; |
7d817b03 DM |
86 | |
87 | Ok(()) | |
88 | } | |
a4e871f5 DC |
89 | |
90 | fn remove_password(&self, username: &UsernameRef) -> Result<(), Error> { | |
94d6a65d SS |
91 | let _guard = open_backup_lockfile(SHADOW_LOCK_FILENAME, None, true); |
92 | ||
25877d05 | 93 | let mut data = proxmox_sys::fs::file_get_json(SHADOW_CONFIG_FILENAME, Some(json!({})))?; |
a4e871f5 DC |
94 | if let Some(map) = data.as_object_mut() { |
95 | map.remove(username.as_str()); | |
96 | } | |
97 | ||
98 | let mode = nix::sys::stat::Mode::from_bits_truncate(0o0600); | |
9531d2c5 | 99 | let options = proxmox_sys::fs::CreateOptions::new() |
a4e871f5 DC |
100 | .perm(mode) |
101 | .owner(nix::unistd::ROOT) | |
102 | .group(nix::unistd::Gid::from_raw(0)); | |
103 | ||
104 | let data = serde_json::to_vec_pretty(&data)?; | |
25877d05 | 105 | proxmox_sys::fs::replace_file(SHADOW_CONFIG_FILENAME, &data, options, true)?; |
a4e871f5 DC |
106 | |
107 | Ok(()) | |
108 | } | |
7d817b03 DM |
109 | } |
110 | ||
7c418952 LW |
111 | struct OpenIdAuthenticator(); |
112 | /// When a user is manually added, the lookup_authenticator is called to verify that | |
113 | /// the realm exists. Thus, it is necessary to have an (empty) implementation for | |
114 | /// OpendID as well. | |
d97ff8ae | 115 | impl Authenticator for OpenIdAuthenticator { |
7c418952 LW |
116 | fn authenticate_user<'a>( |
117 | &'a self, | |
118 | _username: &'a UsernameRef, | |
119 | _password: &'a str, | |
177ee20b | 120 | _client_ip: Option<&'a IpAddr>, |
7c418952 LW |
121 | ) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'a>> { |
122 | Box::pin(async move { | |
123 | http_bail!( | |
124 | NOT_IMPLEMENTED, | |
125 | "password authentication is not implemented for OpenID realms" | |
126 | ); | |
127 | }) | |
128 | } | |
129 | ||
177ee20b WB |
130 | fn store_password( |
131 | &self, | |
132 | _username: &UsernameRef, | |
133 | _password: &str, | |
134 | _client_ip: Option<&IpAddr>, | |
135 | ) -> Result<(), Error> { | |
7c418952 LW |
136 | http_bail!( |
137 | NOT_IMPLEMENTED, | |
138 | "storing passwords is not implemented for OpenID realms" | |
139 | ); | |
140 | } | |
141 | ||
142 | fn remove_password(&self, _username: &UsernameRef) -> Result<(), Error> { | |
143 | http_bail!( | |
144 | NOT_IMPLEMENTED, | |
145 | "storing passwords is not implemented for OpenID realms" | |
146 | ); | |
147 | } | |
148 | } | |
149 | ||
19dfcfd8 LW |
150 | #[allow(clippy::upper_case_acronyms)] |
151 | pub struct LdapAuthenticator { | |
152 | config: LdapRealmConfig, | |
153 | } | |
154 | ||
d97ff8ae | 155 | impl Authenticator for LdapAuthenticator { |
19dfcfd8 LW |
156 | /// Authenticate user in LDAP realm |
157 | fn authenticate_user<'a>( | |
158 | &'a self, | |
159 | username: &'a UsernameRef, | |
160 | password: &'a str, | |
177ee20b | 161 | _client_ip: Option<&'a IpAddr>, |
19dfcfd8 LW |
162 | ) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'a>> { |
163 | Box::pin(async move { | |
164 | let ldap_config = Self::api_type_to_config(&self.config)?; | |
165 | let ldap = Connection::new(ldap_config); | |
166 | ldap.authenticate_user(username.as_str(), password).await?; | |
167 | Ok(()) | |
168 | }) | |
169 | } | |
170 | ||
177ee20b WB |
171 | fn store_password( |
172 | &self, | |
173 | _username: &UsernameRef, | |
174 | _password: &str, | |
175 | _client_ip: Option<&IpAddr>, | |
176 | ) -> Result<(), Error> { | |
19dfcfd8 LW |
177 | http_bail!( |
178 | NOT_IMPLEMENTED, | |
179 | "storing passwords is not implemented for LDAP realms" | |
180 | ); | |
181 | } | |
182 | ||
183 | fn remove_password(&self, _username: &UsernameRef) -> Result<(), Error> { | |
184 | http_bail!( | |
185 | NOT_IMPLEMENTED, | |
186 | "removing passwords is not implemented for LDAP realms" | |
187 | ); | |
188 | } | |
189 | } | |
190 | ||
191 | impl LdapAuthenticator { | |
192 | pub fn api_type_to_config(config: &LdapRealmConfig) -> Result<Config, Error> { | |
5210f3b5 SS |
193 | Self::api_type_to_config_with_password( |
194 | config, | |
195 | auth_helpers::get_ldap_bind_password(&config.realm)?, | |
196 | ) | |
197 | } | |
198 | ||
199 | pub fn api_type_to_config_with_password( | |
200 | config: &LdapRealmConfig, | |
201 | password: Option<String>, | |
202 | ) -> Result<Config, Error> { | |
19dfcfd8 LW |
203 | let mut servers = vec![config.server1.clone()]; |
204 | if let Some(server) = &config.server2 { | |
205 | servers.push(server.clone()); | |
206 | } | |
207 | ||
ab09f409 | 208 | let (ca_store, trusted_cert) = lookup_ca_store_or_cert_path(config.capath.as_deref()); |
19dfcfd8 LW |
209 | |
210 | Ok(Config { | |
211 | servers, | |
212 | port: config.port, | |
213 | user_attr: config.user_attr.clone(), | |
214 | base_dn: config.base_dn.clone(), | |
215 | bind_dn: config.bind_dn.clone(), | |
5210f3b5 | 216 | bind_password: password, |
30c34f0b | 217 | tls_mode: ldap_to_conn_mode(config.mode.unwrap_or_default()), |
19dfcfd8 LW |
218 | verify_certificate: config.verify.unwrap_or_default(), |
219 | additional_trusted_certificates: trusted_cert, | |
220 | certificate_store_path: ca_store, | |
221 | }) | |
222 | } | |
223 | } | |
224 | ||
c7051f33 CH |
225 | pub struct AdAuthenticator { |
226 | config: AdRealmConfig, | |
227 | } | |
228 | ||
229 | impl AdAuthenticator { | |
230 | pub fn api_type_to_config(config: &AdRealmConfig) -> Result<Config, Error> { | |
231 | Self::api_type_to_config_with_password( | |
232 | config, | |
233 | auth_helpers::get_ldap_bind_password(&config.realm)?, | |
234 | ) | |
235 | } | |
236 | ||
237 | pub fn api_type_to_config_with_password( | |
238 | config: &AdRealmConfig, | |
239 | password: Option<String>, | |
240 | ) -> Result<Config, Error> { | |
241 | let mut servers = vec![config.server1.clone()]; | |
242 | if let Some(server) = &config.server2 { | |
243 | servers.push(server.clone()); | |
244 | } | |
245 | ||
246 | let (ca_store, trusted_cert) = lookup_ca_store_or_cert_path(config.capath.as_deref()); | |
247 | ||
248 | Ok(Config { | |
249 | servers, | |
250 | port: config.port, | |
251 | user_attr: "sAMAccountName".to_owned(), | |
252 | base_dn: config.base_dn.clone().unwrap_or_default(), | |
253 | bind_dn: config.bind_dn.clone(), | |
254 | bind_password: password, | |
255 | tls_mode: ldap_to_conn_mode(config.mode.unwrap_or_default()), | |
256 | verify_certificate: config.verify.unwrap_or_default(), | |
257 | additional_trusted_certificates: trusted_cert, | |
258 | certificate_store_path: ca_store, | |
259 | }) | |
260 | } | |
261 | } | |
262 | ||
263 | impl Authenticator for AdAuthenticator { | |
264 | /// Authenticate user in AD realm | |
265 | fn authenticate_user<'a>( | |
266 | &'a self, | |
267 | username: &'a UsernameRef, | |
268 | password: &'a str, | |
269 | _client_ip: Option<&'a IpAddr>, | |
270 | ) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'a>> { | |
271 | Box::pin(async move { | |
272 | let ldap_config = Self::api_type_to_config(&self.config)?; | |
273 | let ldap = Connection::new(ldap_config); | |
274 | ldap.authenticate_user(username.as_str(), password).await?; | |
275 | Ok(()) | |
276 | }) | |
277 | } | |
278 | ||
279 | fn store_password( | |
280 | &self, | |
281 | _username: &UsernameRef, | |
282 | _password: &str, | |
283 | _client_ip: Option<&IpAddr>, | |
284 | ) -> Result<(), Error> { | |
285 | http_bail!( | |
286 | NOT_IMPLEMENTED, | |
287 | "storing passwords is not implemented for Active Directory realms" | |
288 | ); | |
289 | } | |
290 | ||
291 | fn remove_password(&self, _username: &UsernameRef) -> Result<(), Error> { | |
292 | http_bail!( | |
293 | NOT_IMPLEMENTED, | |
294 | "removing passwords is not implemented for Active Directory realms" | |
295 | ); | |
296 | } | |
297 | } | |
298 | ||
30c34f0b CH |
299 | fn ldap_to_conn_mode(mode: LdapMode) -> ConnectionMode { |
300 | match mode { | |
301 | LdapMode::Ldap => ConnectionMode::Ldap, | |
302 | LdapMode::StartTls => ConnectionMode::StartTls, | |
303 | LdapMode::Ldaps => ConnectionMode::Ldaps, | |
304 | } | |
305 | } | |
306 | ||
ab09f409 CH |
307 | fn lookup_ca_store_or_cert_path(capath: Option<&str>) -> (Option<PathBuf>, Option<Vec<PathBuf>>) { |
308 | if let Some(capath) = capath { | |
309 | let path = PathBuf::from(capath); | |
310 | if path.is_dir() { | |
311 | (Some(path), None) | |
312 | } else { | |
313 | (None, Some(vec![path])) | |
314 | } | |
315 | } else { | |
316 | (None, None) | |
317 | } | |
318 | } | |
319 | ||
6685122c | 320 | /// Lookup the authenticator for the specified realm |
d97ff8ae | 321 | pub(crate) fn lookup_authenticator( |
19dfcfd8 | 322 | realm: &RealmRef, |
d97ff8ae | 323 | ) -> Result<Box<dyn Authenticator + Send + Sync>, Error> { |
e7cb4dc5 | 324 | match realm.as_str() { |
d97ff8ae WB |
325 | "pam" => Ok(Box::new(proxmox_auth_api::Pam::new("proxmox-backup-auth"))), |
326 | "pbs" => Ok(Box::new(PbsAuthenticator)), | |
19dfcfd8 LW |
327 | realm => { |
328 | let (domains, _digest) = pbs_config::domains::config()?; | |
329 | if let Ok(config) = domains.lookup::<LdapRealmConfig>("ldap", realm) { | |
330 | Ok(Box::new(LdapAuthenticator { config })) | |
d07013a4 CH |
331 | } else if let Ok(config) = domains.lookup::<AdRealmConfig>("ad", realm) { |
332 | Ok(Box::new(AdAuthenticator { config })) | |
7c418952 LW |
333 | } else if domains.lookup::<OpenIdRealmConfig>("openid", realm).is_ok() { |
334 | Ok(Box::new(OpenIdAuthenticator())) | |
19dfcfd8 LW |
335 | } else { |
336 | bail!("unknown realm '{}'", realm); | |
337 | } | |
338 | } | |
7d817b03 DM |
339 | } |
340 | } | |
341 | ||
4b40148c | 342 | /// Authenticate users |
d97ff8ae | 343 | pub(crate) fn authenticate_user<'a>( |
19dfcfd8 LW |
344 | userid: &'a Userid, |
345 | password: &'a str, | |
177ee20b | 346 | client_ip: Option<&'a IpAddr>, |
19dfcfd8 LW |
347 | ) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'a>> { |
348 | Box::pin(async move { | |
349 | lookup_authenticator(userid.realm())? | |
177ee20b | 350 | .authenticate_user(userid.name(), password, client_ip) |
19dfcfd8 LW |
351 | .await?; |
352 | Ok(()) | |
353 | }) | |
7d817b03 | 354 | } |
d97ff8ae | 355 | |
569324cb | 356 | static PRIVATE_KEYRING: Lazy<Keyring> = |
048a81cc | 357 | Lazy::new(|| Keyring::with_private_key(crate::auth_helpers::private_auth_key().clone())); |
569324cb | 358 | static PUBLIC_KEYRING: Lazy<Keyring> = |
048a81cc | 359 | Lazy::new(|| Keyring::with_public_key(crate::auth_helpers::public_auth_key().clone())); |
d97ff8ae WB |
360 | static AUTH_CONTEXT: OnceCell<PbsAuthContext> = OnceCell::new(); |
361 | ||
362 | pub fn setup_auth_context(use_private_key: bool) { | |
363 | let keyring = if use_private_key { | |
569324cb | 364 | &*PRIVATE_KEYRING |
d97ff8ae | 365 | } else { |
569324cb | 366 | &*PUBLIC_KEYRING |
d97ff8ae WB |
367 | }; |
368 | ||
369 | AUTH_CONTEXT | |
370 | .set(PbsAuthContext { | |
371 | keyring, | |
cf71dc24 | 372 | csrf_secret: crate::auth_helpers::csrf_secret(), |
d97ff8ae WB |
373 | }) |
374 | .map_err(drop) | |
375 | .expect("auth context setup twice"); | |
376 | ||
377 | proxmox_auth_api::set_auth_context(AUTH_CONTEXT.get().unwrap()); | |
378 | } | |
379 | ||
569324cb | 380 | pub(crate) fn private_auth_keyring() -> &'static Keyring { |
12c841b4 | 381 | &PRIVATE_KEYRING |
569324cb WB |
382 | } |
383 | ||
384 | pub(crate) fn public_auth_keyring() -> &'static Keyring { | |
12c841b4 | 385 | &PUBLIC_KEYRING |
d97ff8ae WB |
386 | } |
387 | ||
388 | struct PbsAuthContext { | |
569324cb | 389 | keyring: &'static Keyring, |
cf71dc24 | 390 | csrf_secret: &'static HMACKey, |
d97ff8ae WB |
391 | } |
392 | ||
393 | impl proxmox_auth_api::api::AuthContext for PbsAuthContext { | |
394 | fn lookup_realm(&self, realm: &RealmRef) -> Option<Box<dyn Authenticator + Send + Sync>> { | |
395 | lookup_authenticator(realm).ok() | |
396 | } | |
397 | ||
398 | /// Get the current authentication keyring. | |
399 | fn keyring(&self) -> &Keyring { | |
569324cb | 400 | self.keyring |
d97ff8ae WB |
401 | } |
402 | ||
403 | /// The auth prefix without the separating colon. Eg. `"PBS"`. | |
404 | fn auth_prefix(&self) -> &'static str { | |
405 | "PBS" | |
406 | } | |
407 | ||
408 | /// API token prefix (without the `'='`). | |
409 | fn auth_token_prefix(&self) -> &'static str { | |
410 | "PBSAPIToken" | |
411 | } | |
412 | ||
413 | /// Auth cookie name. | |
414 | fn auth_cookie_name(&self) -> &'static str { | |
415 | "PBSAuthCookie" | |
416 | } | |
417 | ||
418 | /// Check if a userid is enabled and return a [`UserInformation`] handle. | |
419 | fn auth_id_is_active(&self, auth_id: &Authid) -> Result<bool, Error> { | |
420 | Ok(pbs_config::CachedUserInfo::new()?.is_active_auth_id(auth_id)) | |
421 | } | |
422 | ||
423 | /// Access the TFA config with an exclusive lock. | |
424 | fn tfa_config_write_lock(&self) -> Result<Box<dyn LockedTfaConfig>, Error> { | |
425 | Ok(Box::new(PbsLockedTfaConfig { | |
0b449fe8 | 426 | _lock: crate::config::tfa::write_lock()?, |
d97ff8ae WB |
427 | config: crate::config::tfa::read()?, |
428 | })) | |
429 | } | |
430 | ||
431 | /// CSRF prevention token secret data. | |
cf71dc24 SS |
432 | fn csrf_secret(&self) -> &'static HMACKey { |
433 | self.csrf_secret | |
d97ff8ae WB |
434 | } |
435 | ||
436 | /// Verify a token secret. | |
437 | fn verify_token_secret(&self, token_id: &Authid, token_secret: &str) -> Result<(), Error> { | |
438 | pbs_config::token_shadow::verify_secret(token_id, token_secret) | |
439 | } | |
440 | ||
441 | /// Check path based tickets. (Used for terminal tickets). | |
442 | fn check_path_ticket( | |
443 | &self, | |
444 | userid: &Userid, | |
445 | password: &str, | |
446 | path: String, | |
447 | privs: String, | |
448 | port: u16, | |
449 | ) -> Result<Option<bool>, Error> { | |
450 | if !password.starts_with("PBSTERM:") { | |
451 | return Ok(None); | |
452 | } | |
453 | ||
454 | if let Ok(Empty) = Ticket::parse(password).and_then(|ticket| { | |
455 | ticket.verify( | |
cd0daa8b | 456 | self.keyring, |
d97ff8ae WB |
457 | TERM_PREFIX, |
458 | Some(&crate::tools::ticket::term_aad(userid, &path, port)), | |
459 | ) | |
460 | }) { | |
461 | let user_info = pbs_config::CachedUserInfo::new()?; | |
462 | let auth_id = Authid::from(userid.clone()); | |
463 | for (name, privilege) in pbs_api_types::PRIVILEGES { | |
464 | if *name == privs { | |
465 | let mut path_vec = Vec::new(); | |
466 | for part in path.split('/') { | |
467 | if !part.is_empty() { | |
468 | path_vec.push(part); | |
469 | } | |
470 | } | |
471 | user_info.check_privs(&auth_id, &path_vec, *privilege, false)?; | |
472 | return Ok(Some(true)); | |
473 | } | |
474 | } | |
475 | } | |
476 | ||
477 | Ok(Some(false)) | |
478 | } | |
479 | } | |
480 | ||
481 | struct PbsLockedTfaConfig { | |
482 | _lock: pbs_config::BackupLockGuard, | |
483 | config: TfaConfig, | |
484 | } | |
485 | ||
486 | static USER_ACCESS: crate::config::tfa::UserAccess = crate::config::tfa::UserAccess; | |
487 | ||
488 | impl LockedTfaConfig for PbsLockedTfaConfig { | |
489 | fn config_mut(&mut self) -> (&dyn OpenUserChallengeData, &mut TfaConfig) { | |
490 | (&USER_ACCESS, &mut self.config) | |
491 | } | |
492 | ||
493 | fn save_config(&mut self) -> Result<(), Error> { | |
494 | crate::config::tfa::write(&self.config) | |
495 | } | |
496 | } |