From c798322ad8cd72f3f33d8738e4cb3606ee92fdcc Mon Sep 17 00:00:00 2001 From: Firstyear Date: Fri, 6 Sep 2019 13:04:58 +1000 Subject: [PATCH] 60 authsession gc (#80) Implements #60 authsession garbage collection. If we assume that an authsession is around 1024 bytes (this assumes a 16 char name + groups + claims) then this means that in 1Gb of ram we can store about 1 million in progress auth attempts. Obviously, we don't want infinite memory growth, but we can't use an LRU cache due to the future desire to use concurrent trees. So instead we prune the tree based on a timeout when we start and auth operation. Auth session id's are generated from a timestamp similar to how we'll generate replication csn's. We can then apply a diff that will split all items lower than the csn/sid and remove them from future consideration. We set the default timeout to 5 minutes. This means that assuming 10,000 auths per second, we would require 3GB of ram to process these sessions before they are expired. We expect any deployment with such large loadings can affort 3Gb of ram :) --- rsidmd/Cargo.toml | 2 +- rsidmd/src/lib/actors/v1.rs | 12 ++++++- rsidmd/src/lib/config.rs | 4 +++ rsidmd/src/lib/constants.rs | 2 ++ rsidmd/src/lib/core.rs | 8 +++-- rsidmd/src/lib/idm/macros.rs | 2 +- rsidmd/src/lib/idm/server.rs | 64 +++++++++++++++++++++++++++++++----- rsidmd/src/lib/lib.rs | 1 + rsidmd/src/lib/utils.rs | 39 ++++++++++++++++++++++ 9 files changed, 119 insertions(+), 15 deletions(-) create mode 100644 rsidmd/src/lib/utils.rs 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() + ); + } +}