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_sys
::{identity, sortable}
;
11 http_err
, list_subdirs_api_method
, Router
, RpcEnvironment
, SubdirMap
, Permission
,
13 use proxmox_schema
::api
;
16 Userid
, Authid
, PASSWORD_SCHEMA
, ACL_PATH_SCHEMA
,
17 PRIVILEGES
, PRIV_PERMISSIONS_MODIFY
, PRIV_SYS_AUDIT
,
19 use pbs_tools
::ticket
::{self, Empty, Ticket}
;
20 use pbs_config
::acl
::AclTreeNode
;
21 use pbs_config
::CachedUserInfo
;
23 use crate::auth_helpers
::*;
24 use crate::config
::tfa
::TfaChallenge
;
25 use crate::server
::ticket
::ApiTicket
;
34 #[allow(clippy::large_enum_variant)]
36 /// Successful authentication which does not require a new ticket.
39 /// Successful authentication which requires a ticket to be created.
42 /// A partial ticket which requires a 2nd factor will be created.
43 Partial(TfaChallenge
),
50 privs
: Option
<String
>,
52 tfa_challenge
: Option
<String
>,
53 ) -> Result
<AuthResult
, Error
> {
54 let user_info
= CachedUserInfo
::new()?
;
56 let auth_id
= Authid
::from(userid
.clone());
57 if !user_info
.is_active_auth_id(&auth_id
) {
58 bail
!("user account disabled or expired.");
61 if let Some(tfa_challenge
) = tfa_challenge
{
62 return authenticate_2nd(userid
, &tfa_challenge
, password
);
65 if password
.starts_with("PBS:") {
66 if let Ok(ticket_userid
) = Ticket
::<Userid
>::parse(password
)
67 .and_then(|ticket
| ticket
.verify(public_auth_key(), "PBS", None
))
69 if *userid
== ticket_userid
{
70 return Ok(AuthResult
::CreateTicket
);
72 bail
!("ticket login failed - wrong userid");
74 } else if password
.starts_with("PBSTERM:") {
75 if path
.is_none() || privs
.is_none() || port
.is_none() {
76 bail
!("cannot check termnal ticket without path, priv and port");
79 let path
= path
.ok_or_else(|| format_err
!("missing path for termproxy ticket"))?
;
81 privs
.ok_or_else(|| format_err
!("missing privilege name for termproxy ticket"))?
;
82 let port
= port
.ok_or_else(|| format_err
!("missing port for termproxy ticket"))?
;
84 if let Ok(Empty
) = Ticket
::parse(password
).and_then(|ticket
| {
88 Some(&crate::tools
::ticket
::term_aad(userid
, &path
, port
)),
91 for (name
, privilege
) in PRIVILEGES
{
92 if *name
== privilege_name
{
93 let mut path_vec
= Vec
::new();
94 for part
in path
.split('
/'
) {
99 user_info
.check_privs(&auth_id
, &path_vec
, *privilege
, false)?
;
100 return Ok(AuthResult
::Success
);
104 bail
!("No such privilege");
108 let _
: () = crate::auth
::authenticate_user(userid
, password
)?
;
110 Ok(match crate::config
::tfa
::login_challenge(userid
)?
{
111 None
=> AuthResult
::CreateTicket
,
112 Some(challenge
) => AuthResult
::Partial(challenge
),
118 challenge_ticket
: &str,
120 ) -> Result
<AuthResult
, Error
> {
121 let challenge
: TfaChallenge
= Ticket
::<ApiTicket
>::parse(&challenge_ticket
)?
122 .verify_with_time_frame(public_auth_key(), "PBS", Some(userid
.as_str()), -60..600)?
125 let _
: () = crate::config
::tfa
::verify_challenge(userid
, &challenge
, response
.parse()?
)?
;
127 Ok(AuthResult
::CreateTicket
)
137 schema
: PASSWORD_SCHEMA
,
141 description
: "Path for verifying terminal tickets.",
146 description
: "Privilege for verifying terminal tickets.",
151 description
: "Port for verifying terminal tickets.",
156 description
: "The signed TFA challenge string the user wants to respond to.",
165 description
: "User name.",
169 description
: "Auth ticket.",
171 CSRFPreventionToken
: {
174 "Cross Site Request Forgery Prevention Token. \
175 For partial tickets this is the string \"invalid\".",
181 permission
: &Permission
::World
,
184 /// Create or verify authentication ticket.
186 /// Returns: An authentication ticket with additional infos.
187 pub fn create_ticket(
190 path
: Option
<String
>,
191 privs
: Option
<String
>,
193 tfa_challenge
: Option
<String
>,
194 rpcenv
: &mut dyn RpcEnvironment
,
195 ) -> Result
<Value
, Error
> {
197 use proxmox_rest_server
::RestEnvironment
;
199 let env
: &RestEnvironment
= rpcenv
.as_any().downcast_ref
::<RestEnvironment
>()
200 .ok_or_else(|| format_err
!("detected worng RpcEnvironment type"))?
;
202 match authenticate_user(&username
, &password
, path
, privs
, port
, tfa_challenge
) {
203 Ok(AuthResult
::Success
) => Ok(json
!({ "username": username }
)),
204 Ok(AuthResult
::CreateTicket
) => {
205 let api_ticket
= ApiTicket
::full(username
.clone());
206 let ticket
= Ticket
::new("PBS", &api_ticket
)?
.sign(private_auth_key(), None
)?
;
207 let token
= assemble_csrf_prevention_token(csrf_secret(), &username
);
209 env
.log_auth(username
.as_str());
212 "username": username
,
214 "CSRFPreventionToken": token
,
217 Ok(AuthResult
::Partial(challenge
)) => {
218 let api_ticket
= ApiTicket
::partial(challenge
);
219 let ticket
= Ticket
::new("PBS", &api_ticket
)?
220 .sign(private_auth_key(), Some(username
.as_str()))?
;
222 "username": username
,
224 "CSRFPreventionToken": "invalid",
228 env
.log_failed_auth(Some(username
.to_string()), &err
.to_string());
229 Err(http_err
!(UNAUTHORIZED
, "permission check failed."))
242 schema
: PASSWORD_SCHEMA
,
247 description
: "Everybody is allowed to change their own password. In addition, users with 'Permissions:Modify' privilege may change any password on @pbs realm.",
248 permission
: &Permission
::Anybody
,
251 /// Change user password
253 /// Each user is allowed to change his own password. Superuser
254 /// can change all passwords.
255 pub fn change_password(
258 rpcenv
: &mut dyn RpcEnvironment
,
259 ) -> Result
<Value
, Error
> {
260 let current_auth
: Authid
= rpcenv
262 .ok_or_else(|| format_err
!("no authid available"))?
265 if current_auth
.is_token() {
266 bail
!("API tokens cannot access this API endpoint");
269 let current_user
= current_auth
.user();
271 let mut allowed
= userid
== *current_user
;
274 let user_info
= CachedUserInfo
::new()?
;
275 let privs
= user_info
.lookup_privs(¤t_auth
, &[]);
276 if user_info
.is_superuser(¤t_auth
) {
279 if (privs
& PRIV_PERMISSIONS_MODIFY
) != 0 && userid
.realm() != "pam" {
285 bail
!("you are not authorized to change the password.");
288 let authenticator
= crate::auth
::lookup_authenticator(userid
.realm())?
;
289 authenticator
.store_password(userid
.name(), &password
)?
;
302 schema
: ACL_PATH_SCHEMA
,
308 permission
: &Permission
::Anybody
,
309 description
: "Requires Sys.Audit on '/access', limited to own privileges otherwise.",
312 description
: "Map of ACL path to Map of privilege to propagate bit",
315 additional_properties
: true,
318 /// List permissions of given or currently authenticated user / API token.
320 /// Optionally limited to specific path.
321 pub fn list_permissions(
322 auth_id
: Option
<Authid
>,
323 path
: Option
<String
>,
324 rpcenv
: &dyn RpcEnvironment
,
325 ) -> Result
<HashMap
<String
, HashMap
<String
, bool
>>, Error
> {
326 let current_auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
328 let user_info
= CachedUserInfo
::new()?
;
329 let user_privs
= user_info
.lookup_privs(¤t_auth_id
, &["access"]);
331 let auth_id
= match auth_id
{
332 Some(auth_id
) if auth_id
== current_auth_id
=> current_auth_id
,
334 if user_privs
& PRIV_SYS_AUDIT
!= 0
335 || (auth_id
.is_token()
336 && !current_auth_id
.is_token()
337 && auth_id
.user() == current_auth_id
.user())
341 bail
!("not allowed to list permissions of {}", auth_id
);
344 None
=> current_auth_id
,
347 fn populate_acl_paths(
348 mut paths
: HashSet
<String
>,
351 ) -> HashSet
<String
> {
352 for (sub_path
, child_node
) in node
.children
{
353 let sub_path
= format
!("{}/{}", path
, &sub_path
);
354 paths
= populate_acl_paths(paths
, child_node
, &sub_path
);
355 paths
.insert(sub_path
);
360 let paths
= match path
{
362 let mut paths
= HashSet
::new();
367 let mut paths
= HashSet
::new();
369 let (acl_tree
, _
) = pbs_config
::acl
::config()?
;
370 paths
= populate_acl_paths(paths
, acl_tree
.root
, "");
372 // default paths, returned even if no ACL exists
373 paths
.insert("/".to_string());
374 paths
.insert("/access".to_string());
375 paths
.insert("/datastore".to_string());
376 paths
.insert("/remote".to_string());
377 paths
.insert("/system".to_string());
383 let map
= paths
.into_iter().fold(
385 |mut map
: HashMap
<String
, HashMap
<String
, bool
>>, path
: String
| {
386 let split_path
= pbs_config
::acl
::split_acl_path(path
.as_str());
387 let (privs
, propagated_privs
) = user_info
.lookup_privs_details(&auth_id
, &split_path
);
390 0 => map
, // Don't leak ACL paths where we don't have any privileges
395 .fold(HashMap
::new(), |mut priv_map
, (name
, value
)| {
396 if value
& privs
!= 0 {
398 .insert(name
.to_string(), value
& propagated_privs
!= 0);
403 map
.insert(path
, priv_map
);
414 const SUBDIRS
: SubdirMap
= &sorted
!([
415 ("acl", &acl
::ROUTER
),
416 ("password", &Router
::new().put(&API_METHOD_CHANGE_PASSWORD
)),
419 &Router
::new().get(&API_METHOD_LIST_PERMISSIONS
)
421 ("ticket", &Router
::new().post(&API_METHOD_CREATE_TICKET
)),
422 ("openid", &openid
::ROUTER
),
423 ("domains", &domain
::ROUTER
),
424 ("roles", &role
::ROUTER
),
425 ("users", &user
::ROUTER
),
426 ("tfa", &tfa
::ROUTER
),
429 pub const ROUTER
: Router
= Router
::new()
430 .get(&list_subdirs_api_method
!(SUBDIRS
))