41bfd24919
Previously only Datastore.Modify was required for creating a new datastore. But, that endpoint allows one to pass an arbitrary path, of which all parent directories will be created, this can allow any user with the "Datastore Admin" role on "/datastores" to do some damage to the system. Further, it is effectively a side channel for revealing the systems directory structure through educated guessing and error handling. Add a new privilege "Datastore.Allocate" which, for now, is used specifically for the create datastore API endpoint. Add it only to the "Admin" role. Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
815 lines
25 KiB
Rust
815 lines
25 KiB
Rust
use std::io::Write;
|
|
use std::collections::{HashMap, HashSet, BTreeMap, BTreeSet};
|
|
use std::path::{PathBuf, Path};
|
|
use std::sync::{Arc, RwLock};
|
|
use std::str::FromStr;
|
|
|
|
use anyhow::{bail, Error};
|
|
|
|
use lazy_static::lazy_static;
|
|
|
|
use ::serde::{Deserialize, Serialize};
|
|
use serde::de::{value, IntoDeserializer};
|
|
|
|
use proxmox::tools::{fs::replace_file, fs::CreateOptions};
|
|
use proxmox::constnamedbitmap;
|
|
use proxmox::api::{api, schema::*};
|
|
|
|
use crate::api2::types::Userid;
|
|
|
|
// define Privilege bitfield
|
|
|
|
constnamedbitmap! {
|
|
/// Contains a list of Privileges
|
|
PRIVILEGES: u64 => {
|
|
PRIV_SYS_AUDIT("Sys.Audit");
|
|
PRIV_SYS_MODIFY("Sys.Modify");
|
|
PRIV_SYS_POWER_MANAGEMENT("Sys.PowerManagement");
|
|
|
|
PRIV_DATASTORE_AUDIT("Datastore.Audit");
|
|
PRIV_DATASTORE_ALLOCATE("Datastore.Allocate");
|
|
PRIV_DATASTORE_MODIFY("Datastore.Modify");
|
|
PRIV_DATASTORE_READ("Datastore.Read");
|
|
|
|
/// Datastore.Backup also requires backup ownership
|
|
PRIV_DATASTORE_BACKUP("Datastore.Backup");
|
|
/// Datastore.Prune also requires backup ownership
|
|
PRIV_DATASTORE_PRUNE("Datastore.Prune");
|
|
|
|
PRIV_PERMISSIONS_MODIFY("Permissions.Modify");
|
|
|
|
PRIV_REMOTE_AUDIT("Remote.Audit");
|
|
PRIV_REMOTE_MODIFY("Remote.Modify");
|
|
PRIV_REMOTE_READ("Remote.Read");
|
|
PRIV_REMOTE_PRUNE("Remote.Prune");
|
|
|
|
PRIV_SYS_CONSOLE("Sys.Console");
|
|
}
|
|
}
|
|
|
|
|
|
/// Admin always has all privileges. It can do everything except a few actions
|
|
/// which are limited to the 'root@pam` superuser
|
|
pub const ROLE_ADMIN: u64 = std::u64::MAX;
|
|
|
|
/// NoAccess can be used to remove privileges from specific paths
|
|
pub const ROLE_NO_ACCESS: u64 = 0;
|
|
|
|
pub const ROLE_AUDIT: u64 =
|
|
PRIV_SYS_AUDIT |
|
|
PRIV_DATASTORE_AUDIT;
|
|
|
|
/// Datastore.Admin can do anything on the datastore.
|
|
pub const ROLE_DATASTORE_ADMIN: u64 =
|
|
PRIV_DATASTORE_AUDIT |
|
|
PRIV_DATASTORE_MODIFY |
|
|
PRIV_DATASTORE_READ |
|
|
PRIV_DATASTORE_BACKUP |
|
|
PRIV_DATASTORE_PRUNE;
|
|
|
|
/// Datastore.Reader can read datastore content an do restore
|
|
pub const ROLE_DATASTORE_READER: u64 =
|
|
PRIV_DATASTORE_AUDIT |
|
|
PRIV_DATASTORE_READ;
|
|
|
|
/// Datastore.Backup can do backup and restore, but no prune.
|
|
pub const ROLE_DATASTORE_BACKUP: u64 =
|
|
PRIV_DATASTORE_BACKUP;
|
|
|
|
/// Datastore.PowerUser can do backup, restore, and prune.
|
|
pub const ROLE_DATASTORE_POWERUSER: u64 =
|
|
PRIV_DATASTORE_PRUNE |
|
|
PRIV_DATASTORE_BACKUP;
|
|
|
|
/// Datastore.Audit can audit the datastore.
|
|
pub const ROLE_DATASTORE_AUDIT: u64 =
|
|
PRIV_DATASTORE_AUDIT;
|
|
|
|
/// Remote.Audit can audit the remote
|
|
pub const ROLE_REMOTE_AUDIT: u64 =
|
|
PRIV_REMOTE_AUDIT;
|
|
|
|
/// Remote.Admin can do anything on the remote.
|
|
pub const ROLE_REMOTE_ADMIN: u64 =
|
|
PRIV_REMOTE_AUDIT |
|
|
PRIV_REMOTE_MODIFY |
|
|
PRIV_REMOTE_READ |
|
|
PRIV_REMOTE_PRUNE;
|
|
|
|
/// Remote.SyncOperator can do read and prune on the remote.
|
|
pub const ROLE_REMOTE_SYNC_OPERATOR: u64 =
|
|
PRIV_REMOTE_AUDIT |
|
|
PRIV_REMOTE_READ |
|
|
PRIV_REMOTE_PRUNE;
|
|
|
|
pub const ROLE_NAME_NO_ACCESS: &str ="NoAccess";
|
|
|
|
#[api()]
|
|
#[repr(u64)]
|
|
#[derive(Serialize, Deserialize)]
|
|
/// Role
|
|
pub enum Role {
|
|
/// Administrator
|
|
Admin = ROLE_ADMIN,
|
|
/// Auditor
|
|
Audit = ROLE_AUDIT,
|
|
/// Disable Access
|
|
NoAccess = ROLE_NO_ACCESS,
|
|
/// Datastore Administrator
|
|
DatastoreAdmin = ROLE_DATASTORE_ADMIN,
|
|
/// Datastore Reader (inspect datastore content and do restores)
|
|
DatastoreReader = ROLE_DATASTORE_READER,
|
|
/// Datastore Backup (backup and restore owned backups)
|
|
DatastoreBackup = ROLE_DATASTORE_BACKUP,
|
|
/// Datastore PowerUser (backup, restore and prune owned backup)
|
|
DatastorePowerUser = ROLE_DATASTORE_POWERUSER,
|
|
/// Datastore Auditor
|
|
DatastoreAudit = ROLE_DATASTORE_AUDIT,
|
|
/// Remote Auditor
|
|
RemoteAudit = ROLE_REMOTE_AUDIT,
|
|
/// Remote Administrator
|
|
RemoteAdmin = ROLE_REMOTE_ADMIN,
|
|
/// Syncronisation Opertator
|
|
RemoteSyncOperator = ROLE_REMOTE_SYNC_OPERATOR,
|
|
}
|
|
|
|
impl FromStr for Role {
|
|
type Err = value::Error;
|
|
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
Self::deserialize(s.into_deserializer())
|
|
}
|
|
}
|
|
|
|
lazy_static! {
|
|
pub static ref ROLE_NAMES: HashMap<&'static str, (u64, &'static str)> = {
|
|
let mut map = HashMap::new();
|
|
|
|
let list = match Role::API_SCHEMA {
|
|
Schema::String(StringSchema { format: Some(ApiStringFormat::Enum(list)), .. }) => list,
|
|
_ => unreachable!(),
|
|
};
|
|
|
|
for entry in list.iter() {
|
|
let privs: u64 = Role::from_str(entry.value).unwrap() as u64;
|
|
map.insert(entry.value, (privs, entry.description));
|
|
}
|
|
|
|
map
|
|
};
|
|
}
|
|
|
|
pub fn split_acl_path(path: &str) -> Vec<&str> {
|
|
|
|
let items = path.split('/');
|
|
|
|
let mut components = vec![];
|
|
|
|
for name in items {
|
|
if name.is_empty() { continue; }
|
|
components.push(name);
|
|
}
|
|
|
|
components
|
|
}
|
|
|
|
pub fn check_acl_path(path: &str) -> Result<(), Error> {
|
|
|
|
let components = split_acl_path(path);
|
|
|
|
let components_len = components.len();
|
|
|
|
if components_len == 0 { return Ok(()); }
|
|
match components[0] {
|
|
"access" => {
|
|
if components_len == 1 { return Ok(()); }
|
|
match components[1] {
|
|
"acl" | "users" => {
|
|
if components_len == 2 { return Ok(()); }
|
|
}
|
|
_ => {},
|
|
}
|
|
}
|
|
"datastore" => { // /datastore/{store}
|
|
if components_len <= 2 { return Ok(()); }
|
|
}
|
|
"remote" => { // /remote/{remote}/{store}
|
|
if components_len <= 3 { return Ok(()); }
|
|
}
|
|
"system" => {
|
|
if components_len == 1 { return Ok(()); }
|
|
match components[1] {
|
|
"disks" | "log" | "status" | "tasks" | "time" => {
|
|
if components_len == 2 { return Ok(()); }
|
|
}
|
|
"services" => { // /system/services/{service}
|
|
if components_len <= 3 { return Ok(()); }
|
|
}
|
|
"network" => {
|
|
if components_len == 2 { return Ok(()); }
|
|
match components[2] {
|
|
"dns" => {
|
|
if components_len == 3 { return Ok(()); }
|
|
}
|
|
"interfaces" => { // /system/network/interfaces/{iface}
|
|
if components_len <= 4 { return Ok(()); }
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
|
|
bail!("invalid acl path '{}'.", path);
|
|
}
|
|
|
|
pub struct AclTree {
|
|
pub root: AclTreeNode,
|
|
}
|
|
|
|
pub struct AclTreeNode {
|
|
pub users: HashMap<Userid, HashMap<String, bool>>,
|
|
pub groups: HashMap<String, HashMap<String, bool>>,
|
|
pub children: BTreeMap<String, AclTreeNode>,
|
|
}
|
|
|
|
impl AclTreeNode {
|
|
|
|
pub fn new() -> Self {
|
|
Self {
|
|
users: HashMap::new(),
|
|
groups: HashMap::new(),
|
|
children: BTreeMap::new(),
|
|
}
|
|
}
|
|
|
|
pub fn extract_roles(&self, user: &Userid, all: bool) -> HashSet<String> {
|
|
let user_roles = self.extract_user_roles(user, all);
|
|
if !user_roles.is_empty() {
|
|
// user privs always override group privs
|
|
return user_roles
|
|
};
|
|
|
|
self.extract_group_roles(user, all)
|
|
}
|
|
|
|
pub fn extract_user_roles(&self, user: &Userid, all: bool) -> HashSet<String> {
|
|
|
|
let mut set = HashSet::new();
|
|
|
|
let roles = match self.users.get(user) {
|
|
Some(m) => m,
|
|
None => return set,
|
|
};
|
|
|
|
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;
|
|
}
|
|
set.insert(role.to_string());
|
|
}
|
|
}
|
|
|
|
set
|
|
}
|
|
|
|
pub fn extract_group_roles(&self, _user: &Userid, all: bool) -> HashSet<String> {
|
|
|
|
let mut set = HashSet::new();
|
|
|
|
for (_group, roles) in &self.groups {
|
|
let is_member = false; // fixme: check if user is member of the group
|
|
if !is_member { continue; }
|
|
|
|
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;
|
|
}
|
|
set.insert(role.to_string());
|
|
}
|
|
}
|
|
}
|
|
|
|
set
|
|
}
|
|
|
|
pub fn delete_group_role(&mut self, group: &str, role: &str) {
|
|
let roles = match self.groups.get_mut(group) {
|
|
Some(r) => r,
|
|
None => return,
|
|
};
|
|
roles.remove(role);
|
|
}
|
|
|
|
pub fn delete_user_role(&mut self, userid: &Userid, role: &str) {
|
|
let roles = match self.users.get_mut(userid) {
|
|
Some(r) => r,
|
|
None => return,
|
|
};
|
|
roles.remove(role);
|
|
}
|
|
|
|
pub fn insert_group_role(&mut self, group: String, role: String, propagate: bool) {
|
|
let map = self.groups.entry(group).or_insert_with(|| HashMap::new());
|
|
if role == ROLE_NAME_NO_ACCESS {
|
|
map.clear();
|
|
map.insert(role, propagate);
|
|
} else {
|
|
map.remove(ROLE_NAME_NO_ACCESS);
|
|
map.insert(role, propagate);
|
|
}
|
|
}
|
|
|
|
pub fn insert_user_role(&mut self, user: Userid, role: String, propagate: bool) {
|
|
let map = self.users.entry(user).or_insert_with(|| HashMap::new());
|
|
if role == ROLE_NAME_NO_ACCESS {
|
|
map.clear();
|
|
map.insert(role, propagate);
|
|
} else {
|
|
map.remove(ROLE_NAME_NO_ACCESS);
|
|
map.insert(role, propagate);
|
|
}
|
|
}
|
|
}
|
|
|
|
impl AclTree {
|
|
|
|
pub fn new() -> Self {
|
|
Self { root: AclTreeNode::new() }
|
|
}
|
|
|
|
pub fn find_node(&mut self, path: &str) -> Option<&mut AclTreeNode> {
|
|
let path = split_acl_path(path);
|
|
return self.get_node(&path);
|
|
}
|
|
|
|
fn get_node(&mut self, path: &[&str]) -> Option<&mut AclTreeNode> {
|
|
let mut node = &mut self.root;
|
|
for comp in path {
|
|
node = match node.children.get_mut(*comp) {
|
|
Some(n) => n,
|
|
None => return None,
|
|
};
|
|
}
|
|
Some(node)
|
|
}
|
|
|
|
fn get_or_insert_node(&mut self, path: &[&str]) -> &mut AclTreeNode {
|
|
let mut node = &mut self.root;
|
|
for comp in path {
|
|
node = node.children.entry(String::from(*comp))
|
|
.or_insert_with(|| AclTreeNode::new());
|
|
}
|
|
node
|
|
}
|
|
|
|
pub fn delete_group_role(&mut self, path: &str, group: &str, role: &str) {
|
|
let path = split_acl_path(path);
|
|
let node = match self.get_node(&path) {
|
|
Some(n) => n,
|
|
None => return,
|
|
};
|
|
node.delete_group_role(group, role);
|
|
}
|
|
|
|
pub fn delete_user_role(&mut self, path: &str, userid: &Userid, role: &str) {
|
|
let path = split_acl_path(path);
|
|
let node = match self.get_node(&path) {
|
|
Some(n) => n,
|
|
None => return,
|
|
};
|
|
node.delete_user_role(userid, role);
|
|
}
|
|
|
|
pub fn insert_group_role(&mut self, path: &str, group: &str, role: &str, propagate: bool) {
|
|
let path = split_acl_path(path);
|
|
let node = self.get_or_insert_node(&path);
|
|
node.insert_group_role(group.to_string(), role.to_string(), propagate);
|
|
}
|
|
|
|
pub fn insert_user_role(&mut self, path: &str, user: &Userid, role: &str, propagate: bool) {
|
|
let path = split_acl_path(path);
|
|
let node = self.get_or_insert_node(&path);
|
|
node.insert_user_role(user.to_owned(), role.to_string(), propagate);
|
|
}
|
|
|
|
fn write_node_config(
|
|
node: &AclTreeNode,
|
|
path: &str,
|
|
w: &mut dyn Write,
|
|
) -> Result<(), Error> {
|
|
|
|
let mut role_ug_map0 = HashMap::new();
|
|
let mut role_ug_map1 = HashMap::new();
|
|
|
|
for (user, roles) in &node.users {
|
|
// no need to save, because root is always 'Administrator'
|
|
if user == "root@pam" { continue; }
|
|
for (role, propagate) in roles {
|
|
let role = role.as_str();
|
|
let user = user.to_string();
|
|
if *propagate {
|
|
role_ug_map1.entry(role).or_insert_with(|| BTreeSet::new())
|
|
.insert(user);
|
|
} else {
|
|
role_ug_map0.entry(role).or_insert_with(|| BTreeSet::new())
|
|
.insert(user);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (group, roles) in &node.groups {
|
|
for (role, propagate) in roles {
|
|
let group = format!("@{}", group);
|
|
if *propagate {
|
|
role_ug_map1.entry(role).or_insert_with(|| BTreeSet::new())
|
|
.insert(group);
|
|
} else {
|
|
role_ug_map0.entry(role).or_insert_with(|| BTreeSet::new())
|
|
.insert(group);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn group_by_property_list(
|
|
item_property_map: &HashMap<&str, BTreeSet<String>>,
|
|
) -> BTreeMap<String, BTreeSet<String>> {
|
|
let mut result_map = BTreeMap::new();
|
|
for (item, property_map) in item_property_map {
|
|
let item_list = property_map.iter().fold(String::new(), |mut acc, v| {
|
|
if !acc.is_empty() { acc.push(','); }
|
|
acc.push_str(v);
|
|
acc
|
|
});
|
|
result_map.entry(item_list).or_insert_with(|| BTreeSet::new())
|
|
.insert(item.to_string());
|
|
}
|
|
result_map
|
|
}
|
|
|
|
let uglist_role_map0 = group_by_property_list(&role_ug_map0);
|
|
let uglist_role_map1 = group_by_property_list(&role_ug_map1);
|
|
|
|
fn role_list(roles: &BTreeSet<String>) -> String {
|
|
if roles.contains(ROLE_NAME_NO_ACCESS) { return String::from(ROLE_NAME_NO_ACCESS); }
|
|
roles.iter().fold(String::new(), |mut acc, v| {
|
|
if !acc.is_empty() { acc.push(','); }
|
|
acc.push_str(v);
|
|
acc
|
|
})
|
|
}
|
|
|
|
for (uglist, roles) in &uglist_role_map0 {
|
|
let role_list = role_list(roles);
|
|
writeln!(w, "acl:0:{}:{}:{}", if path.is_empty() { "/" } else { path }, uglist, role_list)?;
|
|
}
|
|
|
|
for (uglist, roles) in &uglist_role_map1 {
|
|
let role_list = role_list(roles);
|
|
writeln!(w, "acl:1:{}:{}:{}", if path.is_empty() { "/" } else { path }, uglist, role_list)?;
|
|
}
|
|
|
|
for (name, child) in node.children.iter() {
|
|
let child_path = format!("{}/{}", path, name);
|
|
Self::write_node_config(child, &child_path, w)?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn write_config(&self, w: &mut dyn Write) -> Result<(), Error> {
|
|
Self::write_node_config(&self.root, "", w)
|
|
}
|
|
|
|
fn parse_acl_line(&mut self, line: &str) -> Result<(), Error> {
|
|
|
|
let items: Vec<&str> = line.split(':').collect();
|
|
|
|
if items.len() != 5 {
|
|
bail!("wrong number of items.");
|
|
}
|
|
|
|
if items[0] != "acl" {
|
|
bail!("line does not start with 'acl'.");
|
|
}
|
|
|
|
let propagate = if items[1] == "0" {
|
|
false
|
|
} else if items[1] == "1" {
|
|
true
|
|
} else {
|
|
bail!("expected '0' or '1' for propagate flag.");
|
|
};
|
|
|
|
let path = split_acl_path(items[2]);
|
|
let node = self.get_or_insert_node(&path);
|
|
|
|
let uglist: Vec<&str> = items[3].split(',').map(|v| v.trim()).collect();
|
|
|
|
let rolelist: Vec<&str> = items[4].split(',').map(|v| v.trim()).collect();
|
|
|
|
for user_or_group in &uglist {
|
|
for role in &rolelist {
|
|
if !ROLE_NAMES.contains_key(role) {
|
|
bail!("unknown role '{}'", role);
|
|
}
|
|
if user_or_group.starts_with('@') {
|
|
let group = &user_or_group[1..];
|
|
node.insert_group_role(group.to_string(), role.to_string(), propagate);
|
|
} else {
|
|
node.insert_user_role(user_or_group.parse()?, role.to_string(), propagate);
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn load(filename: &Path) -> Result<(Self, [u8;32]), Error> {
|
|
let mut tree = Self::new();
|
|
|
|
let raw = match std::fs::read_to_string(filename) {
|
|
Ok(v) => v,
|
|
Err(err) => {
|
|
if err.kind() == std::io::ErrorKind::NotFound {
|
|
String::new()
|
|
} else {
|
|
bail!("unable to read acl config {:?} - {}", filename, err);
|
|
}
|
|
}
|
|
};
|
|
|
|
let digest = openssl::sha::sha256(raw.as_bytes());
|
|
|
|
for (linenr, line) in raw.lines().enumerate() {
|
|
let line = line.trim();
|
|
if line.is_empty() { continue; }
|
|
if let Err(err) = tree.parse_acl_line(line) {
|
|
bail!("unable to parse acl config {:?}, line {} - {}",
|
|
filename, linenr+1, err);
|
|
}
|
|
}
|
|
|
|
Ok((tree, digest))
|
|
}
|
|
|
|
pub fn from_raw(raw: &str) -> Result<Self, Error> {
|
|
let mut tree = Self::new();
|
|
for (linenr, line) in raw.lines().enumerate() {
|
|
let line = line.trim();
|
|
if line.is_empty() { continue; }
|
|
if let Err(err) = tree.parse_acl_line(line) {
|
|
bail!("unable to parse acl config data, line {} - {}", linenr+1, err);
|
|
}
|
|
}
|
|
Ok(tree)
|
|
}
|
|
|
|
pub fn roles(&self, userid: &Userid, path: &[&str]) -> HashSet<String> {
|
|
|
|
let mut node = &self.root;
|
|
let mut role_set = 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
|
|
};
|
|
let new_set = node.extract_roles(userid, last_comp);
|
|
if !new_set.is_empty() {
|
|
// overwrite previous settings
|
|
role_set = new_set;
|
|
}
|
|
}
|
|
|
|
role_set
|
|
}
|
|
}
|
|
|
|
pub const ACL_CFG_FILENAME: &str = "/etc/proxmox-backup/acl.cfg";
|
|
pub const ACL_CFG_LOCKFILE: &str = "/etc/proxmox-backup/.acl.lck";
|
|
|
|
pub fn config() -> Result<(AclTree, [u8; 32]), Error> {
|
|
let path = PathBuf::from(ACL_CFG_FILENAME);
|
|
AclTree::load(&path)
|
|
}
|
|
|
|
pub fn cached_config() -> Result<Arc<AclTree>, Error> {
|
|
|
|
struct ConfigCache {
|
|
data: Option<Arc<AclTree>>,
|
|
last_mtime: i64,
|
|
last_mtime_nsec: i64,
|
|
}
|
|
|
|
lazy_static! {
|
|
static ref CACHED_CONFIG: RwLock<ConfigCache> = RwLock::new(
|
|
ConfigCache { data: None, last_mtime: 0, last_mtime_nsec: 0 });
|
|
}
|
|
|
|
let stat = match nix::sys::stat::stat(ACL_CFG_FILENAME) {
|
|
Ok(stat) => Some(stat),
|
|
Err(nix::Error::Sys(nix::errno::Errno::ENOENT)) => None,
|
|
Err(err) => bail!("unable to stat '{}' - {}", ACL_CFG_FILENAME, err),
|
|
};
|
|
|
|
{ // limit scope
|
|
let cache = CACHED_CONFIG.read().unwrap();
|
|
if let Some(ref config) = cache.data {
|
|
if let Some(stat) = stat {
|
|
if stat.st_mtime == cache.last_mtime && stat.st_mtime_nsec == cache.last_mtime_nsec {
|
|
return Ok(config.clone());
|
|
}
|
|
} else if cache.last_mtime == 0 && cache.last_mtime_nsec == 0 {
|
|
return Ok(config.clone());
|
|
}
|
|
}
|
|
}
|
|
|
|
let (config, _digest) = config()?;
|
|
let config = Arc::new(config);
|
|
|
|
let mut cache = CACHED_CONFIG.write().unwrap();
|
|
if let Some(stat) = stat {
|
|
cache.last_mtime = stat.st_mtime;
|
|
cache.last_mtime_nsec = stat.st_mtime_nsec;
|
|
}
|
|
cache.data = Some(config.clone());
|
|
|
|
Ok(config)
|
|
}
|
|
|
|
pub fn save_config(acl: &AclTree) -> Result<(), Error> {
|
|
let mut raw: Vec<u8> = Vec::new();
|
|
|
|
acl.write_config(&mut raw)?;
|
|
|
|
let backup_user = crate::backup::backup_user()?;
|
|
let mode = nix::sys::stat::Mode::from_bits_truncate(0o0640);
|
|
// set the correct owner/group/permissions while saving file
|
|
// owner(rw) = root, group(r)= backup
|
|
let options = CreateOptions::new()
|
|
.perm(mode)
|
|
.owner(nix::unistd::ROOT)
|
|
.group(backup_user.gid);
|
|
|
|
replace_file(ACL_CFG_FILENAME, &raw, options)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use anyhow::{Error};
|
|
use super::AclTree;
|
|
|
|
use crate::api2::types::Userid;
|
|
|
|
fn check_roles(
|
|
tree: &AclTree,
|
|
user: &Userid,
|
|
path: &str,
|
|
expected_roles: &str,
|
|
) {
|
|
|
|
let path_vec = super::split_acl_path(path);
|
|
let mut roles = tree.roles(user, &path_vec)
|
|
.iter().map(|v| v.clone()).collect::<Vec<String>>();
|
|
roles.sort();
|
|
let roles = roles.join(",");
|
|
|
|
assert_eq!(roles, expected_roles, "\nat check_roles for '{}' on '{}'", user, path);
|
|
}
|
|
|
|
#[test]
|
|
fn test_acl_line_compression() {
|
|
|
|
let tree = AclTree::from_raw(
|
|
"\
|
|
acl:0:/store/store2:user1@pbs:Admin\n\
|
|
acl:0:/store/store2:user2@pbs:Admin\n\
|
|
acl:0:/store/store2:user1@pbs:DatastoreBackup\n\
|
|
acl:0:/store/store2:user2@pbs:DatastoreBackup\n\
|
|
",
|
|
)
|
|
.expect("failed to parse acl tree");
|
|
|
|
let mut raw: Vec<u8> = Vec::new();
|
|
tree.write_config(&mut raw).expect("failed to write acl tree");
|
|
let raw = std::str::from_utf8(&raw).expect("acl tree is not valid utf8");
|
|
|
|
assert_eq!(raw, "acl:0:/store/store2:user1@pbs,user2@pbs:Admin,DatastoreBackup\n");
|
|
}
|
|
|
|
#[test]
|
|
fn test_roles_1() -> Result<(), Error> {
|
|
|
|
let tree = AclTree::from_raw(r###"
|
|
acl:1:/storage:user1@pbs:Admin
|
|
acl:1:/storage/store1:user1@pbs:DatastoreBackup
|
|
acl:1:/storage/store2:user2@pbs:DatastoreBackup
|
|
"###)?;
|
|
let user1: Userid = "user1@pbs".parse()?;
|
|
check_roles(&tree, &user1, "/", "");
|
|
check_roles(&tree, &user1, "/storage", "Admin");
|
|
check_roles(&tree, &user1, "/storage/store1", "DatastoreBackup");
|
|
check_roles(&tree, &user1, "/storage/store2", "Admin");
|
|
|
|
let user2: Userid = "user2@pbs".parse()?;
|
|
check_roles(&tree, &user2, "/", "");
|
|
check_roles(&tree, &user2, "/storage", "");
|
|
check_roles(&tree, &user2, "/storage/store1", "");
|
|
check_roles(&tree, &user2, "/storage/store2", "DatastoreBackup");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_role_no_access() -> Result<(), Error> {
|
|
|
|
let tree = AclTree::from_raw(r###"
|
|
acl:1:/:user1@pbs:Admin
|
|
acl:1:/storage:user1@pbs:NoAccess
|
|
acl:1:/storage/store1:user1@pbs:DatastoreBackup
|
|
"###)?;
|
|
let user1: Userid = "user1@pbs".parse()?;
|
|
check_roles(&tree, &user1, "/", "Admin");
|
|
check_roles(&tree, &user1, "/storage", "NoAccess");
|
|
check_roles(&tree, &user1, "/storage/store1", "DatastoreBackup");
|
|
check_roles(&tree, &user1, "/storage/store2", "NoAccess");
|
|
check_roles(&tree, &user1, "/system", "Admin");
|
|
|
|
let tree = AclTree::from_raw(r###"
|
|
acl:1:/:user1@pbs:Admin
|
|
acl:0:/storage:user1@pbs:NoAccess
|
|
acl:1:/storage/store1:user1@pbs:DatastoreBackup
|
|
"###)?;
|
|
check_roles(&tree, &user1, "/", "Admin");
|
|
check_roles(&tree, &user1, "/storage", "NoAccess");
|
|
check_roles(&tree, &user1, "/storage/store1", "DatastoreBackup");
|
|
check_roles(&tree, &user1, "/storage/store2", "Admin");
|
|
check_roles(&tree, &user1, "/system", "Admin");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_role_add_delete() -> Result<(), Error> {
|
|
|
|
let mut tree = AclTree::new();
|
|
|
|
let user1: Userid = "user1@pbs".parse()?;
|
|
|
|
tree.insert_user_role("/", &user1, "Admin", true);
|
|
tree.insert_user_role("/", &user1, "Audit", true);
|
|
|
|
check_roles(&tree, &user1, "/", "Admin,Audit");
|
|
|
|
tree.insert_user_role("/", &user1, "NoAccess", true);
|
|
check_roles(&tree, &user1, "/", "NoAccess");
|
|
|
|
let mut raw: Vec<u8> = Vec::new();
|
|
tree.write_config(&mut raw)?;
|
|
let raw = std::str::from_utf8(&raw)?;
|
|
|
|
assert_eq!(raw, "acl:1:/:user1@pbs:NoAccess\n");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_no_access_overwrite() -> Result<(), Error> {
|
|
|
|
let mut tree = AclTree::new();
|
|
|
|
let user1: Userid = "user1@pbs".parse()?;
|
|
|
|
tree.insert_user_role("/storage", &user1, "NoAccess", true);
|
|
|
|
check_roles(&tree, &user1, "/storage", "NoAccess");
|
|
|
|
tree.insert_user_role("/storage", &user1, "Admin", true);
|
|
tree.insert_user_role("/storage", &user1, "Audit", true);
|
|
|
|
check_roles(&tree, &user1, "/storage", "Admin,Audit");
|
|
|
|
tree.insert_user_role("/storage", &user1, "NoAccess", true);
|
|
|
|
check_roles(&tree, &user1, "/storage", "NoAccess");
|
|
|
|
Ok(())
|
|
}
|
|
}
|