mirror of
https://gitlab.com/fabinfra/fabaccess/bffh.git
synced 2024-11-21 14:37:56 +01:00
An initial working state
PoC, Authentication works but not much more
This commit is contained in:
parent
d5f92e41d1
commit
46c06305b0
@ -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"
|
||||
|
||||
|
@ -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 );
|
||||
}
|
||||
}
|
||||
|
@ -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()));
|
||||
|
64
src/api.rs
64
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<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
219
src/auth.rs
Normal 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(())
|
||||
}
|
||||
}
|
@ -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
|
||||
|
32
src/error.rs
32
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<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
62
src/machine.rs
Normal 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(())
|
||||
}
|
22
src/main.rs
22
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");
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user