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