]>
Commit | Line | Data |
---|---|---|
bf78f708 DM |
1 | //! Access control (Users, Permissions and Authentication) |
2 | ||
f7d4e4b5 | 3 | use anyhow::{bail, format_err, Error}; |
34f956bc | 4 | |
552c2259 | 5 | use serde_json::{json, Value}; |
babab85b FG |
6 | use std::collections::HashMap; |
7 | use std::collections::HashSet; | |
552c2259 | 8 | |
9ea4bce4 | 9 | use proxmox::api::router::{Router, SubdirMap}; |
027ef213 | 10 | use proxmox::api::{api, Permission, RpcEnvironment}; |
9ea4bce4 | 11 | use proxmox::{http_err, list_subdirs_api_method}; |
027ef213 | 12 | use proxmox::{identity, sortable}; |
552c2259 | 13 | |
8cc3760e DM |
14 | use pbs_api_types::{ |
15 | Userid, Authid, PASSWORD_SCHEMA, ACL_PATH_SCHEMA, | |
16 | PRIVILEGES, PRIV_PERMISSIONS_MODIFY, PRIV_SYS_AUDIT, | |
17 | }; | |
4805edc4 | 18 | use pbs_tools::auth::private_auth_key; |
9eb78407 | 19 | use pbs_tools::ticket::{self, Empty, Ticket}; |
8cc3760e | 20 | use pbs_config::acl::AclTreeNode; |
9eb78407 | 21 | |
027ef213 WB |
22 | use crate::auth_helpers::*; |
23 | use crate::server::ticket::ApiTicket; | |
4f66423f | 24 | |
4b40148c | 25 | use crate::config::cached_user_info::CachedUserInfo; |
027ef213 | 26 | use crate::config::tfa::TfaChallenge; |
685e1334 | 27 | |
ed3e60ae | 28 | pub mod acl; |
027ef213 | 29 | pub mod domain; |
3fff55b2 | 30 | pub mod role; |
027ef213 WB |
31 | pub mod tfa; |
32 | pub mod user; | |
c2e2078b FG |
33 | |
34 | #[cfg(openid)] | |
3b7b1dfb | 35 | pub mod openid; |
027ef213 | 36 | |
4d08e259 | 37 | #[allow(clippy::large_enum_variant)] |
027ef213 WB |
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 | } | |
34f956bc | 48 | |
a4d16755 | 49 | fn authenticate_user( |
e7cb4dc5 | 50 | userid: &Userid, |
a4d16755 DC |
51 | password: &str, |
52 | path: Option<String>, | |
53 | privs: Option<String>, | |
54 | port: Option<u16>, | |
027ef213 WB |
55 | tfa_challenge: Option<String>, |
56 | ) -> Result<AuthResult, Error> { | |
4b40148c DM |
57 | let user_info = CachedUserInfo::new()?; |
58 | ||
e6dc35ac FG |
59 | let auth_id = Authid::from(userid.clone()); |
60 | if !user_info.is_active_auth_id(&auth_id) { | |
4b40148c DM |
61 | bail!("user account disabled or expired."); |
62 | } | |
63 | ||
027ef213 WB |
64 | if let Some(tfa_challenge) = tfa_challenge { |
65 | return authenticate_2nd(userid, &tfa_challenge, password); | |
66 | } | |
67 | ||
f8f94534 | 68 | if password.starts_with("PBS:") { |
72dc6832 WB |
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 { | |
027ef213 | 73 | return Ok(AuthResult::CreateTicket); |
f8f94534 | 74 | } |
72dc6832 | 75 | bail!("ticket login failed - wrong userid"); |
f8f94534 | 76 | } |
a4d16755 DC |
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 | ||
72dc6832 | 82 | let path = path.ok_or_else(|| format_err!("missing path for termproxy ticket"))?; |
027ef213 WB |
83 | let privilege_name = |
84 | privs.ok_or_else(|| format_err!("missing privilege name for termproxy ticket"))?; | |
72dc6832 WB |
85 | let port = port.ok_or_else(|| format_err!("missing port for termproxy ticket"))?; |
86 | ||
027ef213 WB |
87 | if let Ok(Empty) = Ticket::parse(password).and_then(|ticket| { |
88 | ticket.verify( | |
72dc6832 WB |
89 | public_auth_key(), |
90 | ticket::TERM_PREFIX, | |
9eb78407 | 91 | Some(&crate::tools::ticket::term_aad(userid, &path, port)), |
027ef213 WB |
92 | ) |
93 | }) { | |
a4d16755 DC |
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 | } | |
e6dc35ac | 102 | user_info.check_privs(&auth_id, &path_vec, *privilege, false)?; |
027ef213 | 103 | return Ok(AuthResult::Success); |
a4d16755 DC |
104 | } |
105 | } | |
106 | ||
107 | bail!("No such privilege"); | |
108 | } | |
f8f94534 DM |
109 | } |
110 | ||
027ef213 WB |
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)? | |
6248e517 | 125 | .verify_with_time_frame(public_auth_key(), "PBS", Some(userid.as_str()), -60..600)? |
027ef213 WB |
126 | .require_partial()?; |
127 | ||
128 | let _: () = crate::config::tfa::verify_challenge(userid, &challenge, response.parse()?)?; | |
129 | ||
130 | Ok(AuthResult::CreateTicket) | |
34f956bc DM |
131 | } |
132 | ||
7b6c4107 WB |
133 | #[api( |
134 | input: { | |
135 | properties: { | |
136 | username: { | |
e7cb4dc5 | 137 | type: Userid, |
7b6c4107 WB |
138 | }, |
139 | password: { | |
685e1334 | 140 | schema: PASSWORD_SCHEMA, |
7b6c4107 | 141 | }, |
a4d16755 DC |
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 | }, | |
027ef213 WB |
157 | "tfa-challenge": { |
158 | type: String, | |
159 | description: "The signed TFA challenge string the user wants to respond to.", | |
160 | optional: true, | |
161 | }, | |
6486cb85 WB |
162 | }, |
163 | }, | |
7b6c4107 WB |
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, | |
ad0ed40a WB |
176 | description: |
177 | "Cross Site Request Forgery Prevention Token. \ | |
178 | For partial tickets this is the string \"invalid\".", | |
7b6c4107 | 179 | }, |
6486cb85 WB |
180 | }, |
181 | }, | |
7b6c4107 | 182 | protected: true, |
4b40148c DM |
183 | access: { |
184 | permission: &Permission::World, | |
185 | }, | |
7b6c4107 | 186 | )] |
6486cb85 WB |
187 | /// Create or verify authentication ticket. |
188 | /// | |
189 | /// Returns: An authentication ticket with additional infos. | |
bf78f708 | 190 | pub fn create_ticket( |
e7cb4dc5 | 191 | username: Userid, |
a4d16755 DC |
192 | password: String, |
193 | path: Option<String>, | |
194 | privs: Option<String>, | |
195 | port: Option<u16>, | |
027ef213 | 196 | tfa_challenge: Option<String>, |
29633e2f | 197 | rpcenv: &mut dyn RpcEnvironment, |
a4d16755 | 198 | ) -> Result<Value, Error> { |
027ef213 WB |
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)?; | |
2905f2b5 | 204 | let token = assemble_csrf_prevention_token(csrf_secret(), &username); |
34f956bc | 205 | |
027ef213 WB |
206 | crate::server::rest::auth_logger()? |
207 | .log(format!("successful auth for user '{}'", username)); | |
34f956bc | 208 | |
62ee2eb4 | 209 | Ok(json!({ |
34f956bc DM |
210 | "username": username, |
211 | "ticket": ticket, | |
212 | "CSRFPreventionToken": token, | |
62ee2eb4 | 213 | })) |
34f956bc | 214 | } |
027ef213 WB |
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, | |
ad0ed40a | 222 | "CSRFPreventionToken": "invalid", |
027ef213 WB |
223 | })) |
224 | } | |
34f956bc | 225 | Err(err) => { |
29633e2f TL |
226 | let client_ip = match rpcenv.get_client_ip().map(|addr| addr.ip()) { |
227 | Some(ip) => format!("{}", ip), | |
228 | None => "unknown".into(), | |
229 | }; | |
230 | ||
d39d095f TL |
231 | let msg = format!( |
232 | "authentication failure; rhost={} user={} msg={}", | |
233 | client_ip, | |
234 | username, | |
235 | err.to_string() | |
236 | ); | |
4fdf13f9 | 237 | crate::server::rest::auth_logger()?.log(&msg); |
d39d095f TL |
238 | log::error!("{}", msg); |
239 | ||
8aa67ee7 | 240 | Err(http_err!(UNAUTHORIZED, "permission check failed.")) |
34f956bc DM |
241 | } |
242 | } | |
243 | } | |
244 | ||
685e1334 | 245 | #[api( |
ec002004 | 246 | protected: true, |
685e1334 DM |
247 | input: { |
248 | properties: { | |
249 | userid: { | |
e7cb4dc5 | 250 | type: Userid, |
685e1334 DM |
251 | }, |
252 | password: { | |
253 | schema: PASSWORD_SCHEMA, | |
254 | }, | |
255 | }, | |
256 | }, | |
4b40148c | 257 | access: { |
6bbe49aa | 258 | description: "Everybody is allowed to change their own password. In addition, users with 'Permissions:Modify' privilege may change any password on @pbs realm.", |
4b40148c DM |
259 | permission: &Permission::Anybody, |
260 | }, | |
685e1334 DM |
261 | )] |
262 | /// Change user password | |
263 | /// | |
264 | /// Each user is allowed to change his own password. Superuser | |
265 | /// can change all passwords. | |
bf78f708 | 266 | pub fn change_password( |
e7cb4dc5 | 267 | userid: Userid, |
685e1334 DM |
268 | password: String, |
269 | rpcenv: &mut dyn RpcEnvironment, | |
270 | ) -> Result<Value, Error> { | |
13f58635 | 271 | let current_auth: Authid = rpcenv |
e6dc35ac | 272 | .get_auth_id() |
13f58635 | 273 | .ok_or_else(|| format_err!("no authid available"))? |
e7cb4dc5 | 274 | .parse()?; |
685e1334 | 275 | |
13f58635 FG |
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; | |
685e1334 | 283 | |
4f66423f | 284 | if !allowed { |
4f66423f | 285 | let user_info = CachedUserInfo::new()?; |
e6dc35ac | 286 | let privs = user_info.lookup_privs(¤t_auth, &[]); |
6bbe49aa | 287 | if user_info.is_superuser(¤t_auth) { |
027ef213 WB |
288 | allowed = true; |
289 | } | |
6bbe49aa OB |
290 | if (privs & PRIV_PERMISSIONS_MODIFY) != 0 && userid.realm() != "pam" { |
291 | allowed = true; | |
292 | } | |
293 | }; | |
4f66423f | 294 | |
685e1334 DM |
295 | if !allowed { |
296 | bail!("you are not authorized to change the password."); | |
297 | } | |
298 | ||
e7cb4dc5 WB |
299 | let authenticator = crate::auth::lookup_authenticator(userid.realm())?; |
300 | authenticator.store_password(userid.name(), &password)?; | |
685e1334 DM |
301 | |
302 | Ok(Value::Null) | |
303 | } | |
304 | ||
babab85b FG |
305 | #[api( |
306 | input: { | |
307 | properties: { | |
8b600f99 | 308 | "auth-id": { |
babab85b FG |
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(¤t_auth_id, &["access"]); | |
341 | ||
4d08e259 FG |
342 | let auth_id = match auth_id { |
343 | Some(auth_id) if auth_id == current_auth_id => current_auth_id, | |
344 | Some(auth_id) => { | |
3b7b1dfb | 345 | if user_privs & PRIV_SYS_AUDIT != 0 |
4d08e259 | 346 | || (auth_id.is_token() |
babab85b | 347 | && !current_auth_id.is_token() |
4d08e259 FG |
348 | && auth_id.user() == current_auth_id.user()) |
349 | { | |
350 | auth_id | |
351 | } else { | |
352 | bail!("not allowed to list permissions of {}", auth_id); | |
027ef213 | 353 | } |
4d08e259 FG |
354 | }, |
355 | None => current_auth_id, | |
babab85b FG |
356 | }; |
357 | ||
babab85b FG |
358 | fn populate_acl_paths( |
359 | mut paths: HashSet<String>, | |
8cc3760e | 360 | node: AclTreeNode, |
027ef213 | 361 | path: &str, |
babab85b FG |
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 | |
027ef213 | 376 | } |
babab85b FG |
377 | None => { |
378 | let mut paths = HashSet::new(); | |
379 | ||
8cc3760e | 380 | let (acl_tree, _) = pbs_config::acl::config()?; |
babab85b FG |
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 | |
027ef213 | 391 | } |
babab85b FG |
392 | }; |
393 | ||
027ef213 WB |
394 | let map = paths.into_iter().fold( |
395 | HashMap::new(), | |
396 | |mut map: HashMap<String, HashMap<String, bool>>, path: String| { | |
8cc3760e | 397 | let split_path = pbs_config::acl::split_acl_path(path.as_str()); |
babab85b FG |
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 | _ => { | |
027ef213 WB |
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 | }); | |
babab85b FG |
413 | |
414 | map.insert(path, priv_map); | |
415 | map | |
027ef213 WB |
416 | } |
417 | } | |
418 | }, | |
419 | ); | |
babab85b FG |
420 | |
421 | Ok(map) | |
422 | } | |
423 | ||
c2e2078b FG |
424 | #[cfg(openid)] |
425 | const OPENID_ROUTER: &Router = &openid::ROUTER; | |
426 | ||
427 | #[cfg(not(openid))] | |
428 | const OPENID_ROUTER: &Router = &Router::new(); | |
429 | ||
552c2259 | 430 | #[sortable] |
73b40e9b | 431 | const SUBDIRS: SubdirMap = &sorted!([ |
ed3e60ae | 432 | ("acl", &acl::ROUTER), |
027ef213 | 433 | ("password", &Router::new().put(&API_METHOD_CHANGE_PASSWORD)), |
685e1334 | 434 | ( |
027ef213 WB |
435 | "permissions", |
436 | &Router::new().get(&API_METHOD_LIST_PERMISSIONS) | |
685e1334 | 437 | ), |
027ef213 | 438 | ("ticket", &Router::new().post(&API_METHOD_CREATE_TICKET)), |
c2e2078b | 439 | ("openid", &OPENID_ROUTER), |
708db4b3 | 440 | ("domains", &domain::ROUTER), |
3fff55b2 | 441 | ("roles", &role::ROUTER), |
685e1334 | 442 | ("users", &user::ROUTER), |
027ef213 | 443 | ("tfa", &tfa::ROUTER), |
73b40e9b | 444 | ]); |
255f378a DM |
445 | |
446 | pub const ROUTER: Router = Router::new() | |
447 | .get(&list_subdirs_api_method!(SUBDIRS)) | |
448 | .subdirs(SUBDIRS); |