1#![deny(warnings)]
2#![warn(unused_extern_crates)]
3#![deny(clippy::todo)]
4#![deny(clippy::unimplemented)]
5#![deny(clippy::unwrap_used)]
6#![deny(clippy::panic)]
7#![deny(clippy::unreachable)]
8#![deny(clippy::await_holding_lock)]
9#![deny(clippy::needless_pass_by_value)]
10#![deny(clippy::trivially_copy_pass_by_ref)]
11
12use kanidm_client::{KanidmClient, KanidmClientBuilder};
13use kanidm_proto::internal::{CURegState, Filter, Modify, ModifyList};
14use kanidmd_core::config::{Configuration, IntegrationTestConfig};
15use kanidmd_core::{create_server_core, CoreHandle};
16use kanidmd_lib::prelude::{Attribute, NAME_SYSTEM_ADMINS};
17use std::net::{IpAddr, Ipv4Addr, SocketAddr, TcpStream};
18use std::str::FromStr;
19use std::sync::atomic::{AtomicU16, Ordering};
20use tokio::task;
21use tracing::error;
22use url::Url;
23use webauthn_authenticator_rs::softpasskey::SoftPasskey;
24use webauthn_authenticator_rs::WebauthnAuthenticator;
25
26pub const ADMIN_TEST_USER: &str = "admin";
27pub const ADMIN_TEST_PASSWORD: &str = "integration test admin password";
28pub const IDM_ADMIN_TEST_USER: &str = "idm_admin";
29pub const IDM_ADMIN_TEST_PASSWORD: &str = "integration idm admin password";
30
31pub const NOT_ADMIN_TEST_USERNAME: &str = "krab_test_user";
32pub const NOT_ADMIN_TEST_PASSWORD: &str = "eicieY7ahchaoCh0eeTa";
33pub const NOT_ADMIN_TEST_EMAIL: &str = "krab_test@example.com";
34
35pub static PORT_ALLOC: AtomicU16 = AtomicU16::new(18080);
36
37pub const TEST_INTEGRATION_RS_ID: &str = "test_integration";
38pub const TEST_INTEGRATION_RS_GROUP_ALL: &str = "idm_all_accounts";
39pub const TEST_INTEGRATION_RS_DISPLAY: &str = "Test Integration";
40pub const TEST_INTEGRATION_RS_URL: &str = "https://demo.example.com";
41pub const TEST_INTEGRATION_RS_REDIRECT_URL: &str = "https://demo.example.com/oauth2/flow";
42
43pub use testkit_macros::test;
44use tracing::trace;
45
46pub fn is_free_port(port: u16) -> bool {
47 TcpStream::connect(("0.0.0.0", port)).is_err()
48}
49
50fn port_loop() -> u16 {
52 let mut counter = 0;
53 loop {
54 let possible_port = PORT_ALLOC.fetch_add(1, Ordering::SeqCst);
55 if is_free_port(possible_port) {
56 break possible_port;
57 }
58 counter += 1;
59 #[allow(clippy::panic)]
60 if counter >= 5 {
61 tracing::error!("Unable to allocate port!");
62 panic!();
63 }
64 }
65}
66
67pub struct AsyncTestEnvironment {
68 pub rsclient: KanidmClient,
69 pub http_sock_addr: SocketAddr,
70 pub core_handle: CoreHandle,
71 pub ldap_url: Option<Url>,
72}
73
74#[allow(dead_code)]
76pub async fn setup_async_test(mut config: Configuration) -> AsyncTestEnvironment {
77 sketching::test_init();
78
79 let port = port_loop();
80
81 let int_config = Box::new(IntegrationTestConfig {
82 admin_user: ADMIN_TEST_USER.to_string(),
83 admin_password: ADMIN_TEST_PASSWORD.to_string(),
84 idm_admin_user: IDM_ADMIN_TEST_USER.to_string(),
85 idm_admin_password: IDM_ADMIN_TEST_PASSWORD.to_string(),
86 });
87
88 #[allow(clippy::expect_used)]
89 let addr =
90 Url::from_str(&format!("http://localhost:{port}")).expect("Failed to parse origin URL");
91
92 let ldap_url = if config.ldapbindaddress.is_some() {
93 let ldapport = port_loop();
94 let ldap_sock_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), ldapport);
95 config.ldapbindaddress = Some(ldap_sock_addr.to_string());
96 Url::parse(&format!("ldap://{ldap_sock_addr}"))
97 .inspect_err(|err| error!(?err, "ldap address setup"))
98 .ok()
99 } else {
100 None
101 };
102
103 let http_sock_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), port);
105
106 config.address = http_sock_addr.to_string();
107 config.integration_test_config = Some(int_config);
108 config.domain = "localhost".to_string();
109 config.origin.clone_from(&addr);
110
111 let core_handle = match create_server_core(config, false).await {
112 Ok(val) => val,
113 #[allow(clippy::panic)]
114 Err(_) => panic!("failed to start server core"),
115 };
116 task::yield_now().await;
118
119 #[allow(clippy::panic)]
120 let rsclient = match KanidmClientBuilder::new()
121 .address(addr.to_string())
122 .enable_native_ca_roots(false)
123 .no_proxy()
124 .build()
125 {
126 Ok(val) => val,
127 Err(_) => panic!("failed to build client"),
128 };
129
130 tracing::info!("Testkit server setup complete - {}", addr);
131
132 AsyncTestEnvironment {
133 rsclient,
134 http_sock_addr,
135 core_handle,
136 ldap_url,
137 }
138}
139
140pub async fn setup_account_passkey(
141 rsclient: &KanidmClient,
142 account_name: &str,
143) -> WebauthnAuthenticator<SoftPasskey> {
144 let intent_token = rsclient
146 .idm_person_account_credential_update_intent(account_name, Some(1234))
147 .await
148 .expect("Unable to setup account passkey");
149
150 let rsclient = rsclient
152 .new_session()
153 .expect("Unable to create new client session");
154
155 let (session_token, _status) = rsclient
157 .idm_account_credential_update_exchange(intent_token.token)
158 .await
159 .expect("Unable to exchange credential update token");
160
161 let _status = rsclient
162 .idm_account_credential_update_status(&session_token)
163 .await
164 .expect("Unable to check credential update status");
165
166 let mut wa = WebauthnAuthenticator::new(SoftPasskey::new(true));
168
169 let status = rsclient
170 .idm_account_credential_update_passkey_init(&session_token)
171 .await
172 .expect("Unable to init passkey update");
173
174 let passkey_chal = match status.mfaregstate {
175 CURegState::Passkey(c) => Some(c),
176 _ => None,
177 }
178 .expect("Unable to access passkey challenge, invalid state");
179
180 eprintln!("{}", rsclient.get_origin());
181 let passkey_resp = wa
182 .do_registration(rsclient.get_origin().clone(), passkey_chal)
183 .expect("Failed to create soft passkey");
184
185 let label = "Soft Passkey".to_string();
186
187 let status = rsclient
188 .idm_account_credential_update_passkey_finish(&session_token, label, passkey_resp)
189 .await
190 .expect("Unable to finish passkey credential");
191
192 assert!(status.can_commit);
193 assert_eq!(status.passkeys.len(), 1);
194
195 rsclient
197 .idm_account_credential_update_commit(&session_token)
198 .await
199 .expect("Unable to commit credential update");
200
201 let _ = rsclient.logout().await;
203
204 wa
205}
206
207pub async fn create_user(rsclient: &KanidmClient, id: &str, group_name: &str) {
209 #[allow(clippy::expect_used)]
210 rsclient
211 .idm_person_account_create(id, id)
212 .await
213 .expect("Failed to create the user");
214
215 #[allow(clippy::panic)]
217 if rsclient
218 .idm_group_get(group_name)
219 .await
220 .unwrap_or_else(|_| panic!("Failed to get group {group_name}"))
221 .is_none()
222 {
223 #[allow(clippy::panic)]
224 rsclient
225 .idm_group_create(group_name, None)
226 .await
227 .unwrap_or_else(|_| panic!("Failed to create group {group_name}"));
228 }
229 #[allow(clippy::panic)]
230 rsclient
231 .idm_group_add_members(group_name, &[id])
232 .await
233 .unwrap_or_else(|_| panic!("Failed to add user {id} to group {group_name}"));
234}
235
236pub async fn create_user_with_all_attrs(
237 rsclient: &KanidmClient,
238 id: &str,
239 optional_group: Option<&str>,
240) {
241 let group_format = format!("{id}_group");
242 let group_name = optional_group.unwrap_or(&group_format);
243
244 create_user(rsclient, id, group_name).await;
245 add_all_attrs(rsclient, id, group_name, Some(id)).await;
246}
247
248pub async fn add_all_attrs(
249 rsclient: &KanidmClient,
250 id: &str,
251 group_name: &str,
252 legalname: Option<&str>,
253) {
254 #[allow(clippy::expect_used)]
256 rsclient
257 .idm_person_account_unix_extend(id, None, Some("/bin/sh"))
258 .await
259 .expect("Failed to set shell to /bin/sh for user");
260 #[allow(clippy::expect_used)]
261 rsclient
262 .idm_group_unix_extend(group_name, None)
263 .await
264 .expect("Failed to extend user group");
265
266 for attr in [Attribute::SshPublicKey, Attribute::Mail].into_iter() {
267 println!("Checking writable for {attr}");
268 #[allow(clippy::expect_used)]
269 let res = is_attr_writable(rsclient, id, attr)
270 .await
271 .expect("Failed to get writable status for attribute");
272 assert!(res);
273 }
274
275 if let Some(legalname) = legalname {
276 #[allow(clippy::expect_used)]
277 let res = is_attr_writable(rsclient, legalname, Attribute::LegalName)
278 .await
279 .expect("Failed to get writable status for legalname field");
280 assert!(res);
281 }
282
283 if id != "anonymous" {
285 login_account(rsclient, id).await;
286 #[allow(clippy::expect_used)]
287 let _ = rsclient
288 .idm_account_radius_credential_regenerate(id)
289 .await
290 .expect("Failed to regen password for user");
291
292 #[allow(clippy::expect_used)]
293 rsclient
294 .auth_simple_password(ADMIN_TEST_USER, ADMIN_TEST_PASSWORD)
295 .await
296 .expect("Failed to auth with password as admin!");
297 }
298}
299
300pub async fn is_attr_writable(rsclient: &KanidmClient, id: &str, attr: Attribute) -> Option<bool> {
301 println!("writing to attribute: {attr}");
302 match attr {
303 Attribute::RadiusSecret => Some(
304 rsclient
305 .idm_account_radius_credential_regenerate(id)
306 .await
307 .is_ok(),
308 ),
309 Attribute::PrimaryCredential => Some(
310 rsclient
311 .idm_person_account_primary_credential_set_password(id, "dsadjasiodqwjk12asdl")
312 .await
313 .is_ok(),
314 ),
315 Attribute::SshPublicKey => Some(
316 rsclient
317 .idm_person_account_post_ssh_pubkey(
318 id,
319 "k1",
320 "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAeGW1P6Pc2rPq0XqbRaDKBcXZUPRklo0\
321 L1EyR30CwoP william@amethyst",
322 )
323 .await
324 .is_ok(),
325 ),
326 Attribute::UnixPassword => Some(
327 rsclient
328 .idm_person_account_unix_cred_put(id, "dsadjasiodqwjk12asdl")
329 .await
330 .is_ok(),
331 ),
332 Attribute::LegalName => Some(
333 rsclient
334 .idm_person_account_set_attr(
335 id,
336 Attribute::LegalName.as_ref(),
337 &["test legal name"],
338 )
339 .await
340 .is_ok(),
341 ),
342 Attribute::Mail => Some(
343 rsclient
344 .idm_person_account_set_attr(
345 id,
346 Attribute::Mail.as_ref(),
347 &[&format!("{id}@example.com")],
348 )
349 .await
350 .is_ok(),
351 ),
352 ref entry => {
353 let new_value = match entry {
354 Attribute::AcpReceiverGroup => "00000000-0000-0000-0000-000000000011".to_string(),
355 Attribute::AcpTargetScope => "{\"and\": [{\"eq\": [\"class\",\"access_control_profile\"]}, {\"andnot\": {\"or\": [{\"eq\": [\"class\", \"tombstone\"]}, {\"eq\": [\"class\", \"recycled\"]}]}}]}".to_string(),
356 _ => id.to_string(),
357 };
358 let m = ModifyList::new_list(vec![
359 Modify::Purged(attr.to_string()),
360 Modify::Present(attr.to_string(), new_value),
361 ]);
362 let f = Filter::Eq(Attribute::Name.to_string(), id.to_string());
363 Some(rsclient.modify(f.clone(), m.clone()).await.is_ok())
364 }
365 }
366}
367
368pub async fn login_account(rsclient: &KanidmClient, id: &str) {
369 #[allow(clippy::expect_used)]
370 rsclient
371 .idm_person_account_primary_credential_set_password(id, NOT_ADMIN_TEST_PASSWORD)
372 .await
373 .expect("Failed to set password for user");
374
375 let _ = rsclient.logout().await;
376 let res = rsclient
377 .auth_simple_password(id, NOT_ADMIN_TEST_PASSWORD)
378 .await;
379
380 println!("{id} logged in");
382 assert!(res.is_ok());
383
384 let res = rsclient
385 .reauth_simple_password(NOT_ADMIN_TEST_PASSWORD)
386 .await;
387 println!("{id} priv granted for");
388 assert!(res.is_ok());
389}
390
391pub async fn login_account_via_admin(rsclient: &KanidmClient, id: &str) {
395 let _ = rsclient.logout().await;
396
397 #[allow(clippy::expect_used)]
398 rsclient
399 .auth_simple_password(ADMIN_TEST_USER, ADMIN_TEST_PASSWORD)
400 .await
401 .expect("Failed to login as admin!");
402 login_account(rsclient, id).await
403}
404
405pub async fn test_read_attrs(
406 rsclient: &KanidmClient,
407 id: &str,
408 attrs: &[Attribute],
409 is_readable: bool,
410) {
411 println!("Test read to {id}, is readable: {is_readable}");
412 #[allow(clippy::expect_used)]
413 let rset = rsclient
414 .search(Filter::Eq(Attribute::Name.to_string(), id.to_string()))
415 .await
416 .expect("Can't get user from search");
417
418 #[allow(clippy::expect_used)]
419 let e = rset.first().expect("Failed to get first user from set");
420
421 for attr in attrs.iter() {
422 trace!("Reading {}", attr);
423 #[allow(clippy::unwrap_used)]
424 let is_ok = match *attr {
425 Attribute::RadiusSecret => rsclient
426 .idm_account_radius_credential_get(id)
427 .await
428 .unwrap()
429 .is_some(),
430 _ => e.attrs.contains_key(attr.as_str()),
431 };
432 trace!("is_ok: {}, is_readable: {}", is_ok, is_readable);
433 assert_eq!(is_ok, is_readable)
434 }
435}
436
437pub async fn test_write_attrs(
438 rsclient: &KanidmClient,
439 id: &str,
440 attrs: &[Attribute],
441 is_writeable: bool,
442) {
443 println!("Test write to {id}, is writeable: {is_writeable}");
444 for attr in attrs.iter() {
445 println!("Writing to {attr} - ex {is_writeable}");
446 #[allow(clippy::unwrap_used)]
447 let is_ok = is_attr_writable(rsclient, id, attr.clone()).await.unwrap();
448 assert_eq!(is_ok, is_writeable)
449 }
450}
451
452pub async fn test_modify_group(
453 rsclient: &KanidmClient,
454 group_names: &[&str],
455 can_be_modified: bool,
456) {
457 for group in group_names.iter() {
459 println!("Testing group: {group}");
460 for attr in [Attribute::Description, Attribute::Name].into_iter() {
461 #[allow(clippy::unwrap_used)]
462 let is_writable = is_attr_writable(rsclient, group, attr.clone())
463 .await
464 .unwrap();
465 dbg!(group, attr, is_writable, can_be_modified);
466 assert_eq!(is_writable, can_be_modified)
467 }
468 assert!(
469 rsclient
470 .idm_group_add_members(group, &[NOT_ADMIN_TEST_USERNAME])
471 .await
472 .is_ok()
473 == can_be_modified
474 );
475 }
476}
477
478pub async fn login_put_admin_idm_admins(rsclient: &KanidmClient) {
480 #[allow(clippy::expect_used)]
481 rsclient
482 .auth_simple_password(ADMIN_TEST_USER, ADMIN_TEST_PASSWORD)
483 .await
484 .expect("Failed to authenticate as admin!");
485
486 #[allow(clippy::expect_used)]
487 rsclient
488 .idm_group_add_members(NAME_SYSTEM_ADMINS, &[ADMIN_TEST_USER])
489 .await
490 .expect("Failed to add admin user to idm_admins")
491}
492
493#[macro_export]
494macro_rules! assert_no_cache {
495 ($response:expr) => {{
496 let cache_header: &str = $response
498 .headers()
499 .get(kanidm_client::http::header::CACHE_CONTROL)
500 .expect("missing cache-control header")
501 .to_str()
502 .expect("invalid cache-control header");
503
504 assert!(cache_header.contains("no-store"));
505 assert!(cache_header.contains("max-age=0"));
506
507 let pragma_header: &str = $response
508 .headers()
509 .get("pragma")
510 .expect("missing cache-control header")
511 .to_str()
512 .expect("invalid cache-control header");
513
514 assert!(pragma_header.contains("no-cache"));
515 }};
516}