From 99b761c96600871509accb6eb146539cb9ee169a Mon Sep 17 00:00:00 2001 From: Firstyear Date: Fri, 28 Jul 2023 10:48:56 +1000 Subject: [PATCH] 20230727 unix int modularity (#1907) --- Cargo.lock | 2 +- proto/src/v1.rs | 4 +- server/lib/src/idm/unix.rs | 4 +- server/testkit/tests/proto_v1_test.rs | 4 +- unix_integration/Cargo.toml | 2 +- unix_integration/src/client.rs | 27 +- unix_integration/src/daemon.rs | 11 +- unix_integration/src/db.rs | 157 +++---- unix_integration/src/idprovider/interface.rs | 67 +++ unix_integration/src/idprovider/kanidm.rs | 264 ++++++++++++ unix_integration/src/idprovider/mod.rs | 2 + unix_integration/src/lib.rs | 2 + unix_integration/src/resolver.rs | 413 +++++++------------ unix_integration/src/ssh_authorizedkeys.rs | 2 +- unix_integration/src/tool.rs | 11 +- unix_integration/tests/cache_layer_test.rs | 10 +- 16 files changed, 614 insertions(+), 368 deletions(-) create mode 100644 unix_integration/src/idprovider/interface.rs create mode 100644 unix_integration/src/idprovider/kanidm.rs create mode 100644 unix_integration/src/idprovider/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 3eb894328..b8eb5c572 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2674,7 +2674,6 @@ dependencies = [ "lru 0.8.1", "notify-debouncer-full", "profiles", - "reqwest", "rpassword 7.2.0", "rusqlite", "selinux", @@ -2687,6 +2686,7 @@ dependencies = [ "tracing", "tss-esapi", "users", + "uuid", "walkdir", ] diff --git a/proto/src/v1.rs b/proto/src/v1.rs index 9ec4f6b1f..95ca14ff3 100644 --- a/proto/src/v1.rs +++ b/proto/src/v1.rs @@ -556,7 +556,7 @@ impl fmt::Display for RadiusAuthToken { pub struct UnixGroupToken { pub name: String, pub spn: String, - pub uuid: String, + pub uuid: Uuid, pub gidnumber: u32, } @@ -580,7 +580,7 @@ pub struct UnixUserToken { pub spn: String, pub displayname: String, pub gidnumber: u32, - pub uuid: String, + pub uuid: Uuid, #[serde(skip_serializing_if = "Option::is_none")] pub shell: Option, pub groups: Vec, diff --git a/server/lib/src/idm/unix.rs b/server/lib/src/idm/unix.rs index cbbe23c77..3e5e2000b 100644 --- a/server/lib/src/idm/unix.rs +++ b/server/lib/src/idm/unix.rs @@ -150,7 +150,7 @@ impl UnixUserAccount { spn: self.spn.clone(), displayname: self.displayname.clone(), gidnumber: self.gidnumber, - uuid: self.uuid.as_hyphenated().to_string(), + uuid: self.uuid, shell: self.shell.clone(), groups, sshkeys: self.sshkeys.clone(), @@ -449,7 +449,7 @@ impl UnixGroup { Ok(UnixGroupToken { name: self.name.clone(), spn: self.spn.clone(), - uuid: self.uuid.as_hyphenated().to_string(), + uuid: self.uuid, gidnumber: self.gidnumber, }) } diff --git a/server/testkit/tests/proto_v1_test.rs b/server/testkit/tests/proto_v1_test.rs index 0ec46416d..cb983e6e2 100644 --- a/server/testkit/tests/proto_v1_test.rs +++ b/server/testkit/tests/proto_v1_test.rs @@ -563,7 +563,7 @@ async fn test_server_rest_posix_lifecycle(rsclient: KanidmClient) { .unwrap(); // get the account by uuid let r3 = rsclient - .idm_account_unix_token_get(r.uuid.as_str()) + .idm_account_unix_token_get(&r.uuid.hyphenated().to_string()) .await .unwrap(); @@ -590,7 +590,7 @@ async fn test_server_rest_posix_lifecycle(rsclient: KanidmClient) { .unwrap(); // get the group by uuid let r3 = rsclient - .idm_group_unix_token_get(r.uuid.as_str()) + .idm_group_unix_token_get(&r.uuid.hyphenated().to_string()) .await .unwrap(); diff --git a/unix_integration/Cargo.toml b/unix_integration/Cargo.toml index 8c54dd343..c2fc4f803 100644 --- a/unix_integration/Cargo.toml +++ b/unix_integration/Cargo.toml @@ -69,7 +69,7 @@ tokio = { workspace = true, features = ["rt", "fs", "macros", "sync", "time", "n tokio-util = { workspace = true, features = ["codec"] } tracing = { workspace = true } tss-esapi = { workspace = true, optional = true } -reqwest = { workspace = true, default-features = false } +uuid = { workspace = true } walkdir = { workspace = true } [target.'cfg(not(target_family = "windows"))'.dependencies] diff --git a/unix_integration/src/client.rs b/unix_integration/src/client.rs index 37d51179e..e28ccbe54 100644 --- a/unix_integration/src/client.rs +++ b/unix_integration/src/client.rs @@ -5,6 +5,7 @@ use bytes::{BufMut, BytesMut}; use futures::{SinkExt, StreamExt}; use tokio::net::UnixStream; // use tokio::runtime::Builder; +use tokio::time::{self, Duration}; use tokio_util::codec::Framed; use tokio_util::codec::{Decoder, Encoder}; @@ -48,8 +49,10 @@ impl ClientCodec { } } -/// Makes a call to kanidm_unixd via a unix socket at `path` -pub async fn call_daemon(path: &str, req: ClientRequest) -> Result> { +async fn call_daemon_inner( + path: &str, + req: ClientRequest, +) -> Result> { trace!(?path, ?req); let stream = UnixStream::connect(path).await?; trace!("connected"); @@ -71,3 +74,23 @@ pub async fn call_daemon(path: &str, req: ClientRequest) -> Result Result> { + let sleep = time::sleep(Duration::from_millis(timeout)); + tokio::pin!(sleep); + + tokio::select! { + _ = &mut sleep => { + error!("Timed out making request to kanidm_unixd"); + Err(Box::new(IoError::new(ErrorKind::Other, "timeout"))) + } + res = call_daemon_inner(path, req) => { + res + } + } +} diff --git a/unix_integration/src/daemon.rs b/unix_integration/src/daemon.rs index 763e43e05..e772fd340 100644 --- a/unix_integration/src/daemon.rs +++ b/unix_integration/src/daemon.rs @@ -27,6 +27,7 @@ use kanidm_client::KanidmClientBuilder; use kanidm_proto::constants::DEFAULT_CLIENT_CONFIG_PATH; use kanidm_unix_common::constants::DEFAULT_CONFIG_PATH; use kanidm_unix_common::db::Db; +use kanidm_unix_common::idprovider::kanidm::KanidmProvider; use kanidm_unix_common::resolver::Resolver; use kanidm_unix_common::unix_config::KanidmUnixdConfig; use kanidm_unix_common::unix_passwd::{parse_etc_group, parse_etc_passwd}; @@ -182,7 +183,7 @@ async fn handle_task_client( async fn handle_client( sock: UnixStream, - cachelayer: Arc, + cachelayer: Arc>, task_channel_tx: &Sender, ) -> Result<(), Box> { debug!("Accepted connection"); @@ -372,7 +373,9 @@ async fn handle_client( Ok(()) } -async fn process_etc_passwd_group(cachelayer: &Resolver) -> Result<(), Box> { +async fn process_etc_passwd_group( + cachelayer: &Resolver, +) -> Result<(), Box> { let mut file = File::open("/etc/passwd").await?; let mut contents = vec![]; file.read_to_end(&mut contents).await?; @@ -661,6 +664,8 @@ async fn main() -> ExitCode { } }; + let idprovider = KanidmProvider::new(rsclient); + let db = match Db::new(cfg.db_path.as_str(), &cfg.tpm_policy) { Ok(db) => db, Err(_e) => { @@ -671,8 +676,8 @@ async fn main() -> ExitCode { let cl_inner = match Resolver::new( db, + idprovider, cfg.cache_timeout, - rsclient, cfg.pam_allowed_login_groups.clone(), cfg.default_shell.clone(), cfg.home_prefix.clone(), diff --git a/unix_integration/src/db.rs b/unix_integration/src/db.rs index 561c8fa40..019387dc6 100644 --- a/unix_integration/src/db.rs +++ b/unix_integration/src/db.rs @@ -2,17 +2,16 @@ use std::convert::TryFrom; use std::fmt; use std::time::Duration; +use crate::idprovider::interface::{GroupToken, Id, UserToken}; use crate::unix_config::TpmPolicy; use async_trait::async_trait; use kanidm_lib_crypto::CryptoPolicy; use kanidm_lib_crypto::DbPasswordV1; use kanidm_lib_crypto::Password; -use kanidm_proto::v1::{UnixGroupToken, UnixUserToken}; use libc::umask; use rusqlite::Connection; use tokio::sync::{Mutex, MutexGuard}; - -use crate::resolver::Id; +use uuid::Uuid; #[async_trait] pub trait Cache { @@ -43,27 +42,27 @@ pub trait CacheTxn { fn clear(&self) -> Result<(), CacheError>; - fn get_account(&self, account_id: &Id) -> Result, CacheError>; + fn get_account(&self, account_id: &Id) -> Result, CacheError>; - fn get_accounts(&self) -> Result, CacheError>; + fn get_accounts(&self) -> Result, CacheError>; - fn update_account(&self, account: &UnixUserToken, expire: u64) -> Result<(), CacheError>; + fn update_account(&self, account: &UserToken, expire: u64) -> Result<(), CacheError>; - fn delete_account(&self, a_uuid: &str) -> Result<(), CacheError>; + fn delete_account(&self, a_uuid: Uuid) -> Result<(), CacheError>; - fn update_account_password(&self, a_uuid: &str, cred: &str) -> Result<(), CacheError>; + fn update_account_password(&self, a_uuid: Uuid, cred: &str) -> Result<(), CacheError>; - fn check_account_password(&self, a_uuid: &str, cred: &str) -> Result; + fn check_account_password(&self, a_uuid: Uuid, cred: &str) -> Result; - fn get_group(&self, grp_id: &Id) -> Result, CacheError>; + fn get_group(&self, grp_id: &Id) -> Result, CacheError>; - fn get_group_members(&self, g_uuid: &str) -> Result, CacheError>; + fn get_group_members(&self, g_uuid: Uuid) -> Result, CacheError>; - fn get_groups(&self) -> Result, CacheError>; + fn get_groups(&self) -> Result, CacheError>; - fn update_group(&self, grp: &UnixGroupToken, expire: u64) -> Result<(), CacheError>; + fn update_group(&self, grp: &GroupToken, expire: u64) -> Result<(), CacheError>; - fn delete_group(&self, g_uuid: &str) -> Result<(), CacheError>; + fn delete_group(&self, g_uuid: Uuid) -> Result<(), CacheError>; } pub struct Db { @@ -208,7 +207,7 @@ impl<'a> DbTxn<'a> { fn get_group_data_name(&self, grp_id: &str) -> Result, i64)>, CacheError> { let mut stmt = self.conn .prepare( - "SELECT token, expiry FROM group_t WHERE uuid = :grp_id OR name = :grp_id OR spn = :grp_id" + "SELECT token, expiry FROM group_t WHERE uuid = :grp_id OR name = :grp_id OR spn = :grp_id" ) .map_err(|e| { self.sqlite_error("select prepare", &e) @@ -347,7 +346,7 @@ impl<'a> CacheTxn for DbTxn<'a> { Ok(()) } - fn get_account(&self, account_id: &Id) -> Result, CacheError> { + fn get_account(&self, account_id: &Id) -> Result, CacheError> { let data = match account_id { Id::Name(n) => self.get_account_data_name(n.as_str()), Id::Gid(g) => self.get_account_data_gid(*g), @@ -381,7 +380,7 @@ impl<'a> CacheTxn for DbTxn<'a> { } } - fn get_accounts(&self) -> Result, CacheError> { + fn get_accounts(&self) -> Result, CacheError> { let mut stmt = self .conn .prepare("SELECT token FROM account_t") @@ -410,7 +409,7 @@ impl<'a> CacheTxn for DbTxn<'a> { .collect()) } - fn update_account(&self, account: &UnixUserToken, expire: u64) -> Result<(), CacheError> { + fn update_account(&self, account: &UserToken, expire: u64) -> Result<(), CacheError> { let data = serde_json::to_vec(account).map_err(|e| { error!("update_account json error -> {:?}", e); CacheError::SerdeJson @@ -423,11 +422,12 @@ impl<'a> CacheTxn for DbTxn<'a> { // This is needed because sqlites 'insert or replace into', will null the password field // if present, and upsert MUST match the exact conflicting column, so that means we have // to manually manage the update or insert :( :( + let account_uuid = account.uuid.as_hyphenated().to_string(); // Find anything conflicting and purge it. self.conn.execute("DELETE FROM account_t WHERE NOT uuid = :uuid AND (name = :name OR spn = :spn OR gidnumber = :gidnumber)", named_params!{ - ":uuid": &account.uuid, + ":uuid": &account_uuid, ":name": &account.name, ":spn": &account.spn, ":gidnumber": &account.gidnumber, @@ -441,7 +441,7 @@ impl<'a> CacheTxn for DbTxn<'a> { let updated = self.conn.execute( "UPDATE account_t SET name=:name, spn=:spn, gidnumber=:gidnumber, token=:token, expiry=:expiry WHERE uuid = :uuid", named_params!{ - ":uuid": &account.uuid, + ":uuid": &account_uuid, ":name": &account.name, ":spn": &account.spn, ":gidnumber": &account.gidnumber, @@ -461,7 +461,7 @@ impl<'a> CacheTxn for DbTxn<'a> { })?; stmt.execute(named_params! { - ":uuid": &account.uuid, + ":uuid": &account_uuid, ":name": &account.name, ":spn": &account.spn, ":gidnumber": &account.gidnumber, @@ -482,7 +482,7 @@ impl<'a> CacheTxn for DbTxn<'a> { .prepare("DELETE FROM memberof_t WHERE a_uuid = :a_uuid") .map_err(|e| self.sqlite_error("prepare", &e))?; - stmt.execute([&account.uuid]) + stmt.execute([&account_uuid]) .map(|r| { debug!("delete memberships -> {:?}", r); }) @@ -495,8 +495,8 @@ impl<'a> CacheTxn for DbTxn<'a> { // Now for each group, add the relation. account.groups.iter().try_for_each(|g| { stmt.execute(named_params! { - ":a_uuid": &account.uuid, - ":g_uuid": &g.uuid, + ":a_uuid": &account_uuid, + ":g_uuid": &g.uuid.as_hyphenated().to_string(), }) .map(|r| { debug!("insert membership -> {:?}", r); @@ -505,11 +505,13 @@ impl<'a> CacheTxn for DbTxn<'a> { }) } - fn delete_account(&self, a_uuid: &str) -> Result<(), CacheError> { + fn delete_account(&self, a_uuid: Uuid) -> Result<(), CacheError> { + let account_uuid = a_uuid.as_hyphenated().to_string(); + self.conn .execute( "DELETE FROM memberof_t WHERE a_uuid = :a_uuid", - params![a_uuid], + params![&account_uuid], ) .map(|_| ()) .map_err(|e| self.sqlite_error("account_t memberof_t cascade delete", &e))?; @@ -517,13 +519,13 @@ impl<'a> CacheTxn for DbTxn<'a> { self.conn .execute( "DELETE FROM account_t WHERE uuid = :a_uuid", - params![a_uuid], + params![&account_uuid], ) .map(|_| ()) .map_err(|e| self.sqlite_error("account_t delete", &e)) } - fn update_account_password(&self, a_uuid: &str, cred: &str) -> Result<(), CacheError> { + fn update_account_password(&self, a_uuid: Uuid, cred: &str) -> Result<(), CacheError> { #[allow(unused_variables)] let pw = if let Some(tcti_str) = self.require_tpm { // Do nothing. @@ -552,7 +554,7 @@ impl<'a> CacheTxn for DbTxn<'a> { .execute( "UPDATE account_t SET password = :data WHERE uuid = :a_uuid", named_params! { - ":a_uuid": &a_uuid, + ":a_uuid": &a_uuid.as_hyphenated().to_string(), ":data": &data, }, ) @@ -560,7 +562,7 @@ impl<'a> CacheTxn for DbTxn<'a> { .map(|_| ()) } - fn check_account_password(&self, a_uuid: &str, cred: &str) -> Result { + fn check_account_password(&self, a_uuid: Uuid, cred: &str) -> Result { #[cfg(not(feature = "tpm"))] if self.require_tpm.is_some() { return Ok(false); @@ -573,7 +575,7 @@ impl<'a> CacheTxn for DbTxn<'a> { // Makes tuple (token, expiry) let data_iter = stmt - .query_map([a_uuid], |row| row.get(0)) + .query_map([a_uuid.as_hyphenated().to_string()], |row| row.get(0)) .map_err(|e| self.sqlite_error("query_map", &e))?; let data: Result>, _> = data_iter .map(|v| v.map_err(|e| self.sqlite_error("map", &e))) @@ -622,7 +624,7 @@ impl<'a> CacheTxn for DbTxn<'a> { } } - fn get_group(&self, grp_id: &Id) -> Result, CacheError> { + fn get_group(&self, grp_id: &Id) -> Result, CacheError> { let data = match grp_id { Id::Name(n) => self.get_group_data_name(n.as_str()), Id::Gid(g) => self.get_group_data_gid(*g), @@ -656,7 +658,7 @@ impl<'a> CacheTxn for DbTxn<'a> { } } - fn get_group_members(&self, g_uuid: &str) -> Result, CacheError> { + fn get_group_members(&self, g_uuid: Uuid) -> Result, CacheError> { let mut stmt = self .conn .prepare("SELECT account_t.token FROM (account_t, memberof_t) WHERE account_t.uuid = memberof_t.a_uuid AND memberof_t.g_uuid = :g_uuid") @@ -665,7 +667,7 @@ impl<'a> CacheTxn for DbTxn<'a> { })?; let data_iter = stmt - .query_map([g_uuid], |row| row.get(0)) + .query_map([g_uuid.as_hyphenated().to_string()], |row| row.get(0)) .map_err(|e| self.sqlite_error("query_map", &e))?; let data: Result>, _> = data_iter .map(|v| v.map_err(|e| self.sqlite_error("map", &e))) @@ -685,7 +687,7 @@ impl<'a> CacheTxn for DbTxn<'a> { .collect() } - fn get_groups(&self) -> Result, CacheError> { + fn get_groups(&self) -> Result, CacheError> { let mut stmt = self .conn .prepare("SELECT token FROM group_t") @@ -714,7 +716,7 @@ impl<'a> CacheTxn for DbTxn<'a> { .collect()) } - fn update_group(&self, grp: &UnixGroupToken, expire: u64) -> Result<(), CacheError> { + fn update_group(&self, grp: &GroupToken, expire: u64) -> Result<(), CacheError> { let data = serde_json::to_vec(grp).map_err(|e| { error!("json error -> {:?}", e); CacheError::SerdeJson @@ -730,8 +732,9 @@ impl<'a> CacheTxn for DbTxn<'a> { self.sqlite_error("prepare", &e) })?; + // We have to to-str uuid as the sqlite impl makes it a blob which breaks our selects in get. stmt.execute(named_params! { - ":uuid": &grp.uuid, + ":uuid": &grp.uuid.as_hyphenated().to_string(), ":name": &grp.name, ":spn": &grp.spn, ":gidnumber": &grp.gidnumber, @@ -744,13 +747,17 @@ impl<'a> CacheTxn for DbTxn<'a> { .map_err(|e| self.sqlite_error("execute", &e)) } - fn delete_group(&self, g_uuid: &str) -> Result<(), CacheError> { + fn delete_group(&self, g_uuid: Uuid) -> Result<(), CacheError> { + let group_uuid = g_uuid.as_hyphenated().to_string(); self.conn - .execute("DELETE FROM memberof_t WHERE g_uuid = :g_uuid", [g_uuid]) + .execute( + "DELETE FROM memberof_t WHERE g_uuid = :g_uuid", + [&group_uuid], + ) .map(|_| ()) .map_err(|e| self.sqlite_error("group_t memberof_t cascade delete", &e))?; self.conn - .execute("DELETE FROM group_t WHERE uuid = :g_uuid", [g_uuid]) + .execute("DELETE FROM group_t WHERE uuid = :g_uuid", [&group_uuid]) .map(|_| ()) .map_err(|e| self.sqlite_error("group_t delete", &e)) } @@ -1129,11 +1136,9 @@ pub(crate) mod tpm { #[cfg(test)] mod tests { - use kanidm_proto::v1::{UnixGroupToken, UnixUserToken}; // use std::assert_matches::assert_matches; - use super::{Cache, CacheTxn, Db}; - use crate::resolver::Id; + use crate::idprovider::interface::{GroupToken, Id, UserToken}; use crate::unix_config::TpmPolicy; const TESTACCOUNT1_PASSWORD_A: &str = "password a for account1 test"; @@ -1146,12 +1151,12 @@ mod tests { let dbtxn = db.write().await; assert!(dbtxn.migrate().is_ok()); - let mut ut1 = UnixUserToken { + let mut ut1 = UserToken { name: "testuser".to_string(), spn: "testuser@example.com".to_string(), displayname: "Test User".to_string(), gidnumber: 2000, - uuid: "0302b99c-f0f6-41ab-9492-852692b0fd16".to_string(), + uuid: uuid::uuid!("0302b99c-f0f6-41ab-9492-852692b0fd16"), shell: None, groups: Vec::new(), sshkeys: vec!["key-a".to_string()], @@ -1230,11 +1235,11 @@ mod tests { let dbtxn = db.write().await; assert!(dbtxn.migrate().is_ok()); - let mut gt1 = UnixGroupToken { + let mut gt1 = GroupToken { name: "testgroup".to_string(), spn: "testgroup@example.com".to_string(), gidnumber: 2000, - uuid: "0302b99c-f0f6-41ab-9492-852692b0fd16".to_string(), + uuid: uuid::uuid!("0302b99c-f0f6-41ab-9492-852692b0fd16"), }; let id_name = Id::Name("testgroup".to_string()); @@ -1305,26 +1310,26 @@ mod tests { let dbtxn = db.write().await; assert!(dbtxn.migrate().is_ok()); - let gt1 = UnixGroupToken { + let gt1 = GroupToken { name: "testuser".to_string(), spn: "testuser@example.com".to_string(), gidnumber: 2000, - uuid: "0302b99c-f0f6-41ab-9492-852692b0fd16".to_string(), + uuid: uuid::uuid!("0302b99c-f0f6-41ab-9492-852692b0fd16"), }; - let gt2 = UnixGroupToken { + let gt2 = GroupToken { name: "testgroup".to_string(), spn: "testgroup@example.com".to_string(), gidnumber: 2001, - uuid: "b500be97-8552-42a5-aca0-668bc5625705".to_string(), + uuid: uuid::uuid!("b500be97-8552-42a5-aca0-668bc5625705"), }; - let mut ut1 = UnixUserToken { + let mut ut1 = UserToken { name: "testuser".to_string(), spn: "testuser@example.com".to_string(), displayname: "Test User".to_string(), gidnumber: 2000, - uuid: "0302b99c-f0f6-41ab-9492-852692b0fd16".to_string(), + uuid: uuid::uuid!("0302b99c-f0f6-41ab-9492-852692b0fd16"), shell: None, groups: vec![gt1.clone(), gt2], sshkeys: vec!["key-a".to_string()], @@ -1341,10 +1346,10 @@ mod tests { // Now, get the memberships of the two groups. let m1 = dbtxn - .get_group_members("0302b99c-f0f6-41ab-9492-852692b0fd16") + .get_group_members(uuid::uuid!("0302b99c-f0f6-41ab-9492-852692b0fd16")) .unwrap(); let m2 = dbtxn - .get_group_members("b500be97-8552-42a5-aca0-668bc5625705") + .get_group_members(uuid::uuid!("b500be97-8552-42a5-aca0-668bc5625705")) .unwrap(); assert!(m1[0].name == "testuser"); assert!(m2[0].name == "testuser"); @@ -1355,10 +1360,10 @@ mod tests { // Check that the memberships have updated correctly. let m1 = dbtxn - .get_group_members("0302b99c-f0f6-41ab-9492-852692b0fd16") + .get_group_members(uuid::uuid!("0302b99c-f0f6-41ab-9492-852692b0fd16")) .unwrap(); let m2 = dbtxn - .get_group_members("b500be97-8552-42a5-aca0-668bc5625705") + .get_group_members(uuid::uuid!("b500be97-8552-42a5-aca0-668bc5625705")) .unwrap(); assert!(m1[0].name == "testuser"); assert!(m2.is_empty()); @@ -1381,13 +1386,13 @@ mod tests { let dbtxn = db.write().await; assert!(dbtxn.migrate().is_ok()); - let uuid1 = "0302b99c-f0f6-41ab-9492-852692b0fd16"; - let mut ut1 = UnixUserToken { + let uuid1 = uuid::uuid!("0302b99c-f0f6-41ab-9492-852692b0fd16"); + let mut ut1 = UserToken { name: "testuser".to_string(), spn: "testuser@example.com".to_string(), displayname: "Test User".to_string(), gidnumber: 2000, - uuid: "0302b99c-f0f6-41ab-9492-852692b0fd16".to_string(), + uuid: uuid1, shell: None, groups: Vec::new(), sshkeys: vec!["key-a".to_string()], @@ -1451,18 +1456,18 @@ mod tests { let dbtxn = db.write().await; assert!(dbtxn.migrate().is_ok()); - let mut gt1 = UnixGroupToken { + let mut gt1 = GroupToken { name: "testgroup".to_string(), spn: "testgroup@example.com".to_string(), gidnumber: 2000, - uuid: "0302b99c-f0f6-41ab-9492-852692b0fd16".to_string(), + uuid: uuid::uuid!("0302b99c-f0f6-41ab-9492-852692b0fd16"), }; - let gt2 = UnixGroupToken { + let gt2 = GroupToken { name: "testgroup".to_string(), spn: "testgroup@example.com".to_string(), gidnumber: 2001, - uuid: "799123b2-3802-4b19-b0b8-1ffae2aa9a4b".to_string(), + uuid: uuid::uuid!("799123b2-3802-4b19-b0b8-1ffae2aa9a4b"), }; let id_name = Id::Name("testgroup".to_string()); @@ -1475,7 +1480,7 @@ mod tests { // test adding a group dbtxn.update_group(>1, 0).unwrap(); let r0 = dbtxn.get_group(&id_name).unwrap(); - assert!(r0.unwrap().0.uuid == "0302b99c-f0f6-41ab-9492-852692b0fd16"); + assert!(r0.unwrap().0.uuid == uuid::uuid!("0302b99c-f0f6-41ab-9492-852692b0fd16")); // Do the "rename" of gt1 which is what would allow gt2 to be valid. gt1.name = "testgroup2".to_string(); @@ -1483,7 +1488,7 @@ mod tests { // Now, add gt2 which dups on gt1 name/spn. dbtxn.update_group(>2, 0).unwrap(); let r2 = dbtxn.get_group(&id_name).unwrap(); - assert!(r2.unwrap().0.uuid == "799123b2-3802-4b19-b0b8-1ffae2aa9a4b"); + assert!(r2.unwrap().0.uuid == uuid::uuid!("799123b2-3802-4b19-b0b8-1ffae2aa9a4b")); let r3 = dbtxn.get_group(&id_name2).unwrap(); assert!(r3.is_none()); @@ -1492,9 +1497,9 @@ mod tests { // Both now coexist let r4 = dbtxn.get_group(&id_name).unwrap(); - assert!(r4.unwrap().0.uuid == "799123b2-3802-4b19-b0b8-1ffae2aa9a4b"); + assert!(r4.unwrap().0.uuid == uuid::uuid!("799123b2-3802-4b19-b0b8-1ffae2aa9a4b")); let r5 = dbtxn.get_group(&id_name2).unwrap(); - assert!(r5.unwrap().0.uuid == "0302b99c-f0f6-41ab-9492-852692b0fd16"); + assert!(r5.unwrap().0.uuid == uuid::uuid!("0302b99c-f0f6-41ab-9492-852692b0fd16")); assert!(dbtxn.commit().is_ok()); } @@ -1506,24 +1511,24 @@ mod tests { let dbtxn = db.write().await; assert!(dbtxn.migrate().is_ok()); - let mut ut1 = UnixUserToken { + let mut ut1 = UserToken { name: "testuser".to_string(), spn: "testuser@example.com".to_string(), displayname: "Test User".to_string(), gidnumber: 2000, - uuid: "0302b99c-f0f6-41ab-9492-852692b0fd16".to_string(), + uuid: uuid::uuid!("0302b99c-f0f6-41ab-9492-852692b0fd16"), shell: None, groups: Vec::new(), sshkeys: vec!["key-a".to_string()], valid: true, }; - let ut2 = UnixUserToken { + let ut2 = UserToken { name: "testuser".to_string(), spn: "testuser@example.com".to_string(), displayname: "Test User".to_string(), gidnumber: 2001, - uuid: "799123b2-3802-4b19-b0b8-1ffae2aa9a4b".to_string(), + uuid: uuid::uuid!("799123b2-3802-4b19-b0b8-1ffae2aa9a4b"), shell: None, groups: Vec::new(), sshkeys: vec!["key-a".to_string()], @@ -1540,7 +1545,7 @@ mod tests { // test adding an account dbtxn.update_account(&ut1, 0).unwrap(); let r0 = dbtxn.get_account(&id_name).unwrap(); - assert!(r0.unwrap().0.uuid == "0302b99c-f0f6-41ab-9492-852692b0fd16"); + assert!(r0.unwrap().0.uuid == uuid::uuid!("0302b99c-f0f6-41ab-9492-852692b0fd16")); // Do the "rename" of gt1 which is what would allow gt2 to be valid. ut1.name = "testuser2".to_string(); @@ -1548,7 +1553,7 @@ mod tests { // Now, add gt2 which dups on gt1 name/spn. dbtxn.update_account(&ut2, 0).unwrap(); let r2 = dbtxn.get_account(&id_name).unwrap(); - assert!(r2.unwrap().0.uuid == "799123b2-3802-4b19-b0b8-1ffae2aa9a4b"); + assert!(r2.unwrap().0.uuid == uuid::uuid!("799123b2-3802-4b19-b0b8-1ffae2aa9a4b")); let r3 = dbtxn.get_account(&id_name2).unwrap(); assert!(r3.is_none()); @@ -1557,9 +1562,9 @@ mod tests { // Both now coexist let r4 = dbtxn.get_account(&id_name).unwrap(); - assert!(r4.unwrap().0.uuid == "799123b2-3802-4b19-b0b8-1ffae2aa9a4b"); + assert!(r4.unwrap().0.uuid == uuid::uuid!("799123b2-3802-4b19-b0b8-1ffae2aa9a4b")); let r5 = dbtxn.get_account(&id_name2).unwrap(); - assert!(r5.unwrap().0.uuid == "0302b99c-f0f6-41ab-9492-852692b0fd16"); + assert!(r5.unwrap().0.uuid == uuid::uuid!("0302b99c-f0f6-41ab-9492-852692b0fd16")); assert!(dbtxn.commit().is_ok()); } diff --git a/unix_integration/src/idprovider/interface.rs b/unix_integration/src/idprovider/interface.rs new file mode 100644 index 000000000..c93a13dba --- /dev/null +++ b/unix_integration/src/idprovider/interface.rs @@ -0,0 +1,67 @@ +use async_trait::async_trait; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +/// Errors that the IdProvider may return. These drive the resolver state machine +/// and should be carefully selected to match your expected errors. +#[derive(Debug)] +pub enum IdpError { + /// An error occurred in the underlying communication to the Idp. A timeout or + /// or other communication issue exists. The resolver will take this provider + /// offline. + Transport, + /// The provider is online but the provider module is not current authorised with + /// the idp. After returning this error the operation will be retried after a + /// successful authentication. + ProviderUnauthorised, + /// The provider made an invalid request to the idp, and the result is not able to + /// be used by the resolver. + BadRequest, + /// The idp has indicated that the requested resource does not exist and should + /// be considered deleted, removed, or not present. + NotFound, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum Id { + Name(String), + Gid(u32), +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct GroupToken { + pub name: String, + pub spn: String, + pub uuid: Uuid, + pub gidnumber: u32, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct UserToken { + pub name: String, + pub spn: String, + pub uuid: Uuid, + pub gidnumber: u32, + pub displayname: String, + pub shell: Option, + pub groups: Vec, + // Could there be a better type here? + pub sshkeys: Vec, + // Defaults to false. + pub valid: bool, +} + +#[async_trait] +pub trait IdProvider { + async fn provider_authenticate(&self) -> Result<(), IdpError>; + + async fn unix_user_get(&self, id: &Id) -> Result; + + async fn unix_user_authenticate( + &self, + id: &Id, + cred: &str, + ) -> Result, IdpError>; + + async fn unix_group_get(&self, id: &Id) -> Result; +} diff --git a/unix_integration/src/idprovider/kanidm.rs b/unix_integration/src/idprovider/kanidm.rs new file mode 100644 index 000000000..06c179990 --- /dev/null +++ b/unix_integration/src/idprovider/kanidm.rs @@ -0,0 +1,264 @@ +use async_trait::async_trait; +use kanidm_client::{ClientError, KanidmClient, StatusCode}; +use kanidm_proto::v1::{OperationError, UnixGroupToken, UnixUserToken}; +use tokio::sync::RwLock; + +use super::interface::{GroupToken, Id, IdProvider, IdpError, UserToken}; + +pub struct KanidmProvider { + client: RwLock, +} + +impl KanidmProvider { + pub fn new(client: KanidmClient) -> Self { + KanidmProvider { + client: RwLock::new(client), + } + } +} + +impl From for UserToken { + fn from(value: UnixUserToken) -> UserToken { + let UnixUserToken { + name, + spn, + displayname, + gidnumber, + uuid, + shell, + groups, + sshkeys, + valid, + } = value; + + let groups = groups + .into_iter() + .map(GroupToken::from) + .collect(); + + UserToken { + name, + spn, + uuid, + gidnumber, + displayname, + shell, + groups, + sshkeys, + valid, + } + } +} + +impl From for GroupToken { + fn from(value: UnixGroupToken) -> GroupToken { + let UnixGroupToken { + name, + spn, + uuid, + gidnumber, + } = value; + + GroupToken { + name, + spn, + uuid, + gidnumber, + } + } +} + +#[async_trait] +impl IdProvider for KanidmProvider { + // Needs .read on all types except re-auth. + + async fn provider_authenticate(&self) -> Result<(), IdpError> { + match self.client.write().await.auth_anonymous().await { + Ok(_uat) => Ok(()), + Err(err) => { + error!(?err, "Provider authentication failed"); + Err(IdpError::ProviderUnauthorised) + } + } + } + + async fn unix_user_get(&self, id: &Id) -> Result { + match self + .client + .read() + .await + .idm_account_unix_token_get(id.to_string().as_str()) + .await + { + Ok(tok) => Ok(UserToken::from(tok)), + Err(ClientError::Transport(err)) => { + error!(?err); + Err(IdpError::Transport) + } + Err(ClientError::Http(StatusCode::UNAUTHORIZED, reason, opid)) => { + match reason { + Some(OperationError::NotAuthenticated) => warn!( + "session not authenticated - attempting reauthentication - eventid {}", + opid + ), + Some(OperationError::SessionExpired) => warn!( + "session expired - attempting reauthentication - eventid {}", + opid + ), + e => error!( + "authentication error {:?}, moving to offline - eventid {}", + e, opid + ), + }; + Err(IdpError::ProviderUnauthorised) + } + Err(ClientError::Http( + StatusCode::BAD_REQUEST, + Some(OperationError::NoMatchingEntries), + opid, + )) + | Err(ClientError::Http( + StatusCode::NOT_FOUND, + Some(OperationError::NoMatchingEntries), + opid, + )) + | Err(ClientError::Http( + StatusCode::BAD_REQUEST, + Some(OperationError::InvalidAccountState(_)), + opid, + )) => { + debug!( + ?opid, + "entry has been removed or is no longer a valid posix account" + ); + Err(IdpError::NotFound) + } + Err(err) => { + error!(?err, "client error"); + Err(IdpError::BadRequest) + } + } + } + + async fn unix_user_authenticate( + &self, + id: &Id, + cred: &str, + ) -> Result, IdpError> { + match self + .client + .read() + .await + .idm_account_unix_cred_verify(id.to_string().as_str(), cred) + .await + { + Ok(Some(n_tok)) => Ok(Some(UserToken::from(n_tok))), + Ok(None) => Ok(None), + Err(ClientError::Transport(err)) => { + error!(?err); + Err(IdpError::Transport) + } + Err(ClientError::Http(StatusCode::UNAUTHORIZED, reason, opid)) => { + match reason { + Some(OperationError::NotAuthenticated) => warn!( + "session not authenticated - attempting reauthentication - eventid {}", + opid + ), + Some(OperationError::SessionExpired) => warn!( + "session expired - attempting reauthentication - eventid {}", + opid + ), + e => error!( + "authentication error {:?}, moving to offline - eventid {}", + e, opid + ), + }; + Err(IdpError::ProviderUnauthorised) + } + Err(ClientError::Http( + StatusCode::BAD_REQUEST, + Some(OperationError::NoMatchingEntries), + opid, + )) + | Err(ClientError::Http( + StatusCode::NOT_FOUND, + Some(OperationError::NoMatchingEntries), + opid, + )) + | Err(ClientError::Http( + StatusCode::BAD_REQUEST, + Some(OperationError::InvalidAccountState(_)), + opid, + )) => { + error!( + "unknown account or is not a valid posix account - eventid {}", + opid + ); + Err(IdpError::NotFound) + } + Err(err) => { + error!(?err, "client error"); + // Some other unknown processing error? + Err(IdpError::BadRequest) + } + } + } + + async fn unix_group_get(&self, id: &Id) -> Result { + match self + .client + .read() + .await + .idm_group_unix_token_get(id.to_string().as_str()) + .await + { + Ok(tok) => Ok(GroupToken::from(tok)), + Err(ClientError::Transport(err)) => { + error!(?err); + Err(IdpError::Transport) + } + Err(ClientError::Http(StatusCode::UNAUTHORIZED, reason, opid)) => { + match reason { + Some(OperationError::NotAuthenticated) => warn!( + "session not authenticated - attempting reauthentication - eventid {}", + opid + ), + Some(OperationError::SessionExpired) => warn!( + "session expired - attempting reauthentication - eventid {}", + opid + ), + e => error!( + "authentication error {:?}, moving to offline - eventid {}", + e, opid + ), + }; + Err(IdpError::ProviderUnauthorised) + } + Err(ClientError::Http( + StatusCode::BAD_REQUEST, + Some(OperationError::NoMatchingEntries), + opid, + )) + | Err(ClientError::Http( + StatusCode::NOT_FOUND, + Some(OperationError::NoMatchingEntries), + opid, + )) + | Err(ClientError::Http( + StatusCode::BAD_REQUEST, + Some(OperationError::InvalidAccountState(_)), + opid, + )) => { + debug!( + ?opid, + "entry has been removed or is no longer a valid posix group" + ); + Err(IdpError::NotFound) + } + Err(err) => { + error!(?err, "client error"); + Err(IdpError::BadRequest) + } + } + } +} diff --git a/unix_integration/src/idprovider/mod.rs b/unix_integration/src/idprovider/mod.rs new file mode 100644 index 000000000..77559216e --- /dev/null +++ b/unix_integration/src/idprovider/mod.rs @@ -0,0 +1,2 @@ +pub mod interface; +pub mod kanidm; diff --git a/unix_integration/src/lib.rs b/unix_integration/src/lib.rs index 97645060c..f2d543704 100644 --- a/unix_integration/src/lib.rs +++ b/unix_integration/src/lib.rs @@ -26,6 +26,8 @@ pub mod constants; #[cfg(target_family = "unix")] pub mod db; #[cfg(target_family = "unix")] +pub mod idprovider; +#[cfg(target_family = "unix")] pub mod resolver; #[cfg(all(target_family = "unix", feature = "selinux"))] pub mod selinux_util; diff --git a/unix_integration/src/resolver.rs b/unix_integration/src/resolver.rs index 7905f7571..19ce9e148 100644 --- a/unix_integration/src/resolver.rs +++ b/unix_integration/src/resolver.rs @@ -1,3 +1,4 @@ +// use async_trait::async_trait; use hashbrown::HashSet; use std::collections::BTreeSet; use std::num::NonZeroUsize; @@ -6,26 +7,18 @@ use std::path::Path; use std::string::ToString; use std::time::{Duration, SystemTime}; -use kanidm_client::{ClientError, KanidmClient}; -use kanidm_proto::v1::{OperationError, UnixGroupToken, UnixUserToken}; use lru::LruCache; -use reqwest::StatusCode; -use tokio::sync::{Mutex, RwLock}; +use tokio::sync::Mutex; +use uuid::Uuid; use crate::db::{Cache, CacheTxn, Db}; - +use crate::idprovider::interface::{GroupToken, Id, IdProvider, IdpError, UserToken}; use crate::unix_config::{HomeAttr, UidAttr}; use crate::unix_proto::{HomeDirectoryInfo, NssGroup, NssUser}; // use crate::unix_passwd::{EtcUser, EtcGroup}; -const NXCACHE_SIZE: NonZeroUsize = unsafe { NonZeroUsize::new_unchecked(2048) }; - -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub enum Id { - Name(String), - Gid(u32), -} +const NXCACHE_SIZE: NonZeroUsize = unsafe { NonZeroUsize::new_unchecked(128) }; #[derive(Debug, Clone)] enum CacheState { @@ -35,9 +28,14 @@ enum CacheState { } #[derive(Debug)] -pub struct Resolver { +pub struct Resolver +where + I: IdProvider, +{ + // Generic / modular types. db: Db, - client: RwLock, + client: I, + // Types to update still. state: Mutex, pam_allow_groups: BTreeSet, timeout_seconds: u64, @@ -61,18 +59,16 @@ impl ToString for Id { } } -impl Resolver { +impl Resolver +where + I: IdProvider, +{ #[allow(clippy::too_many_arguments)] pub async fn new( db: Db, - // need db path - // path: &str, - // tpm_policy: &TpmPolicy, - + client: I, // cache timeout timeout_seconds: u64, - // - client: KanidmClient, pam_allow_groups: Vec, default_shell: String, home_prefix: String, @@ -97,7 +93,7 @@ impl Resolver { // being valid from "now". Ok(Resolver { db, - client: RwLock::new(client), + client, state: Mutex::new(CacheState::OfflineNextCheck(SystemTime::now())), timeout_seconds, pam_allow_groups: pam_allow_groups.into_iter().collect(), @@ -150,12 +146,12 @@ impl Resolver { .map_err(|_| ()) } - async fn get_cached_usertokens(&self) -> Result, ()> { + async fn get_cached_usertokens(&self) -> Result, ()> { let dbtxn = self.db.write().await; dbtxn.get_accounts().map_err(|_| ()) } - async fn get_cached_grouptokens(&self) -> Result, ()> { + async fn get_cached_grouptokens(&self) -> Result, ()> { let dbtxn = self.db.write().await; dbtxn.get_groups().map_err(|_| ()) } @@ -193,10 +189,7 @@ impl Resolver { nxset_txn.contains(&Id::Gid(idnumber)) || nxset_txn.contains(&Id::Name(name.to_string())) } - async fn get_cached_usertoken( - &self, - account_id: &Id, - ) -> Result<(bool, Option), ()> { + async fn get_cached_usertoken(&self, account_id: &Id) -> Result<(bool, Option), ()> { // Account_id could be: // * gidnumber // * name @@ -244,10 +237,7 @@ impl Resolver { } // end match r } - async fn get_cached_grouptoken( - &self, - grp_id: &Id, - ) -> Result<(bool, Option), ()> { + async fn get_cached_grouptoken(&self, grp_id: &Id) -> Result<(bool, Option), ()> { // grp_id could be: // * gidnumber // * name @@ -295,7 +285,7 @@ impl Resolver { } } - async fn set_cache_usertoken(&self, token: &mut UnixUserToken) -> Result<(), ()> { + async fn set_cache_usertoken(&self, token: &mut UserToken) -> Result<(), ()> { // Set an expiry let ex_time = SystemTime::now() + Duration::from_secs(self.timeout_seconds); let offset = ex_time @@ -350,7 +340,7 @@ impl Resolver { .map_err(|_| ()) } - async fn set_cache_grouptoken(&self, token: &UnixGroupToken) -> Result<(), ()> { + async fn set_cache_grouptoken(&self, token: &GroupToken) -> Result<(), ()> { // Set an expiry let ex_time = SystemTime::now() + Duration::from_secs(self.timeout_seconds); let offset = ex_time @@ -366,7 +356,7 @@ impl Resolver { .map_err(|_| ()) } - async fn delete_cache_usertoken(&self, a_uuid: &str) -> Result<(), ()> { + async fn delete_cache_usertoken(&self, a_uuid: Uuid) -> Result<(), ()> { let dbtxn = self.db.write().await; dbtxn .delete_account(a_uuid) @@ -374,7 +364,7 @@ impl Resolver { .map_err(|_| ()) } - async fn delete_cache_grouptoken(&self, g_uuid: &str) -> Result<(), ()> { + async fn delete_cache_grouptoken(&self, g_uuid: Uuid) -> Result<(), ()> { let dbtxn = self.db.write().await; dbtxn .delete_group(g_uuid) @@ -382,7 +372,7 @@ impl Resolver { .map_err(|_| ()) } - async fn set_cache_userpassword(&self, a_uuid: &str, cred: &str) -> Result<(), ()> { + async fn set_cache_userpassword(&self, a_uuid: Uuid, cred: &str) -> Result<(), ()> { let dbtxn = self.db.write().await; dbtxn .update_account_password(a_uuid, cred) @@ -390,7 +380,7 @@ impl Resolver { .map_err(|_| ()) } - async fn check_cache_userpassword(&self, a_uuid: &str, cred: &str) -> Result { + async fn check_cache_userpassword(&self, a_uuid: Uuid, cred: &str) -> Result { let dbtxn = self.db.write().await; dbtxn .check_account_password(a_uuid, cred) @@ -401,19 +391,13 @@ impl Resolver { async fn refresh_usertoken( &self, account_id: &Id, - token: Option, - ) -> Result, ()> { - match self - .client - .read() - .await - .idm_account_unix_token_get(account_id.to_string().as_str()) - .await - { + token: Option, + ) -> Result, ()> { + match self.client.unix_user_get(account_id).await { Ok(mut n_tok) => { if self.check_nxset(&n_tok.name, n_tok.gidnumber).await { // Refuse to release the token, it's in the denied set. - self.delete_cache_usertoken(&n_tok.uuid).await?; + self.delete_cache_usertoken(n_tok.uuid).await?; Ok(None) } else { // We have the token! @@ -421,65 +405,35 @@ impl Resolver { Ok(Some(n_tok)) } } - Err(e) => { - match e { - ClientError::Transport(er) => { - error!("transport error, moving to offline -> {:?}", er); - // Something went wrong, mark offline. - let time = SystemTime::now().add(Duration::from_secs(15)); - self.set_cachestate(CacheState::OfflineNextCheck(time)) - .await; - Ok(token) - } - ClientError::Http(StatusCode::UNAUTHORIZED, reason, opid) => { - match reason { - Some(OperationError::NotAuthenticated) => - warn!("session not authenticated - attempting reauthentication - eventid {}", opid), - Some(OperationError::SessionExpired) => - warn!("session expired - attempting reauthentication - eventid {}", opid), - e => - error!( - "authentication error {:?}, moving to offline - eventid {}", e, opid - ), - }; - // Something went wrong, mark offline to force a re-auth ASAP. - let time = SystemTime::now().sub(Duration::from_secs(1)); - self.set_cachestate(CacheState::OfflineNextCheck(time)) - .await; - Ok(token) - } - ClientError::Http( - StatusCode::BAD_REQUEST, - Some(OperationError::NoMatchingEntries), - opid, - ) - | ClientError::Http( - StatusCode::NOT_FOUND, - Some(OperationError::NoMatchingEntries), - opid, - ) - | ClientError::Http( - StatusCode::BAD_REQUEST, - Some(OperationError::InvalidAccountState(_)), - opid, - ) => { - // We wele able to contact the server but the entry has been removed, or - // is not longer a valid posix account. - debug!("entry has been removed or is no longer a valid posix account, clearing from cache ... - eventid {}", opid); - if let Some(tok) = token { - self.delete_cache_usertoken(&tok.uuid).await?; - }; - // Cache the NX here. - self.set_nxcache(account_id).await; + Err(IdpError::Transport) => { + error!("transport error, moving to offline"); + // Something went wrong, mark offline. + let time = SystemTime::now().add(Duration::from_secs(15)); + self.set_cachestate(CacheState::OfflineNextCheck(time)) + .await; + Ok(token) + } + Err(IdpError::ProviderUnauthorised) => { + // Something went wrong, mark offline to force a re-auth ASAP. + let time = SystemTime::now().sub(Duration::from_secs(1)); + self.set_cachestate(CacheState::OfflineNextCheck(time)) + .await; + Ok(token) + } + Err(IdpError::NotFound) => { + // We were able to contact the server but the entry has been removed, or + // is not longer a valid posix account. + if let Some(tok) = token { + self.delete_cache_usertoken(tok.uuid).await?; + }; + // Cache the NX here. + self.set_nxcache(account_id).await; - Ok(None) - } - er => { - error!("client error -> {:?}", er); - // Some other transient error, continue with the token. - Ok(token) - } - } + Ok(None) + } + Err(IdpError::BadRequest) => { + // Some other transient error, continue with the token. + Ok(token) } } } @@ -487,19 +441,13 @@ impl Resolver { async fn refresh_grouptoken( &self, grp_id: &Id, - token: Option, - ) -> Result, ()> { - match self - .client - .read() - .await - .idm_group_unix_token_get(grp_id.to_string().as_str()) - .await - { + token: Option, + ) -> Result, ()> { + match self.client.unix_group_get(grp_id).await { Ok(n_tok) => { if self.check_nxset(&n_tok.name, n_tok.gidnumber).await { // Refuse to release the token, it's in the denied set. - self.delete_cache_grouptoken(&n_tok.uuid).await?; + self.delete_cache_grouptoken(n_tok.uuid).await?; Ok(None) } else { // We have the token! @@ -507,66 +455,37 @@ impl Resolver { Ok(Some(n_tok)) } } - Err(e) => { - match e { - ClientError::Transport(er) => { - error!("transport error, moving to offline -> {:?}", er); - // Something went wrong, mark offline. - let time = SystemTime::now().add(Duration::from_secs(15)); - self.set_cachestate(CacheState::OfflineNextCheck(time)) - .await; - Ok(token) - } - ClientError::Http( - StatusCode::UNAUTHORIZED, - Some(OperationError::NotAuthenticated), - opid, - ) => { - error!( - "transport unauthenticated, moving to offline - eventid {}", - opid - ); - // Something went wrong, mark offline. - let time = SystemTime::now().add(Duration::from_secs(15)); - self.set_cachestate(CacheState::OfflineNextCheck(time)) - .await; - Ok(token) - } - ClientError::Http( - StatusCode::BAD_REQUEST, - Some(OperationError::NoMatchingEntries), - opid, - ) - | ClientError::Http( - StatusCode::NOT_FOUND, - Some(OperationError::NoMatchingEntries), - opid, - ) - | ClientError::Http( - StatusCode::BAD_REQUEST, - Some(OperationError::InvalidAccountState(_)), - opid, - ) => { - debug!("entry has been removed or is no longer a valid posix group, clearing from cache ... - eventid {}", opid); - if let Some(tok) = token { - self.delete_cache_grouptoken(&tok.uuid).await?; - }; - // Cache the NX here. - self.set_nxcache(grp_id).await; - - Ok(None) - } - er => { - error!("client error -> {:?}", er); - // Some other transient error, continue with the token. - Ok(token) - } - } + Err(IdpError::Transport) => { + error!("transport error, moving to offline"); + // Something went wrong, mark offline. + let time = SystemTime::now().add(Duration::from_secs(15)); + self.set_cachestate(CacheState::OfflineNextCheck(time)) + .await; + Ok(token) + } + Err(IdpError::ProviderUnauthorised) => { + // Something went wrong, mark offline. + let time = SystemTime::now().add(Duration::from_secs(15)); + self.set_cachestate(CacheState::OfflineNextCheck(time)) + .await; + Ok(token) + } + Err(IdpError::NotFound) => { + if let Some(tok) = token { + self.delete_cache_grouptoken(tok.uuid).await?; + }; + // Cache the NX here. + self.set_nxcache(grp_id).await; + Ok(None) + } + Err(IdpError::BadRequest) => { + // Some other transient error, continue with the token. + Ok(token) } } } - async fn get_usertoken(&self, account_id: Id) -> Result, ()> { + async fn get_usertoken(&self, account_id: Id) -> Result, ()> { debug!("get_usertoken"); // get the item from the cache let (expired, item) = self.get_cached_usertoken(&account_id).await.map_err(|e| { @@ -618,7 +537,7 @@ impl Resolver { }) } - async fn get_grouptoken(&self, grp_id: Id) -> Result, ()> { + async fn get_grouptoken(&self, grp_id: Id) -> Result, ()> { debug!("get_grouptoken"); let (expired, item) = self.get_cached_grouptoken(&grp_id).await.map_err(|e| { debug!("get_grouptoken error -> {:?}", e); @@ -665,11 +584,11 @@ impl Resolver { } } - async fn get_groupmembers(&self, uuid: &str) -> Vec { + async fn get_groupmembers(&self, g_uuid: Uuid) -> Vec { let dbtxn = self.db.write().await; dbtxn - .get_group_members(uuid) + .get_group_members(g_uuid) .unwrap_or_else(|_| Vec::new()) .into_iter() .map(|ut| self.token_uidattr(&ut)) @@ -692,37 +611,37 @@ impl Resolver { } #[inline(always)] - fn token_homedirectory_alias(&self, token: &UnixUserToken) -> Option { + fn token_homedirectory_alias(&self, token: &UserToken) -> Option { self.home_alias.map(|t| match t { // If we have an alias. use it. - HomeAttr::Uuid => token.uuid.as_str().to_string(), + HomeAttr::Uuid => token.uuid.hyphenated().to_string(), HomeAttr::Spn => token.spn.as_str().to_string(), HomeAttr::Name => token.name.as_str().to_string(), }) } #[inline(always)] - fn token_homedirectory_attr(&self, token: &UnixUserToken) -> String { + fn token_homedirectory_attr(&self, token: &UserToken) -> String { match self.home_attr { - HomeAttr::Uuid => token.uuid.as_str().to_string(), + HomeAttr::Uuid => token.uuid.hyphenated().to_string(), HomeAttr::Spn => token.spn.as_str().to_string(), HomeAttr::Name => token.name.as_str().to_string(), } } #[inline(always)] - fn token_homedirectory(&self, token: &UnixUserToken) -> String { + fn token_homedirectory(&self, token: &UserToken) -> String { self.token_homedirectory_alias(token) .unwrap_or_else(|| self.token_homedirectory_attr(token)) } #[inline(always)] - fn token_abs_homedirectory(&self, token: &UnixUserToken) -> String { + fn token_abs_homedirectory(&self, token: &UserToken) -> String { format!("{}{}", self.home_prefix, self.token_homedirectory(token)) } #[inline(always)] - fn token_uidattr(&self, token: &UnixUserToken) -> String { + fn token_uidattr(&self, token: &UserToken) -> String { match self.uid_attr_map { UidAttr::Spn => token.spn.as_str(), UidAttr::Name => token.name.as_str(), @@ -764,7 +683,7 @@ impl Resolver { } #[inline(always)] - fn token_gidattr(&self, token: &UnixGroupToken) -> String { + fn token_gidattr(&self, token: &GroupToken) -> String { match self.gid_attr_map { UidAttr::Spn => token.spn.as_str(), UidAttr::Name => token.name.as_str(), @@ -776,7 +695,7 @@ impl Resolver { let l = self.get_cached_grouptokens().await?; let mut r: Vec<_> = Vec::with_capacity(l.len()); for tok in l.into_iter() { - let members = self.get_groupmembers(&tok.uuid).await; + let members = self.get_groupmembers(tok.uuid).await; r.push(NssGroup { name: self.token_gidattr(&tok), gid: tok.gidnumber, @@ -791,7 +710,7 @@ impl Resolver { // Get members set. match token { Some(tok) => { - let members = self.get_groupmembers(&tok.uuid).await; + let members = self.get_groupmembers(tok.uuid).await; Ok(Some(NssGroup { name: self.token_gidattr(&tok), gid: tok.gidnumber, @@ -812,28 +731,22 @@ impl Resolver { async fn online_account_authenticate( &self, - token: &Option, - account_id: &str, + token: &Option, + account_id: &Id, cred: &str, ) -> Result, ()> { debug!("Attempt online password check"); // We are online, attempt the pw to the server. - match self - .client - .read() - .await - .idm_account_unix_cred_verify(account_id, cred) - .await - { + match self.client.unix_user_authenticate(account_id, cred).await { Ok(Some(mut n_tok)) => { if self.check_nxset(&n_tok.name, n_tok.gidnumber).await { // Refuse to release the token, it's in the denied set. - self.delete_cache_usertoken(&n_tok.uuid).await?; + self.delete_cache_usertoken(n_tok.uuid).await?; Ok(None) } else { debug!("online password check success."); self.set_cache_usertoken(&mut n_tok).await?; - self.set_cache_userpassword(&n_tok.uuid, cred).await?; + self.set_cache_userpassword(n_tok.uuid, cred).await?; Ok(Some(true)) } } @@ -842,80 +755,46 @@ impl Resolver { // PW failed the check. Ok(Some(false)) } - Err(e) => { - match e { - ClientError::Transport(er) => { - error!("transport error, moving to offline -> {:?}", er); - // Something went wrong, mark offline. - let time = SystemTime::now().add(Duration::from_secs(15)); - self.set_cachestate(CacheState::OfflineNextCheck(time)) - .await; - match token.as_ref() { - Some(t) => self.check_cache_userpassword(&t.uuid, cred).await.map(Some), - None => Ok(None), - } - } - ClientError::Http(StatusCode::UNAUTHORIZED, reason, opid) => { - match reason { - Some(OperationError::NotAuthenticated) => - warn!("session not authenticated - attempting reauthentication - eventid {}", opid), - Some(OperationError::SessionExpired) => - warn!("session expired - attempting reauthentication - eventid {}", opid), - e => - error!( - "authentication error {:?}, moving to offline - eventid {}", e, opid - ), - }; - // Something went wrong, mark offline to force a re-auth ASAP. - let time = SystemTime::now().sub(Duration::from_secs(1)); - self.set_cachestate(CacheState::OfflineNextCheck(time)) - .await; - match token.as_ref() { - Some(t) => self.check_cache_userpassword(&t.uuid, cred).await.map(Some), - None => Ok(None), - } - } - ClientError::Http( - StatusCode::BAD_REQUEST, - Some(OperationError::NoMatchingEntries), - opid, - ) - | ClientError::Http( - StatusCode::NOT_FOUND, - Some(OperationError::NoMatchingEntries), - opid, - ) - | ClientError::Http( - StatusCode::BAD_REQUEST, - Some(OperationError::InvalidAccountState(_)), - opid, - ) => { - error!( - "unknown account or is not a valid posix account - eventid {}", - opid - ); - Ok(None) - } - er => { - error!("client error -> {:?}", er); - // Some other unknown processing error? - Err(()) - } + Err(IdpError::Transport) => { + error!("transport error, moving to offline"); + // Something went wrong, mark offline. + let time = SystemTime::now().add(Duration::from_secs(15)); + self.set_cachestate(CacheState::OfflineNextCheck(time)) + .await; + match token.as_ref() { + Some(t) => self.check_cache_userpassword(t.uuid, cred).await.map(Some), + None => Ok(None), } } + + Err(IdpError::ProviderUnauthorised) => { + // Something went wrong, mark offline to force a re-auth ASAP. + let time = SystemTime::now().sub(Duration::from_secs(1)); + self.set_cachestate(CacheState::OfflineNextCheck(time)) + .await; + match token.as_ref() { + Some(t) => self.check_cache_userpassword(t.uuid, cred).await.map(Some), + None => Ok(None), + } + } + Err(IdpError::NotFound) => Ok(None), + Err(IdpError::BadRequest) => { + // Some other unknown processing error? + Err(()) + } } } async fn offline_account_authenticate( &self, - token: &Option, + token: &Option, cred: &str, ) -> Result, ()> { debug!("Attempt offline password check"); match token.as_ref() { Some(t) => { if t.valid { - self.check_cache_userpassword(&t.uuid, cred).await.map(Some) + self.check_cache_userpassword(t.uuid, cred).await.map(Some) } else { Ok(Some(false)) } @@ -942,7 +821,7 @@ impl Resolver { let user_set: BTreeSet<_> = tok .groups .iter() - .flat_map(|g| [g.name.clone(), g.spn.clone(), g.uuid.clone()]) + .flat_map(|g| [g.name.clone(), g.uuid.hyphenated().to_string()]) .collect(); debug!( @@ -963,22 +842,18 @@ impl Resolver { account_id: &str, cred: &str, ) -> Result, ()> { + let id = Id::Name(account_id.to_string()); + let state = self.get_cachestate().await; - let (_expired, token) = self - .get_cached_usertoken(&Id::Name(account_id.to_string())) - .await?; + let (_expired, token) = self.get_cached_usertoken(&id).await?; match state { - CacheState::Online => { - self.online_account_authenticate(&token, account_id, cred) - .await - } + CacheState::Online => self.online_account_authenticate(&token, &id, cred).await, CacheState::OfflineNextCheck(_time) => { // Always attempt to go online to attempt the authentication. if self.test_connection().await { // Brought ourselves online, lets check. - self.online_account_authenticate(&token, account_id, cred) - .await + self.online_account_authenticate(&token, &id, cred).await } else { // We are offline, check from the cache if possible. self.offline_account_authenticate(&token, cred).await @@ -1014,8 +889,8 @@ impl Resolver { false } CacheState::OfflineNextCheck(_time) => { - match self.client.write().await.auth_anonymous().await { - Ok(_uat) => { + match self.client.provider_authenticate().await { + Ok(()) => { debug!("OfflineNextCheck -> authenticated"); self.set_cachestate(CacheState::Online).await; true diff --git a/unix_integration/src/ssh_authorizedkeys.rs b/unix_integration/src/ssh_authorizedkeys.rs index adf427e07..4c223bec6 100644 --- a/unix_integration/src/ssh_authorizedkeys.rs +++ b/unix_integration/src/ssh_authorizedkeys.rs @@ -65,7 +65,7 @@ async fn main() -> ExitCode { } let req = ClientRequest::SshKey(opt.account_id); - match call_daemon(cfg.sock_path.as_str(), req).await { + match call_daemon(cfg.sock_path.as_str(), req, cfg.unix_sock_timeout).await { Ok(r) => match r { ClientResponse::SshKeys(sk) => sk.iter().for_each(|k| { println!("{}", k); diff --git a/unix_integration/src/tool.rs b/unix_integration/src/tool.rs index 74962a27b..3e2d2133e 100644 --- a/unix_integration/src/tool.rs +++ b/unix_integration/src/tool.rs @@ -17,7 +17,6 @@ use std::process::ExitCode; use clap::Parser; use kanidm_unix_common::client::call_daemon; -use kanidm_unix_common::client_sync::call_daemon_blocking; use kanidm_unix_common::constants::DEFAULT_CONFIG_PATH; use kanidm_unix_common::unix_config::KanidmUnixdConfig; use kanidm_unix_common::unix_proto::{ClientRequest, ClientResponse}; @@ -70,7 +69,7 @@ async fn main() -> ExitCode { let req = ClientRequest::PamAuthenticate(account_id.clone(), password); let sereq = ClientRequest::PamAccountAllowed(account_id); - match call_daemon(cfg.sock_path.as_str(), req).await { + match call_daemon(cfg.sock_path.as_str(), req, cfg.unix_sock_timeout).await { Ok(r) => match r { ClientResponse::PamStatus(Some(true)) => { println!("auth success!"); @@ -91,7 +90,7 @@ async fn main() -> ExitCode { } }; - match call_daemon(cfg.sock_path.as_str(), sereq).await { + match call_daemon(cfg.sock_path.as_str(), sereq, cfg.unix_sock_timeout).await { Ok(r) => match r { ClientResponse::PamStatus(Some(true)) => { println!("account success!"); @@ -133,7 +132,7 @@ async fn main() -> ExitCode { let req = ClientRequest::ClearCache; - match call_daemon(cfg.sock_path.as_str(), req).await { + match call_daemon(cfg.sock_path.as_str(), req, cfg.unix_sock_timeout).await { Ok(r) => match r { ClientResponse::Ok => info!("success"), _ => { @@ -162,7 +161,7 @@ async fn main() -> ExitCode { let req = ClientRequest::InvalidateCache; - match call_daemon(cfg.sock_path.as_str(), req).await { + match call_daemon(cfg.sock_path.as_str(), req, cfg.unix_sock_timeout).await { Ok(r) => match r { ClientResponse::Ok => info!("success"), _ => { @@ -198,7 +197,7 @@ async fn main() -> ExitCode { cfg.sock_path ) } else { - match call_daemon_blocking(cfg.sock_path.as_str(), &req, cfg.unix_sock_timeout) { + match call_daemon(cfg.sock_path.as_str(), req, cfg.unix_sock_timeout).await { Ok(r) => match r { ClientResponse::Ok => println!("working!"), _ => { diff --git a/unix_integration/tests/cache_layer_test.rs b/unix_integration/tests/cache_layer_test.rs index 99f90d9b1..15f93ccbd 100644 --- a/unix_integration/tests/cache_layer_test.rs +++ b/unix_integration/tests/cache_layer_test.rs @@ -11,7 +11,9 @@ use kanidm_unix_common::constants::{ DEFAULT_SHELL, DEFAULT_UID_ATTR_MAP, }; use kanidm_unix_common::db::Db; -use kanidm_unix_common::resolver::{Id, Resolver}; +use kanidm_unix_common::idprovider::interface::Id; +use kanidm_unix_common::idprovider::kanidm::KanidmProvider; +use kanidm_unix_common::resolver::Resolver; use kanidm_unix_common::unix_config::TpmPolicy; use kanidmd_core::config::{Configuration, IntegrationTestConfig, ServerRole}; use kanidmd_core::create_server_core; @@ -42,7 +44,7 @@ where Box::new(move |n| Box::pin(f(n))) } -async fn setup_test(fix_fn: Fixture) -> (Resolver, KanidmClient) { +async fn setup_test(fix_fn: Fixture) -> (Resolver, KanidmClient) { sketching::test_init(); let mut counter = 0; @@ -100,6 +102,8 @@ async fn setup_test(fix_fn: Fixture) -> (Resolver, KanidmClient) { .build() .expect("Failed to build client"); + let idprovider = KanidmProvider::new(rsclient); + let db = Db::new( "", // The sqlite db path, this is in memory. &TpmPolicy::default(), @@ -108,8 +112,8 @@ async fn setup_test(fix_fn: Fixture) -> (Resolver, KanidmClient) { let cachelayer = Resolver::new( db, + idprovider, 300, - rsclient, vec!["allowed_group".to_string()], DEFAULT_SHELL.to_string(), DEFAULT_HOME_PREFIX.to_string(),