Pushes code around until example loading compiles

This commit is contained in:
Gregor Reitzenstein 2020-11-19 14:53:14 +01:00
parent 5d9c1d5a64
commit 3b63e654e5
9 changed files with 272 additions and 29 deletions

14
examples/machines.toml Normal file
View File

@ -0,0 +1,14 @@
[e5408099-d3e5-440b-a92b-3aabf7683d6b]
name = "Somemachine"
disclose = "lab.some.disclose"
read = "lab.some.read"
write = "lab.some.write"
manage = "lab.some.admin"
[eaabebae-34d1-4a3a-912a-967b495d3d6e]
name = "Testmachine"
description = "An optional description"
disclose = "lab.test.read"
read = "lab.test.read"
write = "lab.test.write"
manage = "lab.test.admin"

20
examples/roles.toml Normal file
View File

@ -0,0 +1,20 @@
[testrole]
name = "Testrole"
permissions = [
"lab.test.*"
]
[somerole]
name = "Somerole"
parents = ["testparent%lmdb"]
permissions = [
"lab.some.admin"
]
[testparent]
name = "Testparent"
permissions = [
"lab.some.write",
"lab.some.read",
"lab.some.disclose",
]

View File

@ -1,5 +1,7 @@
use std::sync::Arc;
use slog::Logger;
use capnp::capability::{Params, Results, Promise};
use crate::schema::connection_capnp;
@ -17,6 +19,7 @@ pub struct Bootstrap {
impl Bootstrap {
pub fn new(session: Arc<Session>) -> Self {
info!(session.log, "Created Bootstrap");
Self { session }
}
}
@ -31,7 +34,7 @@ impl connection_capnp::bootstrap::Server for Bootstrap {
// TODO: When should we allow multiple auth and how do me make sure that does not leak
// priviledges (e.g. due to previously issues caps)?
if self.session.user.is_none() {
res.get().set_auth(capnp_rpc::new_client(auth::Auth::new()))
res.get().set_auth(capnp_rpc::new_client(auth::Auth::new(self.session.clone())))
}
Promise::ok(())

View File

@ -3,23 +3,29 @@
//! Authorization is over in `access.rs`
//! Authentication using SASL
use std::sync::Arc;
use slog::Logger;
use rsasl::{
SASL,
Property,
Session,
Session as SaslSession,
ReturnCode,
Callback,
SaslCtx,
Step,
};
use serde::{Serialize, Deserialize};
use capnp::capability::{Params, Results, Promise};
use crate::error::Result;
use crate::config::Settings;
use crate::api::Session;
pub use crate::schema::auth_capnp;
pub struct AppData;
@ -27,7 +33,7 @@ pub struct SessionData;
struct CB;
impl Callback<AppData, SessionData> for CB {
fn callback(sasl: SaslCtx<AppData, SessionData>, session: Session<SessionData>, prop: Property) -> libc::c_int {
fn callback(sasl: SaslCtx<AppData, SessionData>, session: SaslSession<SessionData>, prop: Property) -> libc::c_int {
let ret = match prop {
Property::GSASL_VALIDATE_SIMPLE => {
let authid = session.get_property(Property::GSASL_AUTHID).unwrap().to_string_lossy();
@ -50,10 +56,11 @@ impl Callback<AppData, SessionData> for CB {
pub struct Auth {
pub ctx: SASL<AppData, SessionData>,
session: Arc<Session>,
}
impl Auth {
pub fn new() -> Self {
pub fn new(session: Arc<Session>) -> Self {
let mut ctx = SASL::new().unwrap();
let mut appdata = Box::new(AppData);
@ -62,7 +69,9 @@ impl Auth {
ctx.install_callback::<CB>();
Self { ctx }
info!(session.log, "Auth created");
Self { ctx, session }
}
}
@ -156,20 +165,19 @@ impl auth_capnp::authentication::Server for Auth {
}
}
pub async fn init(log: Logger, config: Settings) -> Result<Auth> {
Ok(Auth::new())
}
// Use the newtype pattern here to make the type system work for us; even though AuthCId is for all
// intents and purposes just a String the compiler will still complain if you return or more
// importantly pass a String intead of a AuthCId. This prevents bugs where you get an object from
// somewhere and pass it somewhere else and in between don't check if it's the right type and
// accidentally pass the authzid where the authcid should have gone.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
/// Authentication Identity
///
/// Under the hood a string because the form depends heavily on the method
struct AuthCId(String);
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
/// Authorization Identity
///
/// This identity is internal to FabAccess and completely independent from the authentication
@ -191,6 +199,7 @@ struct AuthZId {
}
// What is a man?! A miserable little pile of secrets!
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
/// Authentication/Authorization user object.
///
/// This struct contains the user as is passed to the actual authentication/authorization
@ -218,7 +227,7 @@ pub struct User {
/// Contains the authentication method used
///
/// For the most part this is the SASL method
authMethod: String,
auth_method: String,
/// Method-specific key-value pairs
///
@ -236,6 +245,7 @@ pub struct User {
// b) the given authcid may authenticate as the given authzid. E.g. if a given client certificate
// has been configured for that user, if a GSSAPI user maps to a given user,
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum AuthError {
/// Authentication ID is bad/unknown/..
BadAuthcid,
@ -247,7 +257,3 @@ pub enum AuthError {
NotAllowedAuthzid,
}
fn grant_auth(user: User) -> std::result::Result<(), AuthError> {
unimplemented!()
}

View File

@ -1,5 +1,7 @@
use std::sync::Arc;
use slog::Logger;
use capnp::capability::Promise;
use capnp::Error;
@ -15,6 +17,7 @@ pub struct Machines {
impl Machines {
pub fn new(session: Arc<Session>) -> Self {
info!(session.log, "Machines created");
Self { session }
}
}

View File

@ -15,7 +15,7 @@ use crate::schema::connection_capnp;
/// Connection context
// TODO this should track over several connections
pub struct Session {
log: Logger,
pub log: Logger,
pub user: Option<auth::User>,
}
@ -57,6 +57,7 @@ async fn handshake(log: &Logger, stream: &mut TcpStream) -> Result<()> {
pub async fn handle_connection(log: Logger, stream: TcpStream) -> Result<()> {
//handshake(&log, &mut stream).await?;
info!(log, "New connection from on {:?}", stream);
let session = Arc::new(Session::new(log));
let boots = Bootstrap::new(session);
let rpc: connection_capnp::bootstrap::Client = capnp_rpc::new_client(boots);

View File

@ -4,16 +4,22 @@
use std::fmt;
use std::collections::HashSet;
use std::cmp::Ordering;
use std::convert::TryInto;
use std::path::{Path, PathBuf};
use std::fs;
use std::io::Write;
use std::sync::Arc;
use std::collections::HashMap;
use std::iter::FromIterator;
use std::convert::{TryFrom, Into};
use flexbuffers;
use serde::{Serialize, Deserialize};
use serde::{
Serialize,
Serializer,
Deserialize,
Deserializer,
};
use slog::Logger;
use lmdb::{Environment, Transaction, RwTransaction, Cursor};
@ -98,15 +104,31 @@ pub trait RoleDB {
pub struct Role {
name: String,
// If a role doesn't define parents, default to an empty Vec.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
/// 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>,
// If a role doesn't define permissions, default to an empty Vec.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
permissions: Vec<PermRule>,
}
impl Role {
fn load_file<P: AsRef<Path>>(path: P) -> Result<HashMap<RoleIdentifier, Role>> {
let content = fs::read(path)?;
let file_roles: HashMap<String, Role> = toml::from_slice(&content[..])?;
Ok(HashMap::from_iter(file_roles.into_iter().map(|(key, value)| {
(RoleIdentifier::local_from_str("lmdb".to_string(), key), value)
})))
}
}
type SourceID = String;
fn split_once(s: &str, split: char) -> Option<(&str, &str)> {
@ -116,6 +138,7 @@ fn split_once(s: &str, split: char) -> Option<(&str, &str)> {
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(try_from = "String")]
/// Universal (relative) id of a role
pub enum RoleIdentifier {
/// The role comes from this instance
@ -135,6 +158,16 @@ pub enum RoleIdentifier {
location: String,
}
}
impl fmt::Display for RoleIdentifier {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
RoleIdentifier::Local {name, source} => write!(f, "{}/{}@local", name, source),
RoleIdentifier::Remote {name, location} => write!(f, "{}@{}", name, location),
}
}
}
impl std::str::FromStr for RoleIdentifier {
type Err = RoleFromStrError;
@ -148,21 +181,42 @@ impl std::str::FromStr for RoleIdentifier {
}
}
}
impl fmt::Display for RoleIdentifier {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
RoleIdentifier::Local {name, source} => write!(f, "{}/{}@local", name, source),
RoleIdentifier::Remote {name, location} => write!(f, "{}@{}", name, location),
impl TryFrom<String> for RoleIdentifier {
type Error = RoleFromStrError;
fn try_from(s: String) -> std::result::Result<Self, Self::Error> {
if let Some((name, location)) = split_once(&s, '@') {
Ok(RoleIdentifier::Remote { name: name.to_string(), location: location.to_string() })
} else if let Some((name, source)) = split_once(&s, '%') {
Ok(RoleIdentifier::Local { name: name.to_string(), source: source.to_string() })
} else {
Err(RoleFromStrError::Invalid)
}
}
}
impl RoleIdentifier {
pub fn local_from_str(source: String, name: String) -> Self {
RoleIdentifier::Local { name, source }
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum RoleFromStrError {
/// No '@' or '%' found. That's strange, huh?
Invalid
}
impl fmt::Display for RoleFromStrError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
RoleFromStrError::Invalid
=> write!(f, "Rolename are of form 'name%source' or 'name@realm'."),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
/// An identifier for a permission
// XXX: Does remote permissions ever make sense?
@ -201,6 +255,7 @@ pub struct PrivilegesBuf {
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(transparent)]
#[serde(transparent)]
/// An owned permission string
///
/// This is under the hood just a fancy std::String.
@ -242,6 +297,10 @@ impl PermissionBuf {
pub fn from_string(inner: String) -> Self {
Self { inner }
}
pub fn into_string(self) -> String {
self.inner
}
}
impl AsRef<str> for PermissionBuf {
#[inline(always)]
@ -317,8 +376,16 @@ impl PartialOrd for Permission {
}
}
impl AsRef<Permission> for Permission {
#[inline]
fn as_ref(&self) -> &Permission {
self
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(try_from = "String")]
#[serde(into = "String")]
pub enum PermRule {
/// The permission is precise,
///
@ -364,6 +431,46 @@ impl fmt::Display for PermRule {
}
}
impl Into<String> for PermRule {
fn into(self) -> String {
match self {
PermRule::Base(perm) => perm.into_string(),
PermRule::Children(mut perm) => {
perm.push(Permission::new("+"));
perm.into_string()
},
PermRule::Subtree(mut perm) => {
perm.push(Permission::new("+"));
perm.into_string()
}
}
}
}
impl TryFrom<String> for PermRule {
type Error = &'static str;
fn try_from(mut input: String) -> std::result::Result<Self, Self::Error> {
// Check out specifically the last two chars
let len = input.len();
if len <= 2 {
Err("Input string for PermRule is too short")
} else {
match &input[len-2..len] {
".+" => {
input.truncate(len-2);
Ok(PermRule::Children(PermissionBuf::from_string(input)))
},
".*" => {
input.truncate(len-2);
Ok(PermRule::Subtree(PermissionBuf::from_string(input)))
},
_ => Ok(PermRule::Base(PermissionBuf::from_string(input))),
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
@ -390,4 +497,35 @@ mod tests {
assert!(rule.match_perm(&perm));
}
#[test]
fn load_examples_roles_test() {
let roles = Role::load_file("examples/roles.toml")
.expect("Couldn't load the example role defs. Does `examples/roles.toml` exist?");
assert!(true)
}
#[test]
fn rules_from_string_test() {
assert_eq!(
PermRule::Base(PermissionBuf::from_string("bffh.perm".to_string())),
PermRule::try_from("bffh.perm".to_string()).unwrap()
);
assert_eq!(
PermRule::Children(PermissionBuf::from_string("bffh.perm".to_string())),
PermRule::try_from("bffh.perm.+".to_string()).unwrap()
);
assert_eq!(
PermRule::Subtree(PermissionBuf::from_string("bffh.perm".to_string())),
PermRule::try_from("bffh.perm.*".to_string()).unwrap()
);
}
#[test]
fn rules_from_string_edgecases_test() {
assert!(PermRule::try_from("*".to_string()).is_err());
assert!(PermRule::try_from("+".to_string()).is_err());
}
}

View File

@ -1,3 +1,7 @@
use std::path::Path;
use std::collections::HashMap;
use std::fs;
use serde::{Serialize, Deserialize};
use futures_signals::signal::Signal;
@ -19,6 +23,9 @@ use crate::db::machine::{MachineIdentifier, Status, MachineState};
/// machine, checking that the user who wants the machine (de)activated has the required
/// permissions.
pub struct Machine {
/// Globally unique machine readable identifier
id: MachineIdentifier,
/// Descriptor of the machine
desc: MachineDescription,
@ -30,8 +37,9 @@ pub struct Machine {
}
impl Machine {
pub fn new(desc: MachineDescription, perm: access::PermIdentifier) -> Machine {
pub fn new(id: MachineIdentifier, desc: MachineDescription, perm: access::PermIdentifier) -> Machine {
Machine {
id: id,
desc: desc,
state: Mutable::new(MachineState { state: Status::Free}),
}
@ -72,19 +80,70 @@ impl Machine {
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
/// A description of a machine
///
/// This is the struct that a machine is serialized to/from.
/// Combining this with the actual state of the system will return a machine
pub struct MachineDescription {
/// The main machine identifier. This must be unique.
id: MachineIdentifier,
/// The name of the machine. Doesn't need to be unique but is what humans will be presented.
name: String,
/// An optional description of the Machine.
description: Option<String>,
/// The permission required
#[serde(flatten)]
privs: access::PrivilegesBuf,
}
impl MachineDescription {
fn load_file<P: AsRef<Path>>(path: P) -> Result<HashMap<MachineIdentifier, MachineDescription>> {
let content = fs::read(path)?;
Ok(toml::from_slice(&content[..])?)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::iter::FromIterator;
use crate::db::access::{PermissionBuf, PrivilegesBuf};
#[test]
fn load_examples_descriptions_test() {
let machines = MachineDescription::load_file("examples/machines.toml")
.expect("Couldn't load the example machine defs. Does `examples/machines.toml` exist?");
let expected: HashMap<MachineIdentifier, MachineDescription>
= HashMap::from_iter(vec![
(Uuid::parse_str("e5408099-d3e5-440b-a92b-3aabf7683d6b").unwrap(),
MachineDescription {
name: "Somemachine".to_string(),
description: None,
privs: PrivilegesBuf {
disclose: PermissionBuf::from_string("lab.some.disclose".to_string()),
read: PermissionBuf::from_string("lab.some.read".to_string()),
write: PermissionBuf::from_string("lab.some.write".to_string()),
manage: PermissionBuf::from_string("lab.some.admin".to_string()),
},
}),
(Uuid::parse_str("eaabebae-34d1-4a3a-912a-967b495d3d6e").unwrap(),
MachineDescription {
name: "Testmachine".to_string(),
description: Some("An optional description".to_string()),
privs: PrivilegesBuf {
disclose: PermissionBuf::from_string("lab.test.read".to_string()),
read: PermissionBuf::from_string("lab.test.read".to_string()),
write: PermissionBuf::from_string("lab.test.write".to_string()),
manage: PermissionBuf::from_string("lab.test.admin".to_string()),
},
}),
].into_iter());
for u in ["e5408099-d3e5-440b-a92b-3aabf7683d6b", "eaabebae-34d1-4a3a-912a-967b495d3d6e"].iter() {
let uuid = Uuid::parse_str(u).unwrap();
assert_eq!(machines[&uuid], expected[&uuid]);
}
}
}

View File

@ -143,7 +143,6 @@ fn main() -> Result<(), Error> {
let env = Arc::new(env);
let mdb = db::machine::init(log.new(o!("system" => "machines")), &config, env.clone());
let pdb = db::access::init(log.new(o!("system" => "permissions")), &config, env.clone());
let authentication_f = api::auth::init(log.new(o!("system" => "authentication")), config.clone());
// If --load or --dump is given we can stop at this point and load/dump the database and then
// exit.