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
::ticket
::{self, Empty, Ticket}
;
19 use pbs_config
::acl
::AclTreeNode
;
20 use pbs_config
::CachedUserInfo
;
22 use crate::auth_helpers
::*;
23 use crate::config
::tfa
::TfaChallenge
;
24 use crate::server
::ticket
::ApiTicket
;
33 #[allow(clippy::large_enum_variant)]
35 /// Successful authentication which does not require a new ticket.
38 /// Successful authentication which requires a ticket to be created.
41 /// A partial ticket which requires a 2nd factor will be created.
42 Partial(TfaChallenge
),
49 privs
: Option
<String
>,
51 tfa_challenge
: Option
<String
>,
52 ) -> Result
<AuthResult
, Error
> {
53 let user_info
= CachedUserInfo
::new()?
;
55 let auth_id
= Authid
::from(userid
.clone());
56 if !user_info
.is_active_auth_id(&auth_id
) {
57 bail
!("user account disabled or expired.");
60 if let Some(tfa_challenge
) = tfa_challenge
{
61 return authenticate_2nd(userid
, &tfa_challenge
, password
);
64 if password
.starts_with("PBS:") {
65 if let Ok(ticket_userid
) = Ticket
::<Userid
>::parse(password
)
66 .and_then(|ticket
| ticket
.verify(public_auth_key(), "PBS", None
))
68 if *userid
== ticket_userid
{
69 return Ok(AuthResult
::CreateTicket
);
71 bail
!("ticket login failed - wrong userid");
73 } else if password
.starts_with("PBSTERM:") {
74 if path
.is_none() || privs
.is_none() || port
.is_none() {
75 bail
!("cannot check termnal ticket without path, priv and port");
78 let path
= path
.ok_or_else(|| format_err
!("missing path for termproxy ticket"))?
;
80 privs
.ok_or_else(|| format_err
!("missing privilege name for termproxy ticket"))?
;
81 let port
= port
.ok_or_else(|| format_err
!("missing port for termproxy ticket"))?
;
83 if let Ok(Empty
) = Ticket
::parse(password
).and_then(|ticket
| {
87 Some(&crate::tools
::ticket
::term_aad(userid
, &path
, port
)),
90 for (name
, privilege
) in PRIVILEGES
{
91 if *name
== privilege_name
{
92 let mut path_vec
= Vec
::new();
93 for part
in path
.split('
/'
) {
98 user_info
.check_privs(&auth_id
, &path_vec
, *privilege
, false)?
;
99 return Ok(AuthResult
::Success
);
103 bail
!("No such privilege");
107 let _
: () = crate::auth
::authenticate_user(userid
, password
)?
;
109 Ok(match crate::config
::tfa
::login_challenge(userid
)?
{
110 None
=> AuthResult
::CreateTicket
,
111 Some(challenge
) => AuthResult
::Partial(challenge
),
117 challenge_ticket
: &str,
119 ) -> Result
<AuthResult
, Error
> {
120 let challenge
: TfaChallenge
= Ticket
::<ApiTicket
>::parse(&challenge_ticket
)?
121 .verify_with_time_frame(public_auth_key(), "PBS", Some(userid
.as_str()), -60..600)?
124 let _
: () = crate::config
::tfa
::verify_challenge(userid
, &challenge
, response
.parse()?
)?
;
126 Ok(AuthResult
::CreateTicket
)
136 schema
: PASSWORD_SCHEMA
,
140 description
: "Path for verifying terminal tickets.",
145 description
: "Privilege for verifying terminal tickets.",
150 description
: "Port for verifying terminal tickets.",
155 description
: "The signed TFA challenge string the user wants to respond to.",
164 description
: "User name.",
168 description
: "Auth ticket.",
170 CSRFPreventionToken
: {
173 "Cross Site Request Forgery Prevention Token. \
174 For partial tickets this is the string \"invalid\".",
180 permission
: &Permission
::World
,
183 /// Create or verify authentication ticket.
185 /// Returns: An authentication ticket with additional infos.
186 pub fn create_ticket(
189 path
: Option
<String
>,
190 privs
: Option
<String
>,
192 tfa_challenge
: Option
<String
>,
193 rpcenv
: &mut dyn RpcEnvironment
,
194 ) -> Result
<Value
, Error
> {
196 use proxmox_rest_server
::RestEnvironment
;
198 let env
: &RestEnvironment
= rpcenv
.as_any().downcast_ref
::<RestEnvironment
>()
199 .ok_or_else(|| format_err
!("detected worng RpcEnvironment type"))?
;
201 match authenticate_user(&username
, &password
, path
, privs
, port
, tfa_challenge
) {
202 Ok(AuthResult
::Success
) => Ok(json
!({ "username": username }
)),
203 Ok(AuthResult
::CreateTicket
) => {
204 let api_ticket
= ApiTicket
::full(username
.clone());
205 let ticket
= Ticket
::new("PBS", &api_ticket
)?
.sign(private_auth_key(), None
)?
;
206 let token
= assemble_csrf_prevention_token(csrf_secret(), &username
);
208 env
.log_auth(username
.as_str());
211 "username": username
,
213 "CSRFPreventionToken": token
,
216 Ok(AuthResult
::Partial(challenge
)) => {
217 let api_ticket
= ApiTicket
::partial(challenge
);
218 let ticket
= Ticket
::new("PBS", &api_ticket
)?
219 .sign(private_auth_key(), Some(username
.as_str()))?
;
221 "username": username
,
223 "CSRFPreventionToken": "invalid",
227 env
.log_failed_auth(Some(username
.to_string()), &err
.to_string());
228 Err(http_err
!(UNAUTHORIZED
, "permission check failed."))
241 schema
: PASSWORD_SCHEMA
,
246 description
: "Everybody is allowed to change their own password. In addition, users with 'Permissions:Modify' privilege may change any password on @pbs realm.",
247 permission
: &Permission
::Anybody
,
250 /// Change user password
252 /// Each user is allowed to change his own password. Superuser
253 /// can change all passwords.
254 pub fn change_password(
257 rpcenv
: &mut dyn RpcEnvironment
,
258 ) -> Result
<Value
, Error
> {
259 let current_auth
: Authid
= rpcenv
261 .ok_or_else(|| format_err
!("no authid available"))?
264 if current_auth
.is_token() {
265 bail
!("API tokens cannot access this API endpoint");
268 let current_user
= current_auth
.user();
270 let mut allowed
= userid
== *current_user
;
273 let user_info
= CachedUserInfo
::new()?
;
274 let privs
= user_info
.lookup_privs(¤t_auth
, &[]);
275 if user_info
.is_superuser(¤t_auth
) {
278 if (privs
& PRIV_PERMISSIONS_MODIFY
) != 0 && userid
.realm() != "pam" {
284 bail
!("you are not authorized to change the password.");
287 let authenticator
= crate::auth
::lookup_authenticator(userid
.realm())?
;
288 authenticator
.store_password(userid
.name(), &password
)?
;
301 schema
: ACL_PATH_SCHEMA
,
307 permission
: &Permission
::Anybody
,
308 description
: "Requires Sys.Audit on '/access', limited to own privileges otherwise.",
311 description
: "Map of ACL path to Map of privilege to propagate bit",
314 additional_properties
: true,
317 /// List permissions of given or currently authenticated user / API token.
319 /// Optionally limited to specific path.
320 pub fn list_permissions(
321 auth_id
: Option
<Authid
>,
322 path
: Option
<String
>,
323 rpcenv
: &dyn RpcEnvironment
,
324 ) -> Result
<HashMap
<String
, HashMap
<String
, bool
>>, Error
> {
325 let current_auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
327 let user_info
= CachedUserInfo
::new()?
;
328 let user_privs
= user_info
.lookup_privs(¤t_auth_id
, &["access"]);
330 let auth_id
= match auth_id
{
331 Some(auth_id
) if auth_id
== current_auth_id
=> current_auth_id
,
333 if user_privs
& PRIV_SYS_AUDIT
!= 0
334 || (auth_id
.is_token()
335 && !current_auth_id
.is_token()
336 && auth_id
.user() == current_auth_id
.user())
340 bail
!("not allowed to list permissions of {}", auth_id
);
343 None
=> current_auth_id
,
346 fn populate_acl_paths(
347 mut paths
: HashSet
<String
>,
350 ) -> HashSet
<String
> {
351 for (sub_path
, child_node
) in node
.children
{
352 let sub_path
= format
!("{}/{}", path
, &sub_path
);
353 paths
= populate_acl_paths(paths
, child_node
, &sub_path
);
354 paths
.insert(sub_path
);
359 let paths
= match path
{
361 let mut paths
= HashSet
::new();
366 let mut paths
= HashSet
::new();
368 let (acl_tree
, _
) = pbs_config
::acl
::config()?
;
369 paths
= populate_acl_paths(paths
, acl_tree
.root
, "");
371 // default paths, returned even if no ACL exists
372 paths
.insert("/".to_string());
373 paths
.insert("/access".to_string());
374 paths
.insert("/datastore".to_string());
375 paths
.insert("/remote".to_string());
376 paths
.insert("/system".to_string());
382 let map
= paths
.into_iter().fold(
384 |mut map
: HashMap
<String
, HashMap
<String
, bool
>>, path
: String
| {
385 let split_path
= pbs_config
::acl
::split_acl_path(path
.as_str());
386 let (privs
, propagated_privs
) = user_info
.lookup_privs_details(&auth_id
, &split_path
);
389 0 => map
, // Don't leak ACL paths where we don't have any privileges
394 .fold(HashMap
::new(), |mut priv_map
, (name
, value
)| {
395 if value
& privs
!= 0 {
397 .insert(name
.to_string(), value
& propagated_privs
!= 0);
402 map
.insert(path
, priv_map
);
413 const SUBDIRS
: SubdirMap
= &sorted
!([
414 ("acl", &acl
::ROUTER
),
415 ("password", &Router
::new().put(&API_METHOD_CHANGE_PASSWORD
)),
418 &Router
::new().get(&API_METHOD_LIST_PERMISSIONS
)
420 ("ticket", &Router
::new().post(&API_METHOD_CREATE_TICKET
)),
421 ("openid", &openid
::ROUTER
),
422 ("domains", &domain
::ROUTER
),
423 ("roles", &role
::ROUTER
),
424 ("users", &user
::ROUTER
),
425 ("tfa", &tfa
::ROUTER
),
428 pub const ROUTER
: Router
= Router
::new()
429 .get(&list_subdirs_api_method
!(SUBDIRS
))