diff --git a/rsidmd/Cargo.toml b/rsidmd/Cargo.toml index 6279cffa0..1da57c34e 100644 --- a/rsidmd/Cargo.toml +++ b/rsidmd/Cargo.toml @@ -34,7 +34,7 @@ lru = "0.1" tokio = "0.1" futures = "0.1" -uuid = { version = "0.7", features = ["serde", "v4"] } +uuid = { version = "0.7", features = ["serde", "v4" ] } serde = "1.0" serde_cbor = "0.10" serde_json = "1.0" diff --git a/rsidmd/src/lib/actors/v1.rs b/rsidmd/src/lib/actors/v1.rs index d7a63a3f9..567af6ea0 100644 --- a/rsidmd/src/lib/actors/v1.rs +++ b/rsidmd/src/lib/actors/v1.rs @@ -18,6 +18,7 @@ use rsidm_proto::v1::{ }; use actix::prelude::*; +use std::time::SystemTime; use uuid::Uuid; // These are used when the request (IE Get) has no intrising request @@ -242,10 +243,19 @@ impl Handler for QueryServerV1 { let ae = try_audit!(audit, AuthEvent::from_message(msg)); + let ct = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .expect("Clock failure!"); + + // Trigger a session clean *before* we take any auth steps. + // It's important to do this before to ensure that timeouts on + // the session are enforced. + idm_write.expire_auth_sessions(ct); + // 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) + .auth(&mut audit, &ae, ct) .and_then(|r| idm_write.commit().map(|_| r)); audit_log!(audit, "Sending result -> {:?}", r); diff --git a/rsidmd/src/lib/config.rs b/rsidmd/src/lib/config.rs index bcb416b77..12c8ca2d1 100644 --- a/rsidmd/src/lib/config.rs +++ b/rsidmd/src/lib/config.rs @@ -1,3 +1,4 @@ +use crate::utils::SID; use rand::prelude::*; use std::path::PathBuf; @@ -16,6 +17,7 @@ pub struct Configuration { pub maximum_request: usize, pub secure_cookies: bool, pub cookie_key: [u8; 32], + pub server_id: SID, pub integration_test_config: Option>, } @@ -32,10 +34,12 @@ impl Configuration { // TODO #63: default true in prd secure_cookies: if cfg!(test) { false } else { true }, cookie_key: [0; 32], + server_id: [0; 4], integration_test_config: None, }; let mut rng = StdRng::from_entropy(); rng.fill(&mut c.cookie_key); + rng.fill(&mut c.server_id); c } diff --git a/rsidmd/src/lib/constants.rs b/rsidmd/src/lib/constants.rs index dd4e692a3..fc1a69d9d 100644 --- a/rsidmd/src/lib/constants.rs +++ b/rsidmd/src/lib/constants.rs @@ -6,6 +6,8 @@ pub static PURGE_TIMEOUT: u64 = 60; // For production, 1 hour. #[cfg(not(test))] pub static PURGE_TIMEOUT: u64 = 3600; +// 5 minute auth session window. +pub static AUTH_SESSION_TIMEOUT: u64 = 300; pub static STR_UUID_ADMIN: &'static str = "00000000-0000-0000-0000-000000000000"; pub static STR_UUID_ANONYMOUS: &'static str = "00000000-0000-0000-0000-ffffffffffff"; diff --git a/rsidmd/src/lib/core.rs b/rsidmd/src/lib/core.rs index 08156b735..6edab49d2 100644 --- a/rsidmd/src/lib/core.rs +++ b/rsidmd/src/lib/core.rs @@ -21,6 +21,7 @@ use crate::idm::server::IdmServer; use crate::interval::IntervalActor; use crate::schema::Schema; use crate::server::QueryServer; +use crate::utils::SID; use rsidm_proto::v1::OperationError; use rsidm_proto::v1::{ AuthRequest, AuthState, CreateRequest, DeleteRequest, ModifyRequest, SearchRequest, @@ -274,6 +275,7 @@ fn setup_backend(config: &Configuration) -> Result { fn setup_qs_idms( audit: &mut AuditScope, be: Backend, + sid: SID, ) -> Result<(QueryServer, IdmServer), OperationError> { // Create "just enough" schema for us to be able to load from // disk ... Schema loading is one time where we validate the @@ -301,7 +303,7 @@ fn setup_qs_idms( // We generate a SINGLE idms only! - let idms = IdmServer::new(query_server.clone()); + let idms = IdmServer::new(query_server.clone(), sid); Ok((query_server, idms)) } @@ -403,7 +405,7 @@ pub fn recover_account_core(config: Configuration, name: String, password: Strin } }; // setup the qs - *with* init of the migrations and schema. - let (_qs, idms) = match setup_qs_idms(&mut audit, be) { + let (_qs, idms) = match setup_qs_idms(&mut audit, be, config.server_id.clone()) { Ok(t) => t, Err(e) => { debug!("{}", audit); @@ -459,7 +461,7 @@ pub fn create_server_core(config: Configuration) { let mut audit = AuditScope::new("setup_qs_idms"); // Start the IDM server. - let (qs, idms) = match setup_qs_idms(&mut audit, be) { + let (qs, idms) = match setup_qs_idms(&mut audit, be, config.server_id.clone()) { Ok(t) => t, Err(e) => { debug!("{}", audit); diff --git a/rsidmd/src/lib/idm/macros.rs b/rsidmd/src/lib/idm/macros.rs index 9bcd437e7..b28dabaaa 100644 --- a/rsidmd/src/lib/idm/macros.rs +++ b/rsidmd/src/lib/idm/macros.rs @@ -35,7 +35,7 @@ macro_rules! run_idm_test { .initialise_helper(&mut audit) .expect("init failed"); - let test_idm_server = IdmServer::new(test_server.clone()); + let test_idm_server = IdmServer::new(test_server.clone(), [0; 4]); $test_fn(&test_server, &test_idm_server, &mut audit); // Any needed teardown? diff --git a/rsidmd/src/lib/idm/server.rs b/rsidmd/src/lib/idm/server.rs index 8e37a3453..68313fdbb 100644 --- a/rsidmd/src/lib/idm/server.rs +++ b/rsidmd/src/lib/idm/server.rs @@ -1,9 +1,11 @@ use crate::audit::AuditScope; +use crate::constants::AUTH_SESSION_TIMEOUT; use crate::event::{AuthEvent, AuthEventStep, AuthResult}; use crate::idm::account::Account; use crate::idm::authsession::AuthSession; use crate::idm::event::PasswordChangeEvent; use crate::server::{QueryServer, QueryServerTransaction, QueryServerWriteTransaction}; +use crate::utils::{uuid_from_duration, SID}; use crate::value::PartialValue; use rsidm_proto::v1::AuthState; @@ -11,6 +13,7 @@ use rsidm_proto::v1::OperationError; use concread::cowcell::{CowCell, CowCellWriteTxn}; use std::collections::BTreeMap; +use std::time::Duration; use uuid::Uuid; pub struct IdmServer { @@ -24,6 +27,8 @@ pub struct IdmServer { sessions: CowCell>, // Need a reference to the query server. qs: QueryServer, + // thread/server id + sid: SID, } pub struct IdmServerWriteTransaction<'a> { @@ -32,6 +37,7 @@ pub struct IdmServerWriteTransaction<'a> { // things like authentication sessions: CowCellWriteTxn<'a, BTreeMap>, qs: &'a QueryServer, + sid: &'a SID, } /* @@ -50,10 +56,11 @@ pub struct IdmServerProxyWriteTransaction<'a> { impl IdmServer { // TODO #59: Make number of authsessions configurable!!! - pub fn new(qs: QueryServer) -> IdmServer { + pub fn new(qs: QueryServer, sid: SID) -> IdmServer { IdmServer { sessions: CowCell::new(BTreeMap::new()), qs: qs, + sid: sid, } } @@ -61,6 +68,7 @@ impl IdmServer { IdmServerWriteTransaction { sessions: self.sessions.write(), qs: &self.qs, + sid: &self.sid, } } @@ -78,10 +86,26 @@ impl IdmServer { } impl<'a> IdmServerWriteTransaction<'a> { + #[cfg(test)] + pub fn is_sessionid_present(&self, sessionid: &Uuid) -> bool { + self.sessions.contains_key(sessionid) + } + + pub fn expire_auth_sessions(&mut self, ct: Duration) { + // ct is current time - sub the timeout. and then split. + let expire = ct - Duration::from_secs(AUTH_SESSION_TIMEOUT); + let split_at = uuid_from_duration(expire, self.sid); + let valid = self.sessions.split_off(&split_at); + // swap them? + *self.sessions = valid; + // expired will now be dropped, and can't be used by future sessions. + } + pub fn auth( &mut self, au: &mut AuditScope, ae: &AuthEvent, + ct: Duration, ) -> Result { audit_log!(au, "Received AuthEvent -> {:?}", ae); @@ -91,7 +115,7 @@ impl<'a> IdmServerWriteTransaction<'a> { AuthEventStep::Init(init) => { // Allocate a session id. // TODO: #60 - make this new_v1 and use the tstamp. - let sessionid = Uuid::new_v4(); + let sessionid = uuid_from_duration(ct, self.sid); // Begin the auth procedure! // Start a read @@ -257,7 +281,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> { #[cfg(test)] mod tests { - use crate::constants::UUID_ADMIN; + use crate::constants::{AUTH_SESSION_TIMEOUT, UUID_ADMIN}; use crate::credential::Credential; use crate::event::{AuthEvent, AuthResult, ModifyEvent}; use crate::idm::event::PasswordChangeEvent; @@ -269,10 +293,13 @@ mod tests { use crate::audit::AuditScope; use crate::idm::server::IdmServer; use crate::server::QueryServer; + use std::time::Duration; use uuid::Uuid; static TEST_PASSWORD: &'static str = "ntaoeuntnaoeuhraohuercahu😍"; static TEST_PASSWORD_INC: &'static str = "ntaoentu nkrcgaeunhibwmwmqj;k wqjbkx "; + static TEST_CURRENT_TIME: u64 = 6000; + static TEST_CURRENT_EXPIRE: u64 = TEST_CURRENT_TIME + AUTH_SESSION_TIMEOUT + 1; #[test] fn test_idm_anonymous_auth() { @@ -283,7 +310,7 @@ mod tests { // 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); + let r1 = idms_write.auth(au, &anon_init, Duration::from_secs(TEST_CURRENT_TIME)); /* Some weird lifetime shit happens here ... */ // audit_log!(au, "r1 ==> {:?}", r1); @@ -327,7 +354,7 @@ mod tests { let anon_step = AuthEvent::cred_step_anonymous(sid); // Expect success - let r2 = idms_write.auth(au, &anon_step); + let r2 = idms_write.auth(au, &anon_step, Duration::from_secs(TEST_CURRENT_TIME)); println!("r2 ==> {:?}", r2); match r2 { @@ -370,7 +397,7 @@ mod tests { let anon_step = AuthEvent::cred_step_anonymous(sid); // Expect failure - let r2 = idms_write.auth(au, &anon_step); + let r2 = idms_write.auth(au, &anon_step, Duration::from_secs(TEST_CURRENT_TIME)); println!("r2 ==> {:?}", r2); match r2 { @@ -416,7 +443,7 @@ mod tests { let mut idms_write = idms.write(); let admin_init = AuthEvent::named_init("admin"); - let r1 = idms_write.auth(au, &admin_init); + let r1 = idms_write.auth(au, &admin_init, Duration::from_secs(TEST_CURRENT_TIME)); let ar = r1.unwrap(); let AuthResult { sessionid, state } = ar; @@ -443,7 +470,7 @@ mod tests { let anon_step = AuthEvent::cred_step_password(sid, TEST_PASSWORD); // Expect success - let r2 = idms_write.auth(au, &anon_step); + let r2 = idms_write.auth(au, &anon_step, Duration::from_secs(TEST_CURRENT_TIME)); println!("r2 ==> {:?}", r2); match r2 { @@ -482,7 +509,7 @@ mod tests { let anon_step = AuthEvent::cred_step_password(sid, TEST_PASSWORD_INC); // Expect success - let r2 = idms_write.auth(au, &anon_step); + let r2 = idms_write.auth(au, &anon_step, Duration::from_secs(TEST_CURRENT_TIME)); println!("r2 ==> {:?}", r2); match r2 { @@ -523,4 +550,23 @@ mod tests { assert!(idms_prox_write.commit(au).is_ok()); }) } + + #[test] + fn test_idm_session_expire() { + run_idm_test!(|qs: &QueryServer, idms: &IdmServer, au: &mut AuditScope| { + init_admin_w_password(au, qs, TEST_PASSWORD).expect("Failed to setup admin account"); + let sid = init_admin_authsession_sid(idms, au); + let mut idms_write = idms.write(); + assert!(idms_write.is_sessionid_present(&sid)); + // Expire like we are currently "now". Should not affect our session. + idms_write.expire_auth_sessions(Duration::from_secs(TEST_CURRENT_TIME)); + assert!(idms_write.is_sessionid_present(&sid)); + // Expire as though we are in the future. + idms_write.expire_auth_sessions(Duration::from_secs(TEST_CURRENT_EXPIRE)); + assert!(!idms_write.is_sessionid_present(&sid)); + assert!(idms_write.commit().is_ok()); + let idms_write = idms.write(); + assert!(!idms_write.is_sessionid_present(&sid)); + }) + } } diff --git a/rsidmd/src/lib/lib.rs b/rsidmd/src/lib/lib.rs index bff102609..baea23c29 100644 --- a/rsidmd/src/lib/lib.rs +++ b/rsidmd/src/lib/lib.rs @@ -12,6 +12,7 @@ extern crate lazy_static; // This has to be before be so the import order works #[macro_use] mod macros; +mod utils; #[macro_use] mod async_log; #[macro_use] diff --git a/rsidmd/src/lib/utils.rs b/rsidmd/src/lib/utils.rs new file mode 100644 index 000000000..efe28f345 --- /dev/null +++ b/rsidmd/src/lib/utils.rs @@ -0,0 +1,39 @@ +use std::time::Duration; +use uuid::{Builder, Uuid}; + +pub type SID = [u8; 4]; + +fn uuid_from_u64_u32(a: u64, b: u32, sid: &SID) -> Uuid { + let mut v: Vec = Vec::with_capacity(16); + v.extend_from_slice(&a.to_be_bytes()); + v.extend_from_slice(&b.to_be_bytes()); + v.extend_from_slice(sid); + + Builder::from_slice(v.as_slice()).unwrap().build() +} + +// SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap(); +pub fn uuid_from_duration(d: Duration, sid: &SID) -> Uuid { + uuid_from_u64_u32(d.as_secs(), d.subsec_nanos(), sid) +} + +#[cfg(test)] +mod tests { + use crate::utils::uuid_from_duration; + use std::time::Duration; + + #[test] + fn test_utils_uuid_from_duration() { + let u1 = uuid_from_duration(Duration::from_secs(1), &[0xff; 4]); + assert_eq!( + "00000000-0000-0001-0000-0000ffffffff", + u1.to_hyphenated().to_string() + ); + + let u2 = uuid_from_duration(Duration::from_secs(1000), &[0xff; 4]); + assert_eq!( + "00000000-0000-03e8-0000-0000ffffffff", + u2.to_hyphenated().to_string() + ); + } +}