]> git.proxmox.com Git - proxmox-backup.git/blame - src/auth.rs
auth: factor out CA store and cert lookup into own fn
[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};
19dfcfd8 12use proxmox_router::http_bail;
7d817b03
DM
13use serde_json::json;
14
d97ff8ae
WB
15use proxmox_auth_api::api::{Authenticator, LockedTfaConfig};
16use proxmox_auth_api::ticket::{Empty, Ticket};
17use proxmox_auth_api::types::Authid;
18use proxmox_auth_api::Keyring;
19use proxmox_ldap::{Config, Connection, ConnectionMode};
20use proxmox_tfa::api::{OpenUserChallengeData, TfaConfig};
21
7c418952 22use pbs_api_types::{LdapMode, LdapRealmConfig, OpenIdRealmConfig, RealmRef, Userid, UsernameRef};
af06decd 23use pbs_buildcfg::configdir;
e7cb4dc5 24
19dfcfd8 25use crate::auth_helpers;
19dfcfd8 26
d97ff8ae 27pub const TERM_PREFIX: &str = "PBSTERM";
7d817b03 28
d97ff8ae 29struct PbsAuthenticator;
7d817b03 30
e7cb4dc5 31const SHADOW_CONFIG_FILENAME: &str = configdir!("/shadow.json");
7d817b03 32
d97ff8ae 33impl Authenticator for PbsAuthenticator {
19dfcfd8
LW
34 fn authenticate_user<'a>(
35 &self,
36 username: &'a UsernameRef,
37 password: &'a str,
177ee20b 38 _client_ip: Option<&'a IpAddr>,
19dfcfd8
LW
39 ) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'a>> {
40 Box::pin(async move {
41 let data = proxmox_sys::fs::file_get_json(SHADOW_CONFIG_FILENAME, Some(json!({})))?;
42 match data[username.as_str()].as_str() {
43 None => bail!("no password set"),
44 Some(enc_password) => proxmox_sys::crypt::verify_crypt_pw(password, enc_password)?,
45 }
46 Ok(())
47 })
7d817b03
DM
48 }
49
177ee20b
WB
50 fn store_password(
51 &self,
52 username: &UsernameRef,
53 password: &str,
54 _client_ip: Option<&IpAddr>,
55 ) -> Result<(), Error> {
d5790a9f 56 let enc_password = proxmox_sys::crypt::encrypt_pw(password)?;
25877d05 57 let mut data = proxmox_sys::fs::file_get_json(SHADOW_CONFIG_FILENAME, Some(json!({})))?;
e7cb4dc5 58 data[username.as_str()] = enc_password.into();
7d817b03
DM
59
60 let mode = nix::sys::stat::Mode::from_bits_truncate(0o0600);
9531d2c5 61 let options = proxmox_sys::fs::CreateOptions::new()
7d817b03
DM
62 .perm(mode)
63 .owner(nix::unistd::ROOT)
64 .group(nix::unistd::Gid::from_raw(0));
65
66 let data = serde_json::to_vec_pretty(&data)?;
25877d05 67 proxmox_sys::fs::replace_file(SHADOW_CONFIG_FILENAME, &data, options, true)?;
7d817b03
DM
68
69 Ok(())
70 }
a4e871f5
DC
71
72 fn remove_password(&self, username: &UsernameRef) -> Result<(), Error> {
25877d05 73 let mut data = proxmox_sys::fs::file_get_json(SHADOW_CONFIG_FILENAME, Some(json!({})))?;
a4e871f5
DC
74 if let Some(map) = data.as_object_mut() {
75 map.remove(username.as_str());
76 }
77
78 let mode = nix::sys::stat::Mode::from_bits_truncate(0o0600);
9531d2c5 79 let options = proxmox_sys::fs::CreateOptions::new()
a4e871f5
DC
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)?;
a4e871f5
DC
86
87 Ok(())
88 }
7d817b03
DM
89}
90
7c418952
LW
91struct OpenIdAuthenticator();
92/// When a user is manually added, the lookup_authenticator is called to verify that
93/// the realm exists. Thus, it is necessary to have an (empty) implementation for
94/// OpendID as well.
d97ff8ae 95impl Authenticator for OpenIdAuthenticator {
7c418952
LW
96 fn authenticate_user<'a>(
97 &'a self,
98 _username: &'a UsernameRef,
99 _password: &'a str,
177ee20b 100 _client_ip: Option<&'a IpAddr>,
7c418952
LW
101 ) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'a>> {
102 Box::pin(async move {
103 http_bail!(
104 NOT_IMPLEMENTED,
105 "password authentication is not implemented for OpenID realms"
106 );
107 })
108 }
109
177ee20b
WB
110 fn store_password(
111 &self,
112 _username: &UsernameRef,
113 _password: &str,
114 _client_ip: Option<&IpAddr>,
115 ) -> Result<(), Error> {
7c418952
LW
116 http_bail!(
117 NOT_IMPLEMENTED,
118 "storing passwords is not implemented for OpenID realms"
119 );
120 }
121
122 fn remove_password(&self, _username: &UsernameRef) -> Result<(), Error> {
123 http_bail!(
124 NOT_IMPLEMENTED,
125 "storing passwords is not implemented for OpenID realms"
126 );
127 }
128}
129
19dfcfd8
LW
130#[allow(clippy::upper_case_acronyms)]
131pub struct LdapAuthenticator {
132 config: LdapRealmConfig,
133}
134
d97ff8ae 135impl Authenticator for LdapAuthenticator {
19dfcfd8
LW
136 /// Authenticate user in LDAP realm
137 fn authenticate_user<'a>(
138 &'a self,
139 username: &'a UsernameRef,
140 password: &'a str,
177ee20b 141 _client_ip: Option<&'a IpAddr>,
19dfcfd8
LW
142 ) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'a>> {
143 Box::pin(async move {
144 let ldap_config = Self::api_type_to_config(&self.config)?;
145 let ldap = Connection::new(ldap_config);
146 ldap.authenticate_user(username.as_str(), password).await?;
147 Ok(())
148 })
149 }
150
177ee20b
WB
151 fn store_password(
152 &self,
153 _username: &UsernameRef,
154 _password: &str,
155 _client_ip: Option<&IpAddr>,
156 ) -> Result<(), Error> {
19dfcfd8
LW
157 http_bail!(
158 NOT_IMPLEMENTED,
159 "storing passwords is not implemented for LDAP realms"
160 );
161 }
162
163 fn remove_password(&self, _username: &UsernameRef) -> Result<(), Error> {
164 http_bail!(
165 NOT_IMPLEMENTED,
166 "removing passwords is not implemented for LDAP realms"
167 );
168 }
169}
170
171impl LdapAuthenticator {
172 pub fn api_type_to_config(config: &LdapRealmConfig) -> Result<Config, Error> {
5210f3b5
SS
173 Self::api_type_to_config_with_password(
174 config,
175 auth_helpers::get_ldap_bind_password(&config.realm)?,
176 )
177 }
178
179 pub fn api_type_to_config_with_password(
180 config: &LdapRealmConfig,
181 password: Option<String>,
182 ) -> Result<Config, Error> {
19dfcfd8
LW
183 let mut servers = vec![config.server1.clone()];
184 if let Some(server) = &config.server2 {
185 servers.push(server.clone());
186 }
187
ab09f409 188 let (ca_store, trusted_cert) = lookup_ca_store_or_cert_path(config.capath.as_deref());
19dfcfd8
LW
189
190 Ok(Config {
191 servers,
192 port: config.port,
193 user_attr: config.user_attr.clone(),
194 base_dn: config.base_dn.clone(),
195 bind_dn: config.bind_dn.clone(),
5210f3b5 196 bind_password: password,
30c34f0b 197 tls_mode: ldap_to_conn_mode(config.mode.unwrap_or_default()),
19dfcfd8
LW
198 verify_certificate: config.verify.unwrap_or_default(),
199 additional_trusted_certificates: trusted_cert,
200 certificate_store_path: ca_store,
201 })
202 }
203}
204
30c34f0b
CH
205fn ldap_to_conn_mode(mode: LdapMode) -> ConnectionMode {
206 match mode {
207 LdapMode::Ldap => ConnectionMode::Ldap,
208 LdapMode::StartTls => ConnectionMode::StartTls,
209 LdapMode::Ldaps => ConnectionMode::Ldaps,
210 }
211}
212
ab09f409
CH
213fn lookup_ca_store_or_cert_path(capath: Option<&str>) -> (Option<PathBuf>, Option<Vec<PathBuf>>) {
214 if let Some(capath) = capath {
215 let path = PathBuf::from(capath);
216 if path.is_dir() {
217 (Some(path), None)
218 } else {
219 (None, Some(vec![path]))
220 }
221 } else {
222 (None, None)
223 }
224}
225
6685122c 226/// Lookup the authenticator for the specified realm
d97ff8ae 227pub(crate) fn lookup_authenticator(
19dfcfd8 228 realm: &RealmRef,
d97ff8ae 229) -> Result<Box<dyn Authenticator + Send + Sync>, Error> {
e7cb4dc5 230 match realm.as_str() {
d97ff8ae
WB
231 "pam" => Ok(Box::new(proxmox_auth_api::Pam::new("proxmox-backup-auth"))),
232 "pbs" => Ok(Box::new(PbsAuthenticator)),
19dfcfd8
LW
233 realm => {
234 let (domains, _digest) = pbs_config::domains::config()?;
235 if let Ok(config) = domains.lookup::<LdapRealmConfig>("ldap", realm) {
236 Ok(Box::new(LdapAuthenticator { config }))
7c418952
LW
237 } else if domains.lookup::<OpenIdRealmConfig>("openid", realm).is_ok() {
238 Ok(Box::new(OpenIdAuthenticator()))
19dfcfd8
LW
239 } else {
240 bail!("unknown realm '{}'", realm);
241 }
242 }
7d817b03
DM
243 }
244}
245
4b40148c 246/// Authenticate users
d97ff8ae 247pub(crate) fn authenticate_user<'a>(
19dfcfd8
LW
248 userid: &'a Userid,
249 password: &'a str,
177ee20b 250 client_ip: Option<&'a IpAddr>,
19dfcfd8
LW
251) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'a>> {
252 Box::pin(async move {
253 lookup_authenticator(userid.realm())?
177ee20b 254 .authenticate_user(userid.name(), password, client_ip)
19dfcfd8
LW
255 .await?;
256 Ok(())
257 })
7d817b03 258}
d97ff8ae 259
569324cb
WB
260static PRIVATE_KEYRING: Lazy<Keyring> =
261 Lazy::new(|| Keyring::with_private_key(crate::auth_helpers::private_auth_key().clone().into()));
262static PUBLIC_KEYRING: Lazy<Keyring> =
263 Lazy::new(|| Keyring::with_public_key(crate::auth_helpers::public_auth_key().clone().into()));
d97ff8ae
WB
264static AUTH_CONTEXT: OnceCell<PbsAuthContext> = OnceCell::new();
265
266pub fn setup_auth_context(use_private_key: bool) {
267 let keyring = if use_private_key {
569324cb 268 &*PRIVATE_KEYRING
d97ff8ae 269 } else {
569324cb 270 &*PUBLIC_KEYRING
d97ff8ae
WB
271 };
272
273 AUTH_CONTEXT
274 .set(PbsAuthContext {
275 keyring,
276 csrf_secret: crate::auth_helpers::csrf_secret().to_vec(),
277 })
278 .map_err(drop)
279 .expect("auth context setup twice");
280
281 proxmox_auth_api::set_auth_context(AUTH_CONTEXT.get().unwrap());
282}
283
569324cb 284pub(crate) fn private_auth_keyring() -> &'static Keyring {
12c841b4 285 &PRIVATE_KEYRING
569324cb
WB
286}
287
288pub(crate) fn public_auth_keyring() -> &'static Keyring {
12c841b4 289 &PUBLIC_KEYRING
d97ff8ae
WB
290}
291
292struct PbsAuthContext {
569324cb 293 keyring: &'static Keyring,
d97ff8ae
WB
294 csrf_secret: Vec<u8>,
295}
296
297impl proxmox_auth_api::api::AuthContext for PbsAuthContext {
298 fn lookup_realm(&self, realm: &RealmRef) -> Option<Box<dyn Authenticator + Send + Sync>> {
299 lookup_authenticator(realm).ok()
300 }
301
302 /// Get the current authentication keyring.
303 fn keyring(&self) -> &Keyring {
569324cb 304 self.keyring
d97ff8ae
WB
305 }
306
307 /// The auth prefix without the separating colon. Eg. `"PBS"`.
308 fn auth_prefix(&self) -> &'static str {
309 "PBS"
310 }
311
312 /// API token prefix (without the `'='`).
313 fn auth_token_prefix(&self) -> &'static str {
314 "PBSAPIToken"
315 }
316
317 /// Auth cookie name.
318 fn auth_cookie_name(&self) -> &'static str {
319 "PBSAuthCookie"
320 }
321
322 /// Check if a userid is enabled and return a [`UserInformation`] handle.
323 fn auth_id_is_active(&self, auth_id: &Authid) -> Result<bool, Error> {
324 Ok(pbs_config::CachedUserInfo::new()?.is_active_auth_id(auth_id))
325 }
326
327 /// Access the TFA config with an exclusive lock.
328 fn tfa_config_write_lock(&self) -> Result<Box<dyn LockedTfaConfig>, Error> {
329 Ok(Box::new(PbsLockedTfaConfig {
330 _lock: crate::config::tfa::read_lock()?,
331 config: crate::config::tfa::read()?,
332 }))
333 }
334
335 /// CSRF prevention token secret data.
336 fn csrf_secret(&self) -> &[u8] {
337 &self.csrf_secret
338 }
339
340 /// Verify a token secret.
341 fn verify_token_secret(&self, token_id: &Authid, token_secret: &str) -> Result<(), Error> {
342 pbs_config::token_shadow::verify_secret(token_id, token_secret)
343 }
344
345 /// Check path based tickets. (Used for terminal tickets).
346 fn check_path_ticket(
347 &self,
348 userid: &Userid,
349 password: &str,
350 path: String,
351 privs: String,
352 port: u16,
353 ) -> Result<Option<bool>, Error> {
354 if !password.starts_with("PBSTERM:") {
355 return Ok(None);
356 }
357
358 if let Ok(Empty) = Ticket::parse(password).and_then(|ticket| {
359 ticket.verify(
cd0daa8b 360 self.keyring,
d97ff8ae
WB
361 TERM_PREFIX,
362 Some(&crate::tools::ticket::term_aad(userid, &path, port)),
363 )
364 }) {
365 let user_info = pbs_config::CachedUserInfo::new()?;
366 let auth_id = Authid::from(userid.clone());
367 for (name, privilege) in pbs_api_types::PRIVILEGES {
368 if *name == privs {
369 let mut path_vec = Vec::new();
370 for part in path.split('/') {
371 if !part.is_empty() {
372 path_vec.push(part);
373 }
374 }
375 user_info.check_privs(&auth_id, &path_vec, *privilege, false)?;
376 return Ok(Some(true));
377 }
378 }
379 }
380
381 Ok(Some(false))
382 }
383}
384
385struct PbsLockedTfaConfig {
386 _lock: pbs_config::BackupLockGuard,
387 config: TfaConfig,
388}
389
390static USER_ACCESS: crate::config::tfa::UserAccess = crate::config::tfa::UserAccess;
391
392impl LockedTfaConfig for PbsLockedTfaConfig {
393 fn config_mut(&mut self) -> (&dyn OpenUserChallengeData, &mut TfaConfig) {
394 (&USER_ACCESS, &mut self.config)
395 }
396
397 fn save_config(&mut self) -> Result<(), Error> {
398 crate::config::tfa::write(&self.config)
399 }
400}