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 :)
This commit is contained in:
Firstyear 2019-09-06 13:04:58 +10:00 committed by GitHub
parent da1af02f2b
commit c798322ad8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 119 additions and 15 deletions

View file

@ -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"

View file

@ -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<AuthMessage> 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);

View file

@ -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<Box<IntegrationTestConfig>>,
}
@ -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
}

View file

@ -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";

View file

@ -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<Backend, OperationError> {
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);

View file

@ -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?

View file

@ -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<BTreeMap<Uuid, AuthSession>>,
// 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<Uuid, AuthSession>>,
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<AuthResult, OperationError> {
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));
})
}
}

View file

@ -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]

39
rsidmd/src/lib/utils.rs Normal file
View file

@ -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<u8> = 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()
);
}
}