]>
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}; |
19dfcfd8 | 12 | use proxmox_router::http_bail; |
7d817b03 DM |
13 | use serde_json::json; |
14 | ||
d97ff8ae WB |
15 | use proxmox_auth_api::api::{Authenticator, LockedTfaConfig}; |
16 | use proxmox_auth_api::ticket::{Empty, Ticket}; | |
17 | use proxmox_auth_api::types::Authid; | |
18 | use proxmox_auth_api::Keyring; | |
19 | use proxmox_ldap::{Config, Connection, ConnectionMode}; | |
20 | use proxmox_tfa::api::{OpenUserChallengeData, TfaConfig}; | |
21 | ||
7c418952 | 22 | use pbs_api_types::{LdapMode, LdapRealmConfig, OpenIdRealmConfig, RealmRef, Userid, UsernameRef}; |
af06decd | 23 | use pbs_buildcfg::configdir; |
e7cb4dc5 | 24 | |
19dfcfd8 | 25 | use crate::auth_helpers; |
19dfcfd8 | 26 | |
d97ff8ae | 27 | pub const TERM_PREFIX: &str = "PBSTERM"; |
7d817b03 | 28 | |
d97ff8ae | 29 | struct PbsAuthenticator; |
7d817b03 | 30 | |
e7cb4dc5 | 31 | const SHADOW_CONFIG_FILENAME: &str = configdir!("/shadow.json"); |
7d817b03 | 32 | |
d97ff8ae | 33 | impl 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 |
91 | struct 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 | 95 | impl 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)] |
131 | pub struct LdapAuthenticator { | |
132 | config: LdapRealmConfig, | |
133 | } | |
134 | ||
d97ff8ae | 135 | impl 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 | ||
171 | impl 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 | ||
19dfcfd8 LW |
188 | let (ca_store, trusted_cert) = if let Some(capath) = config.capath.as_deref() { |
189 | let path = PathBuf::from(capath); | |
190 | if path.is_dir() { | |
191 | (Some(path), None) | |
192 | } else { | |
193 | (None, Some(vec![path])) | |
194 | } | |
195 | } else { | |
196 | (None, None) | |
197 | }; | |
198 | ||
199 | Ok(Config { | |
200 | servers, | |
201 | port: config.port, | |
202 | user_attr: config.user_attr.clone(), | |
203 | base_dn: config.base_dn.clone(), | |
204 | bind_dn: config.bind_dn.clone(), | |
5210f3b5 | 205 | bind_password: password, |
30c34f0b | 206 | tls_mode: ldap_to_conn_mode(config.mode.unwrap_or_default()), |
19dfcfd8 LW |
207 | verify_certificate: config.verify.unwrap_or_default(), |
208 | additional_trusted_certificates: trusted_cert, | |
209 | certificate_store_path: ca_store, | |
210 | }) | |
211 | } | |
212 | } | |
213 | ||
30c34f0b CH |
214 | fn ldap_to_conn_mode(mode: LdapMode) -> ConnectionMode { |
215 | match mode { | |
216 | LdapMode::Ldap => ConnectionMode::Ldap, | |
217 | LdapMode::StartTls => ConnectionMode::StartTls, | |
218 | LdapMode::Ldaps => ConnectionMode::Ldaps, | |
219 | } | |
220 | } | |
221 | ||
6685122c | 222 | /// Lookup the authenticator for the specified realm |
d97ff8ae | 223 | pub(crate) fn lookup_authenticator( |
19dfcfd8 | 224 | realm: &RealmRef, |
d97ff8ae | 225 | ) -> Result<Box<dyn Authenticator + Send + Sync>, Error> { |
e7cb4dc5 | 226 | match realm.as_str() { |
d97ff8ae WB |
227 | "pam" => Ok(Box::new(proxmox_auth_api::Pam::new("proxmox-backup-auth"))), |
228 | "pbs" => Ok(Box::new(PbsAuthenticator)), | |
19dfcfd8 LW |
229 | realm => { |
230 | let (domains, _digest) = pbs_config::domains::config()?; | |
231 | if let Ok(config) = domains.lookup::<LdapRealmConfig>("ldap", realm) { | |
232 | Ok(Box::new(LdapAuthenticator { config })) | |
7c418952 LW |
233 | } else if domains.lookup::<OpenIdRealmConfig>("openid", realm).is_ok() { |
234 | Ok(Box::new(OpenIdAuthenticator())) | |
19dfcfd8 LW |
235 | } else { |
236 | bail!("unknown realm '{}'", realm); | |
237 | } | |
238 | } | |
7d817b03 DM |
239 | } |
240 | } | |
241 | ||
4b40148c | 242 | /// Authenticate users |
d97ff8ae | 243 | pub(crate) fn authenticate_user<'a>( |
19dfcfd8 LW |
244 | userid: &'a Userid, |
245 | password: &'a str, | |
177ee20b | 246 | client_ip: Option<&'a IpAddr>, |
19dfcfd8 LW |
247 | ) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'a>> { |
248 | Box::pin(async move { | |
249 | lookup_authenticator(userid.realm())? | |
177ee20b | 250 | .authenticate_user(userid.name(), password, client_ip) |
19dfcfd8 LW |
251 | .await?; |
252 | Ok(()) | |
253 | }) | |
7d817b03 | 254 | } |
d97ff8ae | 255 | |
569324cb WB |
256 | static PRIVATE_KEYRING: Lazy<Keyring> = |
257 | Lazy::new(|| Keyring::with_private_key(crate::auth_helpers::private_auth_key().clone().into())); | |
258 | static PUBLIC_KEYRING: Lazy<Keyring> = | |
259 | Lazy::new(|| Keyring::with_public_key(crate::auth_helpers::public_auth_key().clone().into())); | |
d97ff8ae WB |
260 | static AUTH_CONTEXT: OnceCell<PbsAuthContext> = OnceCell::new(); |
261 | ||
262 | pub fn setup_auth_context(use_private_key: bool) { | |
263 | let keyring = if use_private_key { | |
569324cb | 264 | &*PRIVATE_KEYRING |
d97ff8ae | 265 | } else { |
569324cb | 266 | &*PUBLIC_KEYRING |
d97ff8ae WB |
267 | }; |
268 | ||
269 | AUTH_CONTEXT | |
270 | .set(PbsAuthContext { | |
271 | keyring, | |
272 | csrf_secret: crate::auth_helpers::csrf_secret().to_vec(), | |
273 | }) | |
274 | .map_err(drop) | |
275 | .expect("auth context setup twice"); | |
276 | ||
277 | proxmox_auth_api::set_auth_context(AUTH_CONTEXT.get().unwrap()); | |
278 | } | |
279 | ||
569324cb | 280 | pub(crate) fn private_auth_keyring() -> &'static Keyring { |
12c841b4 | 281 | &PRIVATE_KEYRING |
569324cb WB |
282 | } |
283 | ||
284 | pub(crate) fn public_auth_keyring() -> &'static Keyring { | |
12c841b4 | 285 | &PUBLIC_KEYRING |
d97ff8ae WB |
286 | } |
287 | ||
288 | struct PbsAuthContext { | |
569324cb | 289 | keyring: &'static Keyring, |
d97ff8ae WB |
290 | csrf_secret: Vec<u8>, |
291 | } | |
292 | ||
293 | impl proxmox_auth_api::api::AuthContext for PbsAuthContext { | |
294 | fn lookup_realm(&self, realm: &RealmRef) -> Option<Box<dyn Authenticator + Send + Sync>> { | |
295 | lookup_authenticator(realm).ok() | |
296 | } | |
297 | ||
298 | /// Get the current authentication keyring. | |
299 | fn keyring(&self) -> &Keyring { | |
569324cb | 300 | self.keyring |
d97ff8ae WB |
301 | } |
302 | ||
303 | /// The auth prefix without the separating colon. Eg. `"PBS"`. | |
304 | fn auth_prefix(&self) -> &'static str { | |
305 | "PBS" | |
306 | } | |
307 | ||
308 | /// API token prefix (without the `'='`). | |
309 | fn auth_token_prefix(&self) -> &'static str { | |
310 | "PBSAPIToken" | |
311 | } | |
312 | ||
313 | /// Auth cookie name. | |
314 | fn auth_cookie_name(&self) -> &'static str { | |
315 | "PBSAuthCookie" | |
316 | } | |
317 | ||
318 | /// Check if a userid is enabled and return a [`UserInformation`] handle. | |
319 | fn auth_id_is_active(&self, auth_id: &Authid) -> Result<bool, Error> { | |
320 | Ok(pbs_config::CachedUserInfo::new()?.is_active_auth_id(auth_id)) | |
321 | } | |
322 | ||
323 | /// Access the TFA config with an exclusive lock. | |
324 | fn tfa_config_write_lock(&self) -> Result<Box<dyn LockedTfaConfig>, Error> { | |
325 | Ok(Box::new(PbsLockedTfaConfig { | |
326 | _lock: crate::config::tfa::read_lock()?, | |
327 | config: crate::config::tfa::read()?, | |
328 | })) | |
329 | } | |
330 | ||
331 | /// CSRF prevention token secret data. | |
332 | fn csrf_secret(&self) -> &[u8] { | |
333 | &self.csrf_secret | |
334 | } | |
335 | ||
336 | /// Verify a token secret. | |
337 | fn verify_token_secret(&self, token_id: &Authid, token_secret: &str) -> Result<(), Error> { | |
338 | pbs_config::token_shadow::verify_secret(token_id, token_secret) | |
339 | } | |
340 | ||
341 | /// Check path based tickets. (Used for terminal tickets). | |
342 | fn check_path_ticket( | |
343 | &self, | |
344 | userid: &Userid, | |
345 | password: &str, | |
346 | path: String, | |
347 | privs: String, | |
348 | port: u16, | |
349 | ) -> Result<Option<bool>, Error> { | |
350 | if !password.starts_with("PBSTERM:") { | |
351 | return Ok(None); | |
352 | } | |
353 | ||
354 | if let Ok(Empty) = Ticket::parse(password).and_then(|ticket| { | |
355 | ticket.verify( | |
cd0daa8b | 356 | self.keyring, |
d97ff8ae WB |
357 | TERM_PREFIX, |
358 | Some(&crate::tools::ticket::term_aad(userid, &path, port)), | |
359 | ) | |
360 | }) { | |
361 | let user_info = pbs_config::CachedUserInfo::new()?; | |
362 | let auth_id = Authid::from(userid.clone()); | |
363 | for (name, privilege) in pbs_api_types::PRIVILEGES { | |
364 | if *name == privs { | |
365 | let mut path_vec = Vec::new(); | |
366 | for part in path.split('/') { | |
367 | if !part.is_empty() { | |
368 | path_vec.push(part); | |
369 | } | |
370 | } | |
371 | user_info.check_privs(&auth_id, &path_vec, *privilege, false)?; | |
372 | return Ok(Some(true)); | |
373 | } | |
374 | } | |
375 | } | |
376 | ||
377 | Ok(Some(false)) | |
378 | } | |
379 | } | |
380 | ||
381 | struct PbsLockedTfaConfig { | |
382 | _lock: pbs_config::BackupLockGuard, | |
383 | config: TfaConfig, | |
384 | } | |
385 | ||
386 | static USER_ACCESS: crate::config::tfa::UserAccess = crate::config::tfa::UserAccess; | |
387 | ||
388 | impl LockedTfaConfig for PbsLockedTfaConfig { | |
389 | fn config_mut(&mut self) -> (&dyn OpenUserChallengeData, &mut TfaConfig) { | |
390 | (&USER_ACCESS, &mut self.config) | |
391 | } | |
392 | ||
393 | fn save_config(&mut self) -> Result<(), Error> { | |
394 | crate::config::tfa::write(&self.config) | |
395 | } | |
396 | } |