From 31afcabd8354a910395bc1c7fabb6bbd3a793319 Mon Sep 17 00:00:00 2001 From: William Brown Date: Sat, 29 Sep 2018 17:54:16 +1000 Subject: [PATCH] initial --- .gitignore | 3 + CODE_OF_CONDUCT.md | 0 Cargo.toml | 23 ++++ LICENSE.md | 0 README.md | 54 ++++++++ migrations/2018-09-25-052632_initial/down.sql | 3 + migrations/2018-09-25-052632_initial/up.sql | 6 + src/be/filter.rs | 5 + src/be/mem_be/mod.rs | 0 src/be/mod.rs | 76 +++++++++++ src/be/sqlite_be/mod.rs | 10 ++ src/be/sqlite_be/models.rs | 38 ++++++ src/be/sqlite_be/schema.rs | 8 ++ src/entry.rs | 117 ++++++++++++++++ src/event.rs | 21 +++ src/log.rs | 56 ++++++++ src/main.rs | 126 ++++++++++++++++++ src/server.rs | 90 +++++++++++++ tests/integration_test.rs | 29 ++++ 19 files changed, 665 insertions(+) create mode 100644 .gitignore create mode 100644 CODE_OF_CONDUCT.md create mode 100644 Cargo.toml create mode 100644 LICENSE.md create mode 100644 README.md create mode 100644 migrations/2018-09-25-052632_initial/down.sql create mode 100644 migrations/2018-09-25-052632_initial/up.sql create mode 100644 src/be/filter.rs create mode 100644 src/be/mem_be/mod.rs create mode 100644 src/be/mod.rs create mode 100644 src/be/sqlite_be/mod.rs create mode 100644 src/be/sqlite_be/models.rs create mode 100644 src/be/sqlite_be/schema.rs create mode 100644 src/entry.rs create mode 100644 src/event.rs create mode 100644 src/log.rs create mode 100644 src/main.rs create mode 100644 src/server.rs create mode 100644 tests/integration_test.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..70e3cae73 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ + +/target +**/*.rs.bk diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..e69de29bb diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 000000000..d011f36a0 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "rsidm" +version = "0.1.0" +authors = ["William Brown "] + + +# We need three major binaries. The server itself, the unix client, and a cli +# mgmt tool. + +[dependencies] +actix = "0.7" +actix-web = "0.7" + +futures = "0.1" +uuid = { version = "0.5", features = ["serde", "v4"] } +serde = "1.0" +serde_json = "1.0" +serde_derive = "1.0" + +diesel = { version = "^1.1.0", features = ["sqlite", "r2d2"] } +r2d2 = "0.8" + + diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 000000000..e69de29bb diff --git a/README.md b/README.md new file mode 100644 index 000000000..b708eaca1 --- /dev/null +++ b/README.md @@ -0,0 +1,54 @@ +# Rs Identity Manager + +rsidm is an identity management platform written in rust. Our goals are: + +* Modern identity management platform +* Simple to deploy and integrate with +* extensible +* correct + +## Code of Conduct + +See CODE_OF_CONDUCT.md + +## Examples + +## MVP features + +* Pam/nsswitch clients (with offline auth, and local totp) +* CLI for admin +* OIDC/Oauth +* SSH key distribution +* MFA (TOTP) +* In memory read cache (cow) +* backup/restore + +## Planned features + +* Replicated database backend (389-ds, couchdb, or custom repl proto) +* SAML +* Read Only Replicas +* Certificate distribution? +* Web UI for admin +* Account impersonation +* Webauthn +* Sudo rule distribution via nsswitch? + +## Features we want to avoid + +* Audit: This is better solved by ... +* Fully synchronous behaviour: ... +* Generic database: ... (max db size etc) +* Being LDAP: ... + +## More? + +## Get involved + +## Designs + +See the designs folder + + + + diff --git a/migrations/2018-09-25-052632_initial/down.sql b/migrations/2018-09-25-052632_initial/down.sql new file mode 100644 index 000000000..f82a976b0 --- /dev/null +++ b/migrations/2018-09-25-052632_initial/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` +DROP TABLE users + diff --git a/migrations/2018-09-25-052632_initial/up.sql b/migrations/2018-09-25-052632_initial/up.sql new file mode 100644 index 000000000..90fd60012 --- /dev/null +++ b/migrations/2018-09-25-052632_initial/up.sql @@ -0,0 +1,6 @@ +-- Your SQL goes here +CREATE TABLE entries ( + id INTEGER NOT NULL PRIMARY KEY, + entry VARCHAR NOT NULL +) + diff --git a/src/be/filter.rs b/src/be/filter.rs new file mode 100644 index 000000000..a7913a4b3 --- /dev/null +++ b/src/be/filter.rs @@ -0,0 +1,5 @@ + +// This represents a filtering query. This can be done +// in parallel map/reduce style, or directly on a single +// entry to assert it matches. + diff --git a/src/be/mem_be/mod.rs b/src/be/mem_be/mod.rs new file mode 100644 index 000000000..e69de29bb diff --git a/src/be/mod.rs b/src/be/mod.rs new file mode 100644 index 000000000..f7166a3f4 --- /dev/null +++ b/src/be/mod.rs @@ -0,0 +1,76 @@ +//! Db executor actor +use actix::prelude::*; +use diesel; +use diesel::prelude::*; +use diesel::r2d2::{self, ConnectionManager, Pool}; +// use uuid; +use super::log::EventLog; + + +mod sqlite_be; +mod mem_be; +mod filter; + +// HACK HACK HACK remove duplicate code +// Helper for internal logging. +macro_rules! log_event { + ($log_addr:expr, $($arg:tt)*) => ({ + use std::fmt; + use log::LogEvent; + $log_addr.do_send( + LogEvent { + msg: fmt::format( + format_args!($($arg)*) + ) + } + ) + }) +} + +// This contacts the needed backend and starts it up + +pub enum BackendType { + Memory, // isn't memory just sqlite with file :memory: ? + SQLite, +} + +pub fn start(log: actix::Addr, _betype: BackendType, path: &str) -> actix::Addr { + // How can we allow different db names and types? + let manager = ConnectionManager::::new(path); + let pool = r2d2::Pool::builder() + .build(manager) + .expect("Failed to create pool"); + + SyncArbiter::start(8, move || { + BackendActor::new(log.clone(), pool.clone()) + }) +} + +pub struct BackendActor { + log: actix::Addr, + pool: Pool> +} + +impl Actor for BackendActor { + type Context = SyncContext; +} + +// In the future this will do the routing betwene the chosen backends etc. +impl BackendActor { + pub fn new(log: actix::Addr, pool: Pool>) -> Self { + log_event!(log, "Starting DB worker ..."); + BackendActor { + log: log, + pool: pool, + } + } +} + +#[cfg(test)] +mod tests { + #[test] + fn test_simple_create() { + println!("It works!"); + } +} + diff --git a/src/be/sqlite_be/mod.rs b/src/be/sqlite_be/mod.rs new file mode 100644 index 000000000..f591ec130 --- /dev/null +++ b/src/be/sqlite_be/mod.rs @@ -0,0 +1,10 @@ + +// We need tests too + +// need a way to add an index +// need a way to do filters +// need a way to manage idls + +mod models; +mod schema; + diff --git a/src/be/sqlite_be/models.rs b/src/be/sqlite_be/models.rs new file mode 100644 index 000000000..2eb93b45e --- /dev/null +++ b/src/be/sqlite_be/models.rs @@ -0,0 +1,38 @@ + +// Stores models of various types. Given the design of the DB, this +// is reasonably simple for our backend. + +// We have a main id -> entries type, and everything else is a value -> (set id) type or value -> id +// I'll probably make IDL serialisable to cbor or something ... + +use diesel; +use diesel::prelude::*; +use diesel::r2d2::{ConnectionManager, Pool}; + + +use super::schema::entries; + +#[derive(Serialize, Queryable)] +pub struct Entry { + pub id: i64, + pub entry: String, +} + +#[derive(Insertable)] +#[table_name = "entries"] +pub struct NewEntry<'a> { + pub id: i64, + pub entry: &'a str, +} + + + +#[cfg(test)] +mod tests { + #[test] + fn test_simple_create() { + println!("It works!"); + } +} + + diff --git a/src/be/sqlite_be/schema.rs b/src/be/sqlite_be/schema.rs new file mode 100644 index 000000000..007cd80ce --- /dev/null +++ b/src/be/sqlite_be/schema.rs @@ -0,0 +1,8 @@ + +table! { + entries (id) { + id -> BigInt, + entry -> Text, + } +} + diff --git a/src/entry.rs b/src/entry.rs new file mode 100644 index 000000000..3fa6b3895 --- /dev/null +++ b/src/entry.rs @@ -0,0 +1,117 @@ + +use serde_json::{Value, Error}; + + +// make a trait entry for everything to adhere to? +// * How to get indexs out? +// * How to track pending diffs? + +#[derive(Serialize, Deserialize, Debug)] +pub struct Entry { + +} + +// pub trait Entry { + //fn to_json_str(&self) -> String; + // fn to_index_diff -> ??? + // from_json_str() -> Self; + // + // Does this match a filter or not?a + // fn apply_filter -> Result +// } + +//enum Credential { + //? +//} + +#[derive(Serialize, Deserialize, Debug)] +enum Credential { + Password { + name: String, + hash: String, + }, + TOTPPassword { + name: String, + hash: String, + totp_secret: String, + }, + SshPublicKey { + name: String, + data: String, + }, +} + + + +#[derive(Serialize, Deserialize, Debug)] +struct User { + username: String, + // Could this be derived from self? Do we even need schema? + class: Vec, + displayname: String, + legalname: Option, + email: Vec, + // uuid? + // need to support deref later ... + memberof: Vec, + sshpublickey: Vec, + + credentials: Vec, + +} + +impl User { + pub fn new(username: &str, displayname: &str) -> Self { + // Build a blank value + User { + username: String::from(username), + class: Vec::new(), + displayname: String::from(displayname), + legalname: None, + email: Vec::new(), + memberof: Vec::new(), + sshpublickey: Vec::new(), + credentials: Vec::new(), + } + + } + + // We need a way to "diff" two User objects + // as on a modification we want to track the set of changes + // that is occuring -- needed for indexing to function. + + // Basically we just need to check if it changed, remove + // the "former" and add the "newer" value. + + // We have to sort vecs ... + + // Is there a way to call this on serialise? + fn validate() -> Result<(), ()> { + Err(()) + } +} + + +#[cfg(test)] +mod tests { + use super::User; + use serde_json; + + #[test] + fn test_user_basic() { + let u: User = User::new("william", "William Brown"); + + println!("u: {:?}", u); + + let d = serde_json::to_string(&u).unwrap(); + + println!("d: {}", d.as_str()); + + let u2: User = serde_json::from_str(d.as_str()).unwrap(); + + println!("u2: {:?}", u2); + + } +} + + diff --git a/src/event.rs b/src/event.rs new file mode 100644 index 000000000..cdde25cb0 --- /dev/null +++ b/src/event.rs @@ -0,0 +1,21 @@ +use actix::prelude::*; + +// This structure tracks and event lifecycle, and is eventually +// sent to the logging system where it's structured and written +// out to the current logging BE. +#[derive(Debug)] +pub struct Event { + time_start: (), + time_end: (), + // vec of start/end points of various parts of the event? + // We probably need some functions for this. Is there a way in rust + // to automatically annotate line numbers of code? + + // This could probably store the request parameters too? + // The parallel in 389 would be operation struct +} + +impl Message for Event { + type Result = (); +} + diff --git a/src/log.rs b/src/log.rs new file mode 100644 index 000000000..bcaf92c53 --- /dev/null +++ b/src/log.rs @@ -0,0 +1,56 @@ +use actix::prelude::*; + +use super::event::Event; + +// This is the core of the server. It implements all +// the search and modify actions, applies access controls +// and get's everything ready to push back to the fe code + +// We need to pass in config for this later + +pub fn start() -> actix::Addr { + SyncArbiter::start(1, move || { + EventLog{} + }) +} + + +pub struct EventLog { +} + +impl Actor for EventLog { + type Context = SyncContext; +} + +// What messages can we be sent. Basically this is all the possible +// inputs we *could* recieve. + + +// Add a macro for easy msg write + +pub struct LogEvent { + pub msg: String, +} + +impl Message for LogEvent { + type Result = (); +} + +impl Handler for EventLog { + type Result = (); + + fn handle(&mut self, event: LogEvent, _: &mut SyncContext) -> Self::Result { + println!("LOGEVENT: {}", event.msg ); + } +} + +impl Handler for EventLog { + type Result = (); + + fn handle(&mut self, event: Event, _: &mut SyncContext) -> Self::Result { + println!("EVENT: {:?}", event) + } +} + + + diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 000000000..5bfc3fc29 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,126 @@ +extern crate serde; +extern crate serde_json; +#[macro_use] +extern crate serde_derive; +#[macro_use] +extern crate diesel; +extern crate actix; +extern crate actix_web; +extern crate r2d2; +extern crate uuid; +extern crate futures; + +use actix::prelude::*; +use actix_web::{ + http, middleware, App, AsyncResponder, FutureResponse, HttpResponse, Path, HttpRequest, + State, +}; + +use diesel::prelude::*; +use diesel::r2d2::ConnectionManager; +use futures::Future; + +mod be; +mod entry; +mod server; +mod log; +mod event; + +// Helper for internal logging. +macro_rules! log_event { + ($log_addr:expr, $($arg:tt)*) => ({ + use log::LogEvent; + $log_addr.do_send( + LogEvent { + msg: std::fmt::format( + format_args!($($arg)*) + ) + } + ) + }) +} + +struct AppState { + qe: actix::Addr, +} + +// Handle the various end points we need to expose + +/// simple handle +fn index(req: &HttpRequest) -> HttpResponse { + println!("{:?}", req); + + HttpResponse::Ok().body("Hello\n") +} + +fn class_list( + (name, state): (Path, State), +) -> FutureResponse +{ + // println!("request to class_list"); + state + .qe + .send( + server::ListClass { + class_name: name.into_inner(), + } + ) + // What does this do? + .from_err() + .and_then(|res| match res { + // What type is entry? + Ok(entry) => Ok(HttpResponse::Ok().json(entry)), + // Can we properly report this? + Err(_) => Ok(HttpResponse::InternalServerError().into()), + }) + // What does this do? + .responder() +} + +fn main() { + let sys = actix::System::new("rsidm-server"); + + // read the config (if any?) + + // Until this point, we probably want to write to stderr + // Start up the logging system: for now it just maps to stderr + let log_addr = log::start(); + + // Starting the BE chooses the path. + let be_addr = be::start(log_addr.clone(), be::BackendType::SQLite, "test.db"); + + // Start the query server with the given be + let server_addr = server::start(log_addr.clone(), be_addr); + + // start the web server + actix_web::server::new(move || { + App::with_state(AppState { + qe: server_addr.clone(), + }) + // Connect all our end points here. + // .middleware(middleware::Logger::default()) + .resource("/", |r| r.f(index)) + .resource("/{class_list}", |r| r.method(http::Method::GET).with(class_list)) + .resource("/{class_list}/", |r| r.method(http::Method::GET).with(class_list)) + + }) + .bind("127.0.0.1:8080") + .unwrap() + .start(); + + log_event!(log_addr, "Starting rsidm on 127.0.0.1:8080"); + + // all the needed routes / views + + let _ = sys.run(); +} + +#[cfg(test)] +mod tests { + #[test] + fn test_simple_create() { + println!("It works!"); + } +} + + diff --git a/src/server.rs b/src/server.rs new file mode 100644 index 000000000..57ba13834 --- /dev/null +++ b/src/server.rs @@ -0,0 +1,90 @@ +use actix::prelude::*; + +use be::BackendActor; +use log::EventLog; +use entry::Entry; + +// HACK HACK HACK remove duplicate code +// Helper for internal logging. +macro_rules! log_event { + ($log_addr:expr, $($arg:tt)*) => ({ + use std::fmt; + use log::LogEvent; + $log_addr.do_send( + LogEvent { + msg: fmt::format( + format_args!($($arg)*) + ) + } + ) + }) +} + +pub fn start( + log: actix::Addr, + be: actix::Addr +) -> actix::Addr +{ + SyncArbiter::start(8, move || { + QueryServer::new(log.clone(), be.clone()) + }) +} + +// This is the core of the server. It implements all +// the search and modify actions, applies access controls +// and get's everything ready to push back to the fe code + + +pub struct QueryServer { + log: actix::Addr, + be: actix::Addr, +} + +impl QueryServer { + pub fn new (log: actix::Addr, be: actix::Addr) -> Self { + log_event!(log, "Starting query worker ..."); + QueryServer { + log: log, + be: be, + } + } + + // Actually conduct a search request + // This is the core of the server, as it processes the entire event + // applies all parts required in order and more. + pub fn search() -> Result, ()> { + Err(()) + } +} + +impl Actor for QueryServer { + type Context = SyncContext; +} + +// What messages can we be sent. Basically this is all the possible +// inputs we *could* recieve. + +// List All objects of type + +pub struct ListClass { + pub class_name: String, +} + +impl Message for ListClass { + type Result = Result, ()>; +} + +impl Handler for QueryServer { + type Result = Result, ()>; + + fn handle(&mut self, msg: ListClass, _: &mut Self::Context) -> Self::Result { + log_event!(self.log, "Class list for: {}", msg.class_name.as_str()); + Err(()) + } +} + +// Get objects by filter + +// Auth requests? How do we structure these ... + + diff --git a/tests/integration_test.rs b/tests/integration_test.rs new file mode 100644 index 000000000..f6285acc7 --- /dev/null +++ b/tests/integration_test.rs @@ -0,0 +1,29 @@ +use std::panic; + +// Test external behaviorus of the service. + +fn run_test(test: T) -> () + where T: FnOnce() -> () + panic::UnwindSafe +{ + // setup + // Create the db: randomise the name of the file. Memory? + // call out to migrations + // Do we need any fixtures? + + let result = panic::catch_unwind(|| + test() + ); + + // teardown + // remove the db file + + assert!(result.is_ok()); +} + +#[test] +fn test_schema() { + run_test(|| { + println!("It works"); + }); +} +