proxmox-backup/src/config/acl.rs
Thomas Lamprecht 41bfd24919 server: add Datastore.Allocate privilege
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>
2020-10-08 09:12:08 +02:00

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(())
}
}