1 //! Access control (Users, Permissions and Authentication)
3 use anyhow
::{bail, format_err, Error}
;
5 use serde_json
::{json, Value}
;
6 use std
::collections
::HashMap
;
7 use std
::collections
::HashSet
;
9 use proxmox
::api
::router
::{Router, SubdirMap}
;
10 use proxmox
::api
::{api, Permission, RpcEnvironment}
;
11 use proxmox
::{http_err, list_subdirs_api_method}
;
12 use proxmox
::{identity, sortable}
;
15 Userid
, Authid
, PASSWORD_SCHEMA
, ACL_PATH_SCHEMA
,
16 PRIVILEGES
, PRIV_PERMISSIONS_MODIFY
, PRIV_SYS_AUDIT
,
18 use pbs_tools
::auth
::private_auth_key
;
19 use pbs_tools
::ticket
::{self, Empty, Ticket}
;
20 use pbs_config
::acl
::AclTreeNode
;
22 use crate::auth_helpers
::*;
23 use crate::server
::ticket
::ApiTicket
;
25 use pbs_config
::CachedUserInfo
;
26 use crate::config
::tfa
::TfaChallenge
;
37 #[allow(clippy::large_enum_variant)]
39 /// Successful authentication which does not require a new ticket.
42 /// Successful authentication which requires a ticket to be created.
45 /// A partial ticket which requires a 2nd factor will be created.
46 Partial(TfaChallenge
),
53 privs
: Option
<String
>,
55 tfa_challenge
: Option
<String
>,
56 ) -> Result
<AuthResult
, Error
> {
57 let user_info
= CachedUserInfo
::new()?
;
59 let auth_id
= Authid
::from(userid
.clone());
60 if !user_info
.is_active_auth_id(&auth_id
) {
61 bail
!("user account disabled or expired.");
64 if let Some(tfa_challenge
) = tfa_challenge
{
65 return authenticate_2nd(userid
, &tfa_challenge
, password
);
68 if password
.starts_with("PBS:") {
69 if let Ok(ticket_userid
) = Ticket
::<Userid
>::parse(password
)
70 .and_then(|ticket
| ticket
.verify(public_auth_key(), "PBS", None
))
72 if *userid
== ticket_userid
{
73 return Ok(AuthResult
::CreateTicket
);
75 bail
!("ticket login failed - wrong userid");
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");
82 let path
= path
.ok_or_else(|| format_err
!("missing path for termproxy ticket"))?
;
84 privs
.ok_or_else(|| format_err
!("missing privilege name for termproxy ticket"))?
;
85 let port
= port
.ok_or_else(|| format_err
!("missing port for termproxy ticket"))?
;
87 if let Ok(Empty
) = Ticket
::parse(password
).and_then(|ticket
| {
91 Some(&crate::tools
::ticket
::term_aad(userid
, &path
, port
)),
94 for (name
, privilege
) in PRIVILEGES
{
95 if *name
== privilege_name
{
96 let mut path_vec
= Vec
::new();
97 for part
in path
.split('
/'
) {
102 user_info
.check_privs(&auth_id
, &path_vec
, *privilege
, false)?
;
103 return Ok(AuthResult
::Success
);
107 bail
!("No such privilege");
111 let _
: () = crate::auth
::authenticate_user(userid
, password
)?
;
113 Ok(match crate::config
::tfa
::login_challenge(userid
)?
{
114 None
=> AuthResult
::CreateTicket
,
115 Some(challenge
) => AuthResult
::Partial(challenge
),
121 challenge_ticket
: &str,
123 ) -> Result
<AuthResult
, Error
> {
124 let challenge
: TfaChallenge
= Ticket
::<ApiTicket
>::parse(&challenge_ticket
)?
125 .verify_with_time_frame(public_auth_key(), "PBS", Some(userid
.as_str()), -60..600)?
128 let _
: () = crate::config
::tfa
::verify_challenge(userid
, &challenge
, response
.parse()?
)?
;
130 Ok(AuthResult
::CreateTicket
)
140 schema
: PASSWORD_SCHEMA
,
144 description
: "Path for verifying terminal tickets.",
149 description
: "Privilege for verifying terminal tickets.",
154 description
: "Port for verifying terminal tickets.",
159 description
: "The signed TFA challenge string the user wants to respond to.",
168 description
: "User name.",
172 description
: "Auth ticket.",
174 CSRFPreventionToken
: {
177 "Cross Site Request Forgery Prevention Token. \
178 For partial tickets this is the string \"invalid\".",
184 permission
: &Permission
::World
,
187 /// Create or verify authentication ticket.
189 /// Returns: An authentication ticket with additional infos.
190 pub fn create_ticket(
193 path
: Option
<String
>,
194 privs
: Option
<String
>,
196 tfa_challenge
: Option
<String
>,
197 rpcenv
: &mut dyn RpcEnvironment
,
198 ) -> Result
<Value
, Error
> {
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
)?
;
204 let token
= assemble_csrf_prevention_token(csrf_secret(), &username
);
206 crate::server
::rest
::auth_logger()?
207 .log(format
!("successful auth for user '{}'", username
));
210 "username": username
,
212 "CSRFPreventionToken": token
,
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()))?
;
220 "username": username
,
222 "CSRFPreventionToken": "invalid",
226 let client_ip
= match rpcenv
.get_client_ip().map(|addr
| addr
.ip()) {
227 Some(ip
) => format
!("{}", ip
),
228 None
=> "unknown".into(),
232 "authentication failure; rhost={} user={} msg={}",
237 crate::server
::rest
::auth_logger()?
.log(&msg
);
238 log
::error
!("{}", msg
);
240 Err(http_err
!(UNAUTHORIZED
, "permission check failed."))
253 schema
: PASSWORD_SCHEMA
,
258 description
: "Everybody is allowed to change their own password. In addition, users with 'Permissions:Modify' privilege may change any password on @pbs realm.",
259 permission
: &Permission
::Anybody
,
262 /// Change user password
264 /// Each user is allowed to change his own password. Superuser
265 /// can change all passwords.
266 pub fn change_password(
269 rpcenv
: &mut dyn RpcEnvironment
,
270 ) -> Result
<Value
, Error
> {
271 let current_auth
: Authid
= rpcenv
273 .ok_or_else(|| format_err
!("no authid available"))?
276 if current_auth
.is_token() {
277 bail
!("API tokens cannot access this API endpoint");
280 let current_user
= current_auth
.user();
282 let mut allowed
= userid
== *current_user
;
285 let user_info
= CachedUserInfo
::new()?
;
286 let privs
= user_info
.lookup_privs(¤t_auth
, &[]);
287 if user_info
.is_superuser(¤t_auth
) {
290 if (privs
& PRIV_PERMISSIONS_MODIFY
) != 0 && userid
.realm() != "pam" {
296 bail
!("you are not authorized to change the password.");
299 let authenticator
= crate::auth
::lookup_authenticator(userid
.realm())?
;
300 authenticator
.store_password(userid
.name(), &password
)?
;
313 schema
: ACL_PATH_SCHEMA
,
319 permission
: &Permission
::Anybody
,
320 description
: "Requires Sys.Audit on '/access', limited to own privileges otherwise.",
323 description
: "Map of ACL path to Map of privilege to propagate bit",
326 additional_properties
: true,
329 /// List permissions of given or currently authenticated user / API token.
331 /// Optionally limited to specific path.
332 pub 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()?
;
339 let user_info
= CachedUserInfo
::new()?
;
340 let user_privs
= user_info
.lookup_privs(¤t_auth_id
, &["access"]);
342 let auth_id
= match auth_id
{
343 Some(auth_id
) if auth_id
== current_auth_id
=> current_auth_id
,
345 if user_privs
& PRIV_SYS_AUDIT
!= 0
346 || (auth_id
.is_token()
347 && !current_auth_id
.is_token()
348 && auth_id
.user() == current_auth_id
.user())
352 bail
!("not allowed to list permissions of {}", auth_id
);
355 None
=> current_auth_id
,
358 fn populate_acl_paths(
359 mut paths
: HashSet
<String
>,
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
);
371 let paths
= match path
{
373 let mut paths
= HashSet
::new();
378 let mut paths
= HashSet
::new();
380 let (acl_tree
, _
) = pbs_config
::acl
::config()?
;
381 paths
= populate_acl_paths(paths
, acl_tree
.root
, "");
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());
394 let map
= paths
.into_iter().fold(
396 |mut map
: HashMap
<String
, HashMap
<String
, bool
>>, path
: String
| {
397 let split_path
= pbs_config
::acl
::split_acl_path(path
.as_str());
398 let (privs
, propagated_privs
) = user_info
.lookup_privs_details(&auth_id
, &split_path
);
401 0 => map
, // Don't leak ACL paths where we don't have any privileges
406 .fold(HashMap
::new(), |mut priv_map
, (name
, value
)| {
407 if value
& privs
!= 0 {
409 .insert(name
.to_string(), value
& propagated_privs
!= 0);
414 map
.insert(path
, priv_map
);
425 const OPENID_ROUTER
: &Router
= &openid
::ROUTER
;
428 const OPENID_ROUTER
: &Router
= &Router
::new();
431 const SUBDIRS
: SubdirMap
= &sorted
!([
432 ("acl", &acl
::ROUTER
),
433 ("password", &Router
::new().put(&API_METHOD_CHANGE_PASSWORD
)),
436 &Router
::new().get(&API_METHOD_LIST_PERMISSIONS
)
438 ("ticket", &Router
::new().post(&API_METHOD_CREATE_TICKET
)),
439 ("openid", &OPENID_ROUTER
),
440 ("domains", &domain
::ROUTER
),
441 ("roles", &role
::ROUTER
),
442 ("users", &user
::ROUTER
),
443 ("tfa", &tfa
::ROUTER
),
446 pub const ROUTER
: Router
= Router
::new()
447 .get(&list_subdirs_api_method
!(SUBDIRS
))