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