kanidm/unix_integration/resolver/tests/cache_layer_test.rs
Firstyear 63deda350c
20250225 improve test performance ()
* Ignore tests that are no longer used.

Each time a library or binary is added, that requires compilation to create
the *empty* test harness, which then is executed and takes multiple seconds
to start up, do nothing, and return success.

This removes test's for libraries that aren't actually using or running
any tests.

Additionally, each time a new test binary is added, that adds a ton of
compilation time, but also test execution time as the binary for each
test runner must start up, execute, and shutdown. So this merges all
the testkit integration tests to a single running which significantly
speeds up test execution.

* Improve IDL exists behaviour, improve memberof verification

Again to improve test performance. This improves the validation of idx
existance to be a faster SQLite call, caches the results as needed.

Memberof was taking up a large amount of time in verify phases of test
finalisation, and so a better in memory version has been added.

* Disable TLS native roots when not needed

* Cleanup tests that are hitting native certs, or do nothing at all
2025-03-04 10:36:53 +10:00

1245 lines
38 KiB
Rust

#![deny(warnings)]
use std::future::Future;
use std::pin::Pin;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::time::{Duration, SystemTime};
use time::OffsetDateTime;
use kanidm_client::{KanidmClient, KanidmClientBuilder};
use kanidm_proto::constants::ATTR_ACCOUNT_EXPIRE;
use kanidm_unix_common::constants::{
DEFAULT_GID_ATTR_MAP, DEFAULT_HOME_ALIAS, DEFAULT_HOME_ATTR, DEFAULT_HOME_PREFIX,
DEFAULT_SHELL, DEFAULT_UID_ATTR_MAP,
};
use kanidm_unix_common::unix_passwd::{CryptPw, EtcGroup, EtcShadow, EtcUser};
use kanidm_unix_resolver::db::{Cache, Db};
use kanidm_unix_resolver::idprovider::interface::Id;
use kanidm_unix_resolver::idprovider::kanidm::KanidmProvider;
use kanidm_unix_resolver::idprovider::system::SystemProvider;
use kanidm_unix_resolver::resolver::Resolver;
use kanidm_unix_resolver::unix_config::{GroupMap, KanidmConfig};
use kanidmd_core::config::{Configuration, IntegrationTestConfig, ServerRole};
use kanidmd_core::create_server_core;
use kanidmd_testkit::{is_free_port, PORT_ALLOC};
use tokio::task;
use tracing::log::{debug, trace};
use kanidm_hsm_crypto::{soft::SoftTpm, AuthValue, BoxedDynTpm, Tpm};
const ADMIN_TEST_USER: &str = "admin";
const ADMIN_TEST_PASSWORD: &str = "integration test admin password";
const IDM_ADMIN_TEST_USER: &str = "idm_admin";
const IDM_ADMIN_TEST_PASSWORD: &str = "integration test idm_admin password";
const TESTACCOUNT1_PASSWORD_A: &str = "password a for account1 test";
const TESTACCOUNT1_PASSWORD_B: &str = "password b for account1 test";
const TESTACCOUNT1_PASSWORD_INC: &str = "never going to work";
const ACCOUNT_EXPIRE: &str = "1970-01-01T00:00:00+00:00";
type Fixture = Box<dyn FnOnce(KanidmClient) -> Pin<Box<dyn Future<Output = ()>>>>;
fn fixture<T>(f: fn(KanidmClient) -> T) -> Fixture
where
T: Future<Output = ()> + 'static,
{
Box::new(move |n| Box::pin(f(n)))
}
async fn setup_test(fix_fn: Fixture) -> (Resolver, KanidmClient) {
sketching::test_init();
let mut counter = 0;
let port = loop {
let possible_port = PORT_ALLOC.fetch_add(1, Ordering::SeqCst);
if is_free_port(possible_port) {
break possible_port;
}
counter += 1;
#[allow(clippy::assertions_on_constants)]
if counter >= 5 {
eprintln!("Unable to allocate port!");
debug_assert!(false);
}
};
let int_config = Box::new(IntegrationTestConfig {
admin_user: ADMIN_TEST_USER.to_string(),
admin_password: ADMIN_TEST_PASSWORD.to_string(),
idm_admin_user: IDM_ADMIN_TEST_USER.to_string(),
idm_admin_password: IDM_ADMIN_TEST_PASSWORD.to_string(),
});
// Setup the config ...
let mut config = Configuration::new();
config.address = format!("127.0.0.1:{}", port);
config.integration_test_config = Some(int_config);
config.role = ServerRole::WriteReplicaNoUI;
config.threads = 1;
create_server_core(config, false)
.await
.expect("failed to start server core");
// We have to yield now to guarantee that the elements are setup.
task::yield_now().await;
// Setup the client, and the address we selected.
let addr = format!("http://127.0.0.1:{}", port);
// Run fixtures
let adminclient = KanidmClientBuilder::new()
.address(addr.clone())
.enable_native_ca_roots(false)
.no_proxy()
.build()
.expect("Failed to build sync client");
fix_fn(adminclient).await;
let client = KanidmClientBuilder::new()
.address(addr.clone())
.enable_native_ca_roots(false)
.no_proxy()
.build()
.expect("Failed to build async admin client");
let rsclient = KanidmClientBuilder::new()
.address(addr)
.enable_native_ca_roots(false)
.no_proxy()
.build()
.expect("Failed to build client");
let db = Db::new(
"", // The sqlite db path, this is in memory.
)
.expect("Failed to setup DB");
let mut dbtxn = db.write().await;
dbtxn.migrate().expect("Unable to migrate cache db");
let mut hsm = BoxedDynTpm::new(SoftTpm::new());
let auth_value = AuthValue::ephemeral().unwrap();
let loadable_machine_key = hsm.machine_key_create(&auth_value).unwrap();
let machine_key = hsm
.machine_key_load(&auth_value, &loadable_machine_key)
.unwrap();
let system_provider = SystemProvider::new().unwrap();
let idprovider = KanidmProvider::new(
rsclient,
&KanidmConfig {
conn_timeout: 1,
request_timeout: 1,
pam_allowed_login_groups: vec!["allowed_group".to_string()],
map_group: vec![GroupMap {
local: "extensible_group".to_string(),
with: "testgroup1".to_string(),
}],
},
SystemTime::now(),
&mut (&mut dbtxn).into(),
&mut hsm,
&machine_key,
)
.unwrap();
drop(machine_key);
dbtxn.commit().expect("Unable to commit dbtxn");
let cachelayer = Resolver::new(
db,
Arc::new(system_provider),
vec![Arc::new(idprovider)],
hsm,
300,
DEFAULT_SHELL.to_string(),
DEFAULT_HOME_PREFIX.into(),
DEFAULT_HOME_ATTR,
DEFAULT_HOME_ALIAS,
DEFAULT_UID_ATTR_MAP,
DEFAULT_GID_ATTR_MAP,
)
.await
.expect("Failed to build cache layer.");
// test_fn(cachelayer, client);
(cachelayer, client)
// We DO NOT need teardown, as sqlite is in mem
// let the tables hit the floor
}
/// This is the test fixture. It sets up the following:
/// - adds admin to idm_admins
/// - creates a test account (testaccount1)
/// - extends the test account with posix attrs
/// - adds a ssh public key to the test account
/// - sets a posix password for the test account
/// - creates a test group (testgroup1) and adds the test account to the test group
/// - extends testgroup1 with posix attrs
/// - creates two more groups with unix perms (allowed_group, masked_group)
async fn test_fixture(rsclient: KanidmClient) {
let res = rsclient
.auth_simple_password("admin", ADMIN_TEST_PASSWORD)
.await;
debug!("auth_simple_password res: {:?}", res);
trace!("{:?}", &res);
assert!(res.is_ok());
// Create a new account
rsclient
.idm_person_account_create("testaccount1", "Posix Demo Account")
.await
.unwrap();
// Extend the account with posix attrs.
rsclient
.idm_person_account_unix_extend("testaccount1", Some(20000), None)
.await
.unwrap();
// Assign an ssh public key.
rsclient
.idm_person_account_post_ssh_pubkey("testaccount1", "tk",
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAeGW1P6Pc2rPq0XqbRaDKBcXZUPRklo0L1EyR30CwoP william@amethyst")
.await
.unwrap();
// Set a posix password
rsclient
.idm_person_account_unix_cred_put("testaccount1", TESTACCOUNT1_PASSWORD_A)
.await
.unwrap();
// Setup a group
rsclient.idm_group_create("testgroup1", None).await.unwrap();
rsclient
.idm_group_add_members("testgroup1", &["testaccount1"])
.await
.unwrap();
rsclient
.idm_group_unix_extend("testgroup1", Some(20001))
.await
.unwrap();
// Setup the allowed group
rsclient
.idm_group_create("allowed_group", None)
.await
.unwrap();
rsclient
.idm_group_unix_extend("allowed_group", Some(20002))
.await
.unwrap();
// Setup a group that is masked by nxset, but allowed in overrides
rsclient
.idm_group_create("masked_group", None)
.await
.unwrap();
rsclient
.idm_group_unix_extend("masked_group", Some(20003))
.await
.unwrap();
}
#[tokio::test]
async fn test_cache_sshkey() {
let (cachelayer, _adminclient) = setup_test(fixture(test_fixture)).await;
// Force offline. Show we have no keys.
cachelayer.mark_offline().await;
let sk = cachelayer
.get_sshkeys("testaccount1")
.await
.expect("Failed to get from cache.");
assert!(sk.is_empty());
// Bring ourselves online.
cachelayer.mark_next_check_now(SystemTime::now()).await;
assert!(cachelayer.test_connection().await);
let sk = cachelayer
.get_sshkeys("testaccount1")
.await
.expect("Failed to get from cache.");
assert_eq!(sk.len(), 1);
// Go offline, and get from cache.
cachelayer.mark_offline().await;
let sk = cachelayer
.get_sshkeys("testaccount1")
.await
.expect("Failed to get from cache.");
assert_eq!(sk.len(), 1);
}
#[tokio::test]
async fn test_cache_account() {
let (cachelayer, _adminclient) = setup_test(fixture(test_fixture)).await;
// Force offline. Show we have no account
cachelayer.mark_offline().await;
let ut = cachelayer
.get_nssaccount_name("testaccount1")
.await
.expect("Failed to get from cache");
assert!(ut.is_none());
// go online
cachelayer.mark_next_check_now(SystemTime::now()).await;
assert!(cachelayer.test_connection().await);
// get the account
let ut = cachelayer
.get_nssaccount_name("testaccount1")
.await
.expect("Failed to get from cache");
assert!(ut.is_some());
// #392: Check that a `shell=None` is set to `default_shell`.
assert_eq!(ut.unwrap().shell, *DEFAULT_SHELL);
// go offline
cachelayer.mark_offline().await;
// can still get account
let ut = cachelayer
.get_nssaccount_name("testaccount1")
.await
.expect("Failed to get from cache");
assert!(ut.is_some());
// Finally, check we have "all accounts" in the list.
let us = cachelayer
.get_nssaccounts()
.await
.expect("failed to list all accounts");
assert_eq!(us.len(), 1);
}
#[tokio::test]
async fn test_cache_group() {
let (cachelayer, _adminclient) = setup_test(fixture(test_fixture)).await;
// Force offline. Show we have no groups.
cachelayer.mark_offline().await;
let gt = cachelayer
.get_nssgroup_name("testgroup1")
.await
.expect("Failed to get from cache");
assert!(gt.is_none());
// go online. Get the group
cachelayer.mark_next_check_now(SystemTime::now()).await;
assert!(cachelayer.test_connection().await);
let gt = cachelayer
.get_nssgroup_name("testgroup1")
.await
.expect("Failed to get from cache");
assert!(gt.is_some());
// go offline. still works
cachelayer.mark_offline().await;
let gt = cachelayer
.get_nssgroup_name("testgroup1")
.await
.expect("Failed to get from cache");
assert!(gt.is_some());
// And check we have no members in the group. Members are an artifact of
// user lookups!
assert!(gt.unwrap().members.is_empty());
// clear cache, go online
assert!(cachelayer.invalidate().await.is_ok());
cachelayer.mark_next_check_now(SystemTime::now()).await;
assert!(cachelayer.test_connection().await);
// get an account with the group
// DO NOT get the group yet.
let ut = cachelayer
.get_nssaccount_name("testaccount1")
.await
.expect("Failed to get from cache");
assert!(ut.is_some());
// go offline.
cachelayer.mark_offline().await;
// show we have the group despite no direct calls
let gt = cachelayer
.get_nssgroup_name("testgroup1")
.await
.expect("Failed to get from cache");
assert!(gt.is_some());
// And check we have members in the group, since we came from a userlook up
assert_eq!(gt.unwrap().members.len(), 1);
// Finally, check we have "all groups" in the list.
let gs = cachelayer
.get_nssgroups()
.await
.expect("failed to list all groups");
assert_eq!(gs.len(), 2);
}
#[tokio::test]
async fn test_cache_group_delete() {
let (cachelayer, adminclient) = setup_test(fixture(test_fixture)).await;
// get the group
cachelayer.mark_next_check_now(SystemTime::now()).await;
assert!(cachelayer.test_connection().await);
let gt = cachelayer
.get_nssgroup_name("testgroup1")
.await
.expect("Failed to get from cache");
assert!(gt.is_some());
// delete it.
adminclient
.auth_simple_password("admin", ADMIN_TEST_PASSWORD)
.await
.expect("failed to auth as admin");
adminclient
.idm_group_delete("testgroup1")
.await
.expect("failed to delete");
// invalidate cache
assert!(cachelayer.invalidate().await.is_ok());
// "get it"
// should be empty.
let gt = cachelayer
.get_nssgroup_name("testgroup1")
.await
.expect("Failed to get from cache");
assert!(gt.is_none());
}
#[tokio::test]
async fn test_cache_account_delete() {
let (cachelayer, adminclient) = setup_test(fixture(test_fixture)).await;
// get the account
cachelayer.mark_next_check_now(SystemTime::now()).await;
assert!(cachelayer.test_connection().await);
let ut = cachelayer
.get_nssaccount_name("testaccount1")
.await
.expect("Failed to get from cache");
assert!(ut.is_some());
// delete it.
adminclient
.auth_simple_password("admin", ADMIN_TEST_PASSWORD)
.await
.expect("failed to auth as admin");
adminclient
.idm_person_account_delete("testaccount1")
.await
.expect("failed to delete");
// invalidate cache
assert!(cachelayer.invalidate().await.is_ok());
// "get it"
let ut = cachelayer
.get_nssaccount_name("testaccount1")
.await
.expect("Failed to get from cache");
// should be empty.
assert!(ut.is_none());
// The group should be removed too.
let gt = cachelayer
.get_nssgroup_name("testaccount1")
.await
.expect("Failed to get from cache");
assert!(gt.is_none());
}
#[tokio::test]
async fn test_cache_account_password() {
let current_time = OffsetDateTime::now_utc();
let (cachelayer, adminclient) = setup_test(fixture(test_fixture)).await;
cachelayer.mark_next_check_now(SystemTime::now()).await;
// Test authentication failure.
let a1 = cachelayer
.pam_account_authenticate("testaccount1", current_time, TESTACCOUNT1_PASSWORD_INC)
.await
.expect("failed to authenticate");
assert_eq!(a1, Some(false));
// We have to wait due to softlocking.
tokio::time::sleep(Duration::from_secs(1)).await;
// Test authentication success.
let a2 = cachelayer
.pam_account_authenticate("testaccount1", current_time, TESTACCOUNT1_PASSWORD_A)
.await
.expect("failed to authenticate");
assert_eq!(a2, Some(true));
// change pw
adminclient
.auth_simple_password("admin", ADMIN_TEST_PASSWORD)
.await
.expect("failed to auth as admin");
adminclient
.idm_person_account_unix_cred_put("testaccount1", TESTACCOUNT1_PASSWORD_B)
.await
.expect("Failed to change password");
// test auth (old pw) fail
let a3 = cachelayer
.pam_account_authenticate("testaccount1", current_time, TESTACCOUNT1_PASSWORD_A)
.await
.expect("failed to authenticate");
assert_eq!(a3, Some(false));
// We have to wait due to softlocking.
tokio::time::sleep(Duration::from_secs(1)).await;
// test auth (new pw) success
let a4 = cachelayer
.pam_account_authenticate("testaccount1", current_time, TESTACCOUNT1_PASSWORD_B)
.await
.expect("failed to authenticate");
assert_eq!(a4, Some(true));
// Go offline.
cachelayer.mark_offline().await;
// Test auth success
let a5 = cachelayer
.pam_account_authenticate("testaccount1", current_time, TESTACCOUNT1_PASSWORD_B)
.await
.expect("failed to authenticate");
assert_eq!(a5, Some(true));
// No softlock during offline.
// Test auth failure.
let a6 = cachelayer
.pam_account_authenticate("testaccount1", current_time, TESTACCOUNT1_PASSWORD_INC)
.await
.expect("failed to authenticate");
assert_eq!(a6, Some(false));
// clear cache
cachelayer
.clear_cache()
.await
.expect("failed to clear cache");
// test auth good (fail)
let a7 = cachelayer
.pam_account_authenticate("testaccount1", current_time, TESTACCOUNT1_PASSWORD_B)
.await
.expect("failed to authenticate");
assert!(a7.is_none());
// go online
cachelayer.mark_next_check_now(SystemTime::now()).await;
assert!(cachelayer.test_connection().await);
// test auth success
let a8 = cachelayer
.pam_account_authenticate("testaccount1", current_time, TESTACCOUNT1_PASSWORD_B)
.await
.expect("failed to authenticate");
assert_eq!(a8, Some(true));
}
#[tokio::test]
async fn test_cache_account_pam_allowed() {
let (cachelayer, adminclient) = setup_test(fixture(test_fixture)).await;
cachelayer.mark_next_check_now(SystemTime::now()).await;
// Should fail
let a1 = cachelayer
.pam_account_allowed("testaccount1")
.await
.expect("failed to authenticate");
assert_eq!(a1, Some(false));
adminclient
.auth_simple_password("admin", ADMIN_TEST_PASSWORD)
.await
.expect("failed to auth as admin");
adminclient
.idm_group_add_members("allowed_group", &["testaccount1"])
.await
.unwrap();
// Invalidate cache to force a refresh
assert!(cachelayer.invalidate().await.is_ok());
// Should pass
let a2 = cachelayer
.pam_account_allowed("testaccount1")
.await
.expect("failed to authenticate");
assert_eq!(a2, Some(true));
}
#[tokio::test]
async fn test_cache_account_pam_nonexist() {
let current_time = OffsetDateTime::now_utc();
let (cachelayer, _adminclient) = setup_test(fixture(test_fixture)).await;
cachelayer.mark_next_check_now(SystemTime::now()).await;
let a1 = cachelayer
.pam_account_allowed("NO_SUCH_ACCOUNT")
.await
.expect("failed to authenticate");
assert!(a1.is_none());
let a2 = cachelayer
.pam_account_authenticate("NO_SUCH_ACCOUNT", current_time, TESTACCOUNT1_PASSWORD_B)
.await
.expect("failed to authenticate");
assert!(a2.is_none());
cachelayer.mark_offline().await;
let a1 = cachelayer
.pam_account_allowed("NO_SUCH_ACCOUNT")
.await
.expect("failed to authenticate");
assert!(a1.is_none());
let a2 = cachelayer
.pam_account_authenticate("NO_SUCH_ACCOUNT", current_time, TESTACCOUNT1_PASSWORD_B)
.await
.expect("failed to authenticate");
assert!(a2.is_none());
}
#[tokio::test]
async fn test_cache_account_expiry() {
let current_time = OffsetDateTime::now_utc();
let (cachelayer, adminclient) = setup_test(fixture(test_fixture)).await;
cachelayer.mark_next_check_now(SystemTime::now()).await;
assert!(cachelayer.test_connection().await);
// We need one good auth first to prime the cache with a hash.
let a1 = cachelayer
.pam_account_authenticate("testaccount1", current_time, TESTACCOUNT1_PASSWORD_A)
.await
.expect("failed to authenticate");
assert_eq!(a1, Some(true));
// Invalidate to make sure we go online next checks.
assert!(cachelayer.invalidate().await.is_ok());
// expire the account
adminclient
.auth_simple_password("admin", ADMIN_TEST_PASSWORD)
.await
.expect("failed to auth as admin");
adminclient
.idm_person_account_set_attr("testaccount1", ATTR_ACCOUNT_EXPIRE, &[ACCOUNT_EXPIRE])
.await
.unwrap();
// auth will fail
let a2 = cachelayer
.pam_account_authenticate("testaccount1", current_time, TESTACCOUNT1_PASSWORD_A)
.await
.expect("failed to authenticate");
assert_eq!(a2, Some(false));
// ssh keys should be empty
let sk = cachelayer
.get_sshkeys("testaccount1")
.await
.expect("Failed to get from cache.");
assert!(sk.is_empty());
// Pam account allowed should be denied.
let a3 = cachelayer
.pam_account_allowed("testaccount1")
.await
.expect("failed to authenticate");
assert_eq!(a3, Some(false));
// go offline
cachelayer.mark_offline().await;
// Now, check again. Since this uses the cached pw and we are offline, this
// will now succeed.
let a4 = cachelayer
.pam_account_authenticate("testaccount1", current_time, TESTACCOUNT1_PASSWORD_A)
.await
.expect("failed to authenticate");
assert_eq!(a4, Some(true));
// ssh keys should be empty
let sk = cachelayer
.get_sshkeys("testaccount1")
.await
.expect("Failed to get from cache.");
assert!(sk.is_empty());
// Pam account allowed should be denied.
let a5 = cachelayer
.pam_account_allowed("testaccount1")
.await
.expect("failed to authenticate");
assert_eq!(a5, Some(false));
}
#[tokio::test]
async fn test_cache_nxcache() {
let (cachelayer, _adminclient) = setup_test(fixture(test_fixture)).await;
cachelayer.mark_next_check_now(SystemTime::now()).await;
assert!(cachelayer.test_connection().await);
// Is it in the nxcache?
assert!(cachelayer
.check_nxcache(&Id::Name("oracle".to_string()))
.await
.is_none());
assert!(cachelayer.check_nxcache(&Id::Gid(2000)).await.is_none());
assert!(cachelayer
.check_nxcache(&Id::Name("oracle_group".to_string()))
.await
.is_none());
assert!(cachelayer.check_nxcache(&Id::Gid(3000)).await.is_none());
// Look for the acc id + nss id
let ut = cachelayer
.get_nssaccount_name("oracle")
.await
.expect("Failed to get from cache");
assert!(ut.is_none());
let ut = cachelayer
.get_nssaccount_gid(2000)
.await
.expect("Failed to get from cache");
assert!(ut.is_none());
let gt = cachelayer
.get_nssgroup_name("oracle_group")
.await
.expect("Failed to get from cache");
assert!(gt.is_none());
let gt = cachelayer
.get_nssgroup_gid(3000)
.await
.expect("Failed to get from cache");
assert!(gt.is_none());
// Should all now be nxed
assert!(
cachelayer
.check_nxcache(&Id::Name("oracle".to_string()))
.await
.is_some(),
"'oracle' Wasn't in the nxcache!"
);
assert!(cachelayer.check_nxcache(&Id::Gid(2000)).await.is_some());
assert!(cachelayer
.check_nxcache(&Id::Name("oracle_group".to_string()))
.await
.is_some());
assert!(cachelayer.check_nxcache(&Id::Gid(3000)).await.is_some());
// invalidate cache
assert!(cachelayer.invalidate().await.is_ok());
// Both should NOT be in nxcache now.
assert!(cachelayer
.check_nxcache(&Id::Name("oracle".to_string()))
.await
.is_none());
assert!(cachelayer.check_nxcache(&Id::Gid(2000)).await.is_none());
assert!(cachelayer
.check_nxcache(&Id::Name("oracle_group".to_string()))
.await
.is_none());
assert!(cachelayer.check_nxcache(&Id::Gid(3000)).await.is_none());
}
#[tokio::test]
async fn test_cache_nxset_account() {
let (cachelayer, _adminclient) = setup_test(fixture(test_fixture)).await;
// Important! This is what sets up that testaccount1 won't be resolved
// because it's in the "local" user set.
cachelayer
.reload_system_identities(
vec![EtcUser {
name: "testaccount1".to_string(),
uid: 30000,
gid: 30000,
password: Default::default(),
gecos: Default::default(),
homedir: Default::default(),
shell: Default::default(),
}],
None,
vec![],
)
.await;
// go online
cachelayer.mark_next_check_now(SystemTime::now()).await;
assert!(cachelayer.test_connection().await);
// get the account
let ut = cachelayer
.get_nssaccount_name("testaccount1")
.await
.expect("Failed to get from cache");
let ut = ut.unwrap();
// Assert the user is the system version.
assert_eq!(ut.uid, 30000);
// go offline
cachelayer.mark_offline().await;
// still not present, was not cached.
let ut = cachelayer
.get_nssaccount_name("testaccount1")
.await
.expect("Failed to get from cache");
let ut = ut.unwrap();
// Assert the user is the system version.
assert_eq!(ut.uid, 30000);
// Finally, check it's the system version in all accounts.
let us = cachelayer
.get_nssaccounts()
.await
.expect("failed to list all accounts");
let us: Vec<_> = us
.into_iter()
.filter(|nss_user| nss_user.name == "testaccount1")
.collect();
assert_eq!(us.len(), 1);
assert_eq!(us[0].gid, 30000);
}
#[tokio::test]
async fn test_cache_nxset_group() {
let (cachelayer, _adminclient) = setup_test(fixture(test_fixture)).await;
// Important! This is what sets up that testgroup1 won't be resolved
// because it's in the "local" group set.
cachelayer
.reload_system_identities(
vec![],
None,
vec![EtcGroup {
name: "testgroup1".to_string(),
// Important! We set the GID to differ from what kanidm stores so we can
// tell we got the system version.
gid: 30001,
password: Default::default(),
members: Default::default(),
}],
)
.await;
// go online. Get the group
cachelayer.mark_next_check_now(SystemTime::now()).await;
assert!(cachelayer.test_connection().await);
let gt = cachelayer
.get_nssgroup_name("testgroup1")
.await
.expect("Failed to get from cache");
// We get the group, it's the system version. Check the gid.
let gt = gt.unwrap();
assert_eq!(gt.gid, 30001);
// go offline. still works
cachelayer.mark_offline().await;
let gt = cachelayer
.get_nssgroup_name("testgroup1")
.await
.expect("Failed to get from cache");
let gt = gt.unwrap();
assert_eq!(gt.gid, 30001);
// clear cache, go online
assert!(cachelayer.invalidate().await.is_ok());
cachelayer.mark_next_check_now(SystemTime::now()).await;
assert!(cachelayer.test_connection().await);
// get a kanidm account with the kanidm equivalent group
let ut = cachelayer
.get_nssaccount_name("testaccount1")
.await
.expect("Failed to get from cache");
assert!(ut.is_some());
// go offline.
cachelayer.mark_offline().await;
// show that the group we have is still the system version, and lacks our
// member.
let gt = cachelayer
.get_nssgroup_name("testgroup1")
.await
.expect("Failed to get from cache");
let gt = gt.unwrap();
assert_eq!(gt.gid, 30001);
assert!(gt.members.is_empty());
// Finally, check we only have the system group version in the list.
let gs = cachelayer
.get_nssgroups()
.await
.expect("failed to list all groups");
let gs: Vec<_> = gs
.into_iter()
.filter(|nss_group| nss_group.name == "testgroup1")
.collect();
debug!("{:?}", gs);
assert_eq!(gs.len(), 1);
assert_eq!(gs[0].gid, 30001);
}
#[tokio::test]
async fn test_cache_authenticate_system_account() {
const SECURE_PASSWORD: &str = "a";
let current_time = OffsetDateTime::UNIX_EPOCH + time::Duration::days(365);
let expire_time = OffsetDateTime::UNIX_EPOCH + time::Duration::days(380);
let (cachelayer, _adminclient) = setup_test(fixture(test_fixture)).await;
// Important! This is what sets up that testaccount1 won't be resolved
// because it's in the "local" user set.
cachelayer
.reload_system_identities(
vec![
EtcUser {
name: "testaccount1".to_string(),
uid: 30000,
gid: 30000,
password: Default::default(),
gecos: Default::default(),
homedir: Default::default(),
shell: Default::default(),
},
EtcUser {
name: "testaccount2".to_string(),
uid: 30001,
gid: 30001,
password: Default::default(),
gecos: Default::default(),
homedir: Default::default(),
shell: Default::default(),
}
],
Some(vec![
EtcShadow {
name: "testaccount1".to_string(),
// The very secure password, "a".
password: CryptPw::Sha512("$6$5.bXZTIXuVv.xI3.$sAubscCJPwnBWwaLt2JR33lo539UyiDku.aH5WVSX0Tct9nGL2ePMEmrqT3POEdBlgNQ12HJBwskewGu2dpF//".to_string()),
epoch_change_days: None,
days_min_password_age: 0,
days_max_password_age: Some(1),
days_warning_period: 1,
days_inactivity_period: None,
epoch_expire_date: Some(380),
flag_reserved: None
},
EtcShadow {
name: "testaccount2".to_string(),
// The very secure password, "a".
password: CryptPw::Sha512("$6$5.bXZTIXuVv.xI3.$sAubscCJPwnBWwaLt2JR33lo539UyiDku.aH5WVSX0Tct9nGL2ePMEmrqT3POEdBlgNQ12HJBwskewGu2dpF//".to_string()),
epoch_change_days: Some(364),
days_min_password_age: 0,
days_max_password_age: Some(2),
days_warning_period: 1,
days_inactivity_period: None,
epoch_expire_date: Some(380),
flag_reserved: None
},
]),
vec![],
)
.await;
// get the accounts to assert they exist,
let _ = cachelayer
.get_nssaccount_name("testaccount1")
.await
.expect("Failed to get from cache");
let _ = cachelayer
.get_nssaccount_name("testaccount2")
.await
.expect("Failed to get from cache");
// Non exist name
let a1 = cachelayer
.pam_account_authenticate("testaccount69", current_time, SECURE_PASSWORD)
.await
.expect("failed to authenticate");
assert_eq!(a1, None);
// Check wrong pw.
let a1 = cachelayer
.pam_account_authenticate("testaccount1", current_time, "wrong password")
.await
.expect("failed to authenticate");
assert_eq!(a1, Some(false));
// Check correct pw (both accounts)
let a1 = cachelayer
.pam_account_authenticate("testaccount1", current_time, SECURE_PASSWORD)
.await
.expect("failed to authenticate");
assert_eq!(a1, Some(true));
let a1 = cachelayer
.pam_account_authenticate("testaccount2", current_time, SECURE_PASSWORD)
.await
.expect("failed to authenticate");
assert_eq!(a1, Some(true));
// Check expired time (both accounts)
let a1 = cachelayer
.pam_account_authenticate("testaccount1", expire_time, SECURE_PASSWORD)
.await
.expect("failed to authenticate");
assert_eq!(a1, Some(false));
let a1 = cachelayer
.pam_account_authenticate("testaccount2", expire_time, SECURE_PASSWORD)
.await
.expect("failed to authenticate");
assert_eq!(a1, Some(false));
// due to how posix auth works, session and authorisation are simpler, and should
// always just return "true".
let a1 = cachelayer
.pam_account_allowed("testaccount1")
.await
.expect("failed to authorise");
assert_eq!(a1, Some(true));
let a1 = cachelayer
.pam_account_allowed("testaccount2")
.await
.expect("failed to authorise");
assert_eq!(a1, Some(true));
// Should we make home dirs?
let a1 = cachelayer
.pam_account_beginsession("testaccount1")
.await
.expect("failed to begin session");
assert_eq!(a1, None);
let a1 = cachelayer
.pam_account_beginsession("testaccount2")
.await
.expect("failed to begin session");
assert_eq!(a1, None);
}
/// Issue 1830. If cache items expire where we have an account and a group, and we
/// refresh the group *first*, the group appears to drop it's members. This is because
/// sqlite "INSERT OR REPLACE INTO" triggers a delete cascade of the foreign key elements
/// which then makes the group appear empty.
///
/// We can reproduce this by retrieving an account + group, wait for expiry, then retrieve
/// only the group.
#[tokio::test]
async fn test_cache_group_fk_deferred() {
let (cachelayer, _adminclient) = setup_test(fixture(test_fixture)).await;
cachelayer.mark_next_check_now(SystemTime::now()).await;
assert!(cachelayer.test_connection().await);
// Get the account then the group.
let ut = cachelayer
.get_nssaccount_name("testaccount1")
.await
.expect("Failed to get from cache");
assert!(ut.is_some());
let gt = cachelayer
.get_nssgroup_name("testgroup1")
.await
.expect("Failed to get from cache");
assert!(gt.is_some());
assert_eq!(gt.unwrap().members.len(), 1);
// Invalidate all items.
cachelayer.mark_offline().await;
assert!(cachelayer.invalidate().await.is_ok());
cachelayer.mark_next_check_now(SystemTime::now()).await;
assert!(cachelayer.test_connection().await);
// Get the *group*. It *should* still have it's members.
let gt = cachelayer
.get_nssgroup_name("testgroup1")
.await
.expect("Failed to get from cache");
assert!(gt.is_some());
// And check we have members in the group, since we came from a userlook up
assert_eq!(gt.unwrap().members.len(), 1);
}
#[tokio::test]
/// Test group extension. Groups extension is not the same as "overriding". Extension
/// only allows the *members* of a remote group to supplement the members of the local
/// group. This prevents a remote group changing the gidnumber of the local group and
/// causing breakages.
async fn test_cache_extend_group_members() {
let (cachelayer, _adminclient) = setup_test(fixture(test_fixture)).await;
cachelayer
.reload_system_identities(
vec![EtcUser {
name: "local_account".to_string(),
uid: 30000,
gid: 30000,
password: Default::default(),
gecos: Default::default(),
homedir: Default::default(),
shell: Default::default(),
}],
None,
vec![EtcGroup {
// This group is configured to allow extension from
// the group "testgroup1"
name: "extensible_group".to_string(),
gid: 30001,
password: Default::default(),
// We have the local account as a member, it should NOT be stomped.
members: vec!["local_account".to_string()],
}],
)
.await;
// Force offline. Show we have no groups.
cachelayer.mark_offline().await;
let gt = cachelayer
.get_nssgroup_name("testgroup1")
.await
.expect("Failed to get from cache");
assert!(gt.is_none());
// While offline, extensible_group has only local_account as a member.
let gt = cachelayer
.get_nssgroup_name("extensible_group")
.await
.expect("Failed to get from cache");
let gt = gt.unwrap();
assert_eq!(gt.gid, 30001);
assert_eq!(gt.members.as_slice(), &["local_account".to_string()]);
// Go online. Group now exists, extensible_group has group members.
// Need to resolve test-account first so that the membership is linked.
cachelayer.mark_next_check_now(SystemTime::now()).await;
assert!(cachelayer.test_connection().await);
let ut = cachelayer
.get_nssaccount_name("testaccount1")
.await
.expect("Failed to get from cache");
assert!(ut.is_some());
let gt = cachelayer
.get_nssgroup_name("testgroup1")
.await
.expect("Failed to get from cache");
let gt = gt.unwrap();
assert_eq!(gt.gid, 20001);
assert_eq!(
gt.members.as_slice(),
&["testaccount1@idm.example.com".to_string()]
);
let gt = cachelayer
.get_nssgroup_name("extensible_group")
.await
.expect("Failed to get from cache");
let gt = gt.unwrap();
// Even though it's extended, still needs to be the local uid/gid
assert_eq!(gt.gid, 30001);
assert_eq!(
gt.members.as_slice(),
&[
"local_account".to_string(),
"testaccount1@idm.example.com".to_string()
]
);
let groups = cachelayer
.get_nssgroups()
.await
.expect("Failed to get from cache");
assert!(groups.iter().any(|group| {
group.name == "extensible_group"
&& group.members.as_slice()
== &[
"local_account".to_string(),
"testaccount1@idm.example.com".to_string(),
]
}));
// Go offline. Group cached, extensible_group has members.
cachelayer.mark_offline().await;
let gt = cachelayer
.get_nssgroup_name("testgroup1")
.await
.expect("Failed to get from cache");
let gt = gt.unwrap();
assert_eq!(gt.gid, 20001);
assert_eq!(
gt.members.as_slice(),
&["testaccount1@idm.example.com".to_string()]
);
let gt = cachelayer
.get_nssgroup_name("extensible_group")
.await
.expect("Failed to get from cache");
let gt = gt.unwrap();
// Even though it's extended, still needs to be the local uid/gid
assert_eq!(gt.gid, 30001);
assert_eq!(
gt.members.as_slice(),
&[
"local_account".to_string(),
"testaccount1@idm.example.com".to_string()
]
);
// clear cache
cachelayer
.clear_cache()
.await
.expect("failed to clear cache");
// No longer has testaccount.
let gt = cachelayer
.get_nssgroup_name("extensible_group")
.await
.expect("Failed to get from cache");
let gt = gt.unwrap();
assert_eq!(gt.gid, 30001);
assert_eq!(gt.members.as_slice(), &["local_account".to_string()]);
}