mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 12:37:00 +01:00
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:
parent
da1af02f2b
commit
c798322ad8
|
@ -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"
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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));
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
39
rsidmd/src/lib/utils.rs
Normal 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()
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue