An initial working state

PoC, Authentication works but not much more
This commit is contained in:
Gregor Reitzenstein 2020-02-16 16:02:03 +01:00
parent d5f92e41d1
commit 46c06305b0
9 changed files with 479 additions and 17 deletions

View File

@ -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"

View File

@ -1,10 +1,77 @@
@0xfd92ce9be2369b8e;
interface BffhAdmin {
getAllSubjects @0 () -> (subjects :List(Subject));
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 );
}
}

View File

@ -5,6 +5,7 @@ use casbin::prelude::*;
use super::config::Config;
/// This line documents init
pub async fn init(config: &Config) -> Result<Enforcer, Box<dyn std::error::Error>> {
let model = Model::from_file(config.access.model.clone()).await?;
let adapter = Box::new(FileAdapter::new(config.access.policy.clone()));

View File

@ -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<Enforcer>, socket: TcpStream) -> Result<(), capnp::Error> {
let api = Api { e: enforcer };
pub async fn process_socket(e: Mutable<Enforcer>, m: Mutable<MachineDB>, 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<Enforcer>, socket: TcpStream) -> R
struct Api {
e: Mutable<Enforcer>,
m: Mutable<MachineDB>,
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(())
}
}

219
src/auth.rs Normal file
View File

@ -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<String, String>;
pub fn open_passdb(path: &Path) -> Option<PassDB> {
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<PassDB>,
enforcer: Mutable<Enforcer>,
}
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<String>,
plain: Plain,
}
impl Authentication {
pub fn new(passdb: Mutable<PassDB>, enforcer: Mutable<Enforcer>) -> 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<Box<[u8]>>,
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(())
}
}

View File

@ -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<Config> {
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

View File

@ -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<SASLError> for Error {
fn from(e: SASLError) -> Error {
Error::SASL(e)
}
}
impl From<io::Error> for Error {
fn from(e: io::Error) -> Error {
Error::IO(e)
}
}
impl From<toml::de::Error> for Error {
fn from(e: toml::de::Error) -> Error {
Error::TomlDe(e)
}
}
impl From<toml::ser::Error> for Error {
fn from(e: toml::ser::Error) -> Error {
Error::TomlSer(e)
}
}
pub type Result<T> = std::result::Result<T, Error>;

62
src/machine.rs Normal file
View File

@ -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<Name, Machine>;
type Name = String;
pub fn init(config: &Config) -> Result<MachineDB> {
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<Name, Machine> = 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(())
}

View File

@ -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");
}