mirror of
https://github.com/kanidm/kanidm.git
synced 2025-04-29 05:35:05 +02:00
The implementation of the unixd cache relies on inotify to detect changes to files in /etc so that we know when to reload the data for nss/passwd. However, the way that groupadd/del and other tools work is they copy the file, change it, and then move it into place. It turns out that william of the past didn't realise that inotify works on inodes not paths like other tools do (auditctl for example). As a result, when something modified /etc/group or another related file, the removal was seen, but this breaks notifications on any future change until you reload unixd. To resolve this we need to recursively watch /etc with inotify - yep, that's correct. We have to watch everything in /etc for changes because it's the only way to pick up on the add/remove of files. But because we have to watch everything, we need permissions to watch everything. This forces us to move the parsing of the etc passwd/group/shadow files to the unixd tasks daemon - arguably, this is the correct place to read these anyway since that is a high priv (and locked down) daemon. Because of this, we actually end up solving the missing "shadow" group on debian issue, and probably similar on the BSD's in future. In order to make my life easier while testing I also threw in a makefile that symlinks the files to needed locations for testing. It has plenty of warnings as it should. Fixes #3499 Fixes #3407 Fixes #3249
1245 lines
38 KiB
Rust
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(),
|
|
}],
|
|
vec![],
|
|
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![],
|
|
vec![],
|
|
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(),
|
|
}
|
|
],
|
|
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(),
|
|
}],
|
|
vec![],
|
|
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()]);
|
|
}
|