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}
;
14 use pbs_tools
::ticket
::{self, Empty, Ticket}
;
16 use crate::api2
::types
::*;
17 use crate::auth_helpers
::*;
18 use crate::server
::ticket
::ApiTicket
;
20 use crate::config
::acl
as acl_config
;
21 use crate::config
::acl
::{PRIVILEGES, PRIV_PERMISSIONS_MODIFY, PRIV_SYS_AUDIT}
;
22 use crate::config
::cached_user_info
::CachedUserInfo
;
23 use crate::config
::tfa
::TfaChallenge
;
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
> {
196 match authenticate_user(&username
, &password
, path
, privs
, port
, tfa_challenge
) {
197 Ok(AuthResult
::Success
) => Ok(json
!({ "username": username }
)),
198 Ok(AuthResult
::CreateTicket
) => {
199 let api_ticket
= ApiTicket
::full(username
.clone());
200 let ticket
= Ticket
::new("PBS", &api_ticket
)?
.sign(private_auth_key(), None
)?
;
201 let token
= assemble_csrf_prevention_token(csrf_secret(), &username
);
203 crate::server
::rest
::auth_logger()?
204 .log(format
!("successful auth for user '{}'", username
));
207 "username": username
,
209 "CSRFPreventionToken": token
,
212 Ok(AuthResult
::Partial(challenge
)) => {
213 let api_ticket
= ApiTicket
::partial(challenge
);
214 let ticket
= Ticket
::new("PBS", &api_ticket
)?
215 .sign(private_auth_key(), Some(username
.as_str()))?
;
217 "username": username
,
219 "CSRFPreventionToken": "invalid",
223 let client_ip
= match rpcenv
.get_client_ip().map(|addr
| addr
.ip()) {
224 Some(ip
) => format
!("{}", ip
),
225 None
=> "unknown".into(),
229 "authentication failure; rhost={} user={} msg={}",
234 crate::server
::rest
::auth_logger()?
.log(&msg
);
235 log
::error
!("{}", msg
);
237 Err(http_err
!(UNAUTHORIZED
, "permission check failed."))
250 schema
: PASSWORD_SCHEMA
,
255 description
: "Everybody is allowed to change their own password. In addition, users with 'Permissions:Modify' privilege may change any password on @pbs realm.",
256 permission
: &Permission
::Anybody
,
259 /// Change user password
261 /// Each user is allowed to change his own password. Superuser
262 /// can change all passwords.
263 pub fn change_password(
266 rpcenv
: &mut dyn RpcEnvironment
,
267 ) -> Result
<Value
, Error
> {
268 let current_auth
: Authid
= rpcenv
270 .ok_or_else(|| format_err
!("no authid available"))?
273 if current_auth
.is_token() {
274 bail
!("API tokens cannot access this API endpoint");
277 let current_user
= current_auth
.user();
279 let mut allowed
= userid
== *current_user
;
282 let user_info
= CachedUserInfo
::new()?
;
283 let privs
= user_info
.lookup_privs(¤t_auth
, &[]);
284 if user_info
.is_superuser(¤t_auth
) {
287 if (privs
& PRIV_PERMISSIONS_MODIFY
) != 0 && userid
.realm() != "pam" {
293 bail
!("you are not authorized to change the password.");
296 let authenticator
= crate::auth
::lookup_authenticator(userid
.realm())?
;
297 authenticator
.store_password(userid
.name(), &password
)?
;
310 schema
: ACL_PATH_SCHEMA
,
316 permission
: &Permission
::Anybody
,
317 description
: "Requires Sys.Audit on '/access', limited to own privileges otherwise.",
320 description
: "Map of ACL path to Map of privilege to propagate bit",
323 additional_properties
: true,
326 /// List permissions of given or currently authenticated user / API token.
328 /// Optionally limited to specific path.
329 pub fn list_permissions(
330 auth_id
: Option
<Authid
>,
331 path
: Option
<String
>,
332 rpcenv
: &dyn RpcEnvironment
,
333 ) -> Result
<HashMap
<String
, HashMap
<String
, bool
>>, Error
> {
334 let current_auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
336 let user_info
= CachedUserInfo
::new()?
;
337 let user_privs
= user_info
.lookup_privs(¤t_auth_id
, &["access"]);
339 let auth_id
= match auth_id
{
340 Some(auth_id
) if auth_id
== current_auth_id
=> current_auth_id
,
342 if user_privs
& PRIV_SYS_AUDIT
!= 0
343 || (auth_id
.is_token()
344 && !current_auth_id
.is_token()
345 && auth_id
.user() == current_auth_id
.user())
349 bail
!("not allowed to list permissions of {}", auth_id
);
352 None
=> current_auth_id
,
355 fn populate_acl_paths(
356 mut paths
: HashSet
<String
>,
357 node
: acl_config
::AclTreeNode
,
359 ) -> HashSet
<String
> {
360 for (sub_path
, child_node
) in node
.children
{
361 let sub_path
= format
!("{}/{}", path
, &sub_path
);
362 paths
= populate_acl_paths(paths
, child_node
, &sub_path
);
363 paths
.insert(sub_path
);
368 let paths
= match path
{
370 let mut paths
= HashSet
::new();
375 let mut paths
= HashSet
::new();
377 let (acl_tree
, _
) = acl_config
::config()?
;
378 paths
= populate_acl_paths(paths
, acl_tree
.root
, "");
380 // default paths, returned even if no ACL exists
381 paths
.insert("/".to_string());
382 paths
.insert("/access".to_string());
383 paths
.insert("/datastore".to_string());
384 paths
.insert("/remote".to_string());
385 paths
.insert("/system".to_string());
391 let map
= paths
.into_iter().fold(
393 |mut map
: HashMap
<String
, HashMap
<String
, bool
>>, path
: String
| {
394 let split_path
= acl_config
::split_acl_path(path
.as_str());
395 let (privs
, propagated_privs
) = user_info
.lookup_privs_details(&auth_id
, &split_path
);
398 0 => map
, // Don't leak ACL paths where we don't have any privileges
403 .fold(HashMap
::new(), |mut priv_map
, (name
, value
)| {
404 if value
& privs
!= 0 {
406 .insert(name
.to_string(), value
& propagated_privs
!= 0);
411 map
.insert(path
, priv_map
);
422 const OPENID_ROUTER
: &Router
= &openid
::ROUTER
;
425 const OPENID_ROUTER
: &Router
= &Router
::new();
428 const SUBDIRS
: SubdirMap
= &sorted
!([
429 ("acl", &acl
::ROUTER
),
430 ("password", &Router
::new().put(&API_METHOD_CHANGE_PASSWORD
)),
433 &Router
::new().get(&API_METHOD_LIST_PERMISSIONS
)
435 ("ticket", &Router
::new().post(&API_METHOD_CREATE_TICKET
)),
436 ("openid", &OPENID_ROUTER
),
437 ("domains", &domain
::ROUTER
),
438 ("roles", &role
::ROUTER
),
439 ("users", &user
::ROUTER
),
440 ("tfa", &tfa
::ROUTER
),
443 pub const ROUTER
: Router
= Router
::new()
444 .get(&list_subdirs_api_method
!(SUBDIRS
))