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