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