]> git.proxmox.com Git - proxmox-backup.git/blame - src/auth.rs
api-types: factor out `LdapMode` -> `ConnectionMode` conversion 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
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
214fn 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 223pub(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 243pub(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
256static PRIVATE_KEYRING: Lazy<Keyring> =
257 Lazy::new(|| Keyring::with_private_key(crate::auth_helpers::private_auth_key().clone().into()));
258static PUBLIC_KEYRING: Lazy<Keyring> =
259 Lazy::new(|| Keyring::with_public_key(crate::auth_helpers::public_auth_key().clone().into()));
d97ff8ae
WB
260static AUTH_CONTEXT: OnceCell<PbsAuthContext> = OnceCell::new();
261
262pub 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 280pub(crate) fn private_auth_keyring() -> &'static Keyring {
12c841b4 281 &PRIVATE_KEYRING
569324cb
WB
282}
283
284pub(crate) fn public_auth_keyring() -> &'static Keyring {
12c841b4 285 &PUBLIC_KEYRING
d97ff8ae
WB
286}
287
288struct PbsAuthContext {
569324cb 289 keyring: &'static Keyring,
d97ff8ae
WB
290 csrf_secret: Vec<u8>,
291}
292
293impl 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
381struct PbsLockedTfaConfig {
382 _lock: pbs_config::BackupLockGuard,
383 config: TfaConfig,
384}
385
386static USER_ACCESS: crate::config::tfa::UserAccess = crate::config::tfa::UserAccess;
387
388impl 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}