]> git.proxmox.com Git - proxmox-backup.git/blame - src/auth.rs
docs: add note for not using remote storages
[proxmox-backup.git] / src / auth.rs
CommitLineData
7d817b03
DM
1//! Proxmox Backup Server Authentication
2//!
3//! This library contains helper to authenticate users.
4
177ee20b 5use std::net::IpAddr;
19dfcfd8
LW
6use std::path::PathBuf;
7use std::pin::Pin;
7d817b03 8
d97ff8ae 9use anyhow::{bail, Error};
19dfcfd8 10use futures::Future;
569324cb 11use once_cell::sync::{Lazy, OnceCell};
94d6a65d 12use pbs_config::open_backup_lockfile;
19dfcfd8 13use proxmox_router::http_bail;
7d817b03
DM
14use serde_json::json;
15
d97ff8ae
WB
16use proxmox_auth_api::api::{Authenticator, LockedTfaConfig};
17use proxmox_auth_api::ticket::{Empty, Ticket};
18use proxmox_auth_api::types::Authid;
cf71dc24 19use proxmox_auth_api::{HMACKey, Keyring};
d97ff8ae
WB
20use proxmox_ldap::{Config, Connection, ConnectionMode};
21use proxmox_tfa::api::{OpenUserChallengeData, TfaConfig};
22
c7051f33
CH
23use pbs_api_types::{
24 AdRealmConfig, LdapMode, LdapRealmConfig, OpenIdRealmConfig, RealmRef, Userid, UsernameRef,
25};
af06decd 26use pbs_buildcfg::configdir;
e7cb4dc5 27
19dfcfd8 28use crate::auth_helpers;
19dfcfd8 29
d97ff8ae 30pub const TERM_PREFIX: &str = "PBSTERM";
7d817b03 31
d97ff8ae 32struct PbsAuthenticator;
7d817b03 33
8e772602 34pub(crate) const SHADOW_CONFIG_FILENAME: &str = configdir!("/shadow.json");
94d6a65d 35pub(crate) const SHADOW_LOCK_FILENAME: &str = configdir!("/shadow.json.lock");
7d817b03 36
d97ff8ae 37impl 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
111struct 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 115impl 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)]
151pub struct LdapAuthenticator {
152 config: LdapRealmConfig,
153}
154
d97ff8ae 155impl 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
191impl 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
225pub struct AdAuthenticator {
226 config: AdRealmConfig,
227}
228
229impl 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
263impl 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
299fn 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
307fn 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 321pub(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 343pub(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 356static PRIVATE_KEYRING: Lazy<Keyring> =
048a81cc 357 Lazy::new(|| Keyring::with_private_key(crate::auth_helpers::private_auth_key().clone()));
569324cb 358static PUBLIC_KEYRING: Lazy<Keyring> =
048a81cc 359 Lazy::new(|| Keyring::with_public_key(crate::auth_helpers::public_auth_key().clone()));
d97ff8ae
WB
360static AUTH_CONTEXT: OnceCell<PbsAuthContext> = OnceCell::new();
361
362pub 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 380pub(crate) fn private_auth_keyring() -> &'static Keyring {
12c841b4 381 &PRIVATE_KEYRING
569324cb
WB
382}
383
384pub(crate) fn public_auth_keyring() -> &'static Keyring {
12c841b4 385 &PUBLIC_KEYRING
d97ff8ae
WB
386}
387
388struct PbsAuthContext {
569324cb 389 keyring: &'static Keyring,
cf71dc24 390 csrf_secret: &'static HMACKey,
d97ff8ae
WB
391}
392
393impl 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
481struct PbsLockedTfaConfig {
482 _lock: pbs_config::BackupLockGuard,
483 config: TfaConfig,
484}
485
486static USER_ACCESS: crate::config::tfa::UserAccess = crate::config::tfa::UserAccess;
487
488impl 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}