Chasing yaks down dark alleyways (#2207)

* adding some test coverage because there was some rando panic-inducing thing
* ldap constants
* documenting a macro
* helpful weird errors
* the war on strings continues
* less json more better
* testing things fixing bugs
* idm_domain_reset_token_key wasn't working, added a test and fixed it (we weren't testing it)
* idm_domain_set_ldap_basedn - adding tests
* adding testing for idm_account_credential_update_cancel_mfareg
* warning of deprecation
This commit is contained in:
James Hodgkinson 2023-10-11 15:44:29 +10:00 committed by GitHub
parent c66a401b31
commit d9da1eeca0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 507 additions and 269 deletions

1
Cargo.lock generated
View file

@ -2872,6 +2872,7 @@ dependencies = [
"ldap3_client", "ldap3_client",
"serde", "serde",
"serde_json", "serde_json",
"sketching",
"tokio", "tokio",
"toml", "toml",
"tracing", "tracing",

View file

@ -0,0 +1,11 @@
debug = false
dry_run = true
schedule = "* * * * *"
skip_root_check = false
sync_token="cheesemonkey"
ldap_uri="ldaps://example.com:636"
ldap_ca=""
ldap_sync_dn=""
ldap_sync_pw=""
ldap_sync_base_dn=""
ldap_filter="(objectClass=posixAccount)"

View file

@ -32,6 +32,7 @@ pub use reqwest::StatusCode;
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::error::Error as SerdeJsonError; use serde_json::error::Error as SerdeJsonError;
use serde_json::json;
use tokio::sync::{Mutex, RwLock}; use tokio::sync::{Mutex, RwLock};
use url::Url; use url::Url;
use uuid::Uuid; use uuid::Uuid;
@ -108,6 +109,32 @@ impl Display for KanidmClientBuilder {
} }
} }
#[test]
fn test_kanidmclientbuilder_display() {
let foo = KanidmClientBuilder::default();
println!("{}", foo.to_string());
assert!(foo.to_string().contains("verify_ca"));
let foo = KanidmClientBuilder {
address: Some("https://example.com".to_string()),
verify_ca: true,
verify_hostnames: true,
ca: None,
connect_timeout: Some(420),
use_system_proxies: true,
};
println!("foo {}", foo.to_string());
assert!(foo.to_string().contains("verify_ca: true"));
assert!(foo.to_string().contains("verify_hostnames: true"));
let badness = foo.danger_accept_invalid_hostnames(true);
let badness = badness.danger_accept_invalid_certs(true);
println!("badness: {}", badness.to_string());
assert!(badness.to_string().contains("verify_ca: false"));
assert!(badness.to_string().contains("verify_hostnames: false"));
}
#[derive(Debug)] #[derive(Debug)]
pub struct KanidmClient { pub struct KanidmClient {
pub(crate) client: reqwest::Client, pub(crate) client: reqwest::Client,
@ -890,7 +917,8 @@ impl KanidmClient {
let response = self let response = self
.client .client
.delete(self.make_url(dest)) .delete(self.make_url(dest))
.header(CONTENT_TYPE, APPLICATION_JSON); // empty-ish body that makes the parser happy
.json(&json!([]));
let response = { let response = {
let tguard = self.bearer_token.read().await; let tguard = self.bearer_token.read().await;
@ -932,12 +960,10 @@ impl KanidmClient {
dest: &str, dest: &str,
request: R, request: R,
) -> Result<(), ClientError> { ) -> Result<(), ClientError> {
let req_string = serde_json::to_string(&request).map_err(ClientError::JsonEncode)?;
let response = self let response = self
.client .client
.delete(self.make_url(dest)) .delete(self.make_url(dest))
.body(req_string) .json(&request);
.header(CONTENT_TYPE, APPLICATION_JSON);
let response = { let response = {
let tguard = self.bearer_token.read().await; let tguard = self.bearer_token.read().await;
@ -1647,6 +1673,7 @@ impl KanidmClient {
.await .await
} }
// TODO: add test coverage
pub async fn idm_account_credential_update_accept_sha1_totp( pub async fn idm_account_credential_update_accept_sha1_totp(
&self, &self,
session_token: &CUSessionToken, session_token: &CUSessionToken,
@ -1666,6 +1693,7 @@ impl KanidmClient {
.await .await
} }
// TODO: add test coverage
pub async fn idm_account_credential_update_backup_codes_generate( pub async fn idm_account_credential_update_backup_codes_generate(
&self, &self,
session_token: &CUSessionToken, session_token: &CUSessionToken,
@ -1675,6 +1703,7 @@ impl KanidmClient {
.await .await
} }
// TODO: add test coverage
pub async fn idm_account_credential_update_primary_remove( pub async fn idm_account_credential_update_primary_remove(
&self, &self,
session_token: &CUSessionToken, session_token: &CUSessionToken,
@ -1704,6 +1733,7 @@ impl KanidmClient {
.await .await
} }
// TODO: add test coverage
pub async fn idm_account_credential_update_passkey_remove( pub async fn idm_account_credential_update_passkey_remove(
&self, &self,
session_token: &CUSessionToken, session_token: &CUSessionToken,

View file

@ -2,10 +2,12 @@ use crate::{ClientError, KanidmClient};
use kanidm_proto::scim_v1::{ScimSyncRequest, ScimSyncState}; use kanidm_proto::scim_v1::{ScimSyncRequest, ScimSyncState};
impl KanidmClient { impl KanidmClient {
// TODO: testing for this
pub async fn scim_v1_sync_status(&self) -> Result<ScimSyncState, ClientError> { pub async fn scim_v1_sync_status(&self) -> Result<ScimSyncState, ClientError> {
self.perform_get_request("/scim/v1/Sync").await self.perform_get_request("/scim/v1/Sync").await
} }
// TODO: testing for this
pub async fn scim_v1_sync_update( pub async fn scim_v1_sync_update(
&self, &self,
scim_sync_request: &ScimSyncRequest, scim_sync_request: &ScimSyncRequest,

View file

@ -146,8 +146,11 @@ impl KanidmClient {
pub async fn idm_service_account_unix_extend( pub async fn idm_service_account_unix_extend(
&self, &self,
// The username or uuid of the account
id: &str, id: &str,
// The GID number to set for the account
gidnumber: Option<u32>, gidnumber: Option<u32>,
// Set a default login shell
shell: Option<&str>, shell: Option<&str>,
) -> Result<(), ClientError> { ) -> Result<(), ClientError> {
let ux = AccountUnixExtend { let ux = AccountUnixExtend {
@ -158,6 +161,7 @@ impl KanidmClient {
.await .await
} }
// TODO: test coverage for this, but there's a weird issue with ACPs on apply
pub async fn idm_service_account_into_person(&self, id: &str) -> Result<(), ClientError> { pub async fn idm_service_account_into_person(&self, id: &str) -> Result<(), ClientError> {
self.perform_post_request( self.perform_post_request(
format!("/v1/service_account/{}/_into_person", id).as_str(), format!("/v1/service_account/{}/_into_person", id).as_str(),

View file

@ -78,6 +78,7 @@ impl KanidmClient {
.await .await
} }
/// Creates a sync token for a given sync account
pub async fn idm_sync_account_generate_token( pub async fn idm_sync_account_generate_token(
&self, &self,
id: &str, id: &str,

View file

@ -168,6 +168,7 @@ pub const OAUTH2_SCOPE_READ: &str = "read";
pub const OAUTH2_SCOPE_SUPPLEMENT: &str = "supplement"; pub const OAUTH2_SCOPE_SUPPLEMENT: &str = "supplement";
pub const LDAP_ATTR_CN: &str = "cn"; pub const LDAP_ATTR_CN: &str = "cn";
pub const LDAP_ATTR_DISPLAY_NAME: &str = "displayName";
pub const LDAP_ATTR_EMAIL_ALTERNATIVE: &str = "emailalternative"; pub const LDAP_ATTR_EMAIL_ALTERNATIVE: &str = "emailalternative";
pub const LDAP_ATTR_EMAIL_PRIMARY: &str = "emailprimary"; pub const LDAP_ATTR_EMAIL_PRIMARY: &str = "emailprimary";
pub const LDAP_ATTR_ENTRYDN: &str = "entrydn"; pub const LDAP_ATTR_ENTRYDN: &str = "entrydn";
@ -181,6 +182,7 @@ pub const LDAP_ATTR_MEMBER: &str = "member";
pub const LDAP_ATTR_NAME: &str = "name"; pub const LDAP_ATTR_NAME: &str = "name";
pub const LDAP_ATTR_OBJECTCLASS: &str = "objectClass"; pub const LDAP_ATTR_OBJECTCLASS: &str = "objectClass";
pub const LDAP_ATTR_OU: &str = "ou"; pub const LDAP_ATTR_OU: &str = "ou";
pub const LDAP_CLASS_GROUPOFNAMES: &str = "groupofnames";
// rust can't deal with this being compiled out, don't try and #[cfg()] them // rust can't deal with this being compiled out, don't try and #[cfg()] them
pub const TEST_ATTR_NON_EXIST: &str = "non-exist"; pub const TEST_ATTR_NON_EXIST: &str = "non-exist";

View file

@ -273,6 +273,8 @@ pub enum OperationError {
ReplDomainLevelUnsatisfiable, ReplDomainLevelUnsatisfiable,
ReplDomainUuidMismatch, ReplDomainUuidMismatch,
TransactionAlreadyCommitted, TransactionAlreadyCommitted,
/// when you ask for a gid that's lower than a safe minimum
GidOverlapsSystemMin(u32),
} }
impl PartialEq for OperationError { impl PartialEq for OperationError {

View file

@ -896,6 +896,7 @@ pub async fn account_id_radius_token(
} }
/// Expects an `AccountUnixExtend` object /// Expects an `AccountUnixExtend` object
///
#[instrument(name = "account_post_id_unix", level = "INFO", skip(id, state, kopid))] #[instrument(name = "account_post_id_unix", level = "INFO", skip(id, state, kopid))]
pub async fn account_post_id_unix( pub async fn account_post_id_unix(
State(state): State<ServerState>, State(state): State<ServerState>,

View file

@ -1,4 +1,4 @@
use std::time::{Duration, Instant}; use std::time::{Duration, Instant, SystemTime};
use criterion::{ use criterion::{
criterion_group, criterion_main, BenchmarkId, Criterion, SamplingMode, Throughput, criterion_group, criterion_main, BenchmarkId, Criterion, SamplingMode, Throughput,
@ -7,9 +7,15 @@ use criterion::{
use kanidmd_lib::entry::{Entry, EntryInit, EntryNew}; use kanidmd_lib::entry::{Entry, EntryInit, EntryNew};
use kanidmd_lib::entry_init; use kanidmd_lib::entry_init;
use kanidmd_lib::prelude::{Attribute, EntryClass}; use kanidmd_lib::prelude::{Attribute, EntryClass};
use kanidmd_lib::utils::duration_from_epoch_now;
use kanidmd_lib::value::Value; use kanidmd_lib::value::Value;
pub fn duration_from_epoch_now() -> Duration {
#[allow(clippy::expect_used)]
SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.expect("invalid duration from epoch now")
}
pub fn scaling_user_create_single(c: &mut Criterion) { pub fn scaling_user_create_single(c: &mut Criterion) {
let mut group = c.benchmark_group("user_create_single"); let mut group = c.benchmark_group("user_create_single");
group.sample_size(10); group.sample_size(10);

View file

@ -621,6 +621,18 @@ impl From<EntryClass> for &'static str {
} }
} }
impl AsRef<str> for EntryClass {
fn as_ref(&self) -> &str {
self.into()
}
}
impl From<&EntryClass> for &'static str {
fn from(value: &EntryClass) -> Self {
(*value).into()
}
}
impl From<EntryClass> for String { impl From<EntryClass> for String {
fn from(val: EntryClass) -> Self { fn from(val: EntryClass) -> Self {
let s: &'static str = val.into(); let s: &'static str = val.into();

View file

@ -897,10 +897,10 @@ mod tests {
assert_entry_contains!( assert_entry_contains!(
lsre, lsre,
"spn=testperson1@example.com,dc=example,dc=com", "spn=testperson1@example.com,dc=example,dc=com",
(Attribute::ObjectClass, "object"), (Attribute::ObjectClass, EntryClass::Object.as_ref()),
(Attribute::ObjectClass, "person"), (Attribute::ObjectClass, EntryClass::Person.as_ref()),
(Attribute::ObjectClass, "account"), (Attribute::ObjectClass, EntryClass::Account.as_ref()),
(Attribute::ObjectClass, "posixaccount"), (Attribute::ObjectClass, EntryClass::PosixAccount.as_ref()),
(Attribute::DisplayName, "testperson1"), (Attribute::DisplayName, "testperson1"),
(Attribute::Name, "testperson1"), (Attribute::Name, "testperson1"),
(Attribute::GidNumber, "12345678"), (Attribute::GidNumber, "12345678"),

View file

@ -119,16 +119,22 @@ macro_rules! run_create_test {
}}; }};
} }
// #[macro_export]
#[cfg(test)] #[cfg(test)]
/// Runs a test with preloaded entries, then modifies based on a filter/list, then runs a given check
macro_rules! run_modify_test { macro_rules! run_modify_test {
( (
// expected outcome
$expect:expr, $expect:expr,
// things to preload
$preload_entries:ident, $preload_entries:ident,
// the targets to modify
$modify_filter:expr, $modify_filter:expr,
// changes to make
$modify_list:expr, $modify_list:expr,
$internal:expr, $internal:expr,
// something to run after the preload but before the modification, takes `&mut qs_write`
$pre_hook:expr, $pre_hook:expr,
// the result we expect
$check:expr $check:expr
) => {{ ) => {{
use crate::be::{Backend, BackendConfig}; use crate::be::{Backend, BackendConfig};

View file

@ -498,15 +498,12 @@ mod tests {
// Test entry in db, and same name, reject. // Test entry in db, and same name, reject.
#[test] #[test]
fn test_pre_create_name_unique() { fn test_pre_create_name_unique() {
let e: Entry<EntryInit, EntryNew> = Entry::unsafe_from_entry_str(
r#"{ let e: Entry<EntryInit,EntryNew> = entry_init!(
"attrs": { (Attribute::Class, EntryClass::Person.to_value()),
"class": ["person"], (Attribute::Name, Value::new_iname("testperson")),
"name": ["testperson"], (Attribute::Description, Value::new_utf8s("testperson")),
"description": ["testperson"], (Attribute::DisplayName, Value::new_utf8s("testperson"))
"displayname": ["testperson"]
}
}"#,
); );
let create = vec![e.clone()]; let create = vec![e.clone()];
@ -526,15 +523,11 @@ mod tests {
// Test two entries in create that would have same name, reject. // Test two entries in create that would have same name, reject.
#[test] #[test]
fn test_pre_create_name_unique_2() { fn test_pre_create_name_unique_2() {
let e: Entry<EntryInit, EntryNew> = Entry::unsafe_from_entry_str( let e: Entry<EntryInit,EntryNew> = entry_init!(
r#"{ (Attribute::Class, EntryClass::Person.to_value()),
"attrs": { (Attribute::Name, Value::new_iname("testperson")),
"class": ["person"], (Attribute::Description, Value::new_utf8s("testperson")),
"name": ["testperson"], (Attribute::DisplayName, Value::new_utf8s("testperson"))
"description": ["testperson"],
"displayname": ["testperson"]
}
}"#,
); );
let create = vec![e.clone(), e]; let create = vec![e.clone(), e];
@ -557,24 +550,15 @@ mod tests {
// A mod to something that exists, reject. // A mod to something that exists, reject.
#[test] #[test]
fn test_pre_modify_name_unique() { fn test_pre_modify_name_unique() {
let ea: Entry<EntryInit, EntryNew> = Entry::unsafe_from_entry_str( let ea: Entry<EntryInit,EntryNew> = entry_init!(
r#"{ (Attribute::Class, EntryClass::Group.to_value()),
"attrs": { (Attribute::Name, Value::new_iname("testgroup_a")),
"class": ["group"], (Attribute::Description, Value::new_utf8s("testgroup"))
"name": ["testgroup_a"],
"description": ["testgroup"]
}
}"#,
); );
let eb: Entry<EntryInit,EntryNew> = entry_init!(
let eb: Entry<EntryInit, EntryNew> = Entry::unsafe_from_entry_str( (Attribute::Class, EntryClass::Group.to_value()),
r#"{ (Attribute::Name, Value::new_iname("testgroup_b")),
"attrs": { (Attribute::Description, Value::new_utf8s("testgroup"))
"class": ["group"],
"name": ["testgroup_b"],
"description": ["testgroup"]
}
}"#,
); );
let preload = vec![ea, eb]; let preload = vec![ea, eb];
@ -601,24 +585,15 @@ mod tests {
// Two items modded to have the same value, reject. // Two items modded to have the same value, reject.
#[test] #[test]
fn test_pre_modify_name_unique_2() { fn test_pre_modify_name_unique_2() {
let ea: Entry<EntryInit, EntryNew> = Entry::unsafe_from_entry_str( let ea: Entry<EntryInit,EntryNew> = entry_init!(
r#"{ (Attribute::Class, EntryClass::Group.to_value()),
"attrs": { (Attribute::Name, Value::new_iname("testgroup_a")),
"class": ["group"], (Attribute::Description, Value::new_utf8s("testgroup"))
"name": ["testgroup_a"],
"description": ["testgroup"]
}
}"#,
); );
let eb: Entry<EntryInit,EntryNew> = entry_init!(
let eb: Entry<EntryInit, EntryNew> = Entry::unsafe_from_entry_str( (Attribute::Class, EntryClass::Group.to_value()),
r#"{ (Attribute::Name, Value::new_iname("testgroup_b")),
"attrs": { (Attribute::Description, Value::new_utf8s("testgroup"))
"class": ["group"],
"name": ["testgroup_b"],
"description": ["testgroup"]
}
}"#,
); );
let preload = vec![ea, eb]; let preload = vec![ea, eb];

View file

@ -35,12 +35,8 @@ fn apply_gidnumber<T: Clone>(e: &mut Entry<EntryInvalid, T>) -> Result<(), Opera
let gid = uuid_to_gid_u32(u_ref); let gid = uuid_to_gid_u32(u_ref);
// assert the value is greater than the system range. // assert the value is greater than the system range.
if gid < GID_SYSTEM_NUMBER_MIN { if gid < GID_SYSTEM_NUMBER_MIN {
return Err(OperationError::InvalidAttribute(format!( admin_error!("Requested GID {} is lower than system minimum {}", gid, GID_SYSTEM_NUMBER_MIN);
"{} {} may overlap with system range {}", return Err(OperationError::GidOverlapsSystemMin(GID_SYSTEM_NUMBER_MIN));
Attribute::GidNumber,
gid,
GID_SYSTEM_NUMBER_MIN
)));
} }
let gid_v = Value::new_uint32(gid); let gid_v = Value::new_uint32(gid);
@ -50,12 +46,8 @@ fn apply_gidnumber<T: Clone>(e: &mut Entry<EntryInvalid, T>) -> Result<(), Opera
} else if let Some(gid) = e.get_ava_single_uint32(Attribute::GidNumber) { } else if let Some(gid) = e.get_ava_single_uint32(Attribute::GidNumber) {
// If they provided us with a gid number, ensure it's in a safe range. // If they provided us with a gid number, ensure it's in a safe range.
if gid <= GID_SAFETY_NUMBER_MIN { if gid <= GID_SAFETY_NUMBER_MIN {
Err(OperationError::InvalidAttribute(format!( admin_error!("Requested GID {} is lower or equal to a safe value {}", gid, GID_SAFETY_NUMBER_MIN);
"{} {} overlaps into system secure range {}", Err(OperationError::GidOverlapsSystemMin(GID_SAFETY_NUMBER_MIN))
Attribute::GidNumber,
gid,
GID_SAFETY_NUMBER_MIN
)))
} else { } else {
Ok(()) Ok(())
} }
@ -290,9 +282,7 @@ mod tests {
let preload = Vec::new(); let preload = Vec::new();
run_create_test!( run_create_test!(
Err(OperationError::InvalidAttribute( Err(OperationError::GidOverlapsSystemMin(65536)),
"gidnumber 580 may overlap with system range 65536".to_string()
)),
preload, preload,
create, create,
None, None,
@ -315,9 +305,7 @@ mod tests {
let preload = Vec::new(); let preload = Vec::new();
run_create_test!( run_create_test!(
Err(OperationError::InvalidAttribute( Err(OperationError::GidOverlapsSystemMin(1000)),
"gidnumber 500 overlaps into system secure range 1000".to_string()
)),
preload, preload,
create, create,
None, None,
@ -340,9 +328,7 @@ mod tests {
let preload = Vec::new(); let preload = Vec::new();
run_create_test!( run_create_test!(
Err(OperationError::InvalidAttribute( Err(OperationError::GidOverlapsSystemMin(1000)),
"gidnumber 0 overlaps into system secure range 1000".to_string()
)),
preload, preload,
create, create,
None, None,

View file

@ -456,23 +456,24 @@ mod tests {
use crate::prelude::*; use crate::prelude::*;
use crate::value::{Oauth2Session, Session, SessionState}; use crate::value::{Oauth2Session, Session, SessionState};
use time::OffsetDateTime; use time::OffsetDateTime;
use uuid::uuid;
use crate::credential::Credential; use crate::credential::Credential;
use kanidm_lib_crypto::CryptoPolicy; use kanidm_lib_crypto::CryptoPolicy;
const TEST_TESTGROUP_A_UUID: &str = "d2b496bd-8493-47b7-8142-f568b5cf47ee";
const TEST_TESTGROUP_B_UUID: &str = "8cef42bc-2cac-43e4-96b3-8f54561885ca";
// The create references a uuid that doesn't exist - reject // The create references a uuid that doesn't exist - reject
#[test] #[test]
fn test_create_uuid_reference_not_exist() { fn test_create_uuid_reference_not_exist() {
let e: Entry<EntryInit, EntryNew> = Entry::unsafe_from_entry_str( let e = entry_init!(
r#"{ (Attribute::Class, EntryClass::Group.to_value()),
"attrs": { (Attribute::Name, Value::new_iname("testgroup")),
"class": ["group"], (Attribute::Description, Value::new_utf8s("testgroup")),
"name": ["testgroup"], (
"description": ["testperson"], Attribute::Member,
"member": ["ca85168c-91b7-49a8-b7bb-a3d5bb40e97e"] Value::Refer(Uuid::parse_str(TEST_TESTGROUP_B_UUID).unwrap())
} )
}"#,
); );
let create = vec![e]; let create = vec![e];
@ -491,26 +492,23 @@ mod tests {
// The create references a uuid that does exist - validate // The create references a uuid that does exist - validate
#[test] #[test]
fn test_create_uuid_reference_exist() { fn test_create_uuid_reference_exist() {
let ea: Entry<EntryInit, EntryNew> = Entry::unsafe_from_entry_str( let ea = entry_init!(
r#"{ (Attribute::Class, EntryClass::Group.to_value()),
"attrs": { (Attribute::Name, Value::new_iname("testgroup_a")),
"class": ["group"], (Attribute::Description, Value::new_utf8s("testgroup")),
"name": ["testgroup_a"], (
"description": ["testgroup"], Attribute::Uuid,
"uuid": ["d2b496bd-8493-47b7-8142-f568b5cf47ee"] Value::Uuid(Uuid::parse_str(TEST_TESTGROUP_A_UUID).unwrap())
} )
}"#,
); );
let eb = entry_init!(
let eb: Entry<EntryInit, EntryNew> = Entry::unsafe_from_entry_str( (Attribute::Class, EntryClass::Group.to_value()),
r#"{ (Attribute::Name, Value::new_iname("testgroup_b")),
"attrs": { (Attribute::Description, Value::new_utf8s("testgroup")),
"class": ["group"], (
"name": ["testgroup_b"], Attribute::Member,
"description": ["testgroup"], Value::Refer(Uuid::parse_str(TEST_TESTGROUP_A_UUID).unwrap())
"member": ["d2b496bd-8493-47b7-8142-f568b5cf47ee"] )
}
}"#,
); );
let preload = vec![ea]; let preload = vec![ea];
@ -604,7 +602,7 @@ mod tests {
)), )),
ModifyList::new_list(vec![Modify::Present( ModifyList::new_list(vec![Modify::Present(
Attribute::Member.into(), Attribute::Member.into(),
Value::new_refer_s("d2b496bd-8493-47b7-8142-f568b5cf47ee").unwrap() Value::new_refer_s(TEST_TESTGROUP_A_UUID).unwrap()
)]), )]),
None, None,
|_| {}, |_| {},
@ -638,7 +636,7 @@ mod tests {
)), )),
ModifyList::new_list(vec![Modify::Present( ModifyList::new_list(vec![Modify::Present(
Attribute::Member.into(), Attribute::Member.into(),
Value::new_refer_s("d2b496bd-8493-47b7-8142-f568b5cf47ee").unwrap() Value::new_refer_s(TEST_TESTGROUP_A_UUID).unwrap()
)]), )]),
None, None,
|_| {}, |_| {},
@ -685,7 +683,7 @@ mod tests {
ModifyList::new_list(vec![ ModifyList::new_list(vec![
Modify::Present( Modify::Present(
Attribute::Member.into(), Attribute::Member.into(),
Value::Refer(uuid!("d2b496bd-8493-47b7-8142-f568b5cf47ee")) Value::Refer(Uuid::parse_str(TEST_TESTGROUP_A_UUID).unwrap())
), ),
Modify::Present(Attribute::Member.into(), Value::Refer(UUID_DOES_NOT_EXIST)), Modify::Present(Attribute::Member.into(), Value::Refer(UUID_DOES_NOT_EXIST)),
]), ]),
@ -761,7 +759,7 @@ mod tests {
)), )),
ModifyList::new_list(vec![Modify::Present( ModifyList::new_list(vec![Modify::Present(
Attribute::Member.into(), Attribute::Member.into(),
Value::new_refer_s("d2b496bd-8493-47b7-8142-f568b5cf47ee").unwrap() Value::new_refer_s(TEST_TESTGROUP_A_UUID).unwrap()
)]), )]),
None, None,
|_| {}, |_| {},
@ -806,7 +804,7 @@ mod tests {
)), )),
ModifyList::new_list(vec![Modify::Present( ModifyList::new_list(vec![Modify::Present(
Attribute::Member.into(), Attribute::Member.into(),
Value::new_refer_s("d2b496bd-8493-47b7-8142-f568b5cf47ee").unwrap() Value::new_refer_s(TEST_TESTGROUP_A_UUID).unwrap()
)]), )]),
None, None,
|qs: &mut QueryServerWriteTransaction| { |qs: &mut QueryServerWriteTransaction| {
@ -827,26 +825,23 @@ mod tests {
// This is the valid case, where the reference is MAY. // This is the valid case, where the reference is MAY.
#[test] #[test]
fn test_delete_remove_referent_valid() { fn test_delete_remove_referent_valid() {
let ea: Entry<EntryInit, EntryNew> = Entry::unsafe_from_entry_str( let ea: Entry<EntryInit, EntryNew> = entry_init!(
r#"{ (Attribute::Class, EntryClass::Group.to_value()),
"attrs": { (Attribute::Name, Value::new_iname("testgroup_a")),
"class": ["group"], (Attribute::Description, Value::new_utf8s("testgroup")),
"name": ["testgroup_a"], (
"description": ["testgroup"], Attribute::Uuid,
"uuid": ["d2b496bd-8493-47b7-8142-f568b5cf47ee"] Value::Uuid(Uuid::parse_str(TEST_TESTGROUP_A_UUID).unwrap())
} )
}"#,
); );
let eb: Entry<EntryInit, EntryNew> = entry_init!(
let eb: Entry<EntryInit, EntryNew> = Entry::unsafe_from_entry_str( (Attribute::Class, EntryClass::Group.to_value()),
r#"{ (Attribute::Name, Value::new_iname("testgroup_b")),
"attrs": { (Attribute::Description, Value::new_utf8s("testgroup")),
"class": ["group"], (
"name": ["testgroup_b"], Attribute::Member,
"description": ["testgroup"], Value::Refer(Uuid::parse_str(TEST_TESTGROUP_A_UUID).unwrap())
"member": ["d2b496bd-8493-47b7-8142-f568b5cf47ee"] )
}
}"#,
); );
let preload = vec![ea, eb]; let preload = vec![ea, eb];
@ -867,31 +862,30 @@ mod tests {
// //
// this is the invalid case, where the reference is MUST. // this is the invalid case, where the reference is MUST.
#[test] #[test]
fn test_delete_remove_referent_invalid() {} fn test_delete_remove_referent_invalid() {
// TODO: uh.. wot
}
// Delete of something that holds references. // Delete of something that holds references.
#[test] #[test]
fn test_delete_remove_referee() { fn test_delete_remove_referee() {
let ea: Entry<EntryInit, EntryNew> = Entry::unsafe_from_entry_str( let ea: Entry<EntryInit, EntryNew> = entry_init!(
r#"{ (Attribute::Class, EntryClass::Group.to_value()),
"attrs": { (Attribute::Name, Value::new_iname("testgroup_a")),
"class": ["group"], (Attribute::Description, Value::new_utf8s("testgroup")),
"name": ["testgroup_a"], (
"description": ["testgroup"], Attribute::Uuid,
"uuid": ["d2b496bd-8493-47b7-8142-f568b5cf47ee"] Value::Uuid(Uuid::parse_str(TEST_TESTGROUP_A_UUID).unwrap())
} )
}"#,
); );
let eb: Entry<EntryInit, EntryNew> = entry_init!(
let eb: Entry<EntryInit, EntryNew> = Entry::unsafe_from_entry_str( (Attribute::Class, EntryClass::Group.to_value()),
r#"{ (Attribute::Name, Value::new_iname("testgroup_b")),
"attrs": { (Attribute::Description, Value::new_utf8s("testgroup")),
"class": ["group"], (
"name": ["testgroup_b"], Attribute::Member,
"description": ["testgroup"], Value::Refer(Uuid::parse_str(TEST_TESTGROUP_A_UUID).unwrap())
"member": ["d2b496bd-8493-47b7-8142-f568b5cf47ee"] )
}
}"#,
); );
let preload = vec![ea, eb]; let preload = vec![ea, eb];
@ -911,18 +905,19 @@ mod tests {
// Delete something that has a self reference. // Delete something that has a self reference.
#[test] #[test]
fn test_delete_remove_reference_self() { fn test_delete_remove_reference_self() {
let eb: Entry<EntryInit, EntryNew> = Entry::unsafe_from_entry_str( let eb: Entry<EntryInit, EntryNew> = entry_init!(
r#"{ (Attribute::Class, EntryClass::Group.to_value()),
"attrs": { (Attribute::Name, Value::new_iname("testgroup_b")),
"class": ["group"], (Attribute::Description, Value::new_utf8s("testgroup")),
"name": ["testgroup_b"], (
"description": ["testgroup"], Attribute::Uuid,
"uuid": ["d2b496bd-8493-47b7-8142-f568b5cf47ee"], Value::Uuid(Uuid::parse_str(TEST_TESTGROUP_A_UUID).unwrap())
"member": ["d2b496bd-8493-47b7-8142-f568b5cf47ee"] ),
} (
}"#, Attribute::Member,
Value::Refer(Uuid::parse_str(TEST_TESTGROUP_A_UUID).unwrap())
)
); );
let preload = vec![eb]; let preload = vec![eb];
run_delete_test!( run_delete_test!(
@ -964,7 +959,7 @@ mod tests {
( (
Attribute::OAuth2RsScopeMap, Attribute::OAuth2RsScopeMap,
Value::new_oauthscopemap( Value::new_oauthscopemap(
uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930"), Uuid::parse_str(TEST_TESTGROUP_B_UUID).unwrap(),
btreeset![OAUTH2_SCOPE_READ.to_string()] btreeset![OAUTH2_SCOPE_READ.to_string()]
) )
.expect("Invalid scope") .expect("Invalid scope")
@ -976,7 +971,7 @@ mod tests {
(Attribute::Name, Value::new_iname("testgroup")), (Attribute::Name, Value::new_iname("testgroup")),
( (
Attribute::Uuid, Attribute::Uuid,
Value::Uuid(uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930")) Value::Uuid(Uuid::parse_str(TEST_TESTGROUP_B_UUID).unwrap())
), ),
(Attribute::Description, Value::new_utf8s("testgroup")) (Attribute::Description, Value::new_utf8s("testgroup"))
); );
@ -1015,7 +1010,7 @@ mod tests {
// Create a user // Create a user
let mut server_txn = server.write(curtime).await; let mut server_txn = server.write(curtime).await;
let tuuid = uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930"); let tuuid = Uuid::parse_str(TEST_TESTGROUP_B_UUID).unwrap();
let rs_uuid = Uuid::new_v4(); let rs_uuid = Uuid::new_v4();
let e1 = entry_init!( let e1 = entry_init!(

View file

@ -225,16 +225,13 @@ mod tests {
#[test] #[test]
fn test_spn_generate_create() { fn test_spn_generate_create() {
// on create don't provide // on create don't provide the spn, we generate it.
let e: Entry<EntryInit, EntryNew> = Entry::unsafe_from_entry_str( let e: Entry<EntryInit,EntryNew> = entry_init!(
r#"{ (Attribute::Class, EntryClass::Account.to_value()),
"attrs": { (Attribute::Class, EntryClass::ServiceAccount.to_value()),
"class": ["account", "service_account"], (Attribute::Name, Value::new_iname("testperson")),
"name": ["testperson"], (Attribute::Description, Value::new_utf8s("testperson")),
"description": ["testperson"], (Attribute::DisplayName, Value::new_utf8s("testperson"))
"displayname": ["testperson"]
}
}"#,
); );
let create = vec![e]; let create = vec![e];
@ -252,16 +249,13 @@ mod tests {
#[test] #[test]
fn test_spn_generate_modify() { fn test_spn_generate_modify() {
// on a purge of the spen, generate it. // on a purge of the spn, generate it.
let e: Entry<EntryInit, EntryNew> = Entry::unsafe_from_entry_str( let e: Entry<EntryInit,EntryNew> = entry_init!(
r#"{ (Attribute::Class, EntryClass::Account.to_value()),
"attrs": { (Attribute::Class, EntryClass::ServiceAccount.to_value()),
"class": ["account", "service_account"], (Attribute::Name, Value::new_iname("testperson")),
"name": ["testperson"], (Attribute::Description, Value::new_utf8s("testperson")),
"description": ["testperson"], (Attribute::DisplayName, Value::new_utf8s("testperson"))
"displayname": ["testperson"]
}
}"#,
); );
let preload = vec![e]; let preload = vec![e];
@ -280,16 +274,14 @@ mod tests {
#[test] #[test]
fn test_spn_validate_create() { fn test_spn_validate_create() {
// on create providing invalid spn, we over-write it. // on create providing invalid spn, we over-write it.
let e: Entry<EntryInit, EntryNew> = Entry::unsafe_from_entry_str(
r#"{ let e: Entry<EntryInit,EntryNew> = entry_init!(
"attrs": { (Attribute::Class, EntryClass::Account.to_value()),
"class": ["account", "service_account"], (Attribute::Class, EntryClass::ServiceAccount.to_value()),
"spn": ["testperson@invalid_domain.com"], (Attribute::Spn, Value::new_utf8s("testperson@invalid_domain.com")),
"name": ["testperson"], (Attribute::Name, Value::new_iname("testperson")),
"description": ["testperson"], (Attribute::Description, Value::new_utf8s("testperson")),
"displayname": ["testperson"] (Attribute::DisplayName, Value::new_utf8s("testperson"))
}
}"#,
); );
let create = vec![e]; let create = vec![e];
@ -307,15 +299,13 @@ mod tests {
#[test] #[test]
fn test_spn_validate_modify() { fn test_spn_validate_modify() {
// On modify (removed/present) of the spn, just regenerate it. // On modify (removed/present) of the spn, just regenerate it.
let e: Entry<EntryInit, EntryNew> = Entry::unsafe_from_entry_str(
r#"{ let e: Entry<EntryInit,EntryNew> = entry_init!(
"attrs": { (Attribute::Class, EntryClass::Account.to_value()),
"class": ["account", "service_account"], (Attribute::Class, EntryClass::ServiceAccount.to_value()),
"name": ["testperson"], (Attribute::Name, Value::new_iname("testperson")),
"description": ["testperson"], (Attribute::Description, Value::new_utf8s("testperson")),
"displayname": ["testperson"] (Attribute::DisplayName, Value::new_utf8s("testperson"))
}
}"#,
); );
let preload = vec![e]; let preload = vec![e];

View file

@ -2903,7 +2903,7 @@ mod tests {
assert_eq!( assert_eq!(
e_service_person.validate(&schema), e_service_person.validate(&schema),
Err(SchemaError::ExcludesNotSatisfied( Err(SchemaError::ExcludesNotSatisfied(
vec!["person".to_string()] vec![EntryClass::Person.to_string()]
)) ))
); );

View file

@ -1703,7 +1703,7 @@ impl Value {
} }
} }
Value::Spn(n, r) => format!("{n}@{r}"), Value::Spn(n, r) => format!("{n}@{r}"),
_ => unreachable!(), _ => unreachable!("You've specified the wrong type for the attribute, got: {:?}", self),
} }
} }

View file

@ -1,5 +1,8 @@
//! Integration tests using browser automation //! Integration tests using browser automation
use kanidm_client::KanidmClient;
use kanidmd_testkit::login_put_admin_idm_admins;
/// Tries to handle closing the webdriver session if there's an error /// Tries to handle closing the webdriver session if there's an error
#[allow(unused_macros)] #[allow(unused_macros)]
macro_rules! handle_error { macro_rules! handle_error {
@ -197,3 +200,22 @@ async fn test_webdriver_user_login(rsclient: kanidm_client::KanidmClient) {
} }
// tokio::time::sleep(Duration::from_millis(3000)).await; // tokio::time::sleep(Duration::from_millis(3000)).await;
} }
#[kanidmd_testkit::test]
async fn test_domain_reset_token_key(rsclient: KanidmClient) {
login_put_admin_idm_admins(&rsclient).await;
assert!(rsclient.idm_domain_reset_token_key().await.is_ok());
}
#[kanidmd_testkit::test]
async fn test_idm_domain_set_ldap_basedn(rsclient: KanidmClient) {
login_put_admin_idm_admins(&rsclient).await;
assert!(rsclient
.idm_domain_set_ldap_basedn("dc=krabsarekool,dc=example,dc=com")
.await
.is_ok());
assert!(rsclient
.idm_domain_set_ldap_basedn("krabsarekool")
.await
.is_err());
}

View file

@ -22,7 +22,7 @@ use webauthn_authenticator_rs::softpasskey::SoftPasskey;
use webauthn_authenticator_rs::WebauthnAuthenticator; use webauthn_authenticator_rs::WebauthnAuthenticator;
use kanidm_client::{ClientError, KanidmClient}; use kanidm_client::{ClientError, KanidmClient};
use kanidmd_testkit::ADMIN_TEST_PASSWORD; use kanidmd_testkit::{ADMIN_TEST_PASSWORD, ADMIN_TEST_USER};
const UNIX_TEST_PASSWORD: &str = "unix test user password"; const UNIX_TEST_PASSWORD: &str = "unix test user password";
@ -1410,29 +1410,36 @@ async fn test_server_credential_update_session_passkey(rsclient: KanidmClient) {
#[kanidmd_testkit::test] #[kanidmd_testkit::test]
async fn test_server_api_token_lifecycle(rsclient: KanidmClient) { async fn test_server_api_token_lifecycle(rsclient: KanidmClient) {
let res = rsclient let res = rsclient
.auth_simple_password("admin", ADMIN_TEST_PASSWORD) .auth_simple_password(ADMIN_TEST_USER, ADMIN_TEST_PASSWORD)
.await; .await;
assert!(res.is_ok()); assert!(res.is_ok());
let test_service_account_username = "test_service";
// Not recommended in production! // Not recommended in production!
rsclient rsclient
.idm_group_add_members(BUILTIN_GROUP_IDM_ADMINS_V1.name, &["admin"]) .idm_group_add_members(BUILTIN_GROUP_IDM_ADMINS_V1.name, &[ADMIN_TEST_USER])
.await .await
.unwrap(); .unwrap();
rsclient rsclient
.idm_service_account_create("test_service", "Test Service") .idm_service_account_create(test_service_account_username, "Test Service")
.await .await
.expect("Failed to create service account"); .expect("Failed to create service account");
let tokens = rsclient let tokens = rsclient
.idm_service_account_list_api_token("test_service") .idm_service_account_list_api_token(test_service_account_username)
.await .await
.expect("Failed to list service account api tokens"); .expect("Failed to list service account api tokens");
assert!(tokens.is_empty()); assert!(tokens.is_empty());
let token = rsclient let token = rsclient
.idm_service_account_generate_api_token("test_service", "test token", None, false) .idm_service_account_generate_api_token(
test_service_account_username,
"test token",
None,
false,
)
.await .await
.expect("Failed to create service account api token"); .expect("Failed to create service account api token");
@ -1445,7 +1452,7 @@ async fn test_server_api_token_lifecycle(rsclient: KanidmClient) {
.expect("Embedded jwk not found"); .expect("Embedded jwk not found");
let tokens = rsclient let tokens = rsclient
.idm_service_account_list_api_token("test_service") .idm_service_account_list_api_token(test_service_account_username)
.await .await
.expect("Failed to list service account api tokens"); .expect("Failed to list service account api tokens");
@ -1457,24 +1464,134 @@ async fn test_server_api_token_lifecycle(rsclient: KanidmClient) {
.expect("Failed to destroy service account api token"); .expect("Failed to destroy service account api token");
let tokens = rsclient let tokens = rsclient
.idm_service_account_list_api_token("test_service") .idm_service_account_list_api_token(test_service_account_username)
.await .await
.expect("Failed to list service account api tokens"); .expect("Failed to list service account api tokens");
assert!(tokens.is_empty()); assert!(tokens.is_empty());
// test we can add an attribute
assert!(rsclient
.idm_service_account_add_attr(
test_service_account_username,
Attribute::Mail.as_ref(),
&vec!["test@example.com"]
)
.await
.is_ok());
// test we can overwrite an attribute
let new_displayname = vec!["testing displayname 1235"];
assert!(rsclient
.idm_service_account_set_attr(
test_service_account_username,
Attribute::DisplayName.as_ref(),
&new_displayname
)
.await
.is_ok());
// check it actually set
let displayname = rsclient
.idm_service_account_get_attr(
test_service_account_username,
Attribute::DisplayName.as_ref(),
)
.await
.expect("Failed to get displayname")
.expect("Failed to unwrap displayname");
assert!(new_displayname == displayname);
rsclient
.idm_service_account_purge_attr(test_service_account_username, Attribute::Mail.as_ref())
.await
.expect("Failed to purge displayname");
assert!(rsclient
.idm_service_account_get_attr(test_service_account_username, Attribute::Mail.as_ref(),)
.await
.expect("Failed to check mail attr")
.is_none());
assert!(rsclient
.idm_service_account_unix_extend(
test_service_account_username,
Some(58008),
Some("/bin/vim")
)
.await
.is_ok());
assert!(rsclient
.idm_service_account_unix_extend(
test_service_account_username,
Some(1000),
Some("/bin/vim")
)
.await
.is_err());
// because you have to set *something*
assert!(rsclient
.idm_service_account_update(test_service_account_username, None, None, None)
.await
.is_err());
assert!(rsclient
.idm_service_account_update(
test_service_account_username,
Some(&format!("{}lol", test_service_account_username)),
Some(&format!("{}displayzzzz", test_service_account_username)),
Some(&[format!("{}@example.crabs", test_service_account_username)]),
)
.await
.is_err());
let pw = rsclient.idm_service_account_generate_password(test_service_account_username).await.expect("Failed to get a pw for the service account");
assert!(!pw.is_empty());
assert!(pw.is_ascii());
let res = rsclient.idm_service_account_get_credential_status(test_service_account_username).await;
dbg!(&res);
assert!(res.is_ok());
println!(
"testing deletion of service account {}",
test_service_account_username
);
assert!(rsclient
.idm_service_account_delete(test_service_account_username)
.await
.is_ok());
// let's create one and just yolo it into a person
// TODO: Turns out this doesn't work because admin doesn't have the right perms to remove `jws_es256_private_key` from the account?
// rsclient
// .idm_service_account_create(test_service_account_username, "Test Service")
// .await
// .expect("Failed to create service account");
// rsclient.idm_service_account_into_person(test_service_account_username).await.expect("Failed to convert service account into person");
// assert!(rsclient
// .idm_person_account_delete(test_service_account_username)
// .await
// .is_ok());
// No need to test expiry, that's validated in the server internal tests. // No need to test expiry, that's validated in the server internal tests.
} }
#[kanidmd_testkit::test] #[kanidmd_testkit::test]
async fn test_server_user_auth_token_lifecycle(rsclient: KanidmClient) { async fn test_server_user_auth_token_lifecycle(rsclient: KanidmClient) {
let res = rsclient let res = rsclient
.auth_simple_password("admin", ADMIN_TEST_PASSWORD) .auth_simple_password(ADMIN_TEST_USER, ADMIN_TEST_PASSWORD)
.await; .await;
assert!(res.is_ok()); assert!(res.is_ok());
// Not recommended in production! // Not recommended in production!
rsclient rsclient
.idm_group_add_members(BUILTIN_GROUP_IDM_ADMINS_V1.name, &["admin"]) .idm_group_add_members(BUILTIN_GROUP_IDM_ADMINS_V1.name, &[ADMIN_TEST_USER])
.await .await
.unwrap(); .unwrap();
@ -1553,7 +1670,7 @@ async fn test_server_user_auth_token_lifecycle(rsclient: KanidmClient) {
// Since the session is revoked, check with the admin. // Since the session is revoked, check with the admin.
let res = rsclient let res = rsclient
.auth_simple_password("admin", ADMIN_TEST_PASSWORD) .auth_simple_password(ADMIN_TEST_USER, ADMIN_TEST_PASSWORD)
.await; .await;
assert!(res.is_ok()); assert!(res.is_ok());
@ -1564,6 +1681,18 @@ async fn test_server_user_auth_token_lifecycle(rsclient: KanidmClient) {
assert!(tokens.is_empty()); assert!(tokens.is_empty());
// No need to test expiry, that's validated in the server internal tests. // No need to test expiry, that's validated in the server internal tests.
// testing idm_account_credential_update_cancel_mfareg
let (token, _status) = rsclient
.idm_account_credential_update_begin("demo_account")
.await
.expect("Failed to get token for demo_account");
println!("trying to cancel the token we just got");
assert!(rsclient
.idm_account_credential_update_cancel_mfareg(&token)
.await
.is_ok());
} }
#[kanidmd_testkit::test] #[kanidmd_testkit::test]
@ -1793,3 +1922,21 @@ async fn test_server_user_auth_privileged_shortcut(rsclient: KanidmClient) {
} }
} }
} }
// wanna test how long it takes for testkit to start up? here's your biz.
// turns out as of 2023-10-11 on my M2 Max, it's about 1.0 seconds per iteration
// #[kanidmd_testkit::test]
// fn test_teskit_test_test() {
// #[allow(unnameable_test_items)]
// for _ in 0..15 {
// #[kanidmd_testkit::test]
// #[allow(dead_code)]
// async fn test_teskit_test(rsclient: KanidmClient){
// assert!(rsclient.auth_anonymous().await.is_ok());
// }
// tk_test_teskit_test();
// }
// }

View file

@ -1,7 +1,7 @@
use compact_jwt::JwsUnverified; use compact_jwt::JwsUnverified;
use kanidm_client::KanidmClient; use kanidm_client::KanidmClient;
use kanidm_proto::internal::ScimSyncToken; use kanidm_proto::internal::ScimSyncToken;
use kanidmd_testkit::ADMIN_TEST_PASSWORD; use kanidmd_testkit::{ADMIN_TEST_PASSWORD, ADMIN_TEST_USER};
use reqwest::header::HeaderValue; use reqwest::header::HeaderValue;
use std::str::FromStr; use std::str::FromStr;
use url::Url; use url::Url;
@ -9,7 +9,7 @@ use url::Url;
#[kanidmd_testkit::test] #[kanidmd_testkit::test]
async fn test_sync_account_lifecycle(rsclient: KanidmClient) { async fn test_sync_account_lifecycle(rsclient: KanidmClient) {
let a_res = rsclient let a_res = rsclient
.auth_simple_password("admin", ADMIN_TEST_PASSWORD) .auth_simple_password(ADMIN_TEST_USER, ADMIN_TEST_PASSWORD)
.await; .await;
assert!(a_res.is_ok()); assert!(a_res.is_ok());

View file

@ -497,6 +497,7 @@ impl ServiceAccountOpt {
} }
}, // end ServiceAccountOpt::Validity }, // end ServiceAccountOpt::Validity
ServiceAccountOpt::IntoPerson(aopt) => { ServiceAccountOpt::IntoPerson(aopt) => {
warn!("This command is deprecated and will be removed in a future release");
let client = aopt.copt.to_client(OpType::Write).await; let client = aopt.copt.to_client(OpType::Write).await;
match client match client
.idm_service_account_into_person(aopt.aopts.account_id.as_str()) .idm_service_account_into_person(aopt.aopts.account_id.as_str())

View file

@ -515,7 +515,7 @@ pub enum ServiceAccountOpt {
#[clap(subcommand)] #[clap(subcommand)]
commands: AccountValidity, commands: AccountValidity,
}, },
/// Convert a service account into a person. This is used during the alpha.9 /// (Deprecated - due for removal in v1.1.0-15) - Convert a service account into a person. This is used during the alpha.9
/// to alpha.10 migration to "fix up" accounts that were not previously marked /// to alpha.10 migration to "fix up" accounts that were not previously marked
/// as persons. /// as persons.
#[clap(name = "into-person")] #[clap(name = "into-person")]

View file

@ -23,8 +23,8 @@ use base64urlsafedata::Base64UrlSafeData;
use chrono::Utc; use chrono::Utc;
use clap::Parser; use clap::Parser;
use cron::Schedule; use cron::Schedule;
use kanidm_proto::constants::{LDAP_ATTR_CN, LDAP_ATTR_OBJECTCLASS}; use kanidm_proto::constants::{ATTR_UID, LDAP_ATTR_CN, LDAP_ATTR_OBJECTCLASS, LDAP_CLASS_GROUPOFNAMES};
use kanidmd_lib::prelude::Attribute; use kanidmd_lib::prelude::{Attribute, EntryClass};
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::fs::metadata; use std::fs::metadata;
use std::fs::File; use std::fs::File;
@ -583,7 +583,7 @@ async fn process_ipa_sync_result(
.entry .entry
.attrs .attrs
.get(LDAP_ATTR_OBJECTCLASS) .get(LDAP_ATTR_OBJECTCLASS)
.map(|oc| oc.contains("person")) .map(|oc| oc.contains(EntryClass::Person.as_ref()))
.unwrap_or_default() .unwrap_or_default()
{ {
user_dns.push(dn.clone()); user_dns.push(dn.clone());
@ -638,7 +638,7 @@ async fn process_ipa_sync_result(
// We have to split the DN to it's RDN because lol. // We have to split the DN to it's RDN because lol.
dn.split_once(',') dn.split_once(',')
.and_then(|(rdn, _)| rdn.split_once('=')) .and_then(|(rdn, _)| rdn.split_once('='))
.map(|(_, uid)| LdapFilter::Equality("uid".to_string(), uid.to_string())) .map(|(_, uid)| LdapFilter::Equality(ATTR_UID.to_string(), uid.to_string()))
}) })
.collect(); .collect();
@ -903,7 +903,7 @@ fn ipa_to_scim_entry(
} }
.into(), .into(),
)) ))
} else if oc.contains("groupofnames") { } else if oc.contains(LDAP_CLASS_GROUPOFNAMES) {
let LdapSyncReplEntry { let LdapSyncReplEntry {
entry_uuid, entry_uuid,
state: _, state: _,

View file

@ -40,3 +40,6 @@ kanidm_utils_users = { workspace = true }
[build-dependencies] [build-dependencies]
clap = { workspace = true, features = ["derive"] } clap = { workspace = true, features = ["derive"] }
clap_complete = { workspace = true } clap_complete = { workspace = true }
[dev-dependencies]
sketching = { workspace = true }

View file

@ -1,4 +1,5 @@
use kanidmd_lib::prelude::Attribute; use kanidm_proto::constants::{ATTR_UID, LDAP_ATTR_CN, LDAP_CLASS_GROUPOFNAMES};
use kanidmd_lib::prelude::{Attribute, EntryClass};
use serde::Deserialize; use serde::Deserialize;
use std::collections::BTreeMap; use std::collections::BTreeMap;
use url::Url; use url::Url;
@ -7,15 +8,15 @@ use uuid::Uuid;
use ldap3_client::proto::LdapFilter; use ldap3_client::proto::LdapFilter;
fn person_objectclass() -> String { fn person_objectclass() -> String {
"person".to_string() EntryClass::Person.to_string()
} }
fn person_attr_user_name() -> String { fn person_attr_user_name() -> String {
"uid".to_string() ATTR_UID.to_string()
} }
fn person_attr_display_name() -> String { fn person_attr_display_name() -> String {
"cn".to_string() LDAP_ATTR_CN.to_string()
} }
fn person_attr_gidnumber() -> String { fn person_attr_gidnumber() -> String {
@ -39,7 +40,7 @@ fn person_attr_ssh_public_key() -> String {
} }
fn group_objectclass() -> String { fn group_objectclass() -> String {
"groupofnames".to_string() LDAP_CLASS_GROUPOFNAMES.to_string()
} }
fn group_attr_name() -> String { fn group_attr_name() -> String {

View file

@ -62,21 +62,21 @@ async fn driver_main(opt: Opt) {
let mut f = match File::open(&opt.ldap_sync_config) { let mut f = match File::open(&opt.ldap_sync_config) {
Ok(f) => f, Ok(f) => f,
Err(e) => { Err(e) => {
error!("Unable to open profile file [{:?}] 🥺", e); error!("Unable to open ldap sync config from '{}' [{:?}] 🥺", &opt.ldap_sync_config.display(), e);
return; return;
} }
}; };
let mut contents = String::new(); let mut contents = String::new();
if let Err(e) = f.read_to_string(&mut contents) { if let Err(e) = f.read_to_string(&mut contents) {
error!("unable to read profile contents {:?}", e); error!("unable to read file '{}': {:?}", &opt.ldap_sync_config.display(), e);
return; return;
}; };
let sync_config: Config = match toml::from_str(contents.as_str()) { let sync_config: Config = match toml::from_str(contents.as_str()) {
Ok(c) => c, Ok(c) => c,
Err(e) => { Err(e) => {
eprintln!("unable to parse config {:?}", e); eprintln!("Unable to parse config from '{}' error: {:?}", &opt.ldap_sync_config.display(), e);
return; return;
} }
}; };
@ -781,3 +781,41 @@ fn main() {
rt.block_on(async move { driver_main(opt).await }); rt.block_on(async move { driver_main(opt).await });
} }
#[tokio::test]
async fn test_driver_main() {
let testopt = Opt {
client_config: PathBuf::from("test"),
ldap_sync_config: PathBuf::from("test"),
debug: false,
schedule: false,
proto_dump: false,
dry_run: false,
skip_root_check: true,
};
let _ = sketching::test_init();
println!("testing config");
// because it can't find the profile file it'll just stop
assert_eq!(driver_main(testopt.clone()).await, ());
println!("done testing missing config");
let testopt = Opt{
client_config: PathBuf::from(format!("{}/Cargo.toml", env!("CARGO_MANIFEST_DIR"))),
ldap_sync_config: PathBuf::from(format!("{}/Cargo.toml", env!("CARGO_MANIFEST_DIR"))),
..testopt
};
println!("valid file path, invalid contents");
assert_eq!(driver_main(testopt.clone()).await, ());
println!("done with valid file path, invalid contents");
let testopt = Opt{
client_config: PathBuf::from(format!("{}/../../../examples/iam_migration_ldap.toml", env!("CARGO_MANIFEST_DIR"))),
ldap_sync_config: PathBuf::from(format!("{}/../../../examples/iam_migration_ldap.toml", env!("CARGO_MANIFEST_DIR"))),
..testopt
};
println!("valid file path, invalid contents");
assert_eq!(driver_main(testopt).await, ());
println!("done with valid file path, valid contents");
}

View file

@ -1,7 +1,7 @@
use kanidm_proto::constants::DEFAULT_CLIENT_CONFIG_PATH; use kanidm_proto::constants::DEFAULT_CLIENT_CONFIG_PATH;
pub const DEFAULT_LDAP_CONFIG_PATH: &str = "/etc/kanidm/ldap-sync"; pub const DEFAULT_LDAP_CONFIG_PATH: &str = "/etc/kanidm/ldap-sync";
#[derive(Debug, clap::Parser)] #[derive(Debug, clap::Parser, Clone)]
#[clap(about = "Kanidm LDAP Sync Driver")] #[clap(about = "Kanidm LDAP Sync Driver")]
pub struct Opt { pub struct Opt {
/// Enable debugging of the sync driver /// Enable debugging of the sync driver

View file

@ -1,4 +1,5 @@
use hashbrown::{HashMap, HashSet}; use hashbrown::{HashMap, HashSet};
use kanidm_proto::constants::{ATTR_UID, LDAP_ATTR_DISPLAY_NAME, LDAP_ATTR_CN, LDAP_ATTR_OBJECTCLASS, LDAP_ATTR_OU, LDAP_ATTR_GROUPS};
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use ldap3_proto::proto::*; use ldap3_proto::proto::*;
@ -59,7 +60,7 @@ impl DirectoryServer {
let filter = LdapFilter::Or( let filter = LdapFilter::Or(
targets targets
.iter() .iter()
.map(|u| LdapFilter::Equality("cn".to_string(), u.to_string())) .map(|u| LdapFilter::Equality(LDAP_ATTR_CN.to_string(), u.to_string()))
.collect(), .collect(),
); );
@ -86,7 +87,7 @@ impl DirectoryServer {
// Check if ou=people and ou=group exist // Check if ou=people and ou=group exist
let res = self let res = self
.ldap .ldap
.search(LdapFilter::Equality("ou".to_string(), "people".to_string())) .search(LdapFilter::Equality(LDAP_ATTR_OU.to_string(), "people".to_string()))
.await?; .await?;
if res.is_empty() { if res.is_empty() {
@ -96,14 +97,14 @@ impl DirectoryServer {
dn: format!("ou=people,{}", self.ldap.basedn), dn: format!("ou=people,{}", self.ldap.basedn),
attributes: vec![ attributes: vec![
LdapAttribute { LdapAttribute {
atype: "objectClass".to_string(), atype: LDAP_ATTR_OBJECTCLASS.to_string(),
vals: vec![ vals: vec![
"top".as_bytes().into(), "top".as_bytes().into(),
"organizationalUnit".as_bytes().into(), "organizationalUnit".as_bytes().into(),
], ],
}, },
LdapAttribute { LdapAttribute {
atype: "ou".to_string(), atype: LDAP_ATTR_OU.to_string(),
vals: vec!["people".as_bytes().into()], vals: vec!["people".as_bytes().into()],
}, },
], ],
@ -113,25 +114,25 @@ impl DirectoryServer {
let res = self let res = self
.ldap .ldap
.search(LdapFilter::Equality("ou".to_string(), "groups".to_string())) .search(LdapFilter::Equality(LDAP_ATTR_OU.to_string(),LDAP_ATTR_GROUPS.to_string()))
.await?; .await?;
if res.is_empty() { if res.is_empty() {
// Doesn't exist // Doesn't exist
info!("Creating ou=groups"); info!("Creating ou={}", LDAP_ATTR_GROUPS);
let ou_groups = LdapAddRequest { let ou_groups: LdapAddRequest = LdapAddRequest {
dn: format!("ou=groups,{}", self.ldap.basedn), dn: format!("ou={},{}", LDAP_ATTR_GROUPS, self.ldap.basedn),
attributes: vec![ attributes: vec![
LdapAttribute { LdapAttribute {
atype: "objectClass".to_string(), atype: LDAP_ATTR_OBJECTCLASS.to_string(),
vals: vec![ vals: vec![
"top".as_bytes().into(), "top".as_bytes().into(),
"organizationalUnit".as_bytes().into(), "organizationalUnit".as_bytes().into(),
], ],
}, },
LdapAttribute { LdapAttribute {
atype: "ou".to_string(), atype: LDAP_ATTR_OU.to_string(),
vals: vec!["groups".as_bytes().into()], vals: vec![LDAP_ATTR_GROUPS.as_bytes().into()],
}, },
], ],
}; };
@ -144,7 +145,7 @@ impl DirectoryServer {
// does it already exist? // does it already exist?
let res = self let res = self
.ldap .ldap
.search(LdapFilter::Equality("cn".to_string(), u.to_string())) .search(LdapFilter::Equality(LDAP_ATTR_CN.to_string(), u.to_string()))
.await?; .await?;
if !res.is_empty() { if !res.is_empty() {
@ -159,7 +160,7 @@ impl DirectoryServer {
dn, dn,
attributes: vec![ attributes: vec![
LdapAttribute { LdapAttribute {
atype: "objectClass".to_string(), atype: LDAP_ATTR_OBJECTCLASS.to_string(),
vals: vec![ vals: vec![
"top".as_bytes().into(), "top".as_bytes().into(),
"nsPerson".as_bytes().into(), "nsPerson".as_bytes().into(),
@ -169,15 +170,15 @@ impl DirectoryServer {
], ],
}, },
LdapAttribute { LdapAttribute {
atype: "cn".to_string(), atype: LDAP_ATTR_CN.to_string(),
vals: vec![a.uuid.as_bytes().to_vec()], vals: vec![a.uuid.as_bytes().to_vec()],
}, },
LdapAttribute { LdapAttribute {
atype: "uid".to_string(), atype: ATTR_UID.to_string(),
vals: vec![a.name.as_bytes().into()], vals: vec![a.name.as_bytes().into()],
}, },
LdapAttribute { LdapAttribute {
atype: "displayName".to_string(), atype: LDAP_ATTR_DISPLAY_NAME.to_string(),
vals: vec![a.display_name.as_bytes().into()], vals: vec![a.display_name.as_bytes().into()],
}, },
LdapAttribute { LdapAttribute {
@ -205,14 +206,14 @@ impl DirectoryServer {
dn, dn,
attributes: vec![ attributes: vec![
LdapAttribute { LdapAttribute {
atype: "objectClass".to_string(), atype: LDAP_ATTR_OBJECTCLASS.to_string(),
vals: vec![ vals: vec![
"top".as_bytes().into(), "top".as_bytes().into(),
"groupOfNames".as_bytes().into(), "groupOfNames".as_bytes().into(),
], ],
}, },
LdapAttribute { LdapAttribute {
atype: "cn".to_string(), atype: LDAP_ATTR_CN.to_string(),
vals: vec![g.uuid.as_bytes().to_vec(), g.name.as_bytes().into()], vals: vec![g.uuid.as_bytes().to_vec(), g.name.as_bytes().into()],
}, },
], ],
@ -268,7 +269,7 @@ impl DirectoryServer {
let res = self let res = self
.ldap .ldap
.search(LdapFilter::Equality( .search(LdapFilter::Equality(
"cn".to_string(), LDAP_ATTR_CN.to_string(),
"priv_account_manage".to_string(), "priv_account_manage".to_string(),
)) ))
.await?; .await?;
@ -280,11 +281,11 @@ impl DirectoryServer {
dn: format!("cn=priv_account_manage,{}", self.ldap.basedn), dn: format!("cn=priv_account_manage,{}", self.ldap.basedn),
attributes: vec![ attributes: vec![
LdapAttribute { LdapAttribute {
atype: "objectClass".to_string(), atype: LDAP_ATTR_OBJECTCLASS.to_string(),
vals: vec!["top".as_bytes().into(), "groupOfNames".as_bytes().into()], vals: vec!["top".as_bytes().into(), "groupOfNames".as_bytes().into()],
}, },
LdapAttribute { LdapAttribute {
atype: "cn".to_string(), atype: LDAP_ATTR_CN.to_string(),
vals: vec!["priv_account_manage".as_bytes().into()], vals: vec!["priv_account_manage".as_bytes().into()],
}, },
], ],
@ -295,7 +296,7 @@ impl DirectoryServer {
let res = self let res = self
.ldap .ldap
.search(LdapFilter::Equality( .search(LdapFilter::Equality(
"cn".to_string(), LDAP_ATTR_CN.to_string(),
"priv_group_manage".to_string(), "priv_group_manage".to_string(),
)) ))
.await?; .await?;
@ -307,11 +308,11 @@ impl DirectoryServer {
dn: format!("cn=priv_group_manage,{}", self.ldap.basedn), dn: format!("cn=priv_group_manage,{}", self.ldap.basedn),
attributes: vec![ attributes: vec![
LdapAttribute { LdapAttribute {
atype: "objectClass".to_string(), atype: LDAP_ATTR_OBJECTCLASS.to_string(),
vals: vec!["top".as_bytes().into(), "groupOfNames".as_bytes().into()], vals: vec!["top".as_bytes().into(), "groupOfNames".as_bytes().into()],
}, },
LdapAttribute { LdapAttribute {
atype: "cn".to_string(), atype: LDAP_ATTR_CN.to_string(),
vals: vec!["priv_group_manage".as_bytes().into()], vals: vec!["priv_group_manage".as_bytes().into()],
}, },
], ],

View file

@ -1,4 +1,5 @@
use hashbrown::{HashMap, HashSet}; use hashbrown::{HashMap, HashSet};
use kanidm_proto::constants::{ATTR_UID, LDAP_ATTR_DISPLAY_NAME, LDAP_CLASS_GROUPOFNAMES, LDAP_ATTR_OBJECTCLASS, LDAP_ATTR_CN};
use ldap3_proto::proto::*; use ldap3_proto::proto::*;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use uuid::Uuid; use uuid::Uuid;
@ -93,7 +94,7 @@ impl IpaServer {
dn, dn,
attributes: vec![ attributes: vec![
LdapAttribute { LdapAttribute {
atype: "objectClass".to_string(), atype: LDAP_ATTR_OBJECTCLASS.to_string(),
vals: vec![ vals: vec![
"ipaobject".as_bytes().into(), "ipaobject".as_bytes().into(),
"person".as_bytes().into(), "person".as_bytes().into(),
@ -113,11 +114,11 @@ impl IpaServer {
vals: vec!["autogenerate".as_bytes().into()], vals: vec!["autogenerate".as_bytes().into()],
}, },
LdapAttribute { LdapAttribute {
atype: "uid".to_string(), atype: ATTR_UID.to_string(),
vals: vec![a.name.as_bytes().into()], vals: vec![a.name.as_bytes().into()],
}, },
LdapAttribute { LdapAttribute {
atype: "cn".to_string(), atype: LDAP_ATTR_CN.to_string(),
vals: vec![a.name.as_bytes().into()], vals: vec![a.name.as_bytes().into()],
}, },
LdapAttribute { LdapAttribute {
@ -129,7 +130,7 @@ impl IpaServer {
vals: vec![a.name.as_bytes().into()], vals: vec![a.name.as_bytes().into()],
}, },
LdapAttribute { LdapAttribute {
atype: "displayName".to_string(), atype: LDAP_ATTR_DISPLAY_NAME.to_string(),
vals: vec![a.display_name.as_bytes().into()], vals: vec![a.display_name.as_bytes().into()],
}, },
LdapAttribute { LdapAttribute {
@ -200,7 +201,7 @@ impl IpaServer {
atype: "objectClass".to_string(), atype: "objectClass".to_string(),
vals: vec![ vals: vec![
"top".as_bytes().into(), "top".as_bytes().into(),
"groupofnames".as_bytes().into(), LDAP_CLASS_GROUPOFNAMES.as_bytes().into(),
"nestedgroup".as_bytes().into(), "nestedgroup".as_bytes().into(),
"ipausergroup".as_bytes().into(), "ipausergroup".as_bytes().into(),
"ipaobject".as_bytes().into(), "ipaobject".as_bytes().into(),