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