fabaccess-bffh/src/access.rs

467 lines
17 KiB
Rust
Raw Normal View History

2020-02-14 12:20:17 +01:00
//! Access control logic
//!
2020-09-10 11:50:19 +02:00
use std::collections::HashSet;
use std::convert::TryInto;
use std::path::{Path, PathBuf};
use std::fs;
use std::io::Write;
2020-09-10 11:50:19 +02:00
use flexbuffers;
use serde::{Serialize, Deserialize};
2020-02-17 15:07:55 +01:00
use slog::Logger;
use lmdb::{Transaction, RwTransaction, Cursor};
2020-02-17 15:07:55 +01:00
2020-09-15 14:31:10 +02:00
use crate::config::Settings;
2020-09-10 11:50:19 +02:00
use crate::error::Result;
2020-02-18 16:55:19 +01:00
2020-09-11 10:18:43 +02:00
// FIXME: fabinfra/fabaccess/bffh#3
2020-09-17 10:51:51 +02:00
pub type UserIdentifier = u64;
pub type RoleIdentifier = u64;
pub type PermIdentifier = u64;
2020-02-18 16:55:19 +01:00
pub struct PermissionsProvider {
log: Logger,
2020-09-10 11:50:19 +02:00
roledb: lmdb::Database,
permdb: lmdb::Database,
userdb: lmdb::Database,
2020-02-18 16:55:19 +01:00
}
impl PermissionsProvider {
2020-09-10 11:50:19 +02:00
pub fn new(log: Logger, roledb: lmdb::Database, permdb: lmdb::Database, userdb: lmdb::Database) -> Self {
Self { log, roledb, permdb, userdb }
}
/// Check if a given user has the given permission
#[allow(unused)]
pub fn check<T: Transaction>(&self, txn: &T, userID: UserIdentifier, permID: PermIdentifier) -> Result<bool> {
if let Some(user) = self.get_user(txn, userID)? {
// Tally all roles. Makes dependent roles easier
let mut roles = HashSet::new();
for roleID in user.roles {
self.tally_role(txn, &mut roles, roleID)?;
}
// Iter all unique role->permissions we've found and early return on match.
// TODO: Change this for negative permissions?
for role in roles.iter() {
for perm in role.permissions.iter() {
if permID == *perm {
return Ok(true);
}
}
}
}
return Ok(false);
}
fn tally_role<T: Transaction>(&self, txn: &T, roles: &mut HashSet<Role>, roleID: RoleIdentifier) -> Result<()> {
if let Some(role) = self.get_role(txn, roleID)? {
// Only check and tally parents of a role at the role itself if it's the first time we
// see it
if !roles.contains(&role) {
for parent in role.parents.iter() {
self.tally_role(txn, roles, *parent)?;
}
roles.insert(role);
}
}
Ok(())
}
fn get_role<'txn, T: Transaction>(&self, txn: &'txn T, roleID: RoleIdentifier) -> Result<Option<Role>> {
match txn.get(self.roledb, &roleID.to_ne_bytes()) {
Ok(bytes) => {
Ok(Some(flexbuffers::from_slice(bytes)?))
},
Err(lmdb::Error::NotFound) => { Ok(None) },
Err(e) => { Err(e.into()) }
}
}
fn get_user<T: Transaction>(&self, txn: &T, userID: UserIdentifier) -> Result<Option<User>> {
match txn.get(self.userdb, &userID.to_ne_bytes()) {
Ok(bytes) => {
Ok(Some(flexbuffers::from_slice(bytes)?))
},
Err(lmdb::Error::NotFound) => { Ok(None) },
Err(e) => { Err(e.into()) }
}
2020-02-17 14:56:43 +01:00
}
2020-09-10 12:32:33 +02:00
fn get_perm<T: Transaction>(&self, txn: &T, permID: PermIdentifier) -> Result<Option<Perm>> {
match txn.get(self.permdb, &permID.to_ne_bytes()) {
Ok(bytes) => {
Ok(Some(flexbuffers::from_slice(bytes)?))
},
Err(lmdb::Error::NotFound) => { Ok(None) },
Err(e) => { Err(e.into()) }
}
}
fn put_role(&self, txn: &mut RwTransaction, roleID: RoleIdentifier, role: Role) -> Result<()> {
let bytes = flexbuffers::to_vec(role)?;
txn.put(self.roledb, &roleID.to_ne_bytes(), &bytes, lmdb::WriteFlags::empty())?;
Ok(())
}
fn put_user(&self, txn: &mut RwTransaction, userID: UserIdentifier, user: User) -> Result<()> {
let bytes = flexbuffers::to_vec(user)?;
txn.put(self.userdb, &userID.to_ne_bytes(), &bytes, lmdb::WriteFlags::empty())?;
Ok(())
}
2020-09-10 12:32:33 +02:00
fn put_perm(&self, txn: &mut RwTransaction, permID: PermIdentifier, perm: Perm) -> Result<()> {
let bytes = flexbuffers::to_vec(perm)?;
txn.put(self.permdb, &permID.to_ne_bytes(), &bytes, lmdb::WriteFlags::empty())?;
Ok(())
}
pub fn dump_db<T: Transaction>(&mut self, txn: &T, mut path: PathBuf) -> Result<()> {
path.push("roles");
let mut k = Ok(());
if !path.is_dir() {
k = fs::create_dir(&path);
}
if let Err(e) = k {
error!(self.log, "Failed to create 'roles' directory: {}, skipping!", e);
return Ok(())
} else {
2020-10-23 15:29:32 +02:00
// Rust's stdlib considers the last element the file name even when it's a directory so
// we have to put a dummy here for .set_filename() to work correctly
path.push("dummy");
self.dump_roles(txn, path.clone())?;
path.pop();
}
path.pop();
// ====================: PERMS :====================
path.push("perms");
let mut k = Ok(());
if !path.is_dir() {
k = fs::create_dir(&path);
}
if let Err(e) = k {
error!(self.log, "Failed to create 'perms' directory: {}, skipping!", e);
return Ok(())
} else {
2020-10-23 15:29:32 +02:00
// Rust's stdlib considers the last element the file name even when it's a directory so
// we have to put a dummy here for .set_filename() to work correctly
path.push("dummy");
self.dump_perms(txn, path.clone())?;
path.pop();
}
path.pop();
// ====================: USERS :====================
path.push("users");
let mut k = Ok(());
if !path.is_dir() {
k = fs::create_dir(&path);
}
if let Err(e) = k {
error!(self.log, "Failed to create 'users' directory: {}, skipping!", e);
return Ok(())
} else {
2020-10-23 15:29:32 +02:00
// Rust's stdlib considers the last element the file name even when it's a directory so
// we have to put a dummy here for .set_filename() to work correctly
path.push("dummy");
self.dump_users(txn, path.clone())?;
path.pop();
}
path.pop();
Ok(())
}
fn dump_roles<T: Transaction>(&mut self, txn: &T, mut path: PathBuf) -> Result<()> {
let mut role_cursor = txn.open_ro_cursor(self.roledb)?;
for buf in role_cursor.iter_start() {
let (kbuf, vbuf) = buf?;
let (kbytes, _rest) = kbuf.split_at(std::mem::size_of::<u64>());
let roleID = u64::from_ne_bytes(kbytes.try_into().unwrap());
let role: Role = flexbuffers::from_slice(vbuf)?;
2020-10-22 13:00:58 +02:00
let filename = format!("{:x}.toml", roleID);
path.set_file_name(filename);
let mut fp = std::fs::File::create(&path)?;
2020-09-15 14:31:10 +02:00
let out = toml::to_vec(&role)?;
fp.write_all(&out)?;
}
Ok(())
}
fn dump_perms<T: Transaction>(&mut self, txn: &T, mut path: PathBuf) -> Result<()> {
let mut perm_cursor = txn.open_ro_cursor(self.permdb)?;
for buf in perm_cursor.iter_start() {
let (kbuf, vbuf) = buf?;
let (kbytes, _rest) = kbuf.split_at(std::mem::size_of::<u64>());
let permID = u64::from_ne_bytes(kbytes.try_into().unwrap());
let perm: Perm = flexbuffers::from_slice(vbuf)?;
2020-10-22 13:00:58 +02:00
let filename = format!("{:x}.toml", permID);
path.set_file_name(filename);
let mut fp = std::fs::File::create(&path)?;
2020-09-15 14:31:10 +02:00
let out = toml::to_vec(&perm)?;
fp.write_all(&out)?;
}
Ok(())
}
fn dump_users<T: Transaction>(&mut self, txn: &T, mut path: PathBuf) -> Result<()> {
let mut user_cursor = txn.open_ro_cursor(self.userdb)?;
for buf in user_cursor.iter_start() {
let (kbuf, vbuf) = buf?;
let (kbytes, _rest) = kbuf.split_at(std::mem::size_of::<u64>());
let userID = u64::from_ne_bytes(kbytes.try_into().unwrap());
let user: User = flexbuffers::from_slice(vbuf)?;
2020-10-22 13:00:58 +02:00
let filename = format!("{:x}.toml", userID);
path.set_file_name(filename);
let mut fp = std::fs::File::create(&path)?;
2020-09-15 14:31:10 +02:00
let out = toml::to_vec(&user)?;
fp.write_all(&out)?;
}
Ok(())
}
pub fn load_db(&mut self, txn: &mut RwTransaction, mut path: PathBuf) -> Result<()> {
// ====================: ROLES :====================
path.push("roles");
if !path.is_dir() {
error!(self.log, "Given load directory is malformed, no 'roles' subdir, not loading roles!");
} else {
self.load_roles(txn, path.as_path())?;
}
path.pop();
// =================================================
// ====================: PERMS :====================
path.push("perms");
if !path.is_dir() {
error!(self.log, "Given load directory is malformed, no 'perms' subdir, not loading perms!");
} else {
2020-09-11 10:18:43 +02:00
self.load_perms(txn, &path)?;
}
path.pop();
// =================================================
// ====================: USERS :====================
path.push("users");
if !path.is_dir() {
error!(self.log, "Given load directory is malformed, no 'users' subdir, not loading users!");
} else {
2020-09-11 10:18:43 +02:00
self.load_users(txn, &path)?;
}
path.pop();
// =================================================
Ok(())
}
fn load_roles(&mut self, txn: &mut RwTransaction, path: &Path) -> Result<()> {
for entry in std::fs::read_dir(path)? {
let entry = entry?;
let path = entry.path();
if path.is_file() {
// will only ever be none if the path has no file name and then how is it a file?!
let roleID_str = path
.file_stem().expect("Found a file with no filename?")
.to_str().expect("Found an OsStr that isn't valid Unicode. Fix your OS!");
let roleID = match u64::from_str_radix(roleID_str, 16) {
Ok(i) => i,
Err(e) => {
warn!(self.log, "File {} had a invalid name. Expected an u64 in [0-9a-z] hex with optional file ending: {}. Skipping!", path.display(), e);
continue;
}
};
let s = match fs::read_to_string(path.as_path()) {
Ok(s) => s,
Err(e) => {
warn!(self.log, "Failed to open file {}: {}, skipping!"
, path.display()
, e);
continue;
}
};
let role: Role = match toml::from_str(&s) {
Ok(r) => r,
Err(e) => {
warn!(self.log, "Failed to parse role at path {}: {}, skipping!"
, path.display()
, e);
continue;
}
};
self.put_role(txn, roleID, role)?;
debug!(self.log, "Loaded role {}", roleID);
} else {
warn!(self.log, "Path {} is not a file, skipping!", path.display());
}
}
Ok(())
}
2020-09-11 10:18:43 +02:00
fn load_perms(&mut self, txn: &mut RwTransaction, path: &Path) -> Result<()> {
for entry in std::fs::read_dir(path)? {
let entry = entry?;
let path = entry.path();
if path.is_file() {
// will only ever be none if the path has no file name and then how is it a file?!
let permID_str = path
.file_stem().expect("Found a file with no filename?")
.to_str().expect("Found an OsStr that isn't valid Unicode. Fix your OS!");
let permID = match u64::from_str_radix(permID_str, 16) {
Ok(i) => i,
Err(e) => {
warn!(self.log, "File {} had a invalid name. Expected an u64 in [0-9a-z] hex with optional file ending: {}. Skipping!", path.display(), e);
continue;
}
};
let s = match fs::read_to_string(path.as_path()) {
Ok(s) => s,
Err(e) => {
warn!(self.log, "Failed to open file {}: {}, skipping!"
, path.display()
, e);
continue;
}
};
let perm: Perm = match toml::from_str(&s) {
Ok(r) => r,
Err(e) => {
warn!(self.log, "Failed to parse perm at path {}: {}, skipping!"
, path.display()
, e);
continue;
}
};
self.put_perm(txn, permID, perm)?;
debug!(self.log, "Loaded perm {}", permID);
} else {
warn!(self.log, "Path {} is not a file, skipping!", path.display());
}
}
Ok(())
}
fn load_users(&mut self, txn: &mut RwTransaction, path: &Path) -> Result<()> {
for entry in std::fs::read_dir(path)? {
let entry = entry?;
let path = entry.path();
if path.is_file() {
// will only ever be none if the path has no file name and then how is it a file?!
let userID_str = path
.file_stem().expect("Found a file with no filename?")
.to_str().expect("Found an OsStr that isn't valid Unicode. Fix your OS!");
let userID = match u64::from_str_radix(userID_str, 16) {
Ok(i) => i,
Err(e) => {
warn!(self.log, "File {} had a invalid name. Expected an u64 in [0-9a-z] hex with optional file ending: {}. Skipping!", path.display(), e);
continue;
}
};
let s = match fs::read_to_string(path.as_path()) {
Ok(s) => s,
Err(e) => {
warn!(self.log, "Failed to open file {}: {}, skipping!"
, path.display()
, e);
continue;
}
};
let user: User = match toml::from_str(&s) {
Ok(r) => r,
Err(e) => {
warn!(self.log, "Failed to parse user at path {}: {}, skipping!"
, path.display()
, e);
continue;
}
};
self.put_user(txn, userID, user)?;
debug!(self.log, "Loaded user {}", userID);
} else {
warn!(self.log, "Path {} is not a file, skipping!", path.display());
}
}
Ok(())
}
2020-02-17 14:56:43 +01:00
}
2020-02-17 03:44:02 +01:00
2020-09-15 14:34:48 +02:00
/// Initialize the access db by loading all the lmdb databases
2020-09-15 14:31:10 +02:00
pub fn init(log: Logger, config: &Settings, env: &lmdb::Environment) -> std::result::Result<PermissionsProvider, crate::error::Error> {
2020-09-10 11:50:19 +02:00
let mut flags = lmdb::DatabaseFlags::empty();
flags.set(lmdb::DatabaseFlags::INTEGER_KEY, true);
let roledb = env.create_db(Some("role"), flags)?;
debug!(&log, "Opened access database '{}' successfully.", "role");
2020-09-10 11:50:19 +02:00
let permdb = env.create_db(Some("perm"), flags)?;
debug!(&log, "Opened access database '{}' successfully.", "perm");
2020-09-10 11:50:19 +02:00
let userdb = env.create_db(Some("user"), flags)?;
debug!(&log, "Opened access database '{}' successfully.", "user");
info!(&log, "Opened all access databases");
2020-09-10 11:50:19 +02:00
return Ok(PermissionsProvider::new(log, roledb, permdb, userdb));
2020-02-14 12:20:17 +01:00
}
2020-09-10 10:39:46 +02:00
/// A Person, from the Authorization perspective
2020-09-10 11:50:19 +02:00
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
struct User {
2020-09-10 10:39:46 +02:00
name: String,
/// A Person has N ≥ 0 roles.
/// Persons are only ever given roles, not permissions directly
roles: Vec<RoleIdentifier>
}
/// A "Role" from the Authorization perspective
///
/// You can think of a role as a bundle of permissions relating to other roles. In most cases a
/// role represents a real-world education or apprenticeship, which gives a person the education
/// necessary to use a machine safely.
/// Roles are assigned permissions which in most cases evaluate to granting a person the right to
/// use certain (potentially) dangerous machines.
/// Using this indirection makes administration easier in certain ways; instead of maintaining
/// permissions on users directly the user is given a role after having been educated on the safety
/// of a machine; if later on a similar enough machine is put to use the administrator can just add
/// the permission for that machine to an already existing role instead of manually having to
/// assign to all users.
2020-09-10 11:50:19 +02:00
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
2020-09-10 10:39:46 +02:00
struct Role {
name: String,
/// A Role can have parents, inheriting all permissions
///
/// This makes situations where different levels of access are required easier: Each higher
/// level of access sets the lower levels of access as parent, inheriting their permission; if
/// you are allowed to manage a machine you are then also allowed to use it and so on
parents: Vec<RoleIdentifier>,
permissions: Vec<PermIdentifier>,
}
/// A Permission from the Authorization perspective
///
/// Permissions are rather simple flags. A person can have or not have a permission, dictated by
/// its roles and the permissions assigned to those roles.
2020-09-10 11:50:19 +02:00
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
2020-09-10 12:32:33 +02:00
struct Perm {
2020-09-10 10:39:46 +02:00
name: String,
}