kanidmd_testkit/
lib.rs

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
49// Test external behaviours of the service.
50fn 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// allowed because the use of this function is behind a test gate
74#[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    // Setup the address and origin..
103    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    // We have to yield now to guarantee that the elements are setup.
116    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
139/// creates a user (username: `id`) and puts them into a group, creating it if need be.
140pub 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    // Create group and add to user to test read attr: member_of
148    #[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    // Extend with posix attrs to test read attr: gidnumber and loginshell
187    #[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    // Write radius credentials
216    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    // Setup privs
313    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
323// Login to the given account, but first login with default admin credentials.
324// This is necessary when switching between unprivileged accounts, but adds extra calls which
325// create extra debugging noise, so should be avoided when unnecessary.
326pub 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    // need user test created to be added as test part
390    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
410/// Logs in with the admin user and puts them in idm_admins so they can do admin things
411pub 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        // Check we have correct nocache headers.
429        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}