diff --git a/examples/machines.toml b/examples/machines.toml new file mode 100644 index 0000000..866aef1 --- /dev/null +++ b/examples/machines.toml @@ -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" diff --git a/examples/roles.toml b/examples/roles.toml new file mode 100644 index 0000000..cc61b71 --- /dev/null +++ b/examples/roles.toml @@ -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", +] diff --git a/src/api.rs b/src/api.rs index 3787de5..8e1d85b 100644 --- a/src/api.rs +++ b/src/api.rs @@ -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) -> 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(()) diff --git a/src/api/auth.rs b/src/api/auth.rs index 5fe1297..7f4989e 100644 --- a/src/api/auth.rs +++ b/src/api/auth.rs @@ -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 for CB { - fn callback(sasl: SaslCtx, session: Session, prop: Property) -> libc::c_int { + fn callback(sasl: SaslCtx, session: SaslSession, 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 for CB { pub struct Auth { pub ctx: SASL, + session: Arc, } impl Auth { - pub fn new() -> Self { + pub fn new(session: Arc) -> Self { let mut ctx = SASL::new().unwrap(); let mut appdata = Box::new(AppData); @@ -62,7 +69,9 @@ impl Auth { ctx.install_callback::(); - 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 { - 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!() -} diff --git a/src/api/machines.rs b/src/api/machines.rs index 2034681..edf7809 100644 --- a/src/api/machines.rs +++ b/src/api/machines.rs @@ -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) -> Self { + info!(session.log, "Machines created"); Self { session } } } diff --git a/src/connection.rs b/src/connection.rs index 3f2365e..2f878c4 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -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, } @@ -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); diff --git a/src/db/access.rs b/src/db/access.rs index 7c747ab..aa63d88 100644 --- a/src/db/access.rs +++ b/src/db/access.rs @@ -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, + + // If a role doesn't define permissions, default to an empty Vec. + #[serde(default, skip_serializing_if = "Vec::is_empty")] permissions: Vec, } +impl Role { + fn load_file>(path: P) -> Result> { + let content = fs::read(path)?; + let file_roles: HashMap = 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 for RoleIdentifier { + type Error = RoleFromStrError; + + fn try_from(s: String) -> std::result::Result { + 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 for PermissionBuf { #[inline(always)] @@ -317,8 +376,16 @@ impl PartialOrd for Permission { } } +impl AsRef 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 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 for PermRule { + type Error = &'static str; + + fn try_from(mut input: String) -> std::result::Result { + // 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()); + } } diff --git a/src/machine.rs b/src/machine.rs index 9a9cf77..73b13aa 100644 --- a/src/machine.rs +++ b/src/machine.rs @@ -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, /// The permission required + #[serde(flatten)] privs: access::PrivilegesBuf, } + +impl MachineDescription { + fn load_file>(path: P) -> Result> { + 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 + = 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]); + } + } +} diff --git a/src/main.rs b/src/main.rs index 18a7539..8944057 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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.