mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 20:47:01 +01:00
Sync account import improvements (#1873)
This commit is contained in:
parent
41aa315f23
commit
60a1cdf9d8
8
Cargo.lock
generated
8
Cargo.lock
generated
|
@ -2910,9 +2910,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ldap3_client"
|
name = "ldap3_client"
|
||||||
version = "0.3.4"
|
version = "0.3.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "471c4b5d674dbfe41fd479ec0b9435fd765bc838347c5625d2d2f2229a36b104"
|
checksum = "7a229cd5ee2a4e5a1a279b6216494aa2a5053a189c5ce37bb31f9156b63b63de"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.13.1",
|
"base64 0.13.1",
|
||||||
"base64urlsafedata",
|
"base64urlsafedata",
|
||||||
|
@ -2930,9 +2930,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ldap3_proto"
|
name = "ldap3_proto"
|
||||||
version = "0.3.4"
|
version = "0.3.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2f4e16ad62e1881aa010c73dad1e693a208100f889512612cbe4fc717660cc31"
|
checksum = "93d073b5c98def43cec39ccd95e536b3b2448e36289222ecd64dfdf1389d1702"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"lber",
|
"lber",
|
||||||
|
|
|
@ -88,8 +88,8 @@ kanidm_unix_int = { path = "./unix_integration" }
|
||||||
last-git-commit = "0.2.0"
|
last-git-commit = "0.2.0"
|
||||||
# REMOVE this
|
# REMOVE this
|
||||||
lazy_static = "^1.4.0"
|
lazy_static = "^1.4.0"
|
||||||
ldap3_client = "^0.3.4"
|
ldap3_client = "^0.3.5"
|
||||||
ldap3_proto = { version = "^0.3.4", features = ["serde"] }
|
ldap3_proto = { version = "^0.3.5", features = ["serde"] }
|
||||||
|
|
||||||
# ldap3_client = { path = "../ldap3/client", version = "0.3.2" }
|
# ldap3_client = { path = "../ldap3/client", version = "0.3.2" }
|
||||||
# ldap3_proto = { path = "../ldap3/proto", version = "^0.3.2", features = ["serde"] }
|
# ldap3_proto = { path = "../ldap3/proto", version = "^0.3.2", features = ["serde"] }
|
||||||
|
|
|
@ -95,6 +95,26 @@ impl Into<ScimComplexAttr> for ScimTotp {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Debug, Clone)]
|
||||||
|
pub struct ScimSshPubKey {
|
||||||
|
pub label: String,
|
||||||
|
pub value: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::from_over_into)]
|
||||||
|
impl Into<ScimComplexAttr> for ScimSshPubKey {
|
||||||
|
fn into(self) -> ScimComplexAttr {
|
||||||
|
let ScimSshPubKey { label, value } = self;
|
||||||
|
|
||||||
|
let mut attrs = BTreeMap::default();
|
||||||
|
|
||||||
|
attrs.insert("label".to_string(), ScimSimpleAttr::String(label));
|
||||||
|
|
||||||
|
attrs.insert("value".to_string(), ScimSimpleAttr::String(value));
|
||||||
|
ScimComplexAttr { attrs }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Debug, Clone)]
|
#[derive(Serialize, Debug, Clone)]
|
||||||
#[serde(into = "ScimEntry")]
|
#[serde(into = "ScimEntry")]
|
||||||
pub struct ScimSyncPerson {
|
pub struct ScimSyncPerson {
|
||||||
|
@ -107,6 +127,7 @@ pub struct ScimSyncPerson {
|
||||||
pub totp_import: Vec<ScimTotp>,
|
pub totp_import: Vec<ScimTotp>,
|
||||||
pub login_shell: Option<String>,
|
pub login_shell: Option<String>,
|
||||||
pub mail: Vec<MultiValueAttr>,
|
pub mail: Vec<MultiValueAttr>,
|
||||||
|
pub ssh_publickey: Vec<ScimSshPubKey>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Need to allow this because clippy is broken and doesn't realise scimentry is out of crate
|
// Need to allow this because clippy is broken and doesn't realise scimentry is out of crate
|
||||||
|
@ -124,6 +145,7 @@ impl Into<ScimEntry> for ScimSyncPerson {
|
||||||
totp_import,
|
totp_import,
|
||||||
login_shell,
|
login_shell,
|
||||||
mail,
|
mail,
|
||||||
|
ssh_publickey,
|
||||||
} = self;
|
} = self;
|
||||||
|
|
||||||
let schemas = if gidnumber.is_some() {
|
let schemas = if gidnumber.is_some() {
|
||||||
|
@ -148,6 +170,7 @@ impl Into<ScimEntry> for ScimSyncPerson {
|
||||||
set_multi_complex!(attrs, "totp_import", totp_import);
|
set_multi_complex!(attrs, "totp_import", totp_import);
|
||||||
set_option_string!(attrs, "loginshell", login_shell);
|
set_option_string!(attrs, "loginshell", login_shell);
|
||||||
set_multi_complex!(attrs, "mail", mail);
|
set_multi_complex!(attrs, "mail", mail);
|
||||||
|
set_multi_complex!(attrs, "ssh_publickey", ssh_publickey);
|
||||||
|
|
||||||
ScimEntry {
|
ScimEntry {
|
||||||
schemas,
|
schemas,
|
||||||
|
|
|
@ -496,6 +496,37 @@ impl Credential {
|
||||||
.map(|pw| self.update_password(pw))
|
.map(|pw| self.update_password(pw))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn upgrade_password(
|
||||||
|
&self,
|
||||||
|
policy: &CryptoPolicy,
|
||||||
|
cleartext: &str,
|
||||||
|
) -> Result<Option<Self>, OperationError> {
|
||||||
|
let valid = self.password_ref().and_then(|pw| {
|
||||||
|
pw.verify(cleartext).map_err(|e| {
|
||||||
|
error!(crypto_err = ?e);
|
||||||
|
e.into()
|
||||||
|
})
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if valid {
|
||||||
|
let pw = Password::new(policy, cleartext).map_err(|e| {
|
||||||
|
error!(crypto_err = ?e);
|
||||||
|
e.into()
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Note, during update_password we normally rotate the uuid, here we
|
||||||
|
// set it back to our current value. This is because we are just
|
||||||
|
// updating the hash value, not actually changing the password itself.
|
||||||
|
let mut cred = self.update_password(pw);
|
||||||
|
cred.uuid = self.uuid;
|
||||||
|
|
||||||
|
Ok(Some(cred))
|
||||||
|
} else {
|
||||||
|
// No updates needed, password has changed.
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Extend this credential with another alternate webauthn credential. This is especially
|
/// Extend this credential with another alternate webauthn credential. This is especially
|
||||||
/// useful for `PasswordMfa` where you can have many webauthn credentials and a password
|
/// useful for `PasswordMfa` where you can have many webauthn credentials and a password
|
||||||
/// generally so that one is a backup.
|
/// generally so that one is a backup.
|
||||||
|
|
|
@ -380,6 +380,7 @@ impl Account {
|
||||||
self.uuid == UUID_ANONYMOUS
|
self.uuid == UUID_ANONYMOUS
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
pub(crate) fn gen_password_mod(
|
pub(crate) fn gen_password_mod(
|
||||||
&self,
|
&self,
|
||||||
cleartext: &str,
|
cleartext: &str,
|
||||||
|
@ -401,6 +402,30 @@ impl Account {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn gen_password_upgrade_mod(
|
||||||
|
&self,
|
||||||
|
cleartext: &str,
|
||||||
|
crypto_policy: &CryptoPolicy,
|
||||||
|
) -> Result<Option<ModifyList<ModifyInvalid>>, OperationError> {
|
||||||
|
match &self.primary {
|
||||||
|
// Change the cred
|
||||||
|
Some(primary) => {
|
||||||
|
if let Some(ncred) = primary.upgrade_password(crypto_policy, cleartext)? {
|
||||||
|
let vcred = Value::new_credential("primary", ncred);
|
||||||
|
Ok(Some(ModifyList::new_purge_and_set(
|
||||||
|
"primary_credential",
|
||||||
|
vcred,
|
||||||
|
)))
|
||||||
|
} else {
|
||||||
|
// No action, not the same pw
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Nothing to do.
|
||||||
|
None => Ok(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn gen_webauthn_counter_mod(
|
pub(crate) fn gen_webauthn_counter_mod(
|
||||||
&mut self,
|
&mut self,
|
||||||
auth_result: &AuthenticationResult,
|
auth_result: &AuthenticationResult,
|
||||||
|
@ -463,20 +488,6 @@ impl Account {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn check_credential_pw(&self, cleartext: &str) -> Result<bool, OperationError> {
|
|
||||||
self.primary
|
|
||||||
.as_ref()
|
|
||||||
.ok_or(OperationError::InvalidState)
|
|
||||||
.and_then(|cred| {
|
|
||||||
cred.password_ref().and_then(|pw| {
|
|
||||||
pw.verify(cleartext).map_err(|e| {
|
|
||||||
error!(crypto_err = ?e);
|
|
||||||
e.into()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn regenerate_radius_secret_mod(
|
pub(crate) fn regenerate_radius_secret_mod(
|
||||||
&self,
|
&self,
|
||||||
cleartext: &str,
|
cleartext: &str,
|
||||||
|
|
|
@ -1043,6 +1043,51 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
||||||
}
|
}
|
||||||
Ok(vs)
|
Ok(vs)
|
||||||
}
|
}
|
||||||
|
(SyntaxType::SshKey, true, ScimAttr::MultiComplex(values)) => {
|
||||||
|
let mut vs = Vec::with_capacity(values.len());
|
||||||
|
for complex in values.iter() {
|
||||||
|
let label = complex
|
||||||
|
.attrs
|
||||||
|
.get("label")
|
||||||
|
.ok_or_else(|| {
|
||||||
|
error!("Invalid scim complex attr - missing required key label");
|
||||||
|
OperationError::InvalidAttribute(format!(
|
||||||
|
"missing required key label - {scim_attr_name}"
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.and_then(|external_id| match external_id {
|
||||||
|
ScimSimpleAttr::String(value) => Ok(value.clone()),
|
||||||
|
_ => {
|
||||||
|
error!("Invalid value attribute - must be scim simple string");
|
||||||
|
Err(OperationError::InvalidAttribute(format!(
|
||||||
|
"value must be scim simple string - {scim_attr_name}"
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let value = complex
|
||||||
|
.attrs
|
||||||
|
.get("value")
|
||||||
|
.ok_or_else(|| {
|
||||||
|
error!("Invalid scim complex attr - missing required key value");
|
||||||
|
OperationError::InvalidAttribute(format!(
|
||||||
|
"missing required key value - {scim_attr_name}"
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.and_then(|external_id| match external_id {
|
||||||
|
ScimSimpleAttr::String(value) => Ok(value.clone()),
|
||||||
|
_ => {
|
||||||
|
error!("Invalid value attribute - must be scim simple string");
|
||||||
|
Err(OperationError::InvalidAttribute(format!(
|
||||||
|
"value must be scim simple string - {scim_attr_name}"
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
vs.push(Value::SshKey(label, value))
|
||||||
|
}
|
||||||
|
Ok(vs)
|
||||||
|
}
|
||||||
(syn, mv, sa) => {
|
(syn, mv, sa) => {
|
||||||
error!(?syn, ?mv, ?sa, "Unsupported scim attribute conversion. This may be a syntax error in your import, or a missing feature in Kanidm.");
|
error!(?syn, ?mv, ?sa, "Unsupported scim attribute conversion. This may be a syntax error in your import, or a missing feature in Kanidm.");
|
||||||
Err(OperationError::InvalidAttribute(format!(
|
Err(OperationError::InvalidAttribute(format!(
|
||||||
|
@ -2539,6 +2584,12 @@ mod tests {
|
||||||
assert!(testuser.get_ava_single_utf8("displayname") == Some("Test User"));
|
assert!(testuser.get_ava_single_utf8("displayname") == Some("Test User"));
|
||||||
assert!(testuser.get_ava_single_iutf8("loginshell") == Some("/bin/sh"));
|
assert!(testuser.get_ava_single_iutf8("loginshell") == Some("/bin/sh"));
|
||||||
|
|
||||||
|
let mut ssh_keyiter = testuser
|
||||||
|
.get_ava_iter_sshpubkeys("ssh_publickey")
|
||||||
|
.expect("Failed to access ssh pubkeys");
|
||||||
|
assert_eq!(ssh_keyiter.next(), Some("sk-ecdsa-sha2-nistp256@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBENubZikrb8hu+HeVRdZ0pp/VAk2qv4JDbuJhvD0yNdWDL2e3cBbERiDeNPkWx58Q4rVnxkbV1fa8E2waRtT91wAAAAEc3NoOg== testuser@fidokey"));
|
||||||
|
assert_eq!(ssh_keyiter.next(), None);
|
||||||
|
|
||||||
// Check memberof works.
|
// Check memberof works.
|
||||||
let testgroup_mb = testgroup.get_ava_refer("member").expect("No members!");
|
let testgroup_mb = testgroup.get_ava_refer("member").expect("No members!");
|
||||||
assert!(testgroup_mb.contains(&testuser.get_uuid()));
|
assert!(testgroup_mb.contains(&testuser.get_uuid()));
|
||||||
|
@ -2948,6 +2999,12 @@ mod tests {
|
||||||
"value": "testuser@dev.blackhats.net.au"
|
"value": "testuser@dev.blackhats.net.au"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"ssh_publickey": [
|
||||||
|
{
|
||||||
|
"label": "ssh-key",
|
||||||
|
"value": "sk-ecdsa-sha2-nistp256@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBENubZikrb8hu+HeVRdZ0pp/VAk2qv4JDbuJhvD0yNdWDL2e3cBbERiDeNPkWx58Q4rVnxkbV1fa8E2waRtT91wAAAAEc3NoOg== testuser@fidokey"
|
||||||
|
}
|
||||||
|
],
|
||||||
"password_import": "ipaNTHash: iEb36u6PsRetBr3YMLdYbA"
|
"password_import": "ipaNTHash: iEb36u6PsRetBr3YMLdYbA"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -1824,18 +1824,14 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
||||||
|
|
||||||
info!(session_id = %pwu.target_uuid, "Processing password hash upgrade");
|
info!(session_id = %pwu.target_uuid, "Processing password hash upgrade");
|
||||||
|
|
||||||
// check, does the pw still match?
|
let maybe_modlist = account
|
||||||
let same = account.check_credential_pw(pwu.existing_password.as_str())?;
|
.gen_password_upgrade_mod(pwu.existing_password.as_str(), self.crypto_policy)
|
||||||
|
.map_err(|e| {
|
||||||
// if yes, gen the pw mod and apply.
|
admin_error!("Unable to generate password mod {:?}", e);
|
||||||
if same {
|
e
|
||||||
let modlist = account
|
})?;
|
||||||
.gen_password_mod(pwu.existing_password.as_str(), self.crypto_policy)
|
|
||||||
.map_err(|e| {
|
|
||||||
admin_error!("Unable to generate password mod {:?}", e);
|
|
||||||
e
|
|
||||||
})?;
|
|
||||||
|
|
||||||
|
if let Some(modlist) = maybe_modlist {
|
||||||
self.qs_write.internal_modify(
|
self.qs_write.internal_modify(
|
||||||
&filter_all!(f_eq("uuid", PartialValue::Uuid(pwu.target_uuid))),
|
&filter_all!(f_eq("uuid", PartialValue::Uuid(pwu.target_uuid))),
|
||||||
&modlist,
|
&modlist,
|
||||||
|
@ -1860,21 +1856,16 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
||||||
e
|
e
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let same = account.check_existing_pw(pwu.existing_password.as_str())?;
|
let maybe_modlist =
|
||||||
|
account.gen_password_upgrade_mod(pwu.existing_password.as_str(), self.crypto_policy)?;
|
||||||
if same {
|
|
||||||
let modlist = account
|
|
||||||
.gen_password_mod(pwu.existing_password.as_str(), self.crypto_policy)
|
|
||||||
.map_err(|e| {
|
|
||||||
admin_error!("Unable to generate password mod {:?}", e);
|
|
||||||
e
|
|
||||||
})?;
|
|
||||||
|
|
||||||
|
if let Some(modlist) = maybe_modlist {
|
||||||
self.qs_write.internal_modify(
|
self.qs_write.internal_modify(
|
||||||
&filter_all!(f_eq("uuid", PartialValue::Uuid(pwu.target_uuid))),
|
&filter_all!(f_eq("uuid", PartialValue::Uuid(pwu.target_uuid))),
|
||||||
&modlist,
|
&modlist,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
// No action needed, it's probably been changed/updated already.
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2772,9 +2763,28 @@ mod tests {
|
||||||
}
|
}
|
||||||
// Still empty
|
// Still empty
|
||||||
idms_delayed.check_is_empty_or_panic();
|
idms_delayed.check_is_empty_or_panic();
|
||||||
|
|
||||||
|
let mut idms_prox_read = idms.proxy_read().await;
|
||||||
|
let admin_entry = idms_prox_read
|
||||||
|
.qs_read
|
||||||
|
.internal_search_uuid(UUID_ADMIN)
|
||||||
|
.expect("Can't access admin entry.");
|
||||||
|
let cred_before = admin_entry
|
||||||
|
.get_ava_single_credential("primary_credential")
|
||||||
|
.expect("No credential present")
|
||||||
|
.clone();
|
||||||
|
drop(idms_prox_read);
|
||||||
|
|
||||||
// Do an auth, this will trigger the action to send.
|
// Do an auth, this will trigger the action to send.
|
||||||
check_admin_password(idms, "password").await;
|
check_admin_password(idms, "password").await;
|
||||||
|
|
||||||
|
// ⚠️ We have to be careful here. Between these two actions, it's possible
|
||||||
|
// that on the pw upgrade that the credential uuid changes. This immediately
|
||||||
|
// causes the session to be invalidated.
|
||||||
|
|
||||||
|
// We need to check the credential id does not change between these steps to
|
||||||
|
// prevent this!
|
||||||
|
|
||||||
// process it.
|
// process it.
|
||||||
let da = idms_delayed.try_recv().expect("invalid");
|
let da = idms_delayed.try_recv().expect("invalid");
|
||||||
// The first task is the pw upgrade
|
// The first task is the pw upgrade
|
||||||
|
@ -2785,6 +2795,19 @@ mod tests {
|
||||||
assert!(matches!(da, DelayedAction::AuthSessionRecord(_)));
|
assert!(matches!(da, DelayedAction::AuthSessionRecord(_)));
|
||||||
assert!(Ok(true) == r);
|
assert!(Ok(true) == r);
|
||||||
|
|
||||||
|
let mut idms_prox_read = idms.proxy_read().await;
|
||||||
|
let admin_entry = idms_prox_read
|
||||||
|
.qs_read
|
||||||
|
.internal_search_uuid(UUID_ADMIN)
|
||||||
|
.expect("Can't access admin entry.");
|
||||||
|
let cred_after = admin_entry
|
||||||
|
.get_ava_single_credential("primary_credential")
|
||||||
|
.expect("No credential present")
|
||||||
|
.clone();
|
||||||
|
drop(idms_prox_read);
|
||||||
|
|
||||||
|
assert_eq!(cred_before.uuid, cred_after.uuid);
|
||||||
|
|
||||||
// Check the admin pw still matches
|
// Check the admin pw still matches
|
||||||
check_admin_password(idms, "password").await;
|
check_admin_password(idms, "password").await;
|
||||||
// Clear the next auth session record
|
// Clear the next auth session record
|
||||||
|
|
|
@ -178,6 +178,27 @@ impl UnixUserAccount {
|
||||||
Ok(ModifyList::new_purge_and_set("unix_password", vcred))
|
Ok(ModifyList::new_purge_and_set("unix_password", vcred))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn gen_password_upgrade_mod(
|
||||||
|
&self,
|
||||||
|
cleartext: &str,
|
||||||
|
crypto_policy: &CryptoPolicy,
|
||||||
|
) -> Result<Option<ModifyList<ModifyInvalid>>, OperationError> {
|
||||||
|
match &self.cred {
|
||||||
|
// Change the cred
|
||||||
|
Some(ucred) => {
|
||||||
|
if let Some(ncred) = ucred.upgrade_password(crypto_policy, cleartext)? {
|
||||||
|
let vcred = Value::new_credential("primary", ncred);
|
||||||
|
Ok(Some(ModifyList::new_purge_and_set("unix_password", vcred)))
|
||||||
|
} else {
|
||||||
|
// No action, not the same pw
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Nothing to do.
|
||||||
|
None => Ok(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn is_within_valid_time(&self, ct: Duration) -> bool {
|
pub fn is_within_valid_time(&self, ct: Duration) -> bool {
|
||||||
let cot = OffsetDateTime::UNIX_EPOCH + ct;
|
let cot = OffsetDateTime::UNIX_EPOCH + ct;
|
||||||
|
|
||||||
|
@ -271,18 +292,6 @@ impl UnixUserAccount {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn check_existing_pw(&self, cleartext: &str) -> Result<bool, OperationError> {
|
|
||||||
match &self.cred {
|
|
||||||
Some(cred) => cred.password_ref().and_then(|pw| {
|
|
||||||
pw.verify(cleartext).map_err(|e| {
|
|
||||||
error!(crypto_err = ?e);
|
|
||||||
e.into()
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
None => Err(OperationError::InvalidState),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|
|
@ -234,7 +234,7 @@ function addBorrowedObject(obj) {
|
||||||
}
|
}
|
||||||
function __wbg_adapter_48(arg0, arg1, arg2) {
|
function __wbg_adapter_48(arg0, arg1, arg2) {
|
||||||
try {
|
try {
|
||||||
wasm._dyn_core__ops__function__FnMut___A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h91fc06edd1d61942(arg0, arg1, addBorrowedObject(arg2));
|
wasm._dyn_core__ops__function__FnMut___A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h779328ae10e6b850(arg0, arg1, addBorrowedObject(arg2));
|
||||||
} finally {
|
} finally {
|
||||||
heap[stack_pointer++] = undefined;
|
heap[stack_pointer++] = undefined;
|
||||||
}
|
}
|
||||||
|
@ -242,14 +242,14 @@ function __wbg_adapter_48(arg0, arg1, arg2) {
|
||||||
|
|
||||||
function __wbg_adapter_51(arg0, arg1, arg2) {
|
function __wbg_adapter_51(arg0, arg1, arg2) {
|
||||||
try {
|
try {
|
||||||
wasm._dyn_core__ops__function__FnMut___A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h69b5a7e71157cf78(arg0, arg1, addBorrowedObject(arg2));
|
wasm._dyn_core__ops__function__FnMut___A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h0b311056ce39bf03(arg0, arg1, addBorrowedObject(arg2));
|
||||||
} finally {
|
} finally {
|
||||||
heap[stack_pointer++] = undefined;
|
heap[stack_pointer++] = undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function __wbg_adapter_54(arg0, arg1, arg2) {
|
function __wbg_adapter_54(arg0, arg1, arg2) {
|
||||||
wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h2d5c8ecfb4968ae0(arg0, arg1, addHeapObject(arg2));
|
wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__he3ad0511b1590f2c(arg0, arg1, addHeapObject(arg2));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1121,16 +1121,16 @@ function __wbg_get_imports() {
|
||||||
const ret = wasm.memory;
|
const ret = wasm.memory;
|
||||||
return addHeapObject(ret);
|
return addHeapObject(ret);
|
||||||
};
|
};
|
||||||
imports.wbg.__wbindgen_closure_wrapper2571 = function(arg0, arg1, arg2) {
|
imports.wbg.__wbindgen_closure_wrapper2556 = function(arg0, arg1, arg2) {
|
||||||
const ret = makeMutClosure(arg0, arg1, 1196, __wbg_adapter_48);
|
const ret = makeMutClosure(arg0, arg1, 1194, __wbg_adapter_48);
|
||||||
return addHeapObject(ret);
|
return addHeapObject(ret);
|
||||||
};
|
};
|
||||||
imports.wbg.__wbindgen_closure_wrapper3389 = function(arg0, arg1, arg2) {
|
imports.wbg.__wbindgen_closure_wrapper3373 = function(arg0, arg1, arg2) {
|
||||||
const ret = makeMutClosure(arg0, arg1, 1499, __wbg_adapter_51);
|
const ret = makeMutClosure(arg0, arg1, 1496, __wbg_adapter_51);
|
||||||
return addHeapObject(ret);
|
return addHeapObject(ret);
|
||||||
};
|
};
|
||||||
imports.wbg.__wbindgen_closure_wrapper4500 = function(arg0, arg1, arg2) {
|
imports.wbg.__wbindgen_closure_wrapper4484 = function(arg0, arg1, arg2) {
|
||||||
const ret = makeMutClosure(arg0, arg1, 1574, __wbg_adapter_54);
|
const ret = makeMutClosure(arg0, arg1, 1571, __wbg_adapter_54);
|
||||||
return addHeapObject(ret);
|
return addHeapObject(ret);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Binary file not shown.
Binary file not shown.
|
@ -358,6 +358,11 @@ impl ViewsApp {
|
||||||
async fn fetch_logout() -> Result<ViewsMsg, FetchError> {
|
async fn fetch_logout() -> Result<ViewsMsg, FetchError> {
|
||||||
let (kopid, status, value, _) = do_request("/v1/logout", RequestMethod::GET, None).await?;
|
let (kopid, status, value, _) = do_request("/v1/logout", RequestMethod::GET, None).await?;
|
||||||
|
|
||||||
|
// In both cases - clear the local token to prevent our client
|
||||||
|
// thinking we have auth.
|
||||||
|
|
||||||
|
models::clear_bearer_token();
|
||||||
|
|
||||||
if status == 200 {
|
if status == 200 {
|
||||||
Ok(ViewsMsg::LogoutComplete)
|
Ok(ViewsMsg::LogoutComplete)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -49,8 +49,8 @@ use uuid::Uuid;
|
||||||
|
|
||||||
use kanidm_client::KanidmClientBuilder;
|
use kanidm_client::KanidmClientBuilder;
|
||||||
use kanidm_proto::scim_v1::{
|
use kanidm_proto::scim_v1::{
|
||||||
MultiValueAttr, ScimEntry, ScimExternalMember, ScimSyncGroup, ScimSyncPerson, ScimSyncRequest,
|
MultiValueAttr, ScimEntry, ScimExternalMember, ScimSshPubKey, ScimSyncGroup, ScimSyncPerson,
|
||||||
ScimSyncRetentionMode, ScimSyncState, ScimTotp,
|
ScimSyncRequest, ScimSyncRetentionMode, ScimSyncState, ScimTotp,
|
||||||
};
|
};
|
||||||
use kanidmd_lib::utils::file_permissions_readonly;
|
use kanidmd_lib::utils::file_permissions_readonly;
|
||||||
|
|
||||||
|
@ -436,7 +436,8 @@ async fn run_sync(
|
||||||
|
|
||||||
// process the entries to scim.
|
// process the entries to scim.
|
||||||
let entries = match process_ipa_sync_result(
|
let entries = match process_ipa_sync_result(
|
||||||
ipa_client,
|
&mut ipa_client,
|
||||||
|
sync_config.ipa_sync_base_dn.clone(),
|
||||||
entries,
|
entries,
|
||||||
&sync_config.entry_map,
|
&sync_config.entry_map,
|
||||||
is_initialise,
|
is_initialise,
|
||||||
|
@ -500,7 +501,8 @@ async fn run_sync(
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn process_ipa_sync_result(
|
async fn process_ipa_sync_result(
|
||||||
_ipa_client: LdapClient,
|
ipa_client: &mut LdapClient,
|
||||||
|
basedn: String,
|
||||||
ldap_entries: Vec<LdapSyncReplEntry>,
|
ldap_entries: Vec<LdapSyncReplEntry>,
|
||||||
entry_config_map: &HashMap<Uuid, EntryConfig>,
|
entry_config_map: &HashMap<Uuid, EntryConfig>,
|
||||||
is_initialise: bool,
|
is_initialise: bool,
|
||||||
|
@ -644,9 +646,65 @@ async fn process_ipa_sync_result(
|
||||||
|
|
||||||
debug!(?filter);
|
debug!(?filter);
|
||||||
|
|
||||||
// Search
|
// Search - we use syncrepl here and discard the cookie because we need the
|
||||||
// Inject all new entries to our maps. At this point we discard the original content
|
// entry uuid to be given from the nsuniqueid else we have issues.
|
||||||
// of the totp entries since we just fetched them all again anyway.
|
let mode = proto::SyncRequestMode::RefreshOnly;
|
||||||
|
match ipa_client.syncrepl(basedn, filter, None, mode).await {
|
||||||
|
Ok(LdapSyncRepl::Success {
|
||||||
|
cookie: _,
|
||||||
|
refresh_deletes: _,
|
||||||
|
entries: sync_entries,
|
||||||
|
delete_uuids: _,
|
||||||
|
present_uuids: _,
|
||||||
|
}) => {
|
||||||
|
// Inject all new entries to our maps. At this point we discard the original content
|
||||||
|
// of the totp entries since we just fetched them all again anyway.
|
||||||
|
|
||||||
|
totp_entries.clear();
|
||||||
|
|
||||||
|
for lentry in sync_entries.into_iter() {
|
||||||
|
if lentry
|
||||||
|
.entry
|
||||||
|
.attrs
|
||||||
|
.get("objectclass")
|
||||||
|
.map(|oc| oc.contains("ipatokentotp"))
|
||||||
|
.unwrap_or_default()
|
||||||
|
{
|
||||||
|
let token_owner_dn = if let Some(todn) = lentry
|
||||||
|
.entry
|
||||||
|
.attrs
|
||||||
|
.get("ipatokenowner")
|
||||||
|
.and_then(|attr| if attr.len() != 1 { None } else { attr.first() })
|
||||||
|
{
|
||||||
|
debug!("totp with owner {}", todn);
|
||||||
|
todn.clone()
|
||||||
|
} else {
|
||||||
|
warn!("totp with invalid ownership will be ignored");
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
if !totp_entries.contains_key(&token_owner_dn) {
|
||||||
|
totp_entries.insert(token_owner_dn.clone(), Vec::default());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(v) = totp_entries.get_mut(&token_owner_dn) {
|
||||||
|
v.push(lentry)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let dn = lentry.entry.dn.clone();
|
||||||
|
entries.insert(dn, lentry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(LdapSyncRepl::RefreshRequired) => {
|
||||||
|
error!("Failed due to invalid search state from ipa");
|
||||||
|
return Err(());
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!(?e, "Failed to perform search from ipa");
|
||||||
|
return Err(());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// For each updated TOTP -> If it's related DN is not in Hash -> remove from map
|
// For each updated TOTP -> If it's related DN is not in Hash -> remove from map
|
||||||
|
@ -789,6 +847,19 @@ fn ipa_to_scim_entry(
|
||||||
Vec::default()
|
Vec::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let ssh_publickey = entry
|
||||||
|
.remove_ava("ipasshpubkey")
|
||||||
|
.map(|set| {
|
||||||
|
set.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, value)| ScimSshPubKey {
|
||||||
|
label: format!("ipasshpubkey-{}", i),
|
||||||
|
value,
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
let login_shell = entry.remove_ava_single("loginshell");
|
let login_shell = entry.remove_ava_single("loginshell");
|
||||||
let external_id = Some(entry.dn);
|
let external_id = Some(entry.dn);
|
||||||
|
|
||||||
|
@ -803,6 +874,7 @@ fn ipa_to_scim_entry(
|
||||||
totp_import,
|
totp_import,
|
||||||
login_shell,
|
login_shell,
|
||||||
mail,
|
mail,
|
||||||
|
ssh_publickey,
|
||||||
}
|
}
|
||||||
.into(),
|
.into(),
|
||||||
))
|
))
|
||||||
|
|
|
@ -33,6 +33,10 @@ fn person_attr_mail() -> String {
|
||||||
"mail".to_string()
|
"mail".to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn person_attr_ssh_public_key() -> String {
|
||||||
|
"sshpublickey".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
fn group_objectclass() -> String {
|
fn group_objectclass() -> String {
|
||||||
"groupofnames".to_string()
|
"groupofnames".to_string()
|
||||||
}
|
}
|
||||||
|
@ -81,6 +85,8 @@ pub struct Config {
|
||||||
pub person_attr_login_shell: String,
|
pub person_attr_login_shell: String,
|
||||||
#[serde(default = "person_attr_mail")]
|
#[serde(default = "person_attr_mail")]
|
||||||
pub person_attr_mail: String,
|
pub person_attr_mail: String,
|
||||||
|
#[serde(default = "person_attr_ssh_public_key")]
|
||||||
|
pub person_attr_ssh_public_key: String,
|
||||||
|
|
||||||
#[serde(default = "group_objectclass")]
|
#[serde(default = "group_objectclass")]
|
||||||
pub group_objectclass: String,
|
pub group_objectclass: String,
|
||||||
|
|
|
@ -43,8 +43,8 @@ use tracing_subscriber::{fmt, EnvFilter};
|
||||||
|
|
||||||
use kanidm_client::KanidmClientBuilder;
|
use kanidm_client::KanidmClientBuilder;
|
||||||
use kanidm_proto::scim_v1::{
|
use kanidm_proto::scim_v1::{
|
||||||
MultiValueAttr, ScimEntry, ScimExternalMember, ScimSyncGroup, ScimSyncPerson, ScimSyncRequest,
|
MultiValueAttr, ScimEntry, ScimExternalMember, ScimSshPubKey, ScimSyncGroup, ScimSyncPerson,
|
||||||
ScimSyncRetentionMode, ScimSyncState,
|
ScimSyncRequest, ScimSyncRetentionMode, ScimSyncState,
|
||||||
};
|
};
|
||||||
use kanidmd_lib::utils::file_permissions_readonly;
|
use kanidmd_lib::utils::file_permissions_readonly;
|
||||||
|
|
||||||
|
@ -552,6 +552,19 @@ fn ldap_to_scim_entry(
|
||||||
|
|
||||||
let totp_import = Vec::default();
|
let totp_import = Vec::default();
|
||||||
|
|
||||||
|
let ssh_publickey = entry
|
||||||
|
.remove_ava(&sync_config.person_attr_ssh_public_key)
|
||||||
|
.map(|set| {
|
||||||
|
set.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, value)| ScimSshPubKey {
|
||||||
|
label: format!("sshpublickey-{}", i),
|
||||||
|
value,
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
let login_shell = entry.remove_ava_single(&sync_config.person_attr_login_shell);
|
let login_shell = entry.remove_ava_single(&sync_config.person_attr_login_shell);
|
||||||
let external_id = Some(entry.dn);
|
let external_id = Some(entry.dn);
|
||||||
|
|
||||||
|
@ -566,6 +579,7 @@ fn ldap_to_scim_entry(
|
||||||
totp_import,
|
totp_import,
|
||||||
login_shell,
|
login_shell,
|
||||||
mail,
|
mail,
|
||||||
|
ssh_publickey,
|
||||||
}
|
}
|
||||||
.into(),
|
.into(),
|
||||||
))
|
))
|
||||||
|
|
Loading…
Reference in a new issue