mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 20:47:01 +01:00
This completely reworks how we approach and handle cryptographic keys in Kanidm. This is needed as a foundation for replication coordination which will require handling and rotation of cryptographic keys in automated ways. This change influences many other parts of the code base in it's implementation. The primary influences are: * Modification of how domain user signing keys are revoked or rotated. * Merging of all existing service-account token keys are retired (retained) keys into the domain to simplify token signing and validation * Allowing multiple configurations of local command line tools to swap between instances using disparate signing keys. * Modification of key retrieval to be key id based (KID), removing the need to embed the JWK into tokens A side effect of this change is that most user authentication sessions and oauth2 sessions will have to be re-established after upgrade. However we feel that session renewal after upgrade is an expected side effect of an upgrade. In the future this lays the ground work to remove a large number of legacy key handling processes that have evolved, which will allow large parts of code to be removed.
386 lines
12 KiB
Rust
386 lines
12 KiB
Rust
//! Integration tests using browser automation
|
|
|
|
use compact_jwt::{traits::JwsVerifiable, JwsCompact};
|
|
use kanidm_client::KanidmClient;
|
|
use kanidmd_lib::constants::EntryClass;
|
|
use kanidmd_testkit::login_put_admin_idm_admins;
|
|
|
|
use std::str::FromStr;
|
|
|
|
/// Tries to handle closing the webdriver session if there's an error
|
|
#[allow(unused_macros)]
|
|
macro_rules! handle_error {
|
|
($client:ident, $e:expr, $msg:expr) => {
|
|
match $e {
|
|
Ok(e) => e,
|
|
Err(e) => {
|
|
$client.close().await.unwrap();
|
|
panic!("{:?}: {:?}", $msg, e);
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
/// Tries to get the webdriver client, trying the default chromedriver port if the default selenium port doesn't work
|
|
#[allow(dead_code)]
|
|
#[cfg(all(feature = "webdriver", any(test, debug_assertions)))]
|
|
async fn get_webdriver_client() -> fantoccini::Client {
|
|
use fantoccini::wd::Capabilities;
|
|
use serde_json::json;
|
|
|
|
// check if the env var "CI" is set
|
|
let in_ci = match std::env::var("CI") {
|
|
Ok(_) => true,
|
|
Err(_) => false,
|
|
};
|
|
if !in_ci {
|
|
match fantoccini::ClientBuilder::native()
|
|
.connect("http://localhost:4444")
|
|
.await
|
|
{
|
|
Ok(val) => val,
|
|
Err(_) => {
|
|
// trying the default chromedriver port
|
|
eprintln!("Couldn't connect on 4444, trying 9515");
|
|
fantoccini::ClientBuilder::new(hyper_tls::HttpsConnector::new())
|
|
.connect("http://localhost:9515")
|
|
.await
|
|
.unwrap()
|
|
}
|
|
}
|
|
} else {
|
|
println!("In CI setting headless and assuming Chrome");
|
|
let cap = json!({
|
|
"goog:chromeOptions" : {
|
|
"args" : ["--headless", "--no-sandbox", "--disable-gpu", "--disable-dev-shm-usage", "--window-size=1280,1024"]
|
|
}
|
|
});
|
|
let cap: Capabilities = serde_json::from_value(cap).unwrap();
|
|
fantoccini::ClientBuilder::new(hyper_tls::HttpsConnector::new())
|
|
.capabilities(cap)
|
|
.connect("http://localhost:9515")
|
|
.await
|
|
.unwrap()
|
|
}
|
|
}
|
|
|
|
#[kanidmd_testkit::test]
|
|
#[cfg(feature = "webdriver")]
|
|
async fn test_webdriver_user_login(rsclient: kanidm_client::KanidmClient) {
|
|
if !cfg!(feature = "webdriver") {
|
|
println!("Skipping test as webdriver feature is not enabled!");
|
|
return;
|
|
}
|
|
|
|
use fantoccini::elements::Element;
|
|
use fantoccini::Locator;
|
|
use kanidmd_testkit::*;
|
|
use std::time::Duration;
|
|
login_put_admin_idm_admins(&rsclient).await;
|
|
|
|
create_user_with_all_attrs(
|
|
&rsclient,
|
|
NOT_ADMIN_TEST_USERNAME,
|
|
Some(NOT_ADMIN_TEST_PASSWORD),
|
|
)
|
|
.await;
|
|
|
|
let c = get_webdriver_client().await;
|
|
|
|
handle_error!(
|
|
c,
|
|
c.goto(&rsclient.get_url().to_string()).await,
|
|
"Couldn't get URL"
|
|
);
|
|
|
|
println!("Waiting for page to load");
|
|
let mut wait_attempts = 0;
|
|
loop {
|
|
tokio::time::sleep(tokio::time::Duration::from_micros(200)).await;
|
|
c.wait();
|
|
|
|
if c.find(Locator::Id("username")).await.is_ok() {
|
|
break;
|
|
}
|
|
wait_attempts += 1;
|
|
if wait_attempts > 10 {
|
|
panic!("Couldn't find username field after 10 attempts!");
|
|
}
|
|
}
|
|
|
|
let id = handle_error!(
|
|
c,
|
|
c.find(Locator::Id("username")).await,
|
|
"Couldn't find input id=username"
|
|
);
|
|
handle_error!(c, id.click().await, "Couldn't click the username input?");
|
|
|
|
handle_error!(
|
|
c,
|
|
id.send_keys(NOT_ADMIN_TEST_USERNAME).await,
|
|
"Couldn't type the password?"
|
|
);
|
|
|
|
let username_form = handle_error!(
|
|
c,
|
|
c.form(Locator::Id("login")).await,
|
|
"Coudln't find login form"
|
|
);
|
|
handle_error!(
|
|
c,
|
|
username_form.submit().await,
|
|
"Couldn't submit username-login form"
|
|
);
|
|
c.wait();
|
|
tokio::time::sleep(Duration::from_millis(300)).await;
|
|
|
|
let password_form = handle_error!(
|
|
c,
|
|
c.form(Locator::Id("login")).await,
|
|
"Coudln't find login form"
|
|
);
|
|
let id = handle_error!(
|
|
c,
|
|
c.find(Locator::Id("password")).await,
|
|
"Couldn't find input id=password"
|
|
);
|
|
handle_error!(c, id.click().await, "Couldn't click the username input?");
|
|
|
|
handle_error!(
|
|
c,
|
|
id.send_keys(NOT_ADMIN_TEST_PASSWORD).await,
|
|
"Couldn't type the password?"
|
|
);
|
|
handle_error!(
|
|
c,
|
|
password_form.submit().await,
|
|
"Couldn't submit password-login form"
|
|
);
|
|
c.wait();
|
|
|
|
// try clicking the nav links
|
|
let mut navlinks: Vec<Element> = vec![];
|
|
let mut navlinks_attempts = 0;
|
|
while navlinks.is_empty() {
|
|
navlinks = handle_error!(
|
|
c,
|
|
c.find_all(Locator::Css(".nav-link")).await,
|
|
"Couldn't find nav-link CSS items"
|
|
);
|
|
navlinks_attempts += 1;
|
|
tokio::time::sleep(Duration::from_millis(200)).await;
|
|
if navlinks_attempts > 10 {
|
|
panic!("Couldn't find navlinks after 2 seconds!");
|
|
}
|
|
}
|
|
println!("Found navlinks: {:?}", navlinks);
|
|
|
|
for link in navlinks {
|
|
println!("Clicking {:?}", link.text().await);
|
|
handle_error!(c, link.click().await, &format!("Couldn't click {:?}", link));
|
|
if let Ok(text) = link.text().await {
|
|
if text.to_lowercase() == "sign out" {
|
|
println!("looking for the sign out modal to click the cancel button...");
|
|
tokio::time::sleep(Duration::from_secs(1)).await;
|
|
println!("Found the sign out modal, clicking the cancel button");
|
|
// find the cancel button and click it
|
|
let buttons = handle_error!(
|
|
c,
|
|
c.find_all(Locator::Css(".btn")).await,
|
|
"Couldn't find CSS 'btn' items"
|
|
);
|
|
println!("Found the following buttons: {:?}", buttons);
|
|
for button in buttons {
|
|
if let Ok(text) = button.text().await {
|
|
if text == "Cancel" {
|
|
println!("Found the sign out cancel button, clicking it");
|
|
handle_error!(c, button.click().await, "Couldn't click cancel button");
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// tokio::time::sleep(Duration::from_millis(3000)).await;
|
|
}
|
|
|
|
#[kanidmd_testkit::test]
|
|
async fn test_domain_reset_token_key(rsclient: KanidmClient) {
|
|
login_put_admin_idm_admins(&rsclient).await;
|
|
|
|
let token = rsclient.get_token().await.expect("No bearer token present");
|
|
|
|
let jwt = JwsCompact::from_str(&token).expect("Failed to parse jwt");
|
|
|
|
let key_id = jwt.kid().expect("token does not have a key id");
|
|
|
|
assert!(rsclient.idm_domain_revoke_key(&key_id).await.is_ok());
|
|
}
|
|
|
|
#[kanidmd_testkit::test]
|
|
async fn test_idm_domain_set_ldap_basedn(rsclient: KanidmClient) {
|
|
login_put_admin_idm_admins(&rsclient).await;
|
|
assert!(rsclient
|
|
.idm_domain_set_ldap_basedn("dc=krabsarekool,dc=example,dc=com")
|
|
.await
|
|
.is_ok());
|
|
assert!(rsclient
|
|
.idm_domain_set_ldap_basedn("krabsarekool")
|
|
.await
|
|
.is_err());
|
|
}
|
|
|
|
#[kanidmd_testkit::test]
|
|
/// Checks that a built-in group idm_all_persons has the "builtin" class as expected.
|
|
async fn test_all_persons_has_builtin_class(rsclient: KanidmClient) {
|
|
login_put_admin_idm_admins(&rsclient).await;
|
|
let res = rsclient
|
|
.idm_group_get("idm_all_persons")
|
|
.await
|
|
.expect("Failed to get idm_all_persons");
|
|
eprintln!("res: {:?}", res);
|
|
|
|
assert!(res
|
|
.unwrap()
|
|
.attrs
|
|
.get("class")
|
|
.unwrap()
|
|
.contains(&EntryClass::Builtin.as_ref().into()));
|
|
}
|
|
|
|
// /// run a test command as the admin user
|
|
// fn test_cmd_admin(token_cache_path: &str, rsclient: &KanidmClient, cmd: &str) -> Output {
|
|
// let split_cmd: Vec<&str> = cmd.split_ascii_whitespace().collect();
|
|
// test_cmd_admin_split(token_cache_path, rsclient, &split_cmd)
|
|
// }
|
|
// /// run a test command as the admin user
|
|
// fn test_cmd_admin_split(token_cache_path: &str, rsclient: &KanidmClient, cmd: &[&str]) -> Output {
|
|
// println!(
|
|
// "##################################\nrunning {}\n##################################",
|
|
// cmd.join(" ")
|
|
// );
|
|
// let res = cli_kanidm!()
|
|
// .env("KANIDM_PASSWORD", ADMIN_TEST_PASSWORD)
|
|
// .args(cmd)
|
|
// .output()
|
|
// .unwrap();
|
|
// println!("############ result ##################");
|
|
// println!("status: {:?}", res.status);
|
|
// println!("stdout: {}", String::from_utf8_lossy(&res.stdout));
|
|
// println!("stderr: {}", String::from_utf8_lossy(&res.stderr));
|
|
// println!("######################################");
|
|
// assert!(res.status.success());
|
|
// res
|
|
// }
|
|
|
|
// /// run a test command as the idm_admin user
|
|
// fn test_cmd_idm_admin(token_cache_path: &str, rsclient: &KanidmClient, cmd: &str) -> Output {
|
|
// println!("##############################\nrunning {}", cmd);
|
|
// let res = cli_kanidm!()
|
|
// .env("KANIDM_PASSWORD", IDM_ADMIN_TEST_PASSWORD)
|
|
// .args(cmd.split(" "))
|
|
// .output()
|
|
// .unwrap();
|
|
// println!("##############################\n{} result: {:?}", cmd, res);
|
|
// assert!(res.status.success());
|
|
// res
|
|
// }
|
|
|
|
// Disabled due to inconsistent test failures and blocking
|
|
/*
|
|
#[kanidmd_testkit::test]
|
|
/// Testing the CLI doing things.
|
|
async fn test_integration_with_assert_cmd(rsclient: KanidmClient) {
|
|
// setup the admin things
|
|
login_put_admin_idm_admins(&rsclient).await;
|
|
|
|
rsclient
|
|
.idm_person_account_primary_credential_set_password(
|
|
IDM_ADMIN_TEST_USER,
|
|
IDM_ADMIN_TEST_PASSWORD,
|
|
)
|
|
.await
|
|
.expect(&format!("Failed to set {} password", IDM_ADMIN_TEST_USER));
|
|
|
|
let token_cache_dir = tempdir().unwrap();
|
|
let token_cache_path = format!("{}/kanidm_tokens", token_cache_dir.path().display());
|
|
|
|
// we have to spawn in another thread for ... reasons
|
|
assert!(tokio::task::spawn_blocking(move || {
|
|
let anon_login = cli_kanidm!()
|
|
.args(&["login", "-D", "anonymous"])
|
|
.output()
|
|
.unwrap();
|
|
println!("Login Output: {:?}", anon_login);
|
|
|
|
let anon_whoami = cli_kanidm!()
|
|
.args(&["self", "whoami", "-D", "anonymous"])
|
|
.output()
|
|
.unwrap();
|
|
assert!(anon_whoami.status.success());
|
|
println!("Output: {:?}", anon_whoami);
|
|
|
|
test_cmd_admin(&token_cache_path, &rsclient, "login -D admin");
|
|
|
|
// login as idm_admin
|
|
test_cmd_idm_admin(&token_cache_path, &rsclient, "login -D idm_admin");
|
|
test_cmd_admin_split(
|
|
&token_cache_path,
|
|
&rsclient,
|
|
&[
|
|
"service-account",
|
|
"create",
|
|
NOT_ADMIN_TEST_USERNAME,
|
|
"Test account",
|
|
"-D",
|
|
"admin",
|
|
"-o",
|
|
"json",
|
|
],
|
|
);
|
|
|
|
test_cmd_admin(
|
|
&token_cache_path,
|
|
&rsclient,
|
|
&format!("service-account get -D admin {}", NOT_ADMIN_TEST_USERNAME),
|
|
);
|
|
// updating the display name
|
|
test_cmd_admin(
|
|
&token_cache_path,
|
|
&rsclient,
|
|
&format!(
|
|
"service-account update -D admin {} --displayname cheeseballs",
|
|
NOT_ADMIN_TEST_USERNAME
|
|
),
|
|
);
|
|
// updating the email
|
|
test_cmd_admin(
|
|
&token_cache_path,
|
|
&rsclient,
|
|
&format!(
|
|
"service-account update -D admin {} --mail foo@bar.com",
|
|
NOT_ADMIN_TEST_USERNAME
|
|
),
|
|
);
|
|
|
|
// checking the email was changed
|
|
let sad = test_cmd_admin(
|
|
&token_cache_path,
|
|
&rsclient,
|
|
&format!(
|
|
"service-account get -D admin -o json {}",
|
|
NOT_ADMIN_TEST_USERNAME
|
|
),
|
|
);
|
|
let str_output: String = String::from_utf8_lossy(&sad.stdout).into();
|
|
assert!(str_output.contains("foo@bar.com"));
|
|
|
|
true
|
|
})
|
|
.await
|
|
.unwrap());
|
|
println!("Success!");
|
|
}
|
|
*/
|