]> git.proxmox.com Git - proxmox-backup.git/blame - src/api2/access.rs
verify jobs: add permissions
[proxmox-backup.git] / src / api2 / access.rs
CommitLineData
f7d4e4b5 1use anyhow::{bail, format_err, Error};
34f956bc 2
552c2259 3use serde_json::{json, Value};
babab85b
FG
4use std::collections::HashMap;
5use std::collections::HashSet;
552c2259 6
e7cb4dc5 7use proxmox::api::{api, RpcEnvironment, Permission};
9ea4bce4 8use proxmox::api::router::{Router, SubdirMap};
73b40e9b 9use proxmox::{sortable, identity};
9ea4bce4 10use proxmox::{http_err, list_subdirs_api_method};
552c2259 11
72dc6832 12use crate::tools::ticket::{self, Empty, Ticket};
34f956bc 13use crate::auth_helpers::*;
685e1334 14use crate::api2::types::*;
d39d095f 15use crate::tools::{FileLogOptions, FileLogger};
4f66423f 16
babab85b
FG
17use crate::config::acl as acl_config;
18use crate::config::acl::{PRIVILEGES, PRIV_SYS_AUDIT, PRIV_PERMISSIONS_MODIFY};
4b40148c 19use crate::config::cached_user_info::CachedUserInfo;
685e1334
DM
20
21pub mod user;
708db4b3 22pub mod domain;
ed3e60ae 23pub mod acl;
3fff55b2 24pub mod role;
34f956bc 25
a4d16755
DC
26/// returns Ok(true) if a ticket has to be created
27/// and Ok(false) if not
28fn 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 139fn 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.
212fn 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(&current_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.
271pub 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
552c2259 366#[sortable]
73b40e9b 367const 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
386pub const ROUTER: Router = Router::new()
387 .get(&list_subdirs_api_method!(SUBDIRS))
388 .subdirs(SUBDIRS);