]>
Commit | Line | Data |
---|---|---|
f7d4e4b5 | 1 | use anyhow::{bail, format_err, Error}; |
34f956bc | 2 | |
552c2259 | 3 | use serde_json::{json, Value}; |
babab85b FG |
4 | use std::collections::HashMap; |
5 | use std::collections::HashSet; | |
552c2259 | 6 | |
e7cb4dc5 | 7 | use proxmox::api::{api, RpcEnvironment, Permission}; |
9ea4bce4 | 8 | use proxmox::api::router::{Router, SubdirMap}; |
73b40e9b | 9 | use proxmox::{sortable, identity}; |
9ea4bce4 | 10 | use proxmox::{http_err, list_subdirs_api_method}; |
552c2259 | 11 | |
72dc6832 | 12 | use crate::tools::ticket::{self, Empty, Ticket}; |
34f956bc | 13 | use crate::auth_helpers::*; |
685e1334 | 14 | use crate::api2::types::*; |
d39d095f | 15 | use crate::tools::{FileLogOptions, FileLogger}; |
4f66423f | 16 | |
babab85b FG |
17 | use crate::config::acl as acl_config; |
18 | use crate::config::acl::{PRIVILEGES, PRIV_SYS_AUDIT, PRIV_PERMISSIONS_MODIFY}; | |
4b40148c | 19 | use crate::config::cached_user_info::CachedUserInfo; |
685e1334 DM |
20 | |
21 | pub mod user; | |
708db4b3 | 22 | pub mod domain; |
ed3e60ae | 23 | pub mod acl; |
3fff55b2 | 24 | pub mod role; |
34f956bc | 25 | |
a4d16755 DC |
26 | /// returns Ok(true) if a ticket has to be created |
27 | /// and Ok(false) if not | |
28 | fn authenticate_user( | |
e7cb4dc5 | 29 | userid: &Userid, |
a4d16755 DC |
30 | password: &str, |
31 | path: Option<String>, | |
32 | privs: Option<String>, | |
33 | port: Option<u16>, | |
34 | ) -> Result<bool, Error> { | |
4b40148c DM |
35 | let user_info = CachedUserInfo::new()?; |
36 | ||
e6dc35ac FG |
37 | let auth_id = Authid::from(userid.clone()); |
38 | if !user_info.is_active_auth_id(&auth_id) { | |
4b40148c DM |
39 | bail!("user account disabled or expired."); |
40 | } | |
41 | ||
f8f94534 | 42 | if password.starts_with("PBS:") { |
72dc6832 WB |
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 { | |
a4d16755 | 47 | return Ok(true); |
f8f94534 | 48 | } |
72dc6832 | 49 | bail!("ticket login failed - wrong userid"); |
f8f94534 | 50 | } |
a4d16755 DC |
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 | ||
72dc6832 WB |
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 | )) | |
a4d16755 DC |
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 | } | |
e6dc35ac | 76 | user_info.check_privs(&auth_id, &path_vec, *privilege, false)?; |
a4d16755 DC |
77 | return Ok(false); |
78 | } | |
79 | } | |
80 | ||
81 | bail!("No such privilege"); | |
82 | } | |
f8f94534 DM |
83 | } |
84 | ||
e7cb4dc5 | 85 | let _ = crate::auth::authenticate_user(userid, password)?; |
a4d16755 | 86 | Ok(true) |
34f956bc DM |
87 | } |
88 | ||
7b6c4107 WB |
89 | #[api( |
90 | input: { | |
91 | properties: { | |
92 | username: { | |
e7cb4dc5 | 93 | type: Userid, |
7b6c4107 WB |
94 | }, |
95 | password: { | |
685e1334 | 96 | schema: PASSWORD_SCHEMA, |
7b6c4107 | 97 | }, |
a4d16755 DC |
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 | }, | |
6486cb85 WB |
113 | }, |
114 | }, | |
7b6c4107 WB |
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 | }, | |
6486cb85 WB |
129 | }, |
130 | }, | |
7b6c4107 | 131 | protected: true, |
4b40148c DM |
132 | access: { |
133 | permission: &Permission::World, | |
134 | }, | |
7b6c4107 | 135 | )] |
6486cb85 WB |
136 | /// Create or verify authentication ticket. |
137 | /// | |
138 | /// Returns: An authentication ticket with additional infos. | |
a4d16755 | 139 | fn create_ticket( |
e7cb4dc5 | 140 | username: Userid, |
a4d16755 DC |
141 | password: String, |
142 | path: Option<String>, | |
143 | privs: Option<String>, | |
144 | port: Option<u16>, | |
29633e2f | 145 | rpcenv: &mut dyn RpcEnvironment, |
a4d16755 | 146 | ) -> Result<Value, Error> { |
d39d095f TL |
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 | ||
a4d16755 DC |
154 | match authenticate_user(&username, &password, path, privs, port) { |
155 | Ok(true) => { | |
72dc6832 | 156 | let ticket = Ticket::new("PBS", &username)?.sign(private_auth_key(), None)?; |
34f956bc | 157 | |
2905f2b5 | 158 | let token = assemble_csrf_prevention_token(csrf_secret(), &username); |
34f956bc | 159 | |
d39d095f | 160 | auth_log.log(format!("successful auth for user '{}'", username)); |
34f956bc | 161 | |
62ee2eb4 | 162 | Ok(json!({ |
34f956bc DM |
163 | "username": username, |
164 | "ticket": ticket, | |
165 | "CSRFPreventionToken": token, | |
62ee2eb4 | 166 | })) |
34f956bc | 167 | } |
a4d16755 DC |
168 | Ok(false) => Ok(json!({ |
169 | "username": username, | |
170 | })), | |
34f956bc | 171 | Err(err) => { |
29633e2f TL |
172 | let client_ip = match rpcenv.get_client_ip().map(|addr| addr.ip()) { |
173 | Some(ip) => format!("{}", ip), | |
174 | None => "unknown".into(), | |
175 | }; | |
176 | ||
d39d095f TL |
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 | ||
8aa67ee7 | 186 | Err(http_err!(UNAUTHORIZED, "permission check failed.")) |
34f956bc DM |
187 | } |
188 | } | |
189 | } | |
190 | ||
685e1334 DM |
191 | #[api( |
192 | input: { | |
193 | properties: { | |
194 | userid: { | |
e7cb4dc5 | 195 | type: Userid, |
685e1334 DM |
196 | }, |
197 | password: { | |
198 | schema: PASSWORD_SCHEMA, | |
199 | }, | |
200 | }, | |
201 | }, | |
4b40148c | 202 | access: { |
4f66423f | 203 | description: "Anybody is allowed to change there own password. In addition, users with 'Permissions:Modify' privilege may change any password.", |
4b40148c DM |
204 | permission: &Permission::Anybody, |
205 | }, | |
206 | ||
685e1334 DM |
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( | |
e7cb4dc5 | 213 | userid: Userid, |
685e1334 DM |
214 | password: String, |
215 | rpcenv: &mut dyn RpcEnvironment, | |
216 | ) -> Result<Value, Error> { | |
217 | ||
e7cb4dc5 | 218 | let current_user: Userid = rpcenv |
e6dc35ac | 219 | .get_auth_id() |
e7cb4dc5 WB |
220 | .ok_or_else(|| format_err!("unknown user"))? |
221 | .parse()?; | |
e6dc35ac | 222 | let current_auth = Authid::from(current_user.clone()); |
685e1334 DM |
223 | |
224 | let mut allowed = userid == current_user; | |
225 | ||
226 | if userid == "root@pam" { allowed = true; } | |
227 | ||
4f66423f | 228 | if !allowed { |
4f66423f | 229 | let user_info = CachedUserInfo::new()?; |
e6dc35ac | 230 | let privs = user_info.lookup_privs(¤t_auth, &[]); |
4f66423f DM |
231 | if (privs & PRIV_PERMISSIONS_MODIFY) != 0 { allowed = true; } |
232 | } | |
233 | ||
685e1334 DM |
234 | if !allowed { |
235 | bail!("you are not authorized to change the password."); | |
236 | } | |
237 | ||
e7cb4dc5 WB |
238 | let authenticator = crate::auth::lookup_authenticator(userid.realm())?; |
239 | authenticator.store_password(userid.name(), &password)?; | |
685e1334 DM |
240 | |
241 | Ok(Value::Null) | |
242 | } | |
243 | ||
babab85b FG |
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(¤t_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 | ||
552c2259 | 366 | #[sortable] |
73b40e9b | 367 | const SUBDIRS: SubdirMap = &sorted!([ |
ed3e60ae | 368 | ("acl", &acl::ROUTER), |
685e1334 DM |
369 | ( |
370 | "password", &Router::new() | |
371 | .put(&API_METHOD_CHANGE_PASSWORD) | |
372 | ), | |
babab85b FG |
373 | ( |
374 | "permissions", &Router::new() | |
375 | .get(&API_METHOD_LIST_PERMISSIONS) | |
376 | ), | |
255f378a DM |
377 | ( |
378 | "ticket", &Router::new() | |
6486cb85 | 379 | .post(&API_METHOD_CREATE_TICKET) |
685e1334 | 380 | ), |
708db4b3 | 381 | ("domains", &domain::ROUTER), |
3fff55b2 | 382 | ("roles", &role::ROUTER), |
685e1334 | 383 | ("users", &user::ROUTER), |
73b40e9b | 384 | ]); |
255f378a DM |
385 | |
386 | pub const ROUTER: Router = Router::new() | |
387 | .get(&list_subdirs_api_method!(SUBDIRS)) | |
388 | .subdirs(SUBDIRS); |