mirror of
https://git.proxmox.com/git/proxmox
synced 2025-07-15 06:30:34 +00:00
448 lines
15 KiB
Rust
448 lines
15 KiB
Rust
//! Declarative permission system
|
|
//!
|
|
//! A declarative way to define API access permissions.
|
|
|
|
use std::collections::HashMap;
|
|
use std::fmt;
|
|
use std::ops::Deref;
|
|
|
|
/// Access permission
|
|
#[cfg_attr(feature = "test-harness", derive(Eq, PartialEq))]
|
|
pub enum Permission {
|
|
/// Allow Superuser
|
|
Superuser,
|
|
/// Allow the whole World, no authentication required
|
|
World,
|
|
/// Allow any authenticated user
|
|
Anybody,
|
|
/// Allow access for the specified user
|
|
User(&'static str),
|
|
/// Allow access if specified param matches logged in user
|
|
UserParam(&'static str),
|
|
/// Allow access for the specified group of users
|
|
Group(&'static str),
|
|
/// Use a parameter value as userid to run sub-permission tests.
|
|
WithParam(&'static str, &'static Permission),
|
|
/// Check privilege/role on the specified path. The boolean attribute specifies if you want to
|
|
/// allow partial matches (u64 interpreted as bitmask).
|
|
Privilege(&'static [&'static str], u64, bool),
|
|
/// Allow access if all sub-permissions match
|
|
And(&'static [&'static Permission]),
|
|
/// Allow access if any sub-permissions match
|
|
Or(&'static [&'static Permission]),
|
|
}
|
|
|
|
impl fmt::Debug for Permission {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match self {
|
|
Permission::Superuser => f.write_str("Superuser"),
|
|
Permission::World => f.write_str("World"),
|
|
Permission::Anybody => f.write_str("Anybody"),
|
|
Permission::User(ref userid) => write!(f, "User({})", userid),
|
|
Permission::UserParam(param_name) => write!(f, "UserParam({})", param_name),
|
|
Permission::Group(ref group) => write!(f, "Group({})", group),
|
|
Permission::WithParam(param_name, subtest) => {
|
|
write!(f, "WithParam({}, {:?})", param_name, subtest)
|
|
}
|
|
Permission::Privilege(path, privs, partial) => {
|
|
write!(f, "Privilege({:?}, {:0b}, {})", path, privs, partial)
|
|
}
|
|
Permission::And(list) => {
|
|
f.write_str("And(\n")?;
|
|
for subtest in list.iter() {
|
|
writeln!(f, " {:?}", subtest)?;
|
|
}
|
|
f.write_str(")\n")
|
|
}
|
|
Permission::Or(list) => {
|
|
f.write_str("Or(\n")?;
|
|
for subtest in list.iter() {
|
|
writeln!(f, " {:?}", subtest)?;
|
|
}
|
|
f.write_str(")\n")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Trait to query user information (used by check_api_permission)
|
|
pub trait UserInformation {
|
|
fn is_superuser(&self, userid: &str) -> bool;
|
|
fn is_group_member(&self, userid: &str, group: &str) -> bool;
|
|
fn lookup_privs(&self, userid: &str, path: &[&str]) -> u64;
|
|
}
|
|
|
|
impl<T: UserInformation> UserInformation for std::sync::Arc<T> {
|
|
fn is_superuser(&self, userid: &str) -> bool {
|
|
self.deref().is_superuser(userid)
|
|
}
|
|
fn is_group_member(&self, userid: &str, group: &str) -> bool {
|
|
self.deref().is_group_member(userid, group)
|
|
}
|
|
fn lookup_privs(&self, userid: &str, path: &[&str]) -> u64 {
|
|
self.deref().lookup_privs(userid, path)
|
|
}
|
|
}
|
|
|
|
/// Example implementation to check access permissions
|
|
///
|
|
/// This implementation supports URI variables in Privilege path components, i.e. '{storage}'.
|
|
/// We replace this with actual parameter values before calling lookup_privs().
|
|
pub fn check_api_permission(
|
|
perm: &Permission,
|
|
userid: Option<&str>,
|
|
param: &HashMap<String, String>,
|
|
info: &dyn UserInformation,
|
|
) -> bool {
|
|
if let Some(userid) = userid {
|
|
if info.is_superuser(userid) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
check_api_permission_tail(perm, userid, param, info)
|
|
}
|
|
|
|
// some of them are deeply nested
|
|
#[allow(clippy::needless_return)]
|
|
fn check_api_permission_tail(
|
|
perm: &Permission,
|
|
userid: Option<&str>,
|
|
param: &HashMap<String, String>,
|
|
info: &dyn UserInformation,
|
|
) -> bool {
|
|
match perm {
|
|
Permission::World => return true,
|
|
Permission::Anybody => {
|
|
return userid.is_some();
|
|
}
|
|
Permission::Superuser => match userid {
|
|
None => return false,
|
|
Some(userid) => return info.is_superuser(userid),
|
|
},
|
|
Permission::User(expected_userid) => match userid {
|
|
None => return false,
|
|
Some(ref userid) => return userid == expected_userid,
|
|
},
|
|
Permission::UserParam(param_name) => match (userid, param.get(¶m_name.to_string())) {
|
|
(None, _) => return false,
|
|
(_, None) => return false,
|
|
(Some(ref userid), Some(ref expected)) => return userid == expected,
|
|
},
|
|
Permission::Group(expected_group) => match userid {
|
|
None => return false,
|
|
Some(userid) => return info.is_group_member(userid, expected_group),
|
|
},
|
|
Permission::WithParam(param_name, subtest) => {
|
|
return check_api_permission(
|
|
subtest,
|
|
param.get(*param_name).map(|v| v.as_str()),
|
|
param,
|
|
info,
|
|
);
|
|
}
|
|
Permission::Privilege(path, expected_privs, partial) => {
|
|
// replace uri vars
|
|
let mut new_path: Vec<&str> = Vec::new();
|
|
for outer in path.iter() {
|
|
// we can have a whole priv path as one component, e.g., for Namespaces
|
|
for comp in outer.split('/') {
|
|
if comp.starts_with('{') && comp.ends_with('}') {
|
|
let param_name = unsafe { comp.get_unchecked(1..comp.len() - 1) };
|
|
match param.get(param_name) {
|
|
None => return false,
|
|
Some(value) => {
|
|
for subcomp in value.split('/') {
|
|
new_path.push(subcomp);
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
new_path.push(comp);
|
|
}
|
|
}
|
|
}
|
|
match userid {
|
|
None => return false,
|
|
Some(userid) => {
|
|
let privs = info.lookup_privs(userid, &new_path);
|
|
if privs == 0 {
|
|
return false;
|
|
};
|
|
if *partial {
|
|
return (expected_privs & privs) != 0;
|
|
} else {
|
|
return (*expected_privs & privs) == *expected_privs;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Permission::And(list) => {
|
|
for subtest in list.iter() {
|
|
if !check_api_permission_tail(subtest, userid, param, info) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
Permission::Or(list) => {
|
|
for subtest in list.iter() {
|
|
if check_api_permission_tail(subtest, userid, param, info) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use serde_json::{json, Value};
|
|
|
|
use crate::permission::*;
|
|
|
|
struct MockedUserInfo {
|
|
privs: Value,
|
|
groups: Value,
|
|
}
|
|
|
|
impl UserInformation for MockedUserInfo {
|
|
fn is_superuser(&self, userid: &str) -> bool {
|
|
userid == "root"
|
|
}
|
|
|
|
fn is_group_member(&self, userid: &str, group: &str) -> bool {
|
|
if let Some(groups) = self.groups[userid].as_array() {
|
|
return groups.contains(&Value::from(group));
|
|
}
|
|
|
|
false
|
|
}
|
|
|
|
fn lookup_privs(&self, userid: &str, path: &[&str]) -> u64 {
|
|
let path = format!("/{}", path.join("/"));
|
|
if let Some(users) = self.privs.get(path) {
|
|
if let Some(privilege) = users.get(userid) {
|
|
return privilege.as_u64().unwrap();
|
|
}
|
|
}
|
|
|
|
0
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_privileges() {
|
|
let userinfo = MockedUserInfo {
|
|
privs: json!({
|
|
"/": {
|
|
"user1": 0b10,
|
|
},
|
|
"/datastore": {
|
|
"user1": 0b00,
|
|
"user2": 0b01,
|
|
},
|
|
"/datastore/foo": {
|
|
"user1": 0b01,
|
|
},
|
|
"/datastore/foo/bar/baz": {
|
|
"user1": 0b01,
|
|
},
|
|
}),
|
|
groups: json!({
|
|
"user1": [
|
|
"group1",
|
|
],
|
|
"user2": [
|
|
"group2",
|
|
],
|
|
}),
|
|
};
|
|
|
|
let mut param = HashMap::new();
|
|
param.insert("user".to_string(), "user1".to_string());
|
|
param.insert("datastore".to_string(), "foo".to_string());
|
|
param.insert("ns".to_string(), "bar/baz".to_string());
|
|
|
|
let test_check = |perm: &Permission, userid: Option<&str>, should_succeed: bool| {
|
|
println!("{:?} on {:?}: {}", userid, perm, should_succeed);
|
|
assert_eq!(
|
|
check_api_permission(perm, userid, ¶m, &userinfo),
|
|
should_succeed
|
|
)
|
|
};
|
|
|
|
test_check(&Permission::Superuser, Some("root"), true);
|
|
test_check(&Permission::Superuser, Some("user1"), false);
|
|
test_check(&Permission::Superuser, None, false);
|
|
|
|
test_check(&Permission::World, Some("root"), true);
|
|
test_check(&Permission::World, Some("user1"), true);
|
|
test_check(&Permission::World, None, true);
|
|
|
|
test_check(&Permission::Anybody, Some("root"), true);
|
|
test_check(&Permission::Anybody, Some("user1"), true);
|
|
test_check(&Permission::Anybody, None, false);
|
|
|
|
test_check(&Permission::User("user1"), Some("root"), true);
|
|
test_check(&Permission::User("user1"), Some("user1"), true);
|
|
test_check(&Permission::User("user1"), Some("user2"), false);
|
|
test_check(&Permission::User("user1"), None, false);
|
|
|
|
test_check(&Permission::Group("group1"), Some("root"), true);
|
|
test_check(&Permission::Group("group1"), Some("user1"), true);
|
|
test_check(&Permission::Group("group1"), Some("user2"), false);
|
|
test_check(&Permission::Group("group1"), None, false);
|
|
|
|
test_check(
|
|
&Permission::WithParam("user", &Permission::User("root")),
|
|
Some("root"),
|
|
true,
|
|
);
|
|
test_check(
|
|
&Permission::WithParam("user", &Permission::User("user1")),
|
|
Some("user1"),
|
|
true,
|
|
);
|
|
test_check(
|
|
&Permission::WithParam("user", &Permission::User("user2")),
|
|
Some("user2"),
|
|
false,
|
|
);
|
|
test_check(
|
|
&Permission::WithParam("user", &Permission::User("")),
|
|
None,
|
|
false,
|
|
);
|
|
|
|
test_check(
|
|
&Permission::And(&[&Permission::User("user1"), &Permission::Group("group2")]),
|
|
Some("root"),
|
|
true,
|
|
);
|
|
test_check(
|
|
&Permission::And(&[&Permission::User("user1"), &Permission::Group("group2")]),
|
|
Some("user1"),
|
|
false,
|
|
);
|
|
test_check(
|
|
&Permission::And(&[&Permission::User("user1"), &Permission::Group("group1")]),
|
|
Some("user1"),
|
|
true,
|
|
);
|
|
test_check(
|
|
&Permission::And(&[&Permission::User("user1"), &Permission::Group("group2")]),
|
|
None,
|
|
false,
|
|
);
|
|
|
|
test_check(
|
|
&Permission::Or(&[&Permission::User("user1"), &Permission::Group("group2")]),
|
|
Some("root"),
|
|
true,
|
|
);
|
|
test_check(
|
|
&Permission::Or(&[&Permission::User("user1"), &Permission::Group("group2")]),
|
|
Some("user1"),
|
|
true,
|
|
);
|
|
test_check(
|
|
&Permission::Or(&[&Permission::User("user1"), &Permission::Group("group1")]),
|
|
Some("user2"),
|
|
false,
|
|
);
|
|
test_check(
|
|
&Permission::Or(&[&Permission::User("user1"), &Permission::Group("group2")]),
|
|
None,
|
|
false,
|
|
);
|
|
|
|
test_check(&Permission::Privilege(&[], 0b11, true), Some("root"), true);
|
|
test_check(&Permission::Privilege(&[], 0b11, true), Some("user1"), true);
|
|
test_check(
|
|
&Permission::Privilege(&[], 0b11, false),
|
|
Some("user1"),
|
|
false,
|
|
);
|
|
test_check(
|
|
&Permission::Privilege(&[], 0b11, true),
|
|
Some("user2"),
|
|
false,
|
|
);
|
|
test_check(
|
|
&Permission::Privilege(&[], 0b11, false),
|
|
Some("user2"),
|
|
false,
|
|
);
|
|
test_check(&Permission::Privilege(&[], 0b11, true), None, false);
|
|
test_check(&Permission::Privilege(&[], 0b11, false), None, false);
|
|
|
|
test_check(
|
|
&Permission::Privilege(&["datastore"], 0b01, true),
|
|
Some("user1"),
|
|
false,
|
|
);
|
|
test_check(
|
|
&Permission::Privilege(&["datastore"], 0b01, true),
|
|
Some("user2"),
|
|
true,
|
|
);
|
|
test_check(
|
|
&Permission::Privilege(&["datastore"], 0b01, true),
|
|
None,
|
|
false,
|
|
);
|
|
|
|
test_check(
|
|
&Permission::Privilege(&["datastore", "{datastore}"], 0b01, true),
|
|
Some("user1"),
|
|
true,
|
|
);
|
|
test_check(
|
|
&Permission::Privilege(&["datastore", "{datastore}"], 0b01, true),
|
|
Some("user2"),
|
|
false,
|
|
);
|
|
test_check(
|
|
&Permission::Privilege(&["datastore", "{datastore}"], 0b01, true),
|
|
None,
|
|
false,
|
|
);
|
|
// namespace test where {ns} is a combined variable that needs to be split
|
|
test_check(
|
|
&Permission::Privilege(&["datastore", "{datastore}", "{ns}"], 0b01, true),
|
|
Some("user1"),
|
|
true,
|
|
);
|
|
test_check(
|
|
&Permission::Privilege(&["datastore", "{datastore}", "{ns}"], 0b01, true),
|
|
Some("user2"),
|
|
false,
|
|
);
|
|
test_check(
|
|
&Permission::Privilege(&["datastore", "{datastore}", "{ns}"], 0b01, true),
|
|
None,
|
|
false,
|
|
);
|
|
// like above but now even the path itself is combined
|
|
test_check(
|
|
&Permission::Privilege(&["datastore", "{datastore}/{ns}"], 0b01, true),
|
|
Some("user1"),
|
|
true,
|
|
);
|
|
test_check(
|
|
&Permission::Privilege(&["datastore", "{datastore}/{ns}"], 0b01, true),
|
|
Some("user2"),
|
|
false,
|
|
);
|
|
test_check(
|
|
&Permission::Privilege(&["datastore", "{datastore}/{ns}"], 0b01, true),
|
|
None,
|
|
false,
|
|
);
|
|
}
|
|
}
|