]> git.proxmox.com Git - proxmox-backup.git/blob - src/api2/access/mod.rs
update to proxmox-sys 0.2 crate
[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_sys::{identity, sortable};
10 use proxmox_router::{
11 http_err, list_subdirs_api_method, Router, RpcEnvironment, SubdirMap, Permission,
12 };
13 use proxmox_schema::api;
14
15 use pbs_api_types::{
16 Userid, Authid, PASSWORD_SCHEMA, ACL_PATH_SCHEMA,
17 PRIVILEGES, PRIV_PERMISSIONS_MODIFY, PRIV_SYS_AUDIT,
18 };
19 use pbs_tools::ticket::{self, Empty, Ticket};
20 use pbs_config::acl::AclTreeNode;
21 use pbs_config::CachedUserInfo;
22
23 use crate::auth_helpers::*;
24 use crate::config::tfa::TfaChallenge;
25 use crate::server::ticket::ApiTicket;
26
27 pub mod acl;
28 pub mod domain;
29 pub mod openid;
30 pub mod role;
31 pub mod tfa;
32 pub mod user;
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
197 use proxmox_rest_server::RestEnvironment;
198
199 let env: &RestEnvironment = rpcenv.as_any().downcast_ref::<RestEnvironment>()
200 .ok_or_else(|| format_err!("detected worng RpcEnvironment type"))?;
201
202 match authenticate_user(&username, &password, path, privs, port, tfa_challenge) {
203 Ok(AuthResult::Success) => Ok(json!({ "username": username })),
204 Ok(AuthResult::CreateTicket) => {
205 let api_ticket = ApiTicket::full(username.clone());
206 let ticket = Ticket::new("PBS", &api_ticket)?.sign(private_auth_key(), None)?;
207 let token = assemble_csrf_prevention_token(csrf_secret(), &username);
208
209 env.log_auth(username.as_str());
210
211 Ok(json!({
212 "username": username,
213 "ticket": ticket,
214 "CSRFPreventionToken": token,
215 }))
216 }
217 Ok(AuthResult::Partial(challenge)) => {
218 let api_ticket = ApiTicket::partial(challenge);
219 let ticket = Ticket::new("PBS", &api_ticket)?
220 .sign(private_auth_key(), Some(username.as_str()))?;
221 Ok(json!({
222 "username": username,
223 "ticket": ticket,
224 "CSRFPreventionToken": "invalid",
225 }))
226 }
227 Err(err) => {
228 env.log_failed_auth(Some(username.to_string()), &err.to_string());
229 Err(http_err!(UNAUTHORIZED, "permission check failed."))
230 }
231 }
232 }
233
234 #[api(
235 protected: true,
236 input: {
237 properties: {
238 userid: {
239 type: Userid,
240 },
241 password: {
242 schema: PASSWORD_SCHEMA,
243 },
244 },
245 },
246 access: {
247 description: "Everybody is allowed to change their own password. In addition, users with 'Permissions:Modify' privilege may change any password on @pbs realm.",
248 permission: &Permission::Anybody,
249 },
250 )]
251 /// Change user password
252 ///
253 /// Each user is allowed to change his own password. Superuser
254 /// can change all passwords.
255 pub fn change_password(
256 userid: Userid,
257 password: String,
258 rpcenv: &mut dyn RpcEnvironment,
259 ) -> Result<Value, Error> {
260 let current_auth: Authid = rpcenv
261 .get_auth_id()
262 .ok_or_else(|| format_err!("no authid available"))?
263 .parse()?;
264
265 if current_auth.is_token() {
266 bail!("API tokens cannot access this API endpoint");
267 }
268
269 let current_user = current_auth.user();
270
271 let mut allowed = userid == *current_user;
272
273 if !allowed {
274 let user_info = CachedUserInfo::new()?;
275 let privs = user_info.lookup_privs(&current_auth, &[]);
276 if user_info.is_superuser(&current_auth) {
277 allowed = true;
278 }
279 if (privs & PRIV_PERMISSIONS_MODIFY) != 0 && userid.realm() != "pam" {
280 allowed = true;
281 }
282 };
283
284 if !allowed {
285 bail!("you are not authorized to change the password.");
286 }
287
288 let authenticator = crate::auth::lookup_authenticator(userid.realm())?;
289 authenticator.store_password(userid.name(), &password)?;
290
291 Ok(Value::Null)
292 }
293
294 #[api(
295 input: {
296 properties: {
297 "auth-id": {
298 type: Authid,
299 optional: true,
300 },
301 path: {
302 schema: ACL_PATH_SCHEMA,
303 optional: true,
304 },
305 },
306 },
307 access: {
308 permission: &Permission::Anybody,
309 description: "Requires Sys.Audit on '/access', limited to own privileges otherwise.",
310 },
311 returns: {
312 description: "Map of ACL path to Map of privilege to propagate bit",
313 type: Object,
314 properties: {},
315 additional_properties: true,
316 },
317 )]
318 /// List permissions of given or currently authenticated user / API token.
319 ///
320 /// Optionally limited to specific path.
321 pub fn list_permissions(
322 auth_id: Option<Authid>,
323 path: Option<String>,
324 rpcenv: &dyn RpcEnvironment,
325 ) -> Result<HashMap<String, HashMap<String, bool>>, Error> {
326 let current_auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
327
328 let user_info = CachedUserInfo::new()?;
329 let user_privs = user_info.lookup_privs(&current_auth_id, &["access"]);
330
331 let auth_id = match auth_id {
332 Some(auth_id) if auth_id == current_auth_id => current_auth_id,
333 Some(auth_id) => {
334 if user_privs & PRIV_SYS_AUDIT != 0
335 || (auth_id.is_token()
336 && !current_auth_id.is_token()
337 && auth_id.user() == current_auth_id.user())
338 {
339 auth_id
340 } else {
341 bail!("not allowed to list permissions of {}", auth_id);
342 }
343 },
344 None => current_auth_id,
345 };
346
347 fn populate_acl_paths(
348 mut paths: HashSet<String>,
349 node: AclTreeNode,
350 path: &str,
351 ) -> HashSet<String> {
352 for (sub_path, child_node) in node.children {
353 let sub_path = format!("{}/{}", path, &sub_path);
354 paths = populate_acl_paths(paths, child_node, &sub_path);
355 paths.insert(sub_path);
356 }
357 paths
358 }
359
360 let paths = match path {
361 Some(path) => {
362 let mut paths = HashSet::new();
363 paths.insert(path);
364 paths
365 }
366 None => {
367 let mut paths = HashSet::new();
368
369 let (acl_tree, _) = pbs_config::acl::config()?;
370 paths = populate_acl_paths(paths, acl_tree.root, "");
371
372 // default paths, returned even if no ACL exists
373 paths.insert("/".to_string());
374 paths.insert("/access".to_string());
375 paths.insert("/datastore".to_string());
376 paths.insert("/remote".to_string());
377 paths.insert("/system".to_string());
378
379 paths
380 }
381 };
382
383 let map = paths.into_iter().fold(
384 HashMap::new(),
385 |mut map: HashMap<String, HashMap<String, bool>>, path: String| {
386 let split_path = pbs_config::acl::split_acl_path(path.as_str());
387 let (privs, propagated_privs) = user_info.lookup_privs_details(&auth_id, &split_path);
388
389 match privs {
390 0 => map, // Don't leak ACL paths where we don't have any privileges
391 _ => {
392 let priv_map =
393 PRIVILEGES
394 .iter()
395 .fold(HashMap::new(), |mut priv_map, (name, value)| {
396 if value & privs != 0 {
397 priv_map
398 .insert(name.to_string(), value & propagated_privs != 0);
399 }
400 priv_map
401 });
402
403 map.insert(path, priv_map);
404 map
405 }
406 }
407 },
408 );
409
410 Ok(map)
411 }
412
413 #[sortable]
414 const SUBDIRS: SubdirMap = &sorted!([
415 ("acl", &acl::ROUTER),
416 ("password", &Router::new().put(&API_METHOD_CHANGE_PASSWORD)),
417 (
418 "permissions",
419 &Router::new().get(&API_METHOD_LIST_PERMISSIONS)
420 ),
421 ("ticket", &Router::new().post(&API_METHOD_CREATE_TICKET)),
422 ("openid", &openid::ROUTER),
423 ("domains", &domain::ROUTER),
424 ("roles", &role::ROUTER),
425 ("users", &user::ROUTER),
426 ("tfa", &tfa::ROUTER),
427 ]);
428
429 pub const ROUTER: Router = Router::new()
430 .get(&list_subdirs_api_method!(SUBDIRS))
431 .subdirs(SUBDIRS);