diff --git a/Cargo.toml b/Cargo.toml index 7616ebf56..00113e601 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,18 +24,20 @@ name = "rsidm_whoami" path = "src/clients/whoami.rs" - [dependencies] actix = "0.7" actix-web = "0.7" bytes = "0.4" -env_logger = "0.5" +log = "0.4" +env_logger = "0.6" reqwest = "0.9" +# reqwest = { path = "../reqwest" } chrono = "0.4" cookie = "0.11" regex = "1" lazy_static = "1.2.0" +lru = "0.1" tokio = "0.1" futures = "0.1" diff --git a/Dockerfile b/Dockerfile index a0849b316..cd8031dbb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,6 @@ FROM opensuse/tumbleweed:latest MAINTAINER william@blackhats.net.au -# /usr/bin/docker run --restart always --name lifx registry.blackhats.net.au/lifx -RUN echo HTTP_PROXY="http://proxy-bne1.net.blackhats.net.au:3128" > /etc/sysconfig/proxy - COPY . /home/rsidm/ WORKDIR /home/rsidm/ diff --git a/designs/auth.rst b/designs/auth.rst index 92a3a582c..7a6c2eaf1 100644 --- a/designs/auth.rst +++ b/designs/auth.rst @@ -34,6 +34,8 @@ the session, which may request further credentials. They are then redirected to site with an appropriate (oauth) token describing the requested rights. https://developers.google.com/identity/sign-in/web/incremental-auth +https://openid.net/specs/openid-connect-core-1_0.html#UserInfo +https://tools.ietf.org/html/rfc7519 Login to workstation (connected) ================================ @@ -168,6 +170,8 @@ that have unique cookie keys to prevent forgery of writable master cookies) of group uuids + names derferenced so that a client can make all authorisation decisions from a single datapoint +* Groups require the ability to be ephemeral/temporary or permament. + * each token can be unique based on the type of auth (ie 2fa needed to get access to admin groups) @@ -185,14 +189,57 @@ Cookie/Token Auth Detail Clients begin with no cookie, and no session. The client sends an AuthRequest to the server in the Init state. Any other request -results in AuthDenied due to lack of cookie. +results in AuthDenied due to lack of cookie. This should contain the optional +application id. + +struct AuthClientRequest { + name: String + application: Option +} The server issues a cookie, and allocates a session id to the cookie. The session id is also stored in the server with a timeout. The AuthResponse indicates the current possible -auth types that can proceed. +auth types that can proceed. This should provided challenges or nonces if required by the auth type. + +enum AuthAllowed { + Anonymous, + Password, + Webauthn { + challenge: // see the webauthn implementation for this + }, + TOTP, +} + +enum AuthState { + Response { + next: AuthAllowedMech + }, + AuthDenied, + AuthSuccess, +} + +struct AuthServerResponse { + state AuthState +} The client now sends the cookie and an AuthRequest with type Step, that contains the type -of authentication credential being provided. +of authentication credential being provided, and any other details. This COULD contain multiple +credentials, or a single one. + +enum AuthCredential { + Anonymous, + Password { String }, + Webauthn { + // see the webauthn impl for all the bits this will contain ... + }, + TOTP { + String + } +} + +struct AuthClientStep { + Vec +} The server verifies the credential, and marks that type of credential as failed or fufilled. On failure of a credential, AuthDenied is immediately sent. On success of a credential @@ -211,6 +258,9 @@ the state machine part way through. THe server enforces the client must always a initial authRequest, which cause the client to always be denied. * If the AuthRequest is started but not completed, we time it out within a set number of minutes by walking the set of sessions and purging incomplete ones which have passed the time stamp. +* The session id is in the cookie to eliminate leaking of the session id (secure cookies), and +to prevent tampering of the session id if possible. It's not perfect, but it helps to prevent +casual attkcs. The session id itself is really the thing that protects us from replays. Auth Questions -------------- @@ -325,5 +375,91 @@ required tagging or other details. How do we ensure integrity of the token? Do we have to? Is the clients job to trust the token given the TLS tunnel? +More Brain Dumping +================== + +- need a way to just pw check even if mfa is on (for sudo). Perhaps have a seperate sudo password attr? +- ntpassword attr is seperate +- a way to check application pw which attaches certain rights (is this just a generalisation of sudo?) + - the provided token (bearer etc?) contains the "memberof" for the session. + - How to determine what memberof an api provides? Could be policy object that says "api pw of name X + is allowed Y, Z group". Could be that the user is presented with a list or subset of the related? + Could be both? + - Means we need a "name" and "type" for the api password, also need to be able to search + on both of those details potentially. + +- The oauth system is just a case of follow that and provide the scope/groups as required. + +- That would make userPassword and webauthn only for webui and api direct access. + - All other pw validations would use application pw case. + - SSH would just read ssh key - should this have a similar group filter/allow + mechanism like aplication pw? + +- Groups take a "type" + - credentials also have a "type" + - The credential if used can provide groups of "type" to that session during auth token + generation + - An auth request says it as an auth of type X, to associate what creds it might check. + + +- Means a change to auth to take an entry as part of auth, or at least, it's group list for the + session. + + +- policy to define if pw types like sudo or radius are linked. + - Some applications may need to read a credential type. + - attribute/value tagging required? + + +apptype: unix + +apptype: groupware + +group: admins + type: unix <<-- indicates it's a requested group + +group: emailusers + type: groupware <<-- indicates it's a requested group + +user: admin +memberof: admins <<-- Should this be in mo if they are reqgroups? I think yes, because it's only for that "session" + based on the cred do they get the "group list" in cred. +memberof: emailusers +cred: { + 'type': unix, + 'hash': ... + 'grants': 'admins' +} +cred: { + 'type': groupware + 'hash': ..., + 'grants': 'emailusers', +} +cred: { + 'type': blah + 'hash': ..., + 'grants': 'bar', // Can't work because not a memberof bar. Should this only grant valid MO's? +} + +ntpassword: ... <<-- needs limited read, and doesn't allocate groups. +sshPublicKey: ... <<-- different due to needing anon read. + + + + +Some Dirty Rust Brain Dumps +=========================== + +- Credentials need per-cred locking + - This means they have to be in memory and uniquely ided. + - How can we display to a user that a credential back-off is inplace? + +- UAT need to know what Credential was used and it's state. + - The Credential associates the claims + + + + + diff --git a/src/clients/whoami.rs b/src/clients/whoami.rs index 4ebd660ea..97949b50a 100644 --- a/src/clients/whoami.rs +++ b/src/clients/whoami.rs @@ -1,6 +1,7 @@ +extern crate reqwest; extern crate rsidm; -// use rsidm::proto_v1; +use rsidm::proto::v1::{WhoamiRequest, WhoamiResponse}; fn main() { println!("Hello whoami"); @@ -8,4 +9,20 @@ fn main() { // Given the current ~/.rsidm/cookie (or none) // we should check who we are plus show the auth token that the server // would generate for us. + + let whoami_req = WhoamiRequest {}; + + // FIXME TODO: Make this url configurable!!! + let client = reqwest::Client::new(); + + let mut response = client + .get("http://127.0.0.1:8080/v1/whoami") + .send() + .unwrap(); + + println!("{:?}", response); + + // Parse it if desire. + // let r: Response = serde_json::from_str(response.text().unwrap().as_str()).unwrap(); + // println!("{:?}", r); } diff --git a/src/lib/access.rs b/src/lib/access.rs index 9794e62d0..33fe11dec 100644 --- a/src/lib/access.rs +++ b/src/lib/access.rs @@ -23,7 +23,7 @@ use crate::entry::{Entry, EntryCommitted, EntryNew, EntryNormalised, EntryValid} use crate::error::OperationError; use crate::filter::{Filter, FilterValid}; use crate::modify::Modify; -use crate::proto_v1::Filter as ProtoFilter; +use crate::proto::v1::Filter as ProtoFilter; use crate::server::{QueryServerTransaction, QueryServerWriteTransaction}; use crate::event::{CreateEvent, DeleteEvent, EventOrigin, ModifyEvent, SearchEvent}; @@ -47,14 +47,16 @@ impl AccessControlSearch { ) -> Result { if !value.attribute_value_pres("class", "access_control_search") { audit_log!(audit, "class access_control_search not present."); - return Err(OperationError::InvalidACPState); + return Err(OperationError::InvalidACPState( + "Missing access_control_search", + )); } let attrs = try_audit!( audit, value .get_ava("acp_search_attr") - .ok_or(OperationError::InvalidACPState) + .ok_or(OperationError::InvalidACPState("Missing acp_search_attr")) .map(|vs: &Vec| vs.clone()) ); @@ -99,7 +101,9 @@ impl AccessControlDelete { ) -> Result { if !value.attribute_value_pres("class", "access_control_delete") { audit_log!(audit, "class access_control_delete not present."); - return Err(OperationError::InvalidACPState); + return Err(OperationError::InvalidACPState( + "Missing access_control_delete", + )); } Ok(AccessControlDelete { @@ -142,7 +146,9 @@ impl AccessControlCreate { ) -> Result { if !value.attribute_value_pres("class", "access_control_create") { audit_log!(audit, "class access_control_create not present."); - return Err(OperationError::InvalidACPState); + return Err(OperationError::InvalidACPState( + "Missing access_control_create", + )); } let attrs = value @@ -203,7 +209,9 @@ impl AccessControlModify { ) -> Result { if !value.attribute_value_pres("class", "access_control_modify") { audit_log!(audit, "class access_control_modify not present."); - return Err(OperationError::InvalidACPState); + return Err(OperationError::InvalidACPState( + "Missing access_control_modify", + )); } let presattrs = value @@ -273,7 +281,9 @@ impl AccessControlProfile { // Assert we have class access_control_profile if !value.attribute_value_pres("class", "access_control_profile") { audit_log!(audit, "class access_control_profile not present."); - return Err(OperationError::InvalidACPState); + return Err(OperationError::InvalidACPState( + "Missing access_control_profile", + )); } // copy name @@ -281,7 +291,7 @@ impl AccessControlProfile { audit, value .get_ava_single("name") - .ok_or(OperationError::InvalidACPState) + .ok_or(OperationError::InvalidACPState("Missing name")) ); // copy uuid let uuid = value.get_uuid(); @@ -290,21 +300,21 @@ impl AccessControlProfile { audit, value .get_ava_single("acp_receiver") - .ok_or(OperationError::InvalidACPState) + .ok_or(OperationError::InvalidACPState("Missing acp_receiver")) ); // targetscope, and turn to real filter let targetscope_raw = try_audit!( audit, value .get_ava_single("acp_targetscope") - .ok_or(OperationError::InvalidACPState) + .ok_or(OperationError::InvalidACPState("Missing acp_targetscope")) ); audit_log!(audit, "RAW receiver {:?}", receiver_raw); let receiver_f: ProtoFilter = try_audit!( audit, serde_json::from_str(receiver_raw.as_str()) - .map_err(|_| OperationError::InvalidACPState) + .map_err(|_| OperationError::InvalidACPState("Invalid acp_receiver")) ); let receiver_i = try_audit!(audit, Filter::from_rw(audit, &receiver_f, qs)); let receiver = try_audit!( @@ -319,7 +329,7 @@ impl AccessControlProfile { audit, serde_json::from_str(targetscope_raw.as_str()).map_err(|e| { audit_log!(audit, "JSON error {:?}", e); - OperationError::InvalidACPState + OperationError::InvalidACPState("Invalid acp_targetscope") }) ); let targetscope_i = try_audit!(audit, Filter::from_rw(audit, &targetscope_f, qs)); @@ -382,6 +392,7 @@ pub trait AccessControlsTransaction { // If this is an internal search, return our working set. let rec_entry: &Entry = match &se.event.origin { EventOrigin::Internal => { + audit_log!(audit, "Internal operation, bypassing access check"); // No need to check ACS return Ok(entries); } diff --git a/src/lib/audit.rs b/src/lib/audit.rs index 3aa010963..c957fdcec 100644 --- a/src/lib/audit.rs +++ b/src/lib/audit.rs @@ -12,8 +12,11 @@ macro_rules! audit_log { ($audit:expr, $($arg:tt)*) => ({ use std::fmt; if cfg!(test) || cfg!(debug_assertions) { - print!("DEBUG AUDIT ({}:{} {})-> ", file!(), line!(), $audit.id()); - println!($($arg)*) + // debug!("DEBUG AUDIT ({}:{} {})-> ", file!(), line!(), $audit.id()); + // debug!($($arg)*) + // debug!("DEBUG AUDIT ({}:{} {})-> ", file!(), line!(), $audit.id()); + // debug!("line: {}", line!()); + debug!($($arg)*) } $audit.log_event( fmt::format( diff --git a/src/lib/be/mod.rs b/src/lib/be/mod.rs index a59bfb080..2ea9c836e 100644 --- a/src/lib/be/mod.rs +++ b/src/lib/be/mod.rs @@ -168,7 +168,7 @@ impl Drop for BackendReadTransaction { // TODO: Is this correct for RO txn? fn drop(self: &mut Self) { if !self.committed { - println!("Aborting txn"); + debug!("Aborting BE RO txn"); self.conn .execute("ROLLBACK TRANSACTION", NO_PARAMS) // TODO: Can we do this without expect? I think we can't due @@ -183,7 +183,7 @@ impl Drop for BackendReadTransaction { impl BackendReadTransaction { pub fn new(conn: r2d2::PooledConnection) -> Self { // Start the transaction - println!("Starting RO txn ..."); + debug!("Starting BE RO txn ..."); // TODO: Way to flag that this will be a read only? // TODO: Can we do this without expect? I think we need to change the type // signature here if we wanted to... @@ -209,7 +209,7 @@ impl Drop for BackendWriteTransaction { // Abort fn drop(self: &mut Self) { if !self.committed { - println!("Aborting txn"); + debug!("Aborting BE WR txn"); self.conn .execute("ROLLBACK TRANSACTION", NO_PARAMS) // TODO: Can we do this without expect? I think we can't due @@ -230,7 +230,7 @@ impl BackendTransaction for BackendWriteTransaction { impl BackendWriteTransaction { pub fn new(conn: r2d2::PooledConnection) -> Self { // Start the transaction - println!("Starting WR txn ..."); + debug!("Starting BE WR txn ..."); // TODO: Way to flag that this will be a write? // TODO: Can we do this without expect? I think we need to change the type // signature here if we wanted to... @@ -579,7 +579,7 @@ impl BackendWriteTransaction { } pub fn commit(mut self) -> Result<(), OperationError> { - println!("Commiting txn"); + debug!("Commiting BE txn"); assert!(!self.committed); self.committed = true; self.conn diff --git a/src/lib/config.rs b/src/lib/config.rs index ad859c28b..1c68ff364 100644 --- a/src/lib/config.rs +++ b/src/lib/config.rs @@ -1,6 +1,7 @@ #[derive(Serialize, Deserialize, Debug)] pub struct Configuration { pub address: String, + pub domain: String, pub threads: usize, pub db_path: String, pub maximum_request: usize, @@ -12,12 +13,14 @@ impl Configuration { pub fn new() -> Self { Configuration { address: String::from("127.0.0.1:8080"), + domain: String::from("127.0.0.1"), threads: 8, db_path: String::from(""), maximum_request: 262144, // 256k // log type // log path - secure_cookies: true, + // TODO: default true in prd + secure_cookies: false, } } } diff --git a/src/lib/constants.rs b/src/lib/constants.rs index 56126cb7c..a5ef36be2 100644 --- a/src/lib/constants.rs +++ b/src/lib/constants.rs @@ -1,3 +1,8 @@ +// On test builds, define to 60 seconds +#[cfg(test)] +pub static PURGE_TIMEOUT: u64 = 60; +// For production, 1 hour. +#[cfg(not(test))] pub static PURGE_TIMEOUT: u64 = 3600; pub static UUID_ADMIN: &'static str = "00000000-0000-0000-0000-000000000000"; @@ -32,6 +37,21 @@ pub static JSON_IDM_ADMINS_V1: &'static str = r#"{ } }"#; +pub static _UUID_SYSTEM_INFO: &'static str = "00000000-0000-0000-0000-ffffff000001"; +pub static JSON_SYSTEM_INFO_V1: &'static str = r#"{ + "valid": { + "uuid": "00000000-0000-0000-0000-ffffff000001" + }, + "state": null, + "attrs": { + "class": ["object", "system_info"], + "uuid": ["00000000-0000-0000-0000-ffffff000001"], + "description": ["System info and metadata object."], + "version": ["1"], + "domain": ["example.com"] + } +}"#; + pub static _UUID_IDM_ADMINS_ACP_SEARCH_V1: &'static str = "00000000-0000-0000-0000-ffffff000002"; pub static JSON_IDM_ADMINS_ACP_SEARCH_V1: &'static str = r#"{ "valid": { @@ -79,7 +99,30 @@ pub static JSON_IDM_ADMINS_ACP_REVIVE_V1: &'static str = r#"{ } }"#; -pub static _UUID_ANONYMOUS: &'static str = "00000000-0000-0000-0000-ffffffffffff"; +pub static _UUID_IDM_SELF_ACP_READ_V1: &'static str = "00000000-0000-0000-0000-ffffff000004"; +pub static JSON_IDM_SELF_ACP_READ_V1: &'static str = r#"{ + "valid": { + "uuid": "00000000-0000-0000-0000-ffffff000004" + }, + "state": null, + "attrs": { + "class": ["object", "access_control_profile", "access_control_search"], + "name": ["idm_self_acp_read"], + "uuid": ["00000000-0000-0000-0000-ffffff000004"], + "description": ["Builtin IDM Control for self read - required for whoami."], + "version": ["1"], + "acp_enable": ["true"], + "acp_receiver": [ + "\"Self\"" + ], + "acp_targetscope": [ + "\"Self\"" + ], + "acp_search_attr": ["name", "uuid"] + } +}"#; + +pub static UUID_ANONYMOUS: &'static str = "00000000-0000-0000-0000-ffffffffffff"; pub static JSON_ANONYMOUS_V1: &'static str = r#"{ "valid": { "uuid": "00000000-0000-0000-0000-ffffffffffff" @@ -95,21 +138,6 @@ pub static JSON_ANONYMOUS_V1: &'static str = r#"{ } }"#; -pub static _UUID_SYSTEM_INFO: &'static str = "00000000-0000-0000-0000-ffffff000001"; -pub static JSON_SYSTEM_INFO_V1: &'static str = r#"{ - "valid": { - "uuid": "00000000-0000-0000-0000-ffffff000001" - }, - "state": null, - "attrs": { - "class": ["object", "system_info"], - "uuid": ["00000000-0000-0000-0000-ffffff000001"], - "description": ["System info and metadata object."], - "version": ["1"], - "domain": ["example.com"] - } -}"#; - // Core pub static UUID_SCHEMA_ATTR_CLASS: &'static str = "aa0f193f-3010-4783-9c9e-f97edb14d8c2"; pub static UUID_SCHEMA_ATTR_UUID: &'static str = "642a893b-fe1a-4fe1-805d-fb78e7f83ee7"; diff --git a/src/lib/core.rs b/src/lib/core.rs index 79103b240..ccbe8641e 100644 --- a/src/lib/core.rs +++ b/src/lib/core.rs @@ -11,17 +11,34 @@ use futures::{future, Future, Stream}; use crate::config::Configuration; // SearchResult +use crate::async_log; +use crate::error::OperationError; use crate::interval::IntervalActor; -use crate::log; -use crate::proto_v1::{AuthRequest, CreateRequest, DeleteRequest, ModifyRequest, SearchRequest}; -use crate::proto_v1_actors::QueryServerV1; +use crate::proto::v1::actors::QueryServerV1; +use crate::proto::v1::messages::{AuthMessage, WhoamiMessage}; +use crate::proto::v1::{ + AuthRequest, AuthResponse, AuthState, CreateRequest, DeleteRequest, ModifyRequest, + SearchRequest, UserAuthToken, WhoamiRequest, WhoamiResponse, +}; + +use uuid::Uuid; struct AppState { qe: actix::Addr, max_size: usize, } -macro_rules! json_event_decode { +fn get_current_user(req: &HttpRequest) -> Option { + match req.session().get::("uat") { + Ok(maybe_uat) => maybe_uat, + Err(_) => { + // return Box::new(future::err(e)); + None + } + } +} + +macro_rules! json_event_post { ($req:expr, $state:expr, $event_type:ty, $message_type:ty) => {{ // This is copied every request. Is there a better way? // The issue is the fold move takes ownership of state if @@ -52,7 +69,7 @@ macro_rules! json_event_decode { // let r_obj = serde_json::from_slice::(&body); let r_obj = serde_json::from_slice::<$message_type>(&body); - // Send to the db for create + // Send to the db for handling match r_obj { Ok(obj) => { let res = $state @@ -81,33 +98,63 @@ macro_rules! json_event_decode { }}; } +macro_rules! json_event_get { + ($req:expr, $state:expr, $event_type:ty, $message_type:ty) => {{ + // Get current auth data - remember, the QS checks if the + // none/some is okay, because it's too hard to make it work here + // with all the async parts. + let uat = get_current_user(&$req); + + // New event, feed current auth data from the token to it. + let obj = <($message_type)>::new(uat); + + let res = $state.qe.send(obj).from_err().and_then(|res| match res { + Ok(event_result) => Ok(HttpResponse::Ok().json(event_result)), + Err(e) => match e { + OperationError::NotAuthenticated => Ok(HttpResponse::Unauthorized().json(e)), + _ => Ok(HttpResponse::InternalServerError().json(e)), + }, + }); + + Box::new(res) + }}; +} + // Handle the various end points we need to expose fn create( (req, state): (HttpRequest, State), ) -> impl Future { - json_event_decode!(req, state, CreateEvent, CreateRequest) + json_event_post!(req, state, CreateEvent, CreateRequest) } fn modify( (req, state): (HttpRequest, State), ) -> impl Future { - json_event_decode!(req, state, ModifyEvent, ModifyRequest) + json_event_post!(req, state, ModifyEvent, ModifyRequest) } fn delete( (req, state): (HttpRequest, State), ) -> impl Future { - json_event_decode!(req, state, DeleteEvent, DeleteRequest) + json_event_post!(req, state, DeleteEvent, DeleteRequest) } fn search( (req, state): (HttpRequest, State), ) -> impl Future { - json_event_decode!(req, state, SearchEvent, SearchRequest) + json_event_post!(req, state, SearchEvent, SearchRequest) } -// delete, modify +fn whoami( + (req, state): (HttpRequest, State), +) -> impl Future { + // Actually this may not work as it assumes post not get. + json_event_get!(req, state, WhoamiEvent, WhoamiMessage) +} + +// We probably need an extract auth or similar to handle the different +// types (cookie, bearer), and to generic this over get/post. fn auth( (req, state): (HttpRequest, State), @@ -135,46 +182,66 @@ fn auth( // First, deal with some state management. // Do anything here first that's needed like getting the session details // out of the req cookie. - // let mut counter = 1; // From the actix source errors here // seems to be related to the serde_json deserialise of the cookie // content, and because we control it's get/set it SHOULD be fine // provided we use secure cookies. But we can't always trust that ... - let maybe_count = match req.session().get::("counter") { + let maybe_sessionid = match req.session().get::("auth-session-id") { Ok(c) => c, Err(e) => { return Box::new(future::err(e)); } }; - let c = match maybe_count { - Some(count) => { - println!("SESSION value: {}", count); - count + 1 - } - None => { - println!("INIT value: 1"); - 1 - } - }; - - match req.session().set("counter", c) { - Ok(_) => {} - Err(e) => { - return Box::new(future::err(e)); - } - }; + let auth_msg = AuthMessage::new(obj, maybe_sessionid); // We probably need to know if we allocate the cookie, that this is a // new session, and in that case, anything *except* authrequest init is // invalid. - - let res = state.qe.send(obj).from_err().and_then(|res| match res { - Ok(event_result) => Ok(HttpResponse::Ok().json(event_result)), - Err(e) => Ok(HttpResponse::InternalServerError().json(e)), - }); - + let res = + state + .qe + .send(auth_msg) + .from_err() + .and_then(move |res| match res { + Ok(ar) => { + match &ar.state { + AuthState::Success(uat) => { + // Remove the auth-session-id + req.session().remove("auth-session-id"); + // Set the uat into the cookie + match req.session().set("uat", uat) { + Ok(_) => Ok(HttpResponse::Ok().json(ar)), + Err(_) => { + Ok(HttpResponse::InternalServerError() + .json(())) + } + } + } + AuthState::Denied => { + // Remove the auth-session-id + req.session().remove("auth-session-id"); + Ok(HttpResponse::Ok().json(ar)) + } + AuthState::Continue(_) => { + // TODO: Where do we get the auth-session-id from? + // Ensure the auth-session-id is set + match req + .session() + .set("auth-session-id", ar.sessionid) + { + Ok(_) => Ok(HttpResponse::Ok().json(ar)), + Err(_) => { + Ok(HttpResponse::InternalServerError() + .json(())) + } + } + } + } + } + Err(e) => Ok(HttpResponse::InternalServerError().json(e)), + }); Box::new(res) } Err(e) => Box::new(future::err(error::ErrorBadRequest(format!( @@ -186,32 +253,12 @@ fn auth( ) } -fn whoami(req: &HttpRequest) -> Result<&'static str> { - println!("{:?}", req); - - // RequestSession trait is used for session access - let mut counter = 1; - if let Some(count) = req.session().get::("counter")? { - println!("SESSION value: {}", count); - counter = count + 1; - req.session().set("counter", counter)?; - } else { - req.session().set("counter", counter)?; - } - - Ok("welcome!") -} - pub fn create_server_core(config: Configuration) { - // Configure the middleware logger - ::std::env::set_var("RUST_LOG", "actix_web=info"); - env_logger::init(); + // Until this point, we probably want to write to the log macro fns. - // Until this point, we probably want to write to stderr - // Start up the logging system: for now it just maps to stderr - - // The log server is started on it's own thread - let log_addr = log::start(); + // The log server is started on it's own thread, and is contacted + // asynchronously. + let log_addr = async_log::start(); log_event!(log_addr, "Starting rsidm with configuration: {:?}", config); // Similar, create a stats thread which aggregates statistics from the @@ -236,6 +283,7 @@ pub fn create_server_core(config: Configuration) { // Copy the max size let max_size = config.maximum_request; let secure_cookies = config.secure_cookies; + let domain = config.domain.clone(); // start the web server actix_web::server::new(move || { @@ -246,22 +294,25 @@ pub fn create_server_core(config: Configuration) { // Connect all our end points here. .middleware(middleware::Logger::default()) .middleware(session::SessionStorage::new( - // Signed prevents tampering. this 32 byte key MUST + // TODO: Signed prevents tampering. this 32 byte key MUST // be generated (probably stored in DB for cross-host access) session::CookieSessionBackend::signed(&[0; 32]) - .path("/") + // Limit to path? + // .path("/") //.max_age() duration of the token life TODO make this proper! - .domain("localhost") - .same_site(cookie::SameSite::Strict) // constrain to the domain - // Disallow from js - .http_only(true) + // .domain(domain.as_str()) + // .same_site(cookie::SameSite::Strict) // constrain to the domain + // Disallow from js and ...? + .http_only(false) .name("rsidm-session") - // This forces https only + // This forces https only if true .secure(secure_cookies), )) // .resource("/", |r| r.f(index)) // curl --header ...? - .resource("/v1/whoami", |r| r.f(whoami)) + .resource("/v1/whoami", |r| { + r.method(http::Method::GET).with_async(whoami) + }) // .resource("/v1/login", ...) // .resource("/v1/logout", ...) // .resource("/v1/token", ...) generate a token for id servers to use diff --git a/src/lib/entry.rs b/src/lib/entry.rs index b61675f79..2483faa3b 100644 --- a/src/lib/entry.rs +++ b/src/lib/entry.rs @@ -3,7 +3,8 @@ use crate::audit::AuditScope; use crate::error::{OperationError, SchemaError}; use crate::filter::{Filter, FilterInvalid, FilterResolved, FilterValidResolved}; use crate::modify::{Modify, ModifyInvalid, ModifyList, ModifyValid}; -use crate::proto_v1::Entry as ProtoEntry; +use crate::proto::v1::Entry as ProtoEntry; +use crate::proto::v1::UserAuthToken; use crate::schema::{SchemaAttribute, SchemaClass, SchemaTransaction}; use crate::server::{QueryServerTransaction, QueryServerWriteTransaction}; @@ -702,7 +703,10 @@ impl Entry { ))) } - pub fn into(&self) -> ProtoEntry { + // FIXME: This should probably have an entry state for "reduced" + // and then only that state can provide the into_pe type, so that we + // can guarantee that all entries must have been security checked. + pub fn into_pe(&self) -> ProtoEntry { // It's very likely that at this stage we'll need to apply // access controls, dynamic attributes or more. // As a result, this may not even be the right place diff --git a/src/lib/error.rs b/src/lib/error.rs index 21f36792f..36b1a3032 100644 --- a/src/lib/error.rs +++ b/src/lib/error.rs @@ -28,12 +28,16 @@ pub enum OperationError { InvalidRequestState, InvalidState, InvalidEntryState, - InvalidACPState, + InvalidACPState(&'static str), + InvalidAccountState(&'static str), BackendEngine, SQLiteError, //(RusqliteError) FsError, SerdeJsonError, AccessDenied, + NotAuthenticated, + InvalidAuthState(&'static str), + InvalidSessionState, } #[derive(Serialize, Deserialize, Debug, PartialEq)] diff --git a/src/lib/event.rs b/src/lib/event.rs index 92aad6986..14ba5dbe9 100644 --- a/src/lib/event.rs +++ b/src/lib/event.rs @@ -1,10 +1,11 @@ use crate::audit::AuditScope; use crate::entry::{Entry, EntryCommitted, EntryInvalid, EntryNew, EntryValid}; use crate::filter::{Filter, FilterValid}; -use crate::proto_v1::Entry as ProtoEntry; -use crate::proto_v1::{ - AuthRequest, AuthResponse, AuthStatus, CreateRequest, DeleteRequest, ModifyRequest, - OperationResponse, ReviveRecycledRequest, SearchRequest, SearchResponse, +use crate::proto::v1::Entry as ProtoEntry; +use crate::proto::v1::{ + AuthAllowed, AuthCredential, AuthRequest, AuthResponse, AuthState, AuthStep, CreateRequest, + DeleteRequest, ModifyRequest, OperationResponse, ReviveRecycledRequest, SearchRequest, + SearchResponse, UserAuthToken, WhoamiRequest, WhoamiResponse, }; // use error::OperationError; use crate::error::OperationError; @@ -12,6 +13,8 @@ use crate::modify::{ModifyList, ModifyValid}; use crate::server::{ QueryServerReadTransaction, QueryServerTransaction, QueryServerWriteTransaction, }; + +use crate::proto::v1::messages::AuthMessage; // Bring in schematransaction trait for validate // use crate::schema::SchemaTransaction; @@ -21,9 +24,10 @@ use crate::filter::FilterInvalid; #[cfg(test)] use crate::modify::ModifyInvalid; #[cfg(test)] -use crate::proto_v1::SearchRecycledRequest; +use crate::proto::v1::SearchRecycledRequest; use actix::prelude::*; +use uuid::Uuid; // Should the event Result have the log items? // FIXME: Remove seralising here - each type should @@ -48,13 +52,13 @@ pub struct SearchResult { impl SearchResult { pub fn new(entries: Vec>) -> Self { SearchResult { - // FIXME: Can we consume this iter? entries: entries .iter() .map(|e| { - // FIXME: The issue here is this probably is applying transforms - // like access control ... May need to change. - e.into() + // All the needed transforms for this result are done + // in search_ext. This is just an entry -> protoentry + // step. + e.into_pe() }) .collect(), } @@ -106,6 +110,23 @@ impl Event { }) } + pub fn from_ro_uat( + audit: &mut AuditScope, + qs: &QueryServerReadTransaction, + uat: Option, + ) -> Result { + audit_log!(audit, "from_ro_uat -> {:?}", uat); + let uat = uat.ok_or(OperationError::NotAuthenticated)?; + + let e = try_audit!(audit, qs.internal_search_uuid(audit, uat.uuid.as_str())); + // FIXME: Now apply claims from the uat into the Entry + // to allow filtering. + + Ok(Event { + origin: EventOrigin::User(e), + }) + } + pub fn from_rw_request( audit: &mut AuditScope, qs: &QueryServerWriteTransaction, @@ -185,6 +206,22 @@ impl SearchEvent { } } + pub fn from_whoami_request( + audit: &mut AuditScope, + uat: Option, + qs: &QueryServerReadTransaction, + ) -> Result { + Ok(SearchEvent { + event: Event::from_ro_uat(audit, qs, uat)?, + filter: filter!(f_self()) + .validate(qs.get_schema()) + .map_err(|e| OperationError::SchemaViolation(e))?, + filter_orig: filter_all!(f_self()) + .validate(qs.get_schema()) + .map_err(|e| OperationError::SchemaViolation(e))?, + }) + } + // Just impersonate the account with no filter changes. #[cfg(test)] pub unsafe fn new_impersonate_entry_ser(e: &str, filter: Filter) -> Self { @@ -329,7 +366,7 @@ impl CreateEvent { entries: Vec>, ) -> Self { CreateEvent { - event: unsafe { Event::from_impersonate_entry_ser(e) }, + event: Event::from_impersonate_entry_ser(e), entries: entries, } } @@ -519,22 +556,131 @@ impl ModifyEvent { } #[derive(Debug)] -pub struct AuthEvent { - // pub event: Event, +pub struct AuthEventStepInit { + pub name: String, + pub appid: Option, } -impl AuthEvent { - pub fn from_request(_request: AuthRequest) -> Self { - AuthEvent {} +#[derive(Debug)] +pub struct AuthEventStepCreds { + pub sessionid: Uuid, + pub creds: Vec, +} + +#[derive(Debug)] +pub enum AuthEventStep { + Init(AuthEventStepInit), + Creds(AuthEventStepCreds), +} + +impl AuthEventStep { + fn from_authstep(aus: AuthStep, sid: Option) -> Result { + match aus { + AuthStep::Init(name, appid) => { + if sid.is_some() { + Err(OperationError::InvalidAuthState( + "session id present in init", + )) + } else { + Ok(AuthEventStep::Init(AuthEventStepInit { + name: name, + appid: appid, + })) + } + } + AuthStep::Creds(creds) => match sid { + Some(ssid) => Ok(AuthEventStep::Creds(AuthEventStepCreds { + sessionid: ssid, + creds: creds, + })), + None => Err(OperationError::InvalidAuthState( + "session id not present in cred", + )), + }, + } + } + + #[cfg(test)] + pub fn anonymous_init() -> Self { + AuthEventStep::Init(AuthEventStepInit { + name: "anonymous".to_string(), + appid: None, + }) + } + + #[cfg(test)] + pub fn anonymous_cred_step(sid: Uuid) -> Self { + AuthEventStep::Creds(AuthEventStepCreds { + sessionid: sid, + creds: vec![AuthCredential::Anonymous], + }) } } -pub struct AuthResult {} +#[derive(Debug)] +pub struct AuthEvent { + pub event: Option, + pub step: AuthEventStep, + // pub sessionid: Option, +} + +impl AuthEvent { + pub fn from_message(msg: AuthMessage) -> Result { + Ok(AuthEvent { + // TODO: Change to AuthMessage, and fill in uat? + event: None, + step: AuthEventStep::from_authstep(msg.req.step, msg.sessionid)?, + }) + } + + #[cfg(test)] + pub fn anonymous_init() -> Self { + AuthEvent { + event: None, + step: AuthEventStep::anonymous_init(), + } + } + + #[cfg(test)] + pub fn anonymous_cred_step(sid: Uuid) -> Self { + AuthEvent { + event: None, + step: AuthEventStep::anonymous_cred_step(sid), + } + } +} + +// Probably should be a struct with the session id present. +#[derive(Debug)] +pub struct AuthResult { + pub sessionid: Uuid, + // TODO: Make this an event specific authstate type? + pub state: AuthState, +} impl AuthResult { pub fn response(self) -> AuthResponse { AuthResponse { - status: AuthStatus::Begin(String::from("hello")), + sessionid: self.sessionid, + state: self.state, + } + } +} + +pub struct WhoamiResult { + youare: ProtoEntry, +} + +impl WhoamiResult { + pub fn new(e: Entry) -> Self { + WhoamiResult { + youare: e.into_pe(), + } + } + + pub fn response(self) -> WhoamiResponse { + WhoamiResponse { + youare: self.youare, } } } diff --git a/src/lib/filter.rs b/src/lib/filter.rs index 1b375f837..9ca223eef 100644 --- a/src/lib/filter.rs +++ b/src/lib/filter.rs @@ -5,7 +5,7 @@ use crate::audit::AuditScope; use crate::error::{OperationError, SchemaError}; use crate::event::{Event, EventOrigin}; -use crate::proto_v1::Filter as ProtoFilter; +use crate::proto::v1::Filter as ProtoFilter; use crate::schema::SchemaTransaction; use crate::server::{ QueryServerReadTransaction, QueryServerTransaction, QueryServerWriteTransaction, diff --git a/src/lib/idm/account.rs b/src/lib/idm/account.rs new file mode 100644 index 000000000..4652df551 --- /dev/null +++ b/src/lib/idm/account.rs @@ -0,0 +1,119 @@ +use crate::entry::{Entry, EntryCommitted, EntryValid}; +use crate::error::OperationError; + +use crate::proto::v1::{AuthAllowed, UserAuthToken}; + +use crate::idm::claim::Claim; +use crate::idm::group::Group; + +use std::convert::TryFrom; +use uuid::Uuid; + +#[derive(Debug, Clone)] +pub(crate) struct Account { + // Later these could be &str if we cache entry here too ... + // They can't because if we mod the entry, we'll lose the ref. + // + // We do need to decide if we'll cache the entry, or if we just "work out" + // what the ops should be based on the values we cache here ... That's a future + // william problem I think :) + pub name: String, + pub displayname: String, + pub uuid: String, + pub groups: Vec, + // creds (various types) + // groups? + // claims? + // account expiry? +} + +impl TryFrom> for Account { + type Error = OperationError; + + fn try_from(value: Entry) -> Result { + // Check the classes + if !value.attribute_value_pres("class", "account") { + return Err(OperationError::InvalidAccountState( + "Missing class: account", + )); + } + + // Now extract our needed attributes + let name = value + .get_ava_single("name") + .ok_or(OperationError::InvalidAccountState( + "Missing attribute: name", + ))? + .clone(); + + let displayname = value + .get_ava_single("displayname") + .ok_or(OperationError::InvalidAccountState( + "Missing attribute: displayname", + ))? + .clone(); + + // TODO: Resolve groups!!!! + let groups = Vec::new(); + + let uuid = value.get_uuid().clone(); + + Ok(Account { + uuid: uuid, + name: name, + displayname: displayname, + groups: groups, + }) + } +} + +impl Account { + // Could this actually take a claims list and application instead? + pub(crate) fn to_userauthtoken(&self, claims: Vec) -> Option { + // This could consume self? + // The cred handler provided is what authenticated this user, so we can use it to + // process what the proper claims should be. + + // Get the claims from the cred_h + + Some(UserAuthToken { + name: self.name.clone(), + displayname: self.name.clone(), + uuid: self.uuid.clone(), + application: None, + groups: self.groups.iter().map(|g| g.into_proto()).collect(), + claims: claims.iter().map(|c| c.into_proto()).collect(), + }) + } +} + +// Need to also add a "to UserAuthToken" ... + +// Need tests for conversion and the cred validations + +#[cfg(test)] +mod tests { + use crate::constants::JSON_ANONYMOUS_V1; + use crate::entry::{Entry, EntryNew, EntryValid}; + use crate::idm::account::Account; + use crate::proto::v1::AuthAllowed; + + use std::convert::TryFrom; + + #[test] + fn test_idm_account_from_anonymous() { + let anon_e: Entry = + serde_json::from_str(JSON_ANONYMOUS_V1).expect("Json deserialise failure!"); + let anon_e = unsafe { anon_e.to_valid_committed() }; + + let anon_account = Account::try_from(anon_e).expect("Must not fail"); + println!("{:?}", anon_account); + // I think that's it? we may want to check anonymous mech ... + } + + #[test] + fn test_idm_account_from_real() { + // For now, nothing, but later, we'll test different types of cred + // passing. + } +} diff --git a/src/lib/idm/authsession.rs b/src/lib/idm/authsession.rs new file mode 100644 index 000000000..115a7d317 --- /dev/null +++ b/src/lib/idm/authsession.rs @@ -0,0 +1,188 @@ +use crate::constants::UUID_ANONYMOUS; +use crate::error::OperationError; +use crate::idm::account::Account; +use crate::idm::claim::Claim; +use crate::proto::v1::{AuthAllowed, AuthCredential, AuthState}; + +// Each CredHandler takes one or more credentials and determines if the +// handlers requirements can be 100% fufilled. This is where MFA or other +// auth policies would exist, but each credHandler has to be a whole +// encapsulated unit of function. + +enum CredState { + Success(Vec), + Continue(Vec), + // TODO: Should we have a reason in Denied so that we + Denied, +} + +#[derive(Clone, Debug)] +enum CredHandler { + Anonymous, + // AppPassword + // { + // Password + // Webauthn + // Webauthn + Password + // TOTP + // TOTP + Password + // } <<-- could all these be "AccountPrimary" and pass to Account? + // Selection at this level could be premature ... + // Verification Link? +} + +impl CredHandler { + pub fn validate(&mut self, creds: &Vec) -> CredState { + match self { + CredHandler::Anonymous => { + creds.iter().fold(CredState::Denied, |acc, cred| { + // There is no "continuation" from this type. + match cred { + AuthCredential::Anonymous => { + // For anonymous, no claims will ever be issued. + CredState::Success(Vec::new()) + } + _ => { + // Should we have a reason in Denied so that we + CredState::Denied + } + } + }) + } + } + } + + pub fn valid_auth_mechs(&self) -> Vec { + match &self { + CredHandler::Anonymous => vec![AuthAllowed::Anonymous], + } + } +} + +#[derive(Clone)] +pub(crate) struct AuthSession { + // Do we store a copy of the entry? + // How do we know what claims to add? + account: Account, + // Store how we plan to handle this sessions authentication: this is generally + // made apparent by the presentation of an application id or not. If none is presented + // we want the primary-interaction credentials. + // + // This handler will then handle the mfa and stepping up through to generate the auth states + handler: CredHandler, + // Store any related appid we are processing for. + appid: Option, + finished: bool, +} + +impl AuthSession { + pub fn new(account: Account, appid: Option) -> Self { + // During this setup, determine the credential handler that we'll be using + // for this session. This is currently based on presentation of an application + // id. + let handler = match appid { + Some(_) => { + unimplemented!(); + } + None => { + // We want the primary handler - this is where we make a decision + // based on the anonymous ... in theory this could be cleaner + // and interact with the account more? + if account.uuid == UUID_ANONYMOUS { + CredHandler::Anonymous + } else { + unimplemented!(); + } + } + }; + + // Is this handler locked? + // Is the whole account locked? + // What about in memory account locking? Is that something + // we store in the account somehow? + // TODO: Implement handler locking! + + AuthSession { + account: account, + handler: handler, + appid: appid, + finished: false, + } + } + + // This should return a AuthResult or similar state of checking? + // TODO: This needs some logging .... + pub fn validate_creds( + &mut self, + creds: &Vec, + ) -> Result { + if self.finished { + return Err(OperationError::InvalidAuthState( + "session already finalised!", + )); + } + + match self.handler.validate(creds) { + CredState::Success(claims) => { + self.finished = true; + let uat = self + .account + .to_userauthtoken(claims) + .ok_or(OperationError::InvalidState)?; + Ok(AuthState::Success(uat)) + } + CredState::Continue(allowed) => Ok(AuthState::Continue(allowed)), + CredState::Denied => { + self.finished = true; + Ok(AuthState::Denied) + } + } + // Also send an async message to self to log the auth as provided. + // Alternately, open a write, and commit the needed security metadata here + // now rather than async (probably better for lock-outs etc) + // + // TODO: Async message the account owner about the login? + // If this fails, how can we in memory lock the account? + // + // The lockouts could also be an in-memory concept too? + + // If this suceeds audit? + // If success, to authtoken? + } + + pub fn valid_auth_mechs(&self) -> Vec { + // TODO: This needs logging .... + if self.finished { + Vec::new() + } else { + self.handler.valid_auth_mechs() + } + } +} + +#[cfg(test)] +mod tests { + use crate::constants::JSON_ANONYMOUS_V1; + use crate::entry::{Entry, EntryNew, EntryValid}; + use crate::idm::account::Account; + use crate::idm::authsession::AuthSession; + use crate::proto::v1::AuthAllowed; + + use std::convert::TryFrom; + + #[test] + fn test_idm_account_anonymous_auth_mech() { + let anon_account = entry_str_to_account!(JSON_ANONYMOUS_V1); + + let session = AuthSession::new(anon_account, None); + + let auth_mechs = session.valid_auth_mechs(); + + assert!( + true == auth_mechs.iter().fold(false, |acc, x| match x { + AuthAllowed::Anonymous => true, + _ => acc, + }) + ); + } +} diff --git a/src/lib/identity.rs b/src/lib/idm/identity.rs similarity index 100% rename from src/lib/identity.rs rename to src/lib/idm/identity.rs diff --git a/src/lib/idm/macros.rs b/src/lib/idm/macros.rs new file mode 100644 index 000000000..feaf99415 --- /dev/null +++ b/src/lib/idm/macros.rs @@ -0,0 +1,56 @@ +#[cfg(test)] +macro_rules! entry_str_to_account { + ($entry_str:expr) => {{ + use crate::entry::{Entry, EntryNew, EntryValid}; + use crate::idm::account::Account; + + let e: Entry = + serde_json::from_str($entry_str).expect("Json deserialise failure!"); + let e = unsafe { e.to_valid_committed() }; + + Account::try_from(e).expect("Account conversion failure") + }}; +} + +#[cfg(test)] +macro_rules! run_idm_test { + ($test_fn:expr) => {{ + use crate::audit::AuditScope; + use crate::be::Backend; + use crate::idm::server::IdmServer; + use crate::schema::Schema; + use crate::server::QueryServer; + use std::sync::Arc; + + use env_logger; + ::std::env::set_var("RUST_LOG", "actix_web=debug,rsidm=debug"); + let _ = env_logger::builder().is_test(true).try_init(); + + let mut audit = AuditScope::new("run_test"); + + let be = Backend::new(&mut audit, "").expect("Failed to init be"); + let schema_outer = Schema::new(&mut audit).expect("Failed to init schema"); + { + let mut schema = schema_outer.write(); + schema + .bootstrap_core(&mut audit) + .expect("Failed to bootstrap schema"); + schema.commit().expect("Failed to commit schema"); + } + + let test_server = QueryServer::new(be, schema_outer); + + { + let ts_write = test_server.write(); + ts_write.initialise(&mut audit).expect("Init failed!"); + ts_write.commit(&mut audit).expect("Commit failed!"); + } + + let test_idm_server = IdmServer::new(test_server.clone()); + + $test_fn(&test_server, &test_idm_server, &mut audit); + // Any needed teardown? + // Make sure there are no errors. + assert!(test_server.verify(&mut audit).len() == 0); + }}; +} diff --git a/src/lib/idm/mod.rs b/src/lib/idm/mod.rs new file mode 100644 index 000000000..a018c29d5 --- /dev/null +++ b/src/lib/idm/mod.rs @@ -0,0 +1,9 @@ +#[macro_use] +mod macros; + +pub(crate) mod account; +pub(crate) mod authsession; +pub(crate) mod claim; +pub(crate) mod group; +pub(crate) mod server; +// mod identity; diff --git a/src/lib/idm/server.rs b/src/lib/idm/server.rs new file mode 100644 index 000000000..3c7117d00 --- /dev/null +++ b/src/lib/idm/server.rs @@ -0,0 +1,263 @@ +use crate::audit::AuditScope; +use crate::be::Backend; +use crate::constants::UUID_ANONYMOUS; +use crate::error::OperationError; +use crate::event::{AuthEvent, AuthEventStep, AuthEventStepInit, AuthResult, SearchEvent}; +use crate::idm::account::Account; +use crate::idm::authsession::AuthSession; +use crate::proto::v1::{AuthResponse, AuthState, UserAuthToken}; +use crate::schema::Schema; +use crate::server::{QueryServer, QueryServerTransaction}; +use concread::cowcell::{CowCell, CowCellReadTxn, CowCellWriteTxn}; + +use std::collections::BTreeMap; +use std::convert::TryFrom; +use std::sync::Arc; +use uuid::Uuid; +// use lru::LruCache; + +pub struct IdmServer { + // Need an auth-session table to save in progress authentications + // sessions: + // + // TODO: This should be Lru + // TODO: AuthSession should be per-session mutex to keep locking on the + // cell low to allow more concurrent auths. + sessions: CowCell>, + // Need a reference to the query server. + qs: QueryServer, +} + +pub struct IdmServerWriteTransaction<'a> { + // Contains methods that require writes, but in the context of writing to + // the idm in memory structures (maybe the query server too). This is + // things like authentication + sessions: CowCellWriteTxn<'a, BTreeMap>, + qs: &'a QueryServer, +} + +pub struct IdmServerReadTransaction<'a> { + // This contains read-only methods, like getting users, groups + // and other structured content. + qs: &'a QueryServer, +} + +impl IdmServer { + // TODO: Make number of authsessions configurable!!! + pub fn new(qs: QueryServer) -> IdmServer { + IdmServer { + sessions: CowCell::new(BTreeMap::new()), + qs: qs, + } + } + + pub fn write(&self) -> IdmServerWriteTransaction { + IdmServerWriteTransaction { + sessions: self.sessions.write(), + qs: &self.qs, + } + } + + pub fn read(&self) -> IdmServerReadTransaction { + IdmServerReadTransaction { qs: &self.qs } + } +} + +impl<'a> IdmServerWriteTransaction<'a> { + // TODO: This should be something else, not the proto token! + pub fn auth( + &mut self, + au: &mut AuditScope, + ae: &AuthEvent, + ) -> Result { + audit_log!(au, "Received AuthEvent -> {:?}", ae); + + // Match on the auth event, to see what we need to do. + + match &ae.step { + AuthEventStep::Init(init) => { + // Allocate a session id. + let sessionid = Uuid::new_v4(); + + // Begin the auth procedure! + // Start a read + // + // Actually we may not need this - at the time we issue the auth-init + // we could generate the uat, the nonce and cache hashes in memory, + // then this can just be fully without a txn. + // + // We do need a txn so that we can process/search and claims + // or related based on the quality of the provided auth steps + // + // We *DO NOT* need a write though, because I think that lock outs + // and rate limits are *per server* and *in memory* only. + let qs_read = self.qs.read(); + // Check anything needed? Get the current auth-session-id from request + // because it associates to the nonce's etc which were all cached. + + let filter_entry = filter!(f_or!([ + f_eq("name", init.name.as_str()), + // This currently says invalid syntax, which is correct, but also + // annoying because it would be nice to search both ... + // f_eq("uuid", name.as_str()), + ])); + + // Get the first / single entry we expect here .... + let entry = match qs_read.internal_search(au, filter_entry) { + Ok(mut entries) => { + // Get only one entry out ... + if entries.len() >= 2 { + return Err(OperationError::InvalidDBState); + } + entries.pop().ok_or(OperationError::NoMatchingEntries)? + } + Err(e) => { + // Something went wrong! Abort! + return Err(e); + } + }; + + audit_log!(au, "Initiating Authentication Session for ... {:?}", entry); + + // Now, convert the Entry to an account - this gives us some stronger + // typing and functionality so we can assess what auth types can + // continue, and helps to keep non-needed entry specific data + // out of the LRU. + let account = Account::try_from(entry)?; + let auth_session = AuthSession::new(account, init.appid.clone()); + + // Get the set of mechanisms that can proceed. This is tied + // to the session so that it can mutate state and have progression + // of what's next, or ordering. + let next_mech = auth_session.valid_auth_mechs(); + + // TODO: Check that no session id of the same value + // already existed, if so, error! + self.sessions.insert(sessionid, auth_session); + + // Debugging: ensure we really inserted ... + assert!(self.sessions.get(&sessionid).is_some()); + + Ok(AuthResult { + sessionid: sessionid, + // TODO: Change this to a better internal type? + state: AuthState::Continue(next_mech), + }) + } + AuthEventStep::Creds(creds) => { + // Do we have a session? + let auth_session = try_audit!( + au, + (*self.sessions) + // Why is the session missing? + .get_mut(&creds.sessionid) + .ok_or(OperationError::InvalidSessionState) + ); + // Process the credentials here as required. + // Basically throw them at the auth_session and see what + // falls out. + auth_session.validate_creds(&creds.creds).map(|aus| { + AuthResult { + // Is this right? + sessionid: creds.sessionid, + state: aus, + } + }) + } + } + } + + pub fn commit(self) -> Result<(), OperationError> { + self.sessions.commit(); + Ok(()) + } +} + +impl<'a> IdmServerReadTransaction<'a> { + pub fn whoami() -> () {} +} + +// Need tests of the sessions and the auth ... + +#[cfg(test)] +mod tests { + use crate::event::{AuthEvent, AuthResult}; + use crate::proto::v1::{AuthAllowed, AuthState}; + + #[test] + fn test_idm_anonymous_auth() { + run_idm_test!(|_qs: &QueryServer, idms: &IdmServer, au: &mut AuditScope| { + let sid = { + // Start and test anonymous auth. + let mut idms_write = idms.write(); + // Send the initial auth event for initialising the session + let anon_init = AuthEvent::anonymous_init(); + // Expect success + let r1 = idms_write.auth(au, &anon_init); + /* Some weird lifetime shit happens here ... */ + // audit_log!(au, "r1 ==> {:?}", r1); + + let sid = match r1 { + Ok(ar) => { + let AuthResult { sessionid, state } = ar; + match state { + AuthState::Continue(mut conts) => { + // Should only be one auth mech + assert!(conts.len() == 1); + // And it should be anonymous + let m = conts.pop().expect("Should not fail"); + assert!(m == AuthAllowed::Anonymous); + } + _ => { + panic!(); + } + }; + // Now pass back the sessionid, we are good to continue. + sessionid + } + Err(e) => { + // Should not occur! + panic!(); + } + }; + + println!("sessionid is ==> {:?}", sid); + + idms_write.commit().expect("Must not fail"); + + sid + }; + { + let mut idms_write = idms.write(); + // Now send the anonymous request, given the session id. + let anon_step = AuthEvent::anonymous_cred_step(sid); + + // Expect success + let r2 = idms_write.auth(au, &anon_step); + println!("r2 ==> {:?}", r2); + + match r2 { + Ok(ar) => { + let AuthResult { sessionid, state } = ar; + match state { + AuthState::Success(uat) => { + // Check the uat. + } + _ => { + panic!(); + } + } + } + Err(e) => { + // Should not occur! + panic!(); + } + }; + + idms_write.commit().expect("Must not fail"); + } + }); + } + + // Test sending anonymous but with no session init. +} diff --git a/src/lib/interval.rs b/src/lib/interval.rs index edbf9cc8e..c43a2021c 100644 --- a/src/lib/interval.rs +++ b/src/lib/interval.rs @@ -3,7 +3,7 @@ use std::time::Duration; use crate::constants::PURGE_TIMEOUT; use crate::event::{PurgeRecycledEvent, PurgeTombstoneEvent}; -use crate::proto_v1_actors::QueryServerV1; +use crate::proto::v1::actors::QueryServerV1; pub struct IntervalActor { // Store any addresses we require diff --git a/src/lib/lib.rs b/src/lib/lib.rs index 4220c4b46..31256ebf2 100644 --- a/src/lib/lib.rs +++ b/src/lib/lib.rs @@ -1,3 +1,5 @@ +#[macro_use] +extern crate log; extern crate serde; extern crate serde_json; #[macro_use] @@ -32,7 +34,7 @@ extern crate concread; #[macro_use] mod macros; #[macro_use] -mod log; +mod async_log; #[macro_use] mod audit; mod be; @@ -40,18 +42,18 @@ mod be; pub mod constants; mod entry; mod event; -mod identity; +// TODO: Does this need pub? +mod filter; mod interval; mod modify; #[macro_use] mod plugins; mod access; +mod idm; mod schema; mod server; pub mod config; pub mod core; pub mod error; -pub mod filter; -pub mod proto_v1; -mod proto_v1_actors; +pub mod proto; diff --git a/src/lib/log.rs b/src/lib/log.rs deleted file mode 100644 index 1fb32a234..000000000 --- a/src/lib/log.rs +++ /dev/null @@ -1,81 +0,0 @@ -use actix::prelude::*; - -use crate::audit::AuditScope; - -// Helper for internal logging. -// Should only be used at startup/shutdown -#[macro_export] -macro_rules! log_event { - ($log_addr:expr, $($arg:tt)*) => ({ - use crate::log::LogEvent; - use std::fmt; - $log_addr.do_send( - LogEvent { - msg: fmt::format( - format_args!($($arg)*) - ) - } - ) - }) -} - -// We need to pass in config for this later -// Or we need to pass in the settings for it IE level and dest? -// Is there an efficent way to set a log level filter in the macros -// so that we don't msg unless it's the correct level? -// Do we need config in the log macro? - -pub fn start() -> actix::Addr { - SyncArbiter::start(1, move || EventLog {}) -} - -pub struct EventLog {} - -impl Actor for EventLog { - type Context = SyncContext; - - /* - fn started(&mut self, ctx: &mut Self::Context) { - ctx.set_mailbox_capacity(1 << 31); - } - */ -} - -// 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: AuditScope, _: &mut SyncContext) -> Self::Result { - println!("AUDIT: {}", event); - } -} - -/* -impl Handler for EventLog { - type Result = (); - - fn handle(&mut self, event: Event, _: &mut SyncContext) -> Self::Result { - println!("EVENT: {:?}", event) - } -} -*/ diff --git a/src/lib/macros.rs b/src/lib/macros.rs index 9bf5aca8f..aaf8f5f06 100644 --- a/src/lib/macros.rs +++ b/src/lib/macros.rs @@ -5,7 +5,10 @@ macro_rules! run_test { use crate::be::Backend; use crate::schema::Schema; use crate::server::QueryServer; - use std::sync::Arc; + + use env_logger; + ::std::env::set_var("RUST_LOG", "actix_web=debug,rsidm=debug"); + let _ = env_logger::builder().is_test(true).try_init(); let mut audit = AuditScope::new("run_test"); @@ -18,7 +21,7 @@ macro_rules! run_test { .expect("Failed to bootstrap schema"); schema.commit().expect("Failed to commit schema"); } - let test_server = QueryServer::new(be, Arc::new(schema_outer)); + let test_server = QueryServer::new(be, schema_outer); { let ts_write = test_server.write(); @@ -81,7 +84,7 @@ macro_rules! filter { #[allow(unused_imports)] use crate::filter::FC; #[allow(unused_imports)] - use crate::filter::{f_and, f_andnot, f_eq, f_or, f_pres, f_sub}; + use crate::filter::{f_and, f_andnot, f_eq, f_or, f_pres, f_self, f_sub}; Filter::new_ignore_hidden($fc) }}; } @@ -96,7 +99,7 @@ macro_rules! filter_rec { #[allow(unused_imports)] use crate::filter::FC; #[allow(unused_imports)] - use crate::filter::{f_and, f_andnot, f_eq, f_or, f_pres, f_sub}; + use crate::filter::{f_and, f_andnot, f_eq, f_or, f_pres, f_self, f_sub}; Filter::new_recycled($fc) }}; } @@ -111,7 +114,7 @@ macro_rules! filter_all { #[allow(unused_imports)] use crate::filter::FC; #[allow(unused_imports)] - use crate::filter::{f_and, f_andnot, f_eq, f_or, f_pres, f_sub}; + use crate::filter::{f_and, f_andnot, f_eq, f_or, f_pres, f_self, f_sub}; Filter::new($fc) }}; } diff --git a/src/lib/modify.rs b/src/lib/modify.rs index 742e6d019..b04b1a78b 100644 --- a/src/lib/modify.rs +++ b/src/lib/modify.rs @@ -1,6 +1,6 @@ use crate::audit::AuditScope; -use crate::proto_v1::Modify as ProtoModify; -use crate::proto_v1::ModifyList as ProtoModifyList; +use crate::proto::v1::Modify as ProtoModify; +use crate::proto::v1::ModifyList as ProtoModifyList; use crate::error::{OperationError, SchemaError}; use crate::schema::SchemaTransaction; diff --git a/src/lib/plugins/macros.rs b/src/lib/plugins/macros.rs index 449e73fe3..b51529108 100644 --- a/src/lib/plugins/macros.rs +++ b/src/lib/plugins/macros.rs @@ -13,7 +13,7 @@ macro_rules! setup_test { schema.bootstrap_core($au).expect("Failed to init schema"); schema.commit().expect("Failed to commit schema"); } - let qs = QueryServer::new(be, Arc::new(schema_outer)); + let qs = QueryServer::new(be, schema_outer); if !$preload_entries.is_empty() { let qs_write = qs.write(); @@ -42,7 +42,6 @@ macro_rules! run_create_test { use crate::event::CreateEvent; use crate::schema::Schema; use crate::server::QueryServer; - use std::sync::Arc; let mut au = AuditScope::new("run_create_test"); audit_segment!(au, || { @@ -96,7 +95,6 @@ macro_rules! run_modify_test { use crate::event::ModifyEvent; use crate::schema::Schema; use crate::server::QueryServer; - use std::sync::Arc; let mut au = AuditScope::new("run_modify_test"); audit_segment!(au, || { @@ -149,7 +147,6 @@ macro_rules! run_delete_test { use crate::event::DeleteEvent; use crate::schema::Schema; use crate::server::QueryServer; - use std::sync::Arc; let mut au = AuditScope::new("run_delete_test"); audit_segment!(au, || { diff --git a/src/lib/proto/mod.rs b/src/lib/proto/mod.rs new file mode 100644 index 000000000..a3a6d96c3 --- /dev/null +++ b/src/lib/proto/mod.rs @@ -0,0 +1 @@ +pub mod v1; diff --git a/src/lib/proto_v1_actors.rs b/src/lib/proto/v1/actors.rs similarity index 66% rename from src/lib/proto_v1_actors.rs rename to src/lib/proto/v1/actors.rs index 07221be1a..d40aac785 100644 --- a/src/lib/proto_v1_actors.rs +++ b/src/lib/proto/v1/actors.rs @@ -4,24 +4,29 @@ use std::sync::Arc; use crate::audit::AuditScope; use crate::be::Backend; +use crate::async_log::EventLog; use crate::error::OperationError; use crate::event::{ - CreateEvent, DeleteEvent, ModifyEvent, PurgeRecycledEvent, PurgeTombstoneEvent, SearchEvent, - SearchResult, + AuthEvent, AuthResult, CreateEvent, DeleteEvent, ModifyEvent, PurgeRecycledEvent, + PurgeTombstoneEvent, SearchEvent, SearchResult, WhoamiResult, }; -use crate::log::EventLog; use crate::schema::{Schema, SchemaTransaction}; +use crate::constants::UUID_ANONYMOUS; +use crate::idm::server::IdmServer; use crate::server::{QueryServer, QueryServerTransaction}; -use crate::proto_v1::{ - AuthRequest, CreateRequest, DeleteRequest, ModifyRequest, OperationResponse, SearchRequest, - SearchResponse, +use crate::proto::v1::{ + AuthRequest, AuthResponse, AuthState, CreateRequest, DeleteRequest, ModifyRequest, + OperationResponse, SearchRequest, SearchResponse, UserAuthToken, WhoamiRequest, WhoamiResponse, }; +use crate::proto::v1::messages::{AuthMessage, WhoamiMessage}; + pub struct QueryServerV1 { log: actix::Addr, qs: QueryServer, + idms: Arc, } impl Actor for QueryServerV1 { @@ -33,14 +38,19 @@ impl Actor for QueryServerV1 { } impl QueryServerV1 { - pub fn new(log: actix::Addr, be: Backend, schema: Arc) -> Self { + pub fn new(log: actix::Addr, qs: QueryServer, idms: Arc) -> Self { log_event!(log, "Starting query server v1 worker ..."); QueryServerV1 { log: log, - qs: QueryServer::new(be, schema), + qs: qs, + idms: idms, } } + // TODO: We could move most of the be/schema/qs setup and startup + // outside of this call, then pass in "what we need" in a cloneable + // form, this way we could have seperate Idm vs Qs threads, and dedicated + // threads for write vs read pub fn start( log: actix::Addr, path: &str, @@ -53,9 +63,8 @@ impl QueryServerV1 { // Create "just enough" schema for us to be able to load from // disk ... Schema loading is one time where we validate the // entries as we read them, so we need this here. - // FIXME: Handle results in start correctly let schema = match Schema::new(&mut audit) { - Ok(s) => Arc::new(s), + Ok(s) => s, Err(e) => return Err(e), }; @@ -103,10 +112,14 @@ impl QueryServerV1 { } } - // Create a temporary query_server implementation - let query_server = QueryServer::new(be.clone(), schema.clone()); + // Create a query_server implementation + // TODO: FIXME: CRITICAL: Schema must be ARC/Cow properly!!! Right now it's + // not!!! + let query_server = QueryServer::new(be, schema); let mut audit_qsc = AuditScope::new("query_server_init"); + // This may need to be two parts, one for schema, one for everything else + // that way we can reload schema in between. let query_server_write = query_server.write(); match query_server_write.initialise(&mut audit_qsc).and_then(|_| { audit_segment!(audit_qsc, || query_server_write.commit(&mut audit_qsc)) @@ -115,11 +128,17 @@ impl QueryServerV1 { Ok(_) => {} Err(e) => return Err(e), }; + // What's important about this initial setup here is that it also triggers + // the schema and acp reload, so they are now configured correctly! + + // We generate a SINGLE idms only! + + let idms = Arc::new(IdmServer::new(query_server.clone())); audit.append_scope(audit_qsc); let x = SyncArbiter::start(threads, move || { - QueryServerV1::new(log_inner.clone(), be.clone(), schema.clone()) + QueryServerV1::new(log_inner.clone(), query_server.clone(), idms.clone()) }); Ok(x) }); @@ -249,14 +268,39 @@ impl Handler for QueryServerV1 { } } -impl Handler for QueryServerV1 { - type Result = Result; +// Need an auth session storage. LRU? +// requires a lock ... +// needs session id, entry, etc. - fn handle(&mut self, msg: AuthRequest, _: &mut Self::Context) -> Self::Result { +impl Handler for QueryServerV1 { + type Result = Result; + + fn handle(&mut self, msg: AuthMessage, _: &mut Self::Context) -> Self::Result { + // This is probably the first function that really implements logic + // "on top" of the db server concept. In this case we check if + // the credentials provided is sufficient to say if someone is + // "authenticated" or not. let mut audit = AuditScope::new("auth"); let res = audit_segment!(&mut audit, || { audit_log!(audit, "Begin auth event {:?}", msg); - Err(OperationError::InvalidState) + + // Destructure it. + // Convert the AuthRequest to an AuthEvent that the idm server + // can use. + + let mut idm_write = self.idms.write(); + + let ae = try_audit!(audit, AuthEvent::from_message(msg)); + + // Generally things like auth denied are in Ok() msgs + // so true errors should always trigger a rollback. + let r = idm_write + .auth(&mut audit, &ae) + .and_then(|r| idm_write.commit().map(|_| r)); + + audit_log!(audit, "Sending result -> {:?}", r); + // Build the result. + r.map(|r| r.response()) }); // At the end of the event we send it for logging. self.log.do_send(audit); @@ -264,6 +308,60 @@ impl Handler for QueryServerV1 { } } +impl Handler for QueryServerV1 { + type Result = Result; + + fn handle(&mut self, msg: WhoamiMessage, _: &mut Self::Context) -> Self::Result { + let mut audit = AuditScope::new("whoami"); + let res = audit_segment!(&mut audit, || { + // TODO: Move this to IdmServer!!! + // Begin a read + let qs_read = self.qs.read(); + + // Make an event from the whoami request. This will process the event and + // generate a selfuuid search. + // FIXME: This current handles the unauthenticated check, and will + // trigger the failure, but if we can manage to work out async + // then move this to core.rs, and don't allow Option to get + // this far. + let srch = match SearchEvent::from_whoami_request(&mut audit, msg.uat, &qs_read) { + Ok(s) => s, + Err(e) => { + audit_log!(audit, "Failed to begin whoami: {:?}", e); + return Err(e); + } + }; + + audit_log!(audit, "Begin event {:?}", srch); + + match qs_read.search_ext(&mut audit, &srch) { + Ok(mut entries) => { + // assert there is only one ... + match entries.len() { + 0 => Err(OperationError::NoMatchingEntries), + 1 => { + let e = entries.pop().expect("Entry length mismatch!!!"); + // Now convert to a response, and return + let wr = WhoamiResult::new(e); + Ok(wr.response()) + } + // Somehow we matched multiple, which should be impossible. + _ => Err(OperationError::InvalidState), + } + } + // Something else went wrong ... + Err(e) => Err(e), + } + }); + // Should we log the final result? + // At the end of the event we send it for logging. + self.log.do_send(audit); + res + } +} + +// These below are internal only types. + impl Handler for QueryServerV1 { type Result = (); diff --git a/src/lib/proto/v1/client.rs b/src/lib/proto/v1/client.rs new file mode 100644 index 000000000..86061ae72 --- /dev/null +++ b/src/lib/proto/v1/client.rs @@ -0,0 +1,26 @@ +// Implement a client library that use used to interact with +// a kanidm server and perform operations - it is expected that +// this client can store some internal state, and will generally +// attempt to reflect and map to a simple representation of +// the protocol, which was intended to be easy-to-use and accessible. + +struct ClientV1 {} + +impl ClientV1 { + fn auth_anonymous() -> () {} + + fn auth_password() -> () {} + + fn auth_application_password() -> () {} + + fn whoami() -> () {} + + // The four raw operations. + fn search() -> () {} + + fn modify() -> () {} + + fn create() -> () {} + + fn delete() -> () {} +} diff --git a/src/lib/proto/v1/messages.rs b/src/lib/proto/v1/messages.rs new file mode 100644 index 000000000..ccd21b9fb --- /dev/null +++ b/src/lib/proto/v1/messages.rs @@ -0,0 +1,45 @@ +use crate::error::OperationError; +use actix::prelude::*; +use uuid::Uuid; + +use crate::proto::v1::{AuthRequest, AuthResponse, UserAuthToken, WhoamiRequest, WhoamiResponse}; + +// These are used when the request (IE Get) has no intrising request +// type. Additionally, they are used in some requests where we need +// to supplement extra server state (IE userauthtokens) to a request. +// +// Generally we don't need to have the responses here because they are +// part of the protocol. + +pub struct WhoamiMessage { + pub uat: Option, +} + +impl WhoamiMessage { + pub fn new(uat: Option) -> Self { + WhoamiMessage { uat: uat } + } +} + +impl Message for WhoamiMessage { + type Result = Result; +} + +#[derive(Debug)] +pub struct AuthMessage { + pub sessionid: Option, + pub req: AuthRequest, +} + +impl AuthMessage { + pub fn new(req: AuthRequest, sessionid: Option) -> Self { + AuthMessage { + sessionid: sessionid, + req: req, + } + } +} + +impl Message for AuthMessage { + type Result = Result; +} diff --git a/src/lib/proto_v1.rs b/src/lib/proto/v1/mod.rs similarity index 60% rename from src/lib/proto_v1.rs rename to src/lib/proto/v1/mod.rs index 7cdd125d9..599b410ba 100644 --- a/src/lib/proto_v1.rs +++ b/src/lib/proto/v1/mod.rs @@ -3,9 +3,71 @@ use crate::error::OperationError; use actix::prelude::*; use std::collections::BTreeMap; +use uuid::Uuid; + +pub(crate) mod actors; +pub mod client; +pub(crate) mod messages; // These proto implementations are here because they have public definitions +/* ===== higher level types ===== */ +// These are all types that are conceptually layers ontop of entry and +// friends. They allow us to process more complex requests and provide +// domain specific fields for the purposes of IDM, over the normal +// entry/ava/filter types. These related deeply to schema. + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Group { + pub name: String, + pub uuid: String, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Claim { + pub name: String, + pub uuid: String, + // These can be ephemeral, or shortlived in a session. + // some may even need requesting. + // pub expiry: DateTime +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Application { + pub name: String, + pub uuid: String, +} + +// The currently authenticated user, and any required metadata for them +// to properly authorise them. This is similar in nature to oauth and the krb +// PAC/PAD structures. Currently we only use this internally, but we should +// consider making it "parseable" by the client so they can have per-session +// group/authorisation data. +// +// This structure and how it works will *very much* change over time from this +// point onward! +// +// It's likely that this must have a relationship to the server's user structure +// and to the Entry so that filters or access controls can be applied. +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct UserAuthToken { + // When this data should be considered invalid. Interpretation + // may depend on the client application. + // pub expiry: DateTime, + pub name: String, + pub displayname: String, + pub uuid: String, + pub application: Option, + pub groups: Vec, + pub claims: Vec, + // Should we allow supplemental ava's to be added on request? +} + +// UAT will need a downcast to Entry, which adds in the claims to the entry +// for the purpose of filtering. + +/* ===== low level proto types ===== */ + // FIXME: We probably need a proto entry to transform our // server core entry into. We also need to get from proto // entry to our actual entry. @@ -160,46 +222,60 @@ impl Message for ModifyRequest { // // On loginSuccess, we send a cookie, and that allows the token to be // generated. The cookie can be shared between servers. +#[derive(Debug, Serialize, Deserialize)] +pub enum AuthCredential { + Anonymous, + Password(String), + // TOTP(String), +} #[derive(Debug, Serialize, Deserialize)] -pub enum AuthState { - Init(String, Vec), +pub enum AuthStep { + // name, application id? + Init(String, Option), /* Step( Type(params ....) ), */ + Creds(Vec), + // Should we have a "finalise" type to attempt to finish based on + // what we have given? } // Request auth for identity X with roles Y? #[derive(Debug, Serialize, Deserialize)] pub struct AuthRequest { - pub state: AuthState, - pub user_uuid: String, -} - -impl Message for AuthRequest { - type Result = Result; + pub step: AuthStep, } // Respond with the list of auth types and nonce, etc. // It can also contain a denied, or success. +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub enum AuthAllowed { + Anonymous, + Password, + // TOTP, + // Webauthn(String), +} + #[derive(Debug, Serialize, Deserialize)] -pub enum AuthStatus { - Begin(String), // uuid of this session. - // Continue, // Keep going, here are the things you could still provide ... - // Go away, you made a mistake somewhere. - // Provide reason? - // Denied(String), - // Welcome friend. - // On success provide entry "self", for group assertions? - // We also provide the "cookie"/token? - // Success(String, Entry), +pub enum AuthState { + // Everything is good, your cookie has been issued, and a token is set here + // for the client to view. + Success(UserAuthToken), + // Something was bad, your session is terminated and no cookie. + Denied, + // Continue to auth, allowed mechanisms listed. + Continue(Vec), } #[derive(Debug, Serialize, Deserialize)] pub struct AuthResponse { - pub status: AuthStatus, + // TODO: Consider moving to an AuthMessageResponse type, and leave the proto + // without the session id because it's not necesary to know. + pub sessionid: Uuid, + pub state: AuthState, } /* Recycle Requests area */ @@ -236,9 +312,31 @@ impl ReviveRecycledRequest { } } +// This doesn't need seralise because it's only accessed via a "get". +#[derive(Debug)] +pub struct WhoamiRequest {} + +impl WhoamiRequest { + pub fn new() -> Self { + WhoamiRequest {} + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct WhoamiResponse { + // Should we just embed the entry? Or destructure it? + pub youare: Entry, +} + +impl WhoamiResponse { + pub fn new(e: Entry) -> Self { + WhoamiResponse { youare: e } + } +} + #[cfg(test)] mod tests { - use crate::proto_v1::Filter as ProtoFilter; + use crate::proto::v1::Filter as ProtoFilter; #[test] fn test_protofilter_simple() { let pf: ProtoFilter = ProtoFilter::Pres("class".to_string()); diff --git a/src/lib/schema.rs b/src/lib/schema.rs index 9b3820a62..92677211d 100644 --- a/src/lib/schema.rs +++ b/src/lib/schema.rs @@ -1,7 +1,7 @@ use crate::audit::AuditScope; use crate::constants::*; use crate::error::{ConsistencyError, OperationError, SchemaError}; -use crate::proto_v1::Filter as ProtoFilter; +use crate::proto::v1::Filter as ProtoFilter; use regex::Regex; use std::collections::HashMap; use std::convert::TryFrom; @@ -16,8 +16,6 @@ use concread::cowcell::{CowCell, CowCellReadTxn, CowCellWriteTxn}; // In the future this will parse/read it's schema from the db // but we have to bootstrap with some core types. -// TODO: Schema should be copy-on-write - // TODO: Account should be a login-bind-able object // needs account lock, timeout, policy? diff --git a/src/lib/server.rs b/src/lib/server.rs index 5373cb480..df0643ea1 100644 --- a/src/lib/server.rs +++ b/src/lib/server.rs @@ -13,7 +13,7 @@ use crate::access::{ }; use crate::constants::{ JSON_ADMIN_V1, JSON_ANONYMOUS_V1, JSON_IDM_ADMINS_ACP_REVIVE_V1, JSON_IDM_ADMINS_ACP_SEARCH_V1, - JSON_IDM_ADMINS_V1, JSON_SYSTEM_INFO_V1, + JSON_IDM_ADMINS_V1, JSON_IDM_SELF_ACP_READ_V1, JSON_SYSTEM_INFO_V1, }; use crate::entry::{Entry, EntryCommitted, EntryInvalid, EntryNew, EntryNormalised, EntryValid}; use crate::error::{ConsistencyError, OperationError, SchemaError}; @@ -554,6 +554,7 @@ impl<'a> QueryServerTransaction for QueryServerWriteTransaction<'a> { } } +#[derive(Clone)] pub struct QueryServer { // log: actix::Addr, be: Backend, @@ -562,11 +563,11 @@ pub struct QueryServer { } impl QueryServer { - pub fn new(be: Backend, schema: Arc) -> Self { + pub fn new(be: Backend, schema: Schema) -> Self { // log_event!(log, "Starting query worker ..."); QueryServer { be: be, - schema: schema, + schema: Arc::new(schema), accesscontrols: Arc::new(AccessControls::new()), } } @@ -1300,16 +1301,11 @@ impl<'a> QueryServerWriteTransaction<'a> { } // Check the admin object exists (migrations). - let mut audit_an = AuditScope::new("start_admin"); - let res = self.internal_migrate_or_create_str(&mut audit_an, JSON_ADMIN_V1); - audit.append_scope(audit_an); - if res.is_err() { - return res; - } - // Create the default idm_admin group. - let mut audit_an = AuditScope::new("start_idm_admins"); - let res = self.internal_migrate_or_create_str(&mut audit_an, JSON_IDM_ADMINS_V1); + let mut audit_an = AuditScope::new("start_idm_admin_migrations"); + let res = self + .internal_migrate_or_create_str(&mut audit_an, JSON_ADMIN_V1) + .and_then(|_| self.internal_migrate_or_create_str(&mut audit_an, JSON_IDM_ADMINS_V1)); audit.append_scope(audit_an); if res.is_err() { return res; @@ -1318,15 +1314,15 @@ impl<'a> QueryServerWriteTransaction<'a> { // Create any system default schema entries. // Create any system default access profile entries. - let mut audit_an = AuditScope::new("start_idm_admins_acp"); - let res = self.internal_migrate_or_create_str(&mut audit_an, JSON_IDM_ADMINS_ACP_SEARCH_V1); - audit.append_scope(audit_an); - if res.is_err() { - return res; - } - - let mut audit_an = AuditScope::new("start_idm_admins_acp"); - let res = self.internal_migrate_or_create_str(&mut audit_an, JSON_IDM_ADMINS_ACP_REVIVE_V1); + let mut audit_an = AuditScope::new("start_idm_migrations_internal"); + let res = self + .internal_migrate_or_create_str(&mut audit_an, JSON_IDM_ADMINS_ACP_SEARCH_V1) + .and_then(|_| { + self.internal_migrate_or_create_str(&mut audit_an, JSON_IDM_ADMINS_ACP_REVIVE_V1) + }) + .and_then(|_| { + self.internal_migrate_or_create_str(&mut audit_an, JSON_IDM_SELF_ACP_READ_V1) + }); audit.append_scope(audit_an); if res.is_err() { return res; @@ -1459,31 +1455,15 @@ impl<'a> QueryServerWriteTransaction<'a> { #[cfg(test)] mod tests { - /* - extern crate actix; - use actix::prelude::*; - - extern crate futures; - use futures::future; - use futures::future::Future; - - extern crate tokio; - use std::sync::Arc; - use crate::be::Backend; - use crate::filter::Filter; - use crate::schema::Schema; - use crate::audit::AuditScope; - */ - use crate::constants::{JSON_ADMIN_V1, UUID_ADMIN}; use crate::entry::{Entry, EntryInvalid, EntryNew}; use crate::error::{OperationError, SchemaError}; use crate::event::{CreateEvent, DeleteEvent, ModifyEvent, ReviveRecycledEvent, SearchEvent}; use crate::modify::{Modify, ModifyList}; - use crate::proto_v1::Filter as ProtoFilter; - use crate::proto_v1::Modify as ProtoModify; - use crate::proto_v1::ModifyList as ProtoModifyList; - use crate::proto_v1::{DeleteRequest, ModifyRequest, ReviveRecycledRequest}; + use crate::proto::v1::Filter as ProtoFilter; + use crate::proto::v1::Modify as ProtoModify; + use crate::proto::v1::ModifyList as ProtoModifyList; + use crate::proto::v1::{DeleteRequest, ModifyRequest, ReviveRecycledRequest}; use crate::server::QueryServerTransaction; #[test] diff --git a/src/server/main.rs b/src/server/main.rs index 8e6a7fdcf..7cc9f8184 100644 --- a/src/server/main.rs +++ b/src/server/main.rs @@ -1,4 +1,5 @@ extern crate actix; +extern crate env_logger; extern crate rsidm; @@ -6,12 +7,14 @@ use rsidm::config::Configuration; use rsidm::core::create_server_core; fn main() { - // read the config (if any?) - // How do we make the config accesible to all threads/workers? clone it? - // Make it an Arc? - - // FIXME: Pass config to the server core + // Read our config (if any) let config = Configuration::new(); + + // Configure the server logger. This could be adjusted based on what config + // says. + ::std::env::set_var("RUST_LOG", "actix_web=info,rsidm=info"); + env_logger::init(); + let sys = actix::System::new("rsidm-server"); create_server_core(config); diff --git a/tests/proto_v1_test.rs b/tests/proto_v1_test.rs index fe2a76532..d88c14054 100644 --- a/tests/proto_v1_test.rs +++ b/tests/proto_v1_test.rs @@ -5,7 +5,10 @@ extern crate rsidm; use rsidm::config::Configuration; use rsidm::constants::UUID_ADMIN; use rsidm::core::create_server_core; -use rsidm::proto_v1::{CreateRequest, Entry, OperationResponse}; +use rsidm::proto::v1::{ + AuthCredential, AuthRequest, AuthResponse, AuthState, AuthStep, CreateRequest, Entry, + OperationResponse, WhoamiRequest, +}; extern crate reqwest; @@ -13,50 +16,60 @@ extern crate futures; // use futures::future; // use futures::future::Future; +use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::mpsc; use std::thread; +extern crate env_logger; extern crate tokio; +static PORT_ALLOC: AtomicUsize = AtomicUsize::new(8080); + // Test external behaviorus of the service. -macro_rules! run_test { - ($test_fn:expr) => {{ - let (tx, rx) = mpsc::channel(); +fn run_test(test_fn: fn(reqwest::Client, &str) -> ()) { + let _ = env_logger::builder().is_test(true).try_init(); + let (tx, rx) = mpsc::channel(); + let port = PORT_ALLOC.fetch_add(1, Ordering::SeqCst); + let mut config = Configuration::new(); + config.address = format!("127.0.0.1:{}", port); + // Setup the config ... - thread::spawn(|| { - // setup - // Create a server config in memory for use - use test settings - // Create a log: In memory - for now it's just stdout + thread::spawn(move || { + // Spawn a thread for the test runner, this should have a unique + // port.... + System::run(move || { + create_server_core(config); - System::run(move || { - let config = Configuration::new(); - create_server_core(config); - - // This appears to be bind random ... - // let srv = srv.bind("127.0.0.1:0").unwrap(); - let _ = tx.send(System::current()); - }); + // This appears to be bind random ... + // let srv = srv.bind("127.0.0.1:0").unwrap(); + let _ = tx.send(System::current()); }); - let sys = rx.recv().unwrap(); - System::set_current(sys.clone()); + }); + let sys = rx.recv().unwrap(); + System::set_current(sys.clone()); - // Do we need any fixtures? - // Yes probably, but they'll need to be futures as well ... - // later we could accept fixture as it's own future for re-use - $test_fn(); + // Do we need any fixtures? + // Yes probably, but they'll need to be futures as well ... + // later we could accept fixture as it's own future for re-use - // We DO NOT need teardown, as sqlite is in mem - // let the tables hit the floor - let _ = sys.stop(); - }}; + // Setup the client, and the address we selected. + let client = reqwest::Client::builder() + .cookie_store(true) + .build() + .expect("Unexpected reqwest builder failure!"); + let addr = format!("http://127.0.0.1:{}", port); + + test_fn(client, addr.as_str()); + + // We DO NOT need teardown, as sqlite is in mem + // let the tables hit the floor + let _ = sys.stop(); } #[test] fn test_server_proto() { - run_test!(|| { - let client = reqwest::Client::new(); - + run_test(|client: reqwest::Client, addr: &str| { let e: Entry = serde_json::from_str( r#"{ "attrs": { @@ -74,8 +87,10 @@ fn test_server_proto() { user_uuid: UUID_ADMIN.to_string(), }; + let dest = format!("{}/v1/create", addr); + let mut response = client - .post("http://127.0.0.1:8080/v1/create") + .post(dest.as_str()) .body(serde_json::to_string(&c).unwrap()) .send() .unwrap(); @@ -92,6 +107,80 @@ fn test_server_proto() { }); } +#[test] +fn test_server_whoami_anonymous() { + run_test(|client: reqwest::Client, addr: &str| { + // First show we are un-authenticated. + let whoami_dest = format!("{}/v1/whoami", addr); + let auth_dest = format!("{}/v1/auth", addr); + + let response = client.get(whoami_dest.as_str()).send().unwrap(); + + // https://docs.rs/reqwest/0.9.15/reqwest/struct.Response.html + println!("{:?}", response); + + assert!(response.status() == reqwest::StatusCode::UNAUTHORIZED); + + // Now login as anonymous + + // Setup the auth initialisation + let auth_init = AuthRequest { + step: AuthStep::Init("anonymous".to_string(), None), + }; + + let mut response = client + .post(auth_dest.as_str()) + .body(serde_json::to_string(&auth_init).unwrap()) + .send() + .unwrap(); + assert!(response.status() == reqwest::StatusCode::OK); + // Check that we got the next step + let r: AuthResponse = serde_json::from_str(response.text().unwrap().as_str()).unwrap(); + println!("==> AUTHRESPONSE ==> {:?}", r); + + assert!(match &r.state { + AuthState::Continue(all_list) => { + // Check anonymous is present? It will fail on next step if not ... + true + } + _ => false, + }); + + // Send the credentials required now + let auth_anon = AuthRequest { + step: AuthStep::Creds(vec![AuthCredential::Anonymous]), + }; + + let mut response = client + .post(auth_dest.as_str()) + .body(serde_json::to_string(&auth_anon).unwrap()) + .send() + .unwrap(); + assert!(response.status() == reqwest::StatusCode::OK); + // Check that we got the next step + let r: AuthResponse = serde_json::from_str(response.text().unwrap().as_str()).unwrap(); + println!("==> AUTHRESPONSE ==> {:?}", r); + + assert!(match &r.state { + AuthState::Success(uat) => { + println!("==> Authed as uat; {:?}", uat); + true + } + _ => false, + }); + + // Now do a whoami. + let mut response = client.get(whoami_dest.as_str()).send().unwrap(); + println!("WHOAMI -> {}", response.text().unwrap().as_str()); + println!("WHOAMI STATUS -> {}", response.status()); + assert!(response.status() == reqwest::StatusCode::OK); + + // Check the json now ... response.json() + }); +} + +// Test hitting all auth-required endpoints and assert they give unauthorized. + /* #[test] fn test_be_create_user() {