kanidm/server/testkit/tests/scim_test.rs
Firstyear d7834b52e6
Begin the basis of the key provider model (#2640)
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.
2024-04-15 23:44:37 +00:00

161 lines
4.7 KiB
Rust

use compact_jwt::{traits::JwsVerifiable, JwsCompact, JwsEs256Verifier, JwsVerifier};
use kanidm_client::KanidmClient;
use kanidm_proto::internal::ScimSyncToken;
use kanidmd_testkit::{ADMIN_TEST_PASSWORD, ADMIN_TEST_USER};
use reqwest::header::HeaderValue;
use std::str::FromStr;
use url::Url;
#[kanidmd_testkit::test]
async fn test_sync_account_lifecycle(rsclient: KanidmClient) {
let a_res = rsclient
.auth_simple_password(ADMIN_TEST_USER, ADMIN_TEST_PASSWORD)
.await;
assert!(a_res.is_ok());
let a_list = rsclient.idm_sync_account_list().await.unwrap();
assert!(a_list.is_empty());
rsclient
.idm_sync_account_create("ipa_sync_account", Some("Demo of a sync account"))
.await
.unwrap();
let a_list = rsclient.idm_sync_account_list().await.unwrap();
assert!(!a_list.is_empty());
let a = rsclient
.idm_sync_account_get("ipa_sync_account")
.await
.unwrap();
let sync_entry = a.expect("No sync account was created?!");
// Shouldn't have a cred portal.
assert!(!sync_entry.attrs.contains_key("sync_credential_portal"));
let url = Url::parse("https://sink.ipa.example.com/reset").unwrap();
// Set our credential portal.
rsclient
.idm_sync_account_set_credential_portal("ipa_sync_account", Some(&url))
.await
.unwrap();
let a = rsclient
.idm_sync_account_get("ipa_sync_account")
.await
.unwrap();
let sync_entry = a.expect("No sync account present?");
// Should have a cred portal.
let url_a = sync_entry
.attrs
.get("sync_credential_portal")
.and_then(|x| x.get(0));
assert_eq!(
url_a.map(|s| s.as_str()),
Some("https://sink.ipa.example.com/reset")
);
// Also check we can get it direct
let url_b = rsclient
.idm_sync_account_get_credential_portal("ipa_sync_account")
.await
.unwrap();
assert_eq!(url_b, Some(url));
// Get a token
let token = rsclient
.idm_sync_account_generate_token("ipa_sync_account", "token_label")
.await
.expect("Failed to generate token");
let token_unverified = JwsCompact::from_str(&token).expect("Failed to parse apitoken");
let key_id = token_unverified
.kid()
.expect("token does not have a key id");
assert!(token_unverified.get_jwk_pubkey().is_none());
let jwk = rsclient
.get_public_jwk(key_id)
.await
.expect("Unable to get jwk");
let jws_verifier = JwsEs256Verifier::try_from(&jwk).expect("Unable to build verifier");
let token = jws_verifier
.verify(&token_unverified)
.map(|jws| jws.from_json::<ScimSyncToken>().expect("Invalid json"))
.expect("Unable verify token");
println!("{:?}", token);
rsclient
.idm_sync_account_destroy_token("ipa_sync_account")
.await
.expect("Failed to destroy token");
}
#[kanidmd_testkit::test]
async fn test_scim_sync_get(rsclient: KanidmClient) {
// We need to do manual reqwests here.
let mut headers = reqwest::header::HeaderMap::new();
headers.insert(
reqwest::header::AUTHORIZATION,
HeaderValue::from_str(&format!("Bearer {:?}", rsclient.get_token().await)).unwrap(),
);
let client = reqwest::Client::builder()
.danger_accept_invalid_certs(true)
.default_headers(headers)
.build()
.unwrap();
// here we test the /ui/ endpoint which should have the headers
let response = match client.get(rsclient.make_url("/scim/v1/Sync")).send().await {
Ok(value) => value,
Err(error) => {
panic!(
"Failed to query {:?} : {:#?}",
rsclient.make_url("/scim/v1/Sync"),
error
);
}
};
eprintln!("response: {:#?}", response);
assert!(response.status().is_client_error());
// check that the CSP headers are coming back
eprintln!(
"csp headers: {:#?}",
response
.headers()
.get(http::header::CONTENT_SECURITY_POLICY)
);
assert_ne!(
response
.headers()
.get(http::header::CONTENT_SECURITY_POLICY),
None
);
// test that the proper content type comes back
let url = rsclient.make_url("/scim/v1/Sink");
let response = match client.get(url.clone()).send().await {
Ok(value) => value,
Err(error) => {
panic!("Failed to query {:?} : {:#?}", url, error);
}
};
assert!(response.status().is_success());
let content_type = response.headers().get(http::header::CONTENT_TYPE).unwrap();
assert!(content_type.to_str().unwrap().contains("text/html"));
assert!(response.text().await.unwrap().contains("Sink"));
}