]> git.proxmox.com Git - proxmox-backup.git/blob - src/api2/access/mod.rs
58ac8ca414ef9276f218b78d06876d79d2dbbe9e
[proxmox-backup.git] / src / api2 / access / mod.rs
1 //! Access control (Users, Permissions and Authentication)
2
3 use anyhow::{bail, format_err, Error};
4
5 use serde_json::{json, Value};
6 use std::collections::HashMap;
7 use std::collections::HashSet;
8
9 use proxmox::api::router::{Router, SubdirMap};
10 use proxmox::api::{api, Permission, RpcEnvironment};
11 use proxmox::{http_err, list_subdirs_api_method};
12 use proxmox::{identity, sortable};
13
14 use pbs_api_types::{
15 Userid, Authid, PASSWORD_SCHEMA, ACL_PATH_SCHEMA,
16 PRIVILEGES, PRIV_PERMISSIONS_MODIFY, PRIV_SYS_AUDIT,
17 };
18 use pbs_tools::auth::private_auth_key;
19 use pbs_tools::ticket::{self, Empty, Ticket};
20 use pbs_config::acl::AclTreeNode;
21
22 use crate::auth_helpers::*;
23 use crate::server::ticket::ApiTicket;
24
25 use pbs_config::CachedUserInfo;
26 use crate::config::tfa::TfaChallenge;
27
28 pub mod acl;
29 pub mod domain;
30 pub mod role;
31 pub mod tfa;
32 pub mod user;
33
34 #[cfg(openid)]
35 pub mod openid;
36
37 #[allow(clippy::large_enum_variant)]
38 enum AuthResult {
39 /// Successful authentication which does not require a new ticket.
40 Success,
41
42 /// Successful authentication which requires a ticket to be created.
43 CreateTicket,
44
45 /// A partial ticket which requires a 2nd factor will be created.
46 Partial(TfaChallenge),
47 }
48
49 fn authenticate_user(
50 userid: &Userid,
51 password: &str,
52 path: Option<String>,
53 privs: Option<String>,
54 port: Option<u16>,
55 tfa_challenge: Option<String>,
56 ) -> Result<AuthResult, Error> {
57 let user_info = CachedUserInfo::new()?;
58
59 let auth_id = Authid::from(userid.clone());
60 if !user_info.is_active_auth_id(&auth_id) {
61 bail!("user account disabled or expired.");
62 }
63
64 if let Some(tfa_challenge) = tfa_challenge {
65 return authenticate_2nd(userid, &tfa_challenge, password);
66 }
67
68 if password.starts_with("PBS:") {
69 if let Ok(ticket_userid) = Ticket::<Userid>::parse(password)
70 .and_then(|ticket| ticket.verify(public_auth_key(), "PBS", None))
71 {
72 if *userid == ticket_userid {
73 return Ok(AuthResult::CreateTicket);
74 }
75 bail!("ticket login failed - wrong userid");
76 }
77 } else if password.starts_with("PBSTERM:") {
78 if path.is_none() || privs.is_none() || port.is_none() {
79 bail!("cannot check termnal ticket without path, priv and port");
80 }
81
82 let path = path.ok_or_else(|| format_err!("missing path for termproxy ticket"))?;
83 let privilege_name =
84 privs.ok_or_else(|| format_err!("missing privilege name for termproxy ticket"))?;
85 let port = port.ok_or_else(|| format_err!("missing port for termproxy ticket"))?;
86
87 if let Ok(Empty) = Ticket::parse(password).and_then(|ticket| {
88 ticket.verify(
89 public_auth_key(),
90 ticket::TERM_PREFIX,
91 Some(&crate::tools::ticket::term_aad(userid, &path, port)),
92 )
93 }) {
94 for (name, privilege) in PRIVILEGES {
95 if *name == privilege_name {
96 let mut path_vec = Vec::new();
97 for part in path.split('/') {
98 if part != "" {
99 path_vec.push(part);
100 }
101 }
102 user_info.check_privs(&auth_id, &path_vec, *privilege, false)?;
103 return Ok(AuthResult::Success);
104 }
105 }
106
107 bail!("No such privilege");
108 }
109 }
110
111 let _: () = crate::auth::authenticate_user(userid, password)?;
112
113 Ok(match crate::config::tfa::login_challenge(userid)? {
114 None => AuthResult::CreateTicket,
115 Some(challenge) => AuthResult::Partial(challenge),
116 })
117 }
118
119 fn authenticate_2nd(
120 userid: &Userid,
121 challenge_ticket: &str,
122 response: &str,
123 ) -> Result<AuthResult, Error> {
124 let challenge: TfaChallenge = Ticket::<ApiTicket>::parse(&challenge_ticket)?
125 .verify_with_time_frame(public_auth_key(), "PBS", Some(userid.as_str()), -60..600)?
126 .require_partial()?;
127
128 let _: () = crate::config::tfa::verify_challenge(userid, &challenge, response.parse()?)?;
129
130 Ok(AuthResult::CreateTicket)
131 }
132
133 #[api(
134 input: {
135 properties: {
136 username: {
137 type: Userid,
138 },
139 password: {
140 schema: PASSWORD_SCHEMA,
141 },
142 path: {
143 type: String,
144 description: "Path for verifying terminal tickets.",
145 optional: true,
146 },
147 privs: {
148 type: String,
149 description: "Privilege for verifying terminal tickets.",
150 optional: true,
151 },
152 port: {
153 type: Integer,
154 description: "Port for verifying terminal tickets.",
155 optional: true,
156 },
157 "tfa-challenge": {
158 type: String,
159 description: "The signed TFA challenge string the user wants to respond to.",
160 optional: true,
161 },
162 },
163 },
164 returns: {
165 properties: {
166 username: {
167 type: String,
168 description: "User name.",
169 },
170 ticket: {
171 type: String,
172 description: "Auth ticket.",
173 },
174 CSRFPreventionToken: {
175 type: String,
176 description:
177 "Cross Site Request Forgery Prevention Token. \
178 For partial tickets this is the string \"invalid\".",
179 },
180 },
181 },
182 protected: true,
183 access: {
184 permission: &Permission::World,
185 },
186 )]
187 /// Create or verify authentication ticket.
188 ///
189 /// Returns: An authentication ticket with additional infos.
190 pub fn create_ticket(
191 username: Userid,
192 password: String,
193 path: Option<String>,
194 privs: Option<String>,
195 port: Option<u16>,
196 tfa_challenge: Option<String>,
197 rpcenv: &mut dyn RpcEnvironment,
198 ) -> Result<Value, Error> {
199 match authenticate_user(&username, &password, path, privs, port, tfa_challenge) {
200 Ok(AuthResult::Success) => Ok(json!({ "username": username })),
201 Ok(AuthResult::CreateTicket) => {
202 let api_ticket = ApiTicket::full(username.clone());
203 let ticket = Ticket::new("PBS", &api_ticket)?.sign(private_auth_key(), None)?;
204 let token = assemble_csrf_prevention_token(csrf_secret(), &username);
205
206 crate::server::rest::auth_logger()?
207 .log(format!("successful auth for user '{}'", username));
208
209 Ok(json!({
210 "username": username,
211 "ticket": ticket,
212 "CSRFPreventionToken": token,
213 }))
214 }
215 Ok(AuthResult::Partial(challenge)) => {
216 let api_ticket = ApiTicket::partial(challenge);
217 let ticket = Ticket::new("PBS", &api_ticket)?
218 .sign(private_auth_key(), Some(username.as_str()))?;
219 Ok(json!({
220 "username": username,
221 "ticket": ticket,
222 "CSRFPreventionToken": "invalid",
223 }))
224 }
225 Err(err) => {
226 let client_ip = match rpcenv.get_client_ip().map(|addr| addr.ip()) {
227 Some(ip) => format!("{}", ip),
228 None => "unknown".into(),
229 };
230
231 let msg = format!(
232 "authentication failure; rhost={} user={} msg={}",
233 client_ip,
234 username,
235 err.to_string()
236 );
237 crate::server::rest::auth_logger()?.log(&msg);
238 log::error!("{}", msg);
239
240 Err(http_err!(UNAUTHORIZED, "permission check failed."))
241 }
242 }
243 }
244
245 #[api(
246 protected: true,
247 input: {
248 properties: {
249 userid: {
250 type: Userid,
251 },
252 password: {
253 schema: PASSWORD_SCHEMA,
254 },
255 },
256 },
257 access: {
258 description: "Everybody is allowed to change their own password. In addition, users with 'Permissions:Modify' privilege may change any password on @pbs realm.",
259 permission: &Permission::Anybody,
260 },
261 )]
262 /// Change user password
263 ///
264 /// Each user is allowed to change his own password. Superuser
265 /// can change all passwords.
266 pub fn change_password(
267 userid: Userid,
268 password: String,
269 rpcenv: &mut dyn RpcEnvironment,
270 ) -> Result<Value, Error> {
271 let current_auth: Authid = rpcenv
272 .get_auth_id()
273 .ok_or_else(|| format_err!("no authid available"))?
274 .parse()?;
275
276 if current_auth.is_token() {
277 bail!("API tokens cannot access this API endpoint");
278 }
279
280 let current_user = current_auth.user();
281
282 let mut allowed = userid == *current_user;
283
284 if !allowed {
285 let user_info = CachedUserInfo::new()?;
286 let privs = user_info.lookup_privs(&current_auth, &[]);
287 if user_info.is_superuser(&current_auth) {
288 allowed = true;
289 }
290 if (privs & PRIV_PERMISSIONS_MODIFY) != 0 && userid.realm() != "pam" {
291 allowed = true;
292 }
293 };
294
295 if !allowed {
296 bail!("you are not authorized to change the password.");
297 }
298
299 let authenticator = crate::auth::lookup_authenticator(userid.realm())?;
300 authenticator.store_password(userid.name(), &password)?;
301
302 Ok(Value::Null)
303 }
304
305 #[api(
306 input: {
307 properties: {
308 "auth-id": {
309 type: Authid,
310 optional: true,
311 },
312 path: {
313 schema: ACL_PATH_SCHEMA,
314 optional: true,
315 },
316 },
317 },
318 access: {
319 permission: &Permission::Anybody,
320 description: "Requires Sys.Audit on '/access', limited to own privileges otherwise.",
321 },
322 returns: {
323 description: "Map of ACL path to Map of privilege to propagate bit",
324 type: Object,
325 properties: {},
326 additional_properties: true,
327 },
328 )]
329 /// List permissions of given or currently authenticated user / API token.
330 ///
331 /// Optionally limited to specific path.
332 pub fn list_permissions(
333 auth_id: Option<Authid>,
334 path: Option<String>,
335 rpcenv: &dyn RpcEnvironment,
336 ) -> Result<HashMap<String, HashMap<String, bool>>, Error> {
337 let current_auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
338
339 let user_info = CachedUserInfo::new()?;
340 let user_privs = user_info.lookup_privs(&current_auth_id, &["access"]);
341
342 let auth_id = match auth_id {
343 Some(auth_id) if auth_id == current_auth_id => current_auth_id,
344 Some(auth_id) => {
345 if user_privs & PRIV_SYS_AUDIT != 0
346 || (auth_id.is_token()
347 && !current_auth_id.is_token()
348 && auth_id.user() == current_auth_id.user())
349 {
350 auth_id
351 } else {
352 bail!("not allowed to list permissions of {}", auth_id);
353 }
354 },
355 None => current_auth_id,
356 };
357
358 fn populate_acl_paths(
359 mut paths: HashSet<String>,
360 node: AclTreeNode,
361 path: &str,
362 ) -> HashSet<String> {
363 for (sub_path, child_node) in node.children {
364 let sub_path = format!("{}/{}", path, &sub_path);
365 paths = populate_acl_paths(paths, child_node, &sub_path);
366 paths.insert(sub_path);
367 }
368 paths
369 }
370
371 let paths = match path {
372 Some(path) => {
373 let mut paths = HashSet::new();
374 paths.insert(path);
375 paths
376 }
377 None => {
378 let mut paths = HashSet::new();
379
380 let (acl_tree, _) = pbs_config::acl::config()?;
381 paths = populate_acl_paths(paths, acl_tree.root, "");
382
383 // default paths, returned even if no ACL exists
384 paths.insert("/".to_string());
385 paths.insert("/access".to_string());
386 paths.insert("/datastore".to_string());
387 paths.insert("/remote".to_string());
388 paths.insert("/system".to_string());
389
390 paths
391 }
392 };
393
394 let map = paths.into_iter().fold(
395 HashMap::new(),
396 |mut map: HashMap<String, HashMap<String, bool>>, path: String| {
397 let split_path = pbs_config::acl::split_acl_path(path.as_str());
398 let (privs, propagated_privs) = user_info.lookup_privs_details(&auth_id, &split_path);
399
400 match privs {
401 0 => map, // Don't leak ACL paths where we don't have any privileges
402 _ => {
403 let priv_map =
404 PRIVILEGES
405 .iter()
406 .fold(HashMap::new(), |mut priv_map, (name, value)| {
407 if value & privs != 0 {
408 priv_map
409 .insert(name.to_string(), value & propagated_privs != 0);
410 }
411 priv_map
412 });
413
414 map.insert(path, priv_map);
415 map
416 }
417 }
418 },
419 );
420
421 Ok(map)
422 }
423
424 #[cfg(openid)]
425 const OPENID_ROUTER: &Router = &openid::ROUTER;
426
427 #[cfg(not(openid))]
428 const OPENID_ROUTER: &Router = &Router::new();
429
430 #[sortable]
431 const SUBDIRS: SubdirMap = &sorted!([
432 ("acl", &acl::ROUTER),
433 ("password", &Router::new().put(&API_METHOD_CHANGE_PASSWORD)),
434 (
435 "permissions",
436 &Router::new().get(&API_METHOD_LIST_PERMISSIONS)
437 ),
438 ("ticket", &Router::new().post(&API_METHOD_CREATE_TICKET)),
439 ("openid", &OPENID_ROUTER),
440 ("domains", &domain::ROUTER),
441 ("roles", &role::ROUTER),
442 ("users", &user::ROUTER),
443 ("tfa", &tfa::ROUTER),
444 ]);
445
446 pub const ROUTER: Router = Router::new()
447 .get(&list_subdirs_api_method!(SUBDIRS))
448 .subdirs(SUBDIRS);