From 46c06305b0ab21124187c5bd699fc845ab417be2 Mon Sep 17 00:00:00 2001 From: Gregor Reitzenstein Date: Sun, 16 Feb 2020 16:02:03 +0100 Subject: [PATCH] An initial working state PoC, Authentication works but not much more --- Cargo.toml | 3 +- schema/api.capnp | 79 +++++++++++++++-- src/access.rs | 1 + src/api.rs | 64 +++++++++++++- src/auth.rs | 219 +++++++++++++++++++++++++++++++++++++++++++++++ src/config.rs | 14 +-- src/error.rs | 32 ++++++- src/machine.rs | 62 ++++++++++++++ src/main.rs | 22 ++++- 9 files changed, 479 insertions(+), 17 deletions(-) create mode 100644 src/auth.rs create mode 100644 src/machine.rs diff --git a/Cargo.toml b/Cargo.toml index 1cbd209..fdf03b7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,8 +23,7 @@ capnp = "0.12" capnp-rpc = "0.12" toml = "0.5" -serde = "1" -serde_derive = "1" +serde = { version = "1.0", features = ["derive"] } casbin = "0.2" diff --git a/schema/api.capnp b/schema/api.capnp index 219c692..b307b3e 100644 --- a/schema/api.capnp +++ b/schema/api.capnp @@ -1,10 +1,77 @@ @0xfd92ce9be2369b8e; -interface BffhAdmin { - getAllSubjects @0 () -> (subjects :List(Subject)); - - struct Subject { - id @0 :Text; - domain @1 :Text; +struct Maybe(Value) { + union { + some @0 :Value; + none @1 :Void; + } +} + +struct Either(Left, Right) { + union { + left @0 :Left; + right @1 :Right; + } +} + +struct Subject { + id @0 :Text; + domain @1 :Text; +} + +struct Machine { + name @0 :Text; + location @1 :Text; + status @2 :Status; +} + +enum Status { + free @0; + occupied @1; + blocked @2; +} + +interface BffhAdmin { + getAllSubjects @0 () -> (subjects :List(Subject) ); + + getAllMachines @1 () -> (machines :List(Machine) ); + addMachine @2 (name :Text, location :Text ) -> (); + + machineSetState @3 (name :Text, state :Status ) -> (); + + authentication @4 () -> ( auth :Authentication ); +} + +interface Permissions { + getAllSubjects @0 () -> (subjects :List(Subject) ); +} + +interface Notification { + machineChangeState @0 (machine :Machine ) -> (); +} + +interface Authentication { + # List all SASL mechs the server is willing to use + availableMechanisms @0 () -> ( mechanisms :List(Text) ); + + # Start authentication using the given mechanism and optional initial data + initializeAuthentication @1 ( mechanism :Text, initialData :Maybe(Data) ) + -> (response :Either (Challenge, Outcome) ); + + getAuthzid @2 () -> ( authzid :Text ); + + interface Challenge { + # Access the challenge data + read @0 () -> ( data :Maybe(Data) ); + + respond @1 ( data :Maybe(Data) ) + -> ( response :Either (Challenge, Outcome) ); + } + + interface Outcome { + # Outcomes may contain additional data + read @0 () -> ( data :Maybe(Data) ); + # The actual outcome. + value @1 () -> ( granted :Bool ); } } diff --git a/src/access.rs b/src/access.rs index acb2cb8..9e0b033 100644 --- a/src/access.rs +++ b/src/access.rs @@ -5,6 +5,7 @@ use casbin::prelude::*; use super::config::Config; +/// This line documents init pub async fn init(config: &Config) -> Result> { let model = Model::from_file(config.access.model.clone()).await?; let adapter = Box::new(FileAdapter::new(config.access.policy.clone())); diff --git a/src/api.rs b/src/api.rs index 4348894..cb71f23 100644 --- a/src/api.rs +++ b/src/api.rs @@ -11,11 +11,17 @@ use futures_signals::signal::Mutable; use casbin::Enforcer; use casbin::MgmtApi; +use crate::machine::{MachineDB, Machine, Status, save}; +use crate::auth::Authentication; + pub fn init() { } -pub async fn process_socket(enforcer: Mutable, socket: TcpStream) -> Result<(), capnp::Error> { - let api = Api { e: enforcer }; +pub async fn process_socket(e: Mutable, m: Mutable, a: Authentication, socket: TcpStream) + -> Result<(), capnp::Error> +{ + let auth = api_capnp::authentication::ToClient::new(a).into_client::<::capnp_rpc::Server>(); + let api = Api { e, m, auth }; let a = api_capnp::bffh_admin::ToClient::new(api).into_client::<::capnp_rpc::Server>(); let netw = capnp_rpc::twoparty::VatNetwork::new(socket.clone(), socket, capnp_rpc::rpc_twoparty_capnp::Side::Server, Default::default()); @@ -25,6 +31,8 @@ pub async fn process_socket(enforcer: Mutable, socket: TcpStream) -> R struct Api { e: Mutable, + m: Mutable, + auth: api_capnp::authentication::Client, } impl api_capnp::bffh_admin::Server for Api { @@ -45,4 +53,56 @@ impl api_capnp::bffh_admin::Server for Api { ::capnp::capability::Promise::ok(()) } + + fn get_all_machines(&mut self, + _params: api_capnp::bffh_admin::GetAllMachinesParams, + mut results: api_capnp::bffh_admin::GetAllMachinesResults) + -> ::capnp::capability::Promise<(), ::capnp::Error> + { + let machs = self.m.lock_ref(); + + let mut b = results.get() + .init_machines(machs.len() as u32); + + for (i, (name, m)) in machs.iter().enumerate() { + let bldr = b.reborrow(); + let mut mach = bldr.get(i as u32); + mach.set_name(&name); + mach.set_location(&m.location); + mach.set_status(match m.status { + Status::Blocked => api_capnp::Status::Blocked, + Status::Free => api_capnp::Status::Free, + Status::Occupied => api_capnp::Status::Occupied, + }); + } + ::capnp::capability::Promise::ok(()) + } + + fn add_machine(&mut self, + params: api_capnp::bffh_admin::AddMachineParams, + mut results: api_capnp::bffh_admin::AddMachineResults) + -> ::capnp::capability::Promise<(), ::capnp::Error> + { + let params = pry!(params.get()); + + let name = pry!(params.get_name()); + let location = pry!(params.get_location()); + + let m = Machine::new(location.to_string()); + + let mut mdb = self.m.lock_mut(); + mdb.insert(name.to_string(), m); + + ::capnp::capability::Promise::ok(()) + } + + fn authentication(&mut self, + _params: api_capnp::bffh_admin::AuthenticationParams, + mut results: api_capnp::bffh_admin::AuthenticationResults) + -> ::capnp::capability::Promise<(), ::capnp::Error> + { + let mut b = results.get(); + b.set_auth(self.auth.clone()); + ::capnp::capability::Promise::ok(()) + } } diff --git a/src/auth.rs b/src/auth.rs new file mode 100644 index 0000000..9b0759f --- /dev/null +++ b/src/auth.rs @@ -0,0 +1,219 @@ +//! Authentication subsystem +//! +//! Authorization is over in `access.rs` +//! Authentication using SASL + +use std::collections::HashMap; +use std::fmt; +use std::error::Error; +use std::path::Path; +use std::fs::File; +use std::io::{Read, Write}; + +use futures_signals::signal::Mutable; +use casbin::Enforcer; + +use crate::error::Result; + +#[derive(Debug)] +pub enum SASLError { + /// Expected UTF-8, got something else + UTF8, + /// A bad Challenge was provided + BadChallenge, + /// Enforcer Failure + Enforcer, +} +impl fmt::Display for SASLError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "Bad SASL Exchange") + } +} +impl Error for SASLError {} + +type PassDB = HashMap; +pub fn open_passdb(path: &Path) -> Option { + if path.is_file() { + let mut fp = File::open(path).unwrap(); + let mut content = String::new(); + fp.read_to_string(&mut content).unwrap(); + let map = toml::from_str(&content).ok()?; + return Some(map); + } else { + let mut map = HashMap::new(); + map.insert("Testuser".to_string(), "Testpass".to_string()); + let mut fp = File::create(&path).unwrap(); + let toml = toml::to_string(&map).unwrap(); + fp.write_all(&toml.as_bytes()).unwrap(); + return Some(map); + } +} + +#[derive(Clone)] +struct Plain { + // FIXME: I don't want to store passwords. + passdb: Mutable, + enforcer: Mutable, +} + +impl Plain { + pub fn step<'a>(&self, data: &'a [u8]) -> Result<(bool, &'a str)> { + let data = std::str::from_utf8(data).map_err(|_| SASLError::UTF8)?; + if let Some((authzid, authcid, passwd)) = split_nul(data) { + + // Check if we know about that user + if let Some(pwd) = self.passdb.lock_ref().get(authcid) { + // Check the provided password + // FIXME: At least use hashes + if pwd == passwd { + // authzid is the Identity the user wants to act as. + // If that is unset, shortcut to Success + if authzid == "" || authzid == authcid { + return Ok((true, authcid)); + } + + let e = self.enforcer.lock_ref(); + if let Ok(b) = e.enforce(vec![authcid, authzid, "su"]) { + if b { + return Ok((true, authzid)); + } else { + return Ok((false, authzid)); + } + } else { + return Err(SASLError::Enforcer.into()); + } + + } + } + Ok((false, authzid)) + } else { + return Err(SASLError::BadChallenge.into()) + } + } +} + +pub fn split_nul(string: &str) -> Option<(&str, &str, &str)> { + let mut i = string.split(|b| b == '\0'); + + let a = i.next()?; + let b = i.next()?; + let c = i.next()?; + + Some((a,b,c)) +} + +#[derive(Clone)] +pub struct Authentication { + state: Option, + plain: Plain, +} + +impl Authentication { + pub fn new(passdb: Mutable, enforcer: Mutable) -> Self { + Authentication { + state: None, + plain: Plain { passdb, enforcer } + } + } + + pub fn mechs(&self) -> Vec<&'static str> { + vec!["PLAIN"] + } +} + +use crate::api_capnp; + +impl api_capnp::authentication::Server for Authentication { + fn available_mechanisms(&mut self, + _params: api_capnp::authentication::AvailableMechanismsParams, + mut results: api_capnp::authentication::AvailableMechanismsResults) + -> ::capnp::capability::Promise<(), ::capnp::Error> + { + let m = self.mechs(); + let mut b = results.get() + .init_mechanisms(m.len() as u32); + for (i, mech) in m.iter().enumerate() { + let mut bldr = b.reborrow(); + bldr.set(i as u32, mech); + } + + ::capnp::capability::Promise::ok(()) + } + + fn initialize_authentication(&mut self, + params: api_capnp::authentication::InitializeAuthenticationParams, + mut results: api_capnp::authentication::InitializeAuthenticationResults) + -> ::capnp::capability::Promise<(), ::capnp::Error> + { + let params = pry!(params.get()); + let mechanism = pry!(params.get_mechanism()); + match mechanism { + "PLAIN" => { + use api_capnp::maybe::Which; + + let data = pry!(params.get_initial_data()); + if let Ok(Which::Some(data)) = data.which() { + let data = pry!(data); + if let Ok((b, name)) = self.plain.step(data) { + + // If login was successful, also set the current authzid + if b { + self.state = Some(name.to_string()); + } + + let outcome = Outcome::value(b); + results + .get() + .init_response() + .set_right(api_capnp::authentication::outcome::ToClient::new(outcome) + .into_client::<::capnp_rpc::Server>()).unwrap(); + } + ::capnp::capability::Promise::ok(()) + } else { + return + ::capnp::capability::Promise::err(::capnp::Error::unimplemented( + "SASL PLAIN requires initial data set".to_string())); + } + }, + m => { + return + ::capnp::capability::Promise::err(::capnp::Error::unimplemented( + format!("SASL Mechanism {} is not implemented", m))); + } + } + } + + fn get_authzid(&mut self, + _params: api_capnp::authentication::GetAuthzidParams, + mut results: api_capnp::authentication::GetAuthzidResults) + -> ::capnp::capability::Promise<(), ::capnp::Error> + { + if let Some(zid) = &self.state { + results.get().set_authzid(zid); + } else { + results.get().set_authzid(""); + } + ::capnp::capability::Promise::ok(()) + } +} + +struct Outcome { + data: Option>, + value: bool, +} +impl Outcome { + pub fn value(value: bool) -> Self { + Self { data: None, value: value } + } +} + +impl api_capnp::authentication::outcome::Server for Outcome { + fn value(&mut self, + _params: api_capnp::authentication::outcome::ValueParams, + mut results: api_capnp::authentication::outcome::ValueResults) + -> ::capnp::capability::Promise<(), ::capnp::Error> + { + results.get().set_granted(self.value); + ::capnp::capability::Promise::ok(()) + } +} diff --git a/src/config.rs b/src/config.rs index e9d79e9..b7b81c8 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,24 +1,28 @@ use std::str::FromStr; use std::path::PathBuf; -use serde_derive::Deserialize; +use serde::{Serialize, Deserialize}; use crate::error::Result; pub fn read() -> Result { Ok(Config { + machinedb: PathBuf::from_str("/tmp/machines.db").unwrap(), access: Access { model: PathBuf::from_str("/tmp/model.conf").unwrap(), policy: PathBuf::from_str("/tmp/policy.csv").unwrap(), - } + }, + passdb: PathBuf::from_str("/tmp/passwd.db").unwrap(), }) } -#[derive(Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct Config { - pub(crate) access: Access + pub(crate) access: Access, + pub machinedb: PathBuf, + pub passdb: PathBuf, } -#[derive(Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct Access { pub(crate) model: PathBuf, pub(crate) policy: PathBuf diff --git a/src/error.rs b/src/error.rs index e5c9633..25fd6ea 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,8 +1,38 @@ use std::io; +use toml; + +use crate::auth::SASLError; #[derive(Debug)] pub enum Error { - IO(io::Error) + TomlDe(toml::de::Error), + TomlSer(toml::ser::Error), + SASL(SASLError), + IO(io::Error), +} + +impl From for Error { + fn from(e: SASLError) -> Error { + Error::SASL(e) + } +} + +impl From for Error { + fn from(e: io::Error) -> Error { + Error::IO(e) + } +} + +impl From for Error { + fn from(e: toml::de::Error) -> Error { + Error::TomlDe(e) + } +} + +impl From for Error { + fn from(e: toml::ser::Error) -> Error { + Error::TomlSer(e) + } } pub type Result = std::result::Result; diff --git a/src/machine.rs b/src/machine.rs new file mode 100644 index 0000000..e1857ee --- /dev/null +++ b/src/machine.rs @@ -0,0 +1,62 @@ +use std::collections::HashMap; +use std::fs::File; +use std::path::Path; +use std::io::{Read, Write}; + +use serde::{Serialize, Deserialize}; +use toml; + +use futures_signals::signal::{ReadOnlyMutable}; +use casbin::Enforcer; + +use crate::error::Result; +use crate::config::Config; + +/// Status of a Machine +#[derive(PartialEq, Eq, Debug, Serialize, Deserialize)] +pub enum Status { + /// Not currently used by anybody + Free, + /// Used by somebody + Occupied, + /// Not used by anybody but also can not be used. E.g. down for maintenance + Blocked, +} + +#[derive(PartialEq, Eq, Debug, Serialize, Deserialize)] +pub struct Machine { + pub location: String, + pub status: Status, +} + +impl Machine { + pub fn new(location: String) -> Machine { + Machine { + location: location, + status: Status::Free, + } + } +} + +pub type MachineDB = HashMap; + +type Name = String; + +pub fn init(config: &Config) -> Result { + if config.machinedb.is_file() { + let mut fp = File::open(&config.machinedb)?; + let mut content = String::new(); + fp.read_to_string(&mut content)?; + let map: HashMap = toml::from_str(&content)?; + return Ok(map); + } else { + return Ok(HashMap::new()); + } +} + +pub fn save(config: &Config, mdb: &MachineDB) -> Result<()> { + let mut fp = File::create(&config.machinedb)?; + let toml = toml::to_string(mdb)?; + fp.write_all(&toml.as_bytes())?; + Ok(()) +} diff --git a/src/main.rs b/src/main.rs index 6d801c8..00c1a3c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,12 +1,19 @@ #[macro_use] extern crate slog; +#[macro_use] +extern crate capnp_rpc; + +mod auth; mod access; mod modules; mod log; mod api; mod config; mod error; +mod machine; + +use std::ops::Deref; use api::api_capnp; @@ -23,6 +30,12 @@ fn main() { modules::init(log.new(o!())); api::init(); + let m = machine::init(&config).unwrap(); + let m = Mutable::new(m); + let m2 = m.clone(); + let c2 = config.clone(); + + let mut exec = futures::executor::LocalPool::new(); let enf = exec.run_until(async { @@ -30,6 +43,10 @@ fn main() { Mutable::new(e) }); + let p = auth::open_passdb(&config.passdb).unwrap(); + let p = Mutable::new(p); + let a = auth::Authentication::new(p, enf.clone()); + use std::net::ToSocketAddrs; @@ -48,11 +65,14 @@ fn main() { let mut incoming = listener.incoming(); while let Some(socket) = incoming.next().await { let socket = socket?; - let rpc_system = api::process_socket(enf.clone(), socket); + let rpc_system = api::process_socket(enf.clone(), m.clone(), a.clone(), socket); + machine::save(&config, &m.lock_ref()).expect("MachineDB save"); spawner.spawn_local_obj( Box::pin(rpc_system.map_err(|e| println!("error: {:?}", e)).map(|_|())).into()).expect("spawn") } Ok(()) }); result.expect("main"); + + machine::save(&c2, &m2.lock_ref()).expect("MachineDB save"); }