public inbox for pbs-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: "Fabian Grünbichler" <f.gruenbichler@proxmox.com>
To: pbs-devel@lists.proxmox.com
Subject: [pbs-devel] [RFC proxmox-backup 10/15] api: add permissions endpoint
Date: Mon, 19 Oct 2020 09:39:14 +0200	[thread overview]
Message-ID: <20201019073919.588521-11-f.gruenbichler@proxmox.com> (raw)
In-Reply-To: <20201019073919.588521-1-f.gruenbichler@proxmox.com>

and adapt privilege calculation to return propagate flag

Signed-off-by: Fabian Grünbichler <f.gruenbichler@proxmox.com>
---

Notes:
    not too happy with the paths addition to AclTree. could probably be solved with
    some recursive function..
    
    this is compatible with the permissions API call in PVE, except that true is encoded as 1 there.

 src/api2/access.rs             | 95 +++++++++++++++++++++++++++++++++-
 src/config/acl.rs              | 67 +++++++++++++-----------
 src/config/cached_user_info.rs | 19 +++++--
 3 files changed, 146 insertions(+), 35 deletions(-)

diff --git a/src/api2/access.rs b/src/api2/access.rs
index 0c19dab6..c898fd7d 100644
--- a/src/api2/access.rs
+++ b/src/api2/access.rs
@@ -1,6 +1,9 @@
 use anyhow::{bail, format_err, Error};
 
 use serde_json::{json, Value};
+use std::collections::HashMap;
+use std::collections::HashSet;
+use std::convert::TryFrom;
 
 use proxmox::api::{api, RpcEnvironment, Permission};
 use proxmox::api::router::{Router, SubdirMap};
@@ -12,8 +15,9 @@ use crate::auth_helpers::*;
 use crate::api2::types::*;
 use crate::tools::{FileLogOptions, FileLogger};
 
+use crate::config::acl as acl_config;
+use crate::config::acl::{PRIVILEGES, PRIV_SYS_AUDIT, PRIV_PERMISSIONS_MODIFY};
 use crate::config::cached_user_info::CachedUserInfo;
-use crate::config::acl::{PRIVILEGES, PRIV_PERMISSIONS_MODIFY};
 
 pub mod user;
 pub mod domain;
@@ -237,6 +241,91 @@ fn change_password(
     Ok(Value::Null)
 }
 
+#[api(
+    input: {
+        properties: {
+            userid: {
+                schema: PROXMOX_USER_OR_TOKEN_ID_SCHEMA,
+                optional: true,
+            },
+            path: {
+                schema: ACL_PATH_SCHEMA,
+                optional: true,
+            },
+        },
+    },
+    access: {
+        permission: &Permission::Or(&[
+            &Permission::Privilege(&["access"], PRIV_SYS_AUDIT, false),
+            &Permission::UserParam("userid"),
+        ]),
+    },
+    returns: {
+        description: "Map of ACL path to Map of privilege to propagate bit",
+        type: Object,
+        properties: {},
+        additional_properties: true,
+    },
+)]
+/// List permissions of given or currently authenticated user / API token.
+///
+/// Optionally limited to specific path.
+pub fn list_permissions(
+    userid: Option<Userid>,
+    path: Option<String>,
+    rpcenv: &dyn RpcEnvironment,
+) -> Result<HashMap<String, HashMap<String, bool>>, Error> {
+    let user = match userid {
+        Some(user) => user,
+        None => Userid::try_from(rpcenv.get_user().unwrap())?
+    };
+
+    let user_info = CachedUserInfo::new()?;
+
+    let paths = match path {
+        Some(path) => {
+            let mut paths = HashSet::new();
+            paths.insert(path);
+            paths
+        },
+        None => {
+            let mut paths = HashSet::new();
+
+            // default paths, returned even if no ACL exists
+            paths.insert("/".to_string());
+            paths.insert("/access".to_string());
+            paths.insert("/datastore".to_string());
+            paths.insert("/remote".to_string());
+            paths.insert("/system".to_string());
+
+            let (acl_tree, _) = acl_config::config()?;
+            paths.extend(acl_tree.paths);
+
+            paths
+        },
+    };
+
+    let map = paths.into_iter().fold(HashMap::new(), |mut map: HashMap<String, HashMap<String, bool>>, path: String| {
+        let (privs, propagated_privs) = user_info.lookup_privs_details(&user, &acl_config::split_acl_path(path.as_str()));
+
+        let priv_map = match privs {
+            0 => HashMap::new(),
+            _ => PRIVILEGES.iter().fold(HashMap::new(), |mut priv_map, (name, value)| {
+                if value & privs != 0 {
+                    priv_map.insert(name.to_string(), value & propagated_privs != 0);
+                }
+                priv_map
+            }),
+        };
+
+        map.insert(path, priv_map);
+
+        map
+    });
+
+    Ok(map)
+}
+
 #[sortable]
 const SUBDIRS: SubdirMap = &sorted!([
     ("acl", &acl::ROUTER),
@@ -244,6 +333,10 @@ const SUBDIRS: SubdirMap = &sorted!([
         "password", &Router::new()
             .put(&API_METHOD_CHANGE_PASSWORD)
     ),
+    (
+        "permissions", &Router::new()
+            .get(&API_METHOD_LIST_PERMISSIONS)
+    ),
     (
         "ticket", &Router::new()
             .post(&API_METHOD_CREATE_TICKET)
diff --git a/src/config/acl.rs b/src/config/acl.rs
index 39f9d030..b162b875 100644
--- a/src/config/acl.rs
+++ b/src/config/acl.rs
@@ -1,5 +1,5 @@
 use std::io::Write;
-use std::collections::{HashMap, HashSet, BTreeMap, BTreeSet};
+use std::collections::{HashMap, BTreeMap, BTreeSet};
 use std::path::{PathBuf, Path};
 use std::sync::{Arc, RwLock};
 use std::str::FromStr;
@@ -228,6 +228,7 @@ pub fn check_acl_path(path: &str) -> Result<(), Error> {
 
 pub struct AclTree {
     pub root: AclTreeNode,
+    pub paths: Vec<String>,
 }
 
 pub struct AclTreeNode {
@@ -246,7 +247,7 @@ impl AclTreeNode {
         }
     }
 
-    pub fn extract_roles(&self, user: &Userid, all: bool) -> HashSet<String> {
+    pub fn extract_roles(&self, user: &Userid, all: bool) -> HashMap<String, bool> {
         let user_roles = self.extract_user_roles(user, all);
         if !user_roles.is_empty() {
             // user privs always override group privs
@@ -256,33 +257,33 @@ impl AclTreeNode {
         self.extract_group_roles(user, all)
     }
 
-    pub fn extract_user_roles(&self, user: &Userid, all: bool) -> HashSet<String> {
+    pub fn extract_user_roles(&self, user: &Userid, all: bool) -> HashMap<String, bool> {
 
-        let mut set = HashSet::new();
+        let mut map = HashMap::new();
 
         let roles = match self.users.get(user) {
             Some(m) => m,
-            None => return set,
+            None => return map,
         };
 
         for (role, propagate) in roles {
             if *propagate || all {
                 if role == ROLE_NAME_NO_ACCESS {
-                    // return a set with a single role 'NoAccess'
-                    let mut set = HashSet::new();
-                    set.insert(role.to_string());
-                    return set;
+                    // return a map with a single role 'NoAccess'
+                    let mut map = HashMap::new();
+                    map.insert(role.to_string(), false);
+                    return map;
                 }
-                set.insert(role.to_string());
+                map.insert(role.to_string(), *propagate);
             }
         }
 
-        set
+        map
     }
 
-    pub fn extract_group_roles(&self, _user: &Userid, all: bool) -> HashSet<String> {
+    pub fn extract_group_roles(&self, _user: &Userid, all: bool) -> HashMap<String, bool> {
 
-        let mut set = HashSet::new();
+        let mut map = HashMap::new();
 
         for (_group, roles) in &self.groups {
             let is_member = false; // fixme: check if user is member of the group
@@ -291,17 +292,17 @@ impl AclTreeNode {
             for (role, propagate) in roles {
                 if *propagate || all {
                     if role == ROLE_NAME_NO_ACCESS {
-                        // return a set with a single role 'NoAccess'
-                        let mut set = HashSet::new();
-                        set.insert(role.to_string());
-                        return set;
+                        // return a map with a single role 'NoAccess'
+                        let mut map = HashMap::new();
+                        map.insert(role.to_string(), false);
+                        return map;
                     }
-                    set.insert(role.to_string());
+                    map.insert(role.to_string(), *propagate);
                 }
             }
         }
 
-        set
+        map
     }
 
     pub fn delete_group_role(&mut self, group: &str, role: &str) {
@@ -346,7 +347,10 @@ impl AclTreeNode {
 impl AclTree {
 
     pub fn new() -> Self {
-        Self { root: AclTreeNode::new() }
+        Self {
+            root: AclTreeNode::new(),
+            paths: Vec::new(),
+        }
     }
 
     pub fn find_node(&mut self, path: &str) -> Option<&mut AclTreeNode> {
@@ -512,7 +516,8 @@ impl AclTree {
             bail!("expected '0' or '1' for propagate flag.");
         };
 
-        let path = split_acl_path(items[2]);
+        let path_str = items[2];
+        let path = split_acl_path(path_str);
         let node = self.get_or_insert_node(&path);
 
         let uglist: Vec<&str> = items[3].split(',').map(|v| v.trim()).collect();
@@ -533,6 +538,8 @@ impl AclTree {
             }
         }
 
+        self.paths.push(path_str.to_string());
+
         Ok(())
     }
 
@@ -576,25 +583,25 @@ impl AclTree {
         Ok(tree)
     }
 
-    pub fn roles(&self, userid: &Userid, path: &[&str]) -> HashSet<String> {
+    pub fn roles(&self, userid: &Userid, path: &[&str]) -> HashMap<String, bool> {
 
         let mut node = &self.root;
-        let mut role_set = node.extract_roles(userid, path.is_empty());
+        let mut role_map = node.extract_roles(userid, path.is_empty());
 
         for (pos, comp) in path.iter().enumerate() {
             let last_comp = (pos + 1) == path.len();
             node = match node.children.get(*comp) {
                 Some(n) => n,
-                None => return role_set, // path not found
+                None => return role_map, // path not found
             };
-            let new_set = node.extract_roles(userid, last_comp);
-            if !new_set.is_empty() {
-                // overwrite previous settings
-                role_set = new_set;
+            let new_map = node.extract_roles(userid, last_comp);
+            if !new_map.is_empty() {
+                // overwrite previous maptings
+                role_map = new_map;
             }
         }
 
-        role_set
+        role_map
     }
 }
 
@@ -686,7 +693,7 @@ mod test {
 
         let path_vec = super::split_acl_path(path);
         let mut roles = tree.roles(user, &path_vec)
-            .iter().map(|v| v.clone()).collect::<Vec<String>>();
+            .iter().map(|(role, _)| role.clone()).collect::<Vec<String>>();
         roles.sort();
         let roles = roles.join(",");
 
diff --git a/src/config/cached_user_info.rs b/src/config/cached_user_info.rs
index 93f360c8..99e96e67 100644
--- a/src/config/cached_user_info.rs
+++ b/src/config/cached_user_info.rs
@@ -128,26 +128,37 @@ impl CachedUserInfo {
     }
 
     pub fn lookup_privs(&self, userid: &Userid, path: &[&str]) -> u64 {
+        let (privs, _) = self.lookup_privs_details(userid, path);
+        privs
+    }
 
+    pub fn lookup_privs_details(&self, userid: &Userid, path: &[&str]) -> (u64, u64) {
         if self.is_superuser(userid) {
-            return ROLE_ADMIN;
+            return (ROLE_ADMIN, ROLE_ADMIN);
         }
 
         let roles = self.acl_tree.roles(userid, path);
         let mut privs: u64 = 0;
-        for role in roles {
+        let mut propagated_privs: u64 = 0;
+        for (role, propagate) in roles {
             if let Some((role_privs, _)) = ROLE_NAMES.get(role.as_str()) {
+                if propagate {
+                    propagated_privs |= role_privs;
+                }
                 privs |= role_privs;
             }
         }
 
         if userid.is_tokenid() {
             // limit privs to that of owning user
-            privs &= self.lookup_privs(&userid.owner().unwrap(), path);
+            let (owner_privs, owner_propagated_privs) = self.lookup_privs_details(&userid.owner().unwrap(), path);
+            privs &= owner_privs;
+            propagated_privs &= owner_propagated_privs;
         }
 
-        privs
+        (privs, propagated_privs)
     }
+
 }
 
 impl UserInformation for CachedUserInfo {
-- 
2.20.1





  parent reply	other threads:[~2020-10-19  7:40 UTC|newest]

Thread overview: 23+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2020-10-19  7:39 [pbs-devel] [RFC proxmox-backup 00/15] API tokens Fabian Grünbichler
2020-10-19  7:39 ` [pbs-devel] [PATCH proxmox-backup 01/15] fix indentation Fabian Grünbichler
2020-10-19 12:00   ` [pbs-devel] applied: " Thomas Lamprecht
2020-10-19  7:39 ` [pbs-devel] [PATCH proxmox-backup 02/15] fix typos Fabian Grünbichler
2020-10-19 12:01   ` [pbs-devel] applied: " Thomas Lamprecht
2020-10-19  7:39 ` [pbs-devel] [PATCH proxmox-backup 03/15] REST: rename token to csrf_token Fabian Grünbichler
2020-10-19 12:02   ` [pbs-devel] applied: " Thomas Lamprecht
2020-10-19  7:39 ` [pbs-devel] [RFC proxmox-backup 04/15] Userid: extend schema with token name Fabian Grünbichler
2020-10-19  7:39 ` [pbs-devel] [RFC proxmox-backup 05/15] add ApiToken to user.cfg and CachedUserInfo Fabian Grünbichler
2020-10-19  7:39 ` [pbs-devel] [RFC proxmox-backup 06/15] config: add token.shadow file Fabian Grünbichler
2020-10-19  7:39 ` [pbs-devel] [RFC proxmox-backup 07/15] REST: extract and handle API tokens Fabian Grünbichler
2020-10-20  8:34   ` Wolfgang Bumiller
2020-10-19  7:39 ` [pbs-devel] [RFC proxmox-backup 08/15] api: add API token endpoints Fabian Grünbichler
2020-10-20  9:42   ` Wolfgang Bumiller
2020-10-20 10:15     ` Wolfgang Bumiller
2020-10-19  7:39 ` [pbs-devel] [RFC proxmox-backup 09/15] api: allow listing users + tokens Fabian Grünbichler
2020-10-20 10:10   ` Wolfgang Bumiller
2020-10-19  7:39 ` Fabian Grünbichler [this message]
2020-10-19  7:39 ` [pbs-devel] [RFC proxmox-backup 11/15] client: allow using ApiToken + secret Fabian Grünbichler
2020-10-19  7:39 ` [pbs-devel] [RFC proxmox-backup 12/15] owner checks: handle backups owned by API tokens Fabian Grünbichler
2020-10-19  7:39 ` [pbs-devel] [RFC proxmox-backup 13/15] tasks: allow unpriv users to read their tokens' tasks Fabian Grünbichler
2020-10-19  7:39 ` [pbs-devel] [RFC proxmox-backup 14/15] manager: add token commands Fabian Grünbichler
2020-10-19  7:39 ` [pbs-devel] [RFC proxmox-backup 15/15] manager: add user permissions command Fabian Grünbichler

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20201019073919.588521-11-f.gruenbichler@proxmox.com \
    --to=f.gruenbichler@proxmox.com \
    --cc=pbs-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal