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