From 7ec6448db8aace6933a3fb64b75ea42eb60fb714 Mon Sep 17 00:00:00 2001 From: Dietmar Maurer Date: Fri, 10 Apr 2020 11:24:24 +0200 Subject: [PATCH] implement access permission system --- proxmox/src/api/mod.rs | 1 + proxmox/src/api/permission.rs | 161 ++++++++++++++++++++++++++++++++++ proxmox/src/api/router.rs | 24 +++++ 3 files changed, 186 insertions(+) create mode 100644 proxmox/src/api/permission.rs diff --git a/proxmox/src/api/mod.rs b/proxmox/src/api/mod.rs index 223abc13..f8f0c651 100644 --- a/proxmox/src/api/mod.rs +++ b/proxmox/src/api/mod.rs @@ -15,6 +15,7 @@ pub mod const_regex; pub mod error; pub mod schema; pub mod section_config; +pub mod permission; #[doc(inline)] pub use const_regex::ConstRegexPattern; diff --git a/proxmox/src/api/permission.rs b/proxmox/src/api/permission.rs new file mode 100644 index 00000000..c28644de --- /dev/null +++ b/proxmox/src/api/permission.rs @@ -0,0 +1,161 @@ +//! Declarative permission system +//! +//! A declarative way to define API access permissions. + +use std::fmt; +use serde_json::Value; + +/// Access permission +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 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) => { + f.write_fmt(format_args!("User({})", userid)) + } + Permission::Group(ref group) => { + f.write_fmt(format_args!("Group({})", group)) + } + Permission::WithParam(param_name, subtest) => { + f.write_fmt(format_args!("WithParam({}, {:?})", param_name, subtest)) + } + Permission::Privilege(path, privs, partial) => { + f.write_fmt(format_args!("Privilege({:?}, {:0b}, {})", path, privs, partial)) + } + Permission::And(list) => { + f.write_str("And(\n")?; + for subtest in list.iter() { + f.write_fmt(format_args!(" {:?}\n", subtest))?; + } + f.write_str(")\n") + } + Permission::Or(list) => { + f.write_str("Or(\n")?; + for subtest in list.iter() { + f.write_fmt(format_args!(" {:?}\n", 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; +} + +/// 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: &Value, + info: &dyn UserInformation, +) -> bool { + + if let Some(ref userid) = userid { + if info.is_superuser(userid) { return true; } + } + + match perm { + Permission::World => return true, + Permission::Anybody => { + return userid.is_some(); + } + Permission::Superuser => { + match userid { + None => return false, + Some(ref userid) => return info.is_superuser(userid), + } + } + Permission::User(expected_userid) => { + match userid { + None => return false, + Some(ref userid) => return userid == expected_userid, + } + } + Permission::Group(expected_group) => { + match userid { + None => return false, + Some(ref userid) => return info.is_group_member(userid, expected_group), + } + } + Permission::WithParam(param_name, subtest) => { + return check_api_permission(subtest, param[param_name].as_str(), param, info); + } + Permission::Privilege(path, expected_privs, partial) => { + // replace uri vars + let mut new_path: Vec<&str> = Vec::new(); + for comp in path.iter() { + if comp.starts_with('{') && comp.ends_with('}') { + let param_name = unsafe { comp.get_unchecked(1..comp.len()-1) }; + match param[param_name].as_str() { + None => return false, + Some(value) => { + new_path.push(value); + } + } + } + } + match userid { + None => return false, + Some(userid) => { + let privs = info.lookup_privs(userid, &new_path); + 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(subtest, userid, param, info) { return false; } + } + return true; + } + Permission::Or(list) => { + for subtest in list.iter() { + if check_api_permission(subtest, userid, param, info) { return true; } + } + return false; + } + } +} diff --git a/proxmox/src/api/router.rs b/proxmox/src/api/router.rs index e0f2b863..d3c4b2e0 100644 --- a/proxmox/src/api/router.rs +++ b/proxmox/src/api/router.rs @@ -11,6 +11,7 @@ use serde_json::Value; use crate::api::schema::{self, ObjectSchema, Schema}; use crate::api::RpcEnvironment; +use super::permission::Permission; /// A synchronous API handler gets a json Value as input and returns a json Value as output. /// @@ -383,6 +384,12 @@ fn dummy_handler_fn( const DUMMY_HANDLER: ApiHandler = ApiHandler::Sync(&dummy_handler_fn); +/// Access permission with description +pub struct ApiAccessPermissions { + pub description: &'static str, + pub permission: &'static Permission, +} + /// This struct defines a synchronous API call which returns the result as json `Value` #[cfg_attr(feature = "test-harness", derive(Eq, PartialEq))] pub struct ApiMethod { @@ -398,6 +405,8 @@ pub struct ApiMethod { pub returns: &'static schema::Schema, /// Handler function pub handler: &'static ApiHandler, + /// Access Permissions + pub access: ApiAccessPermissions, } impl std::fmt::Debug for ApiMethod { @@ -406,6 +415,7 @@ impl std::fmt::Debug for ApiMethod { write!(f, " parameters: {:?}", self.parameters)?; write!(f, " returns: {:?}", self.returns)?; write!(f, " handler: {:p}", &self.handler)?; + write!(f, " permissions: {:?}", &self.access.permission)?; write!(f, "}}") } } @@ -418,6 +428,10 @@ impl ApiMethod { returns: &NULL_SCHEMA, protected: false, reload_timezone: false, + access: ApiAccessPermissions { + description: "Default access permissions (superuser only).", + permission: &Permission::Superuser + }, } } @@ -428,6 +442,10 @@ impl ApiMethod { returns: &NULL_SCHEMA, protected: false, reload_timezone: false, + access: ApiAccessPermissions { + description: "Default access permissions (superuser only).", + permission: &Permission::Superuser + }, } } @@ -448,4 +466,10 @@ impl ApiMethod { self } + + pub const fn permissions(mut self, description: &'static str, permission: &'static Permission) -> Self { + self.access = ApiAccessPermissions { description, permission }; + + self + } } -- 2.39.5