20231218 ipa sync unix password (#2374)

* Add support for importing the users password as unix password
This commit is contained in:
Firstyear 2023-12-18 11:20:37 +10:00 committed by GitHub
parent 608e4b579d
commit 5c445a4704
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 230 additions and 54 deletions

View file

@ -26,6 +26,15 @@ ipa_sync_pw = "directory manager password"
# The basedn to examine.
ipa_sync_base_dn = "dc=ipa,dc=dev,dc=kanidm,dc=com"
# By default Kanidm seperates the primary account password and credentials from
# the unix credential. This allows the unix password to be isolated from the
# account password so that compromise of one doesn't compromise the other. However
# this can be surprising for new users during a migration. This boolean allows the
# user password to be set as the unix password during the migration for consistency
# and then after the migration they are "unlinked".
#
# sync_password_as_unix_password = false
# The sync tool can alter or exclude entries. These are mapped by their syncuuid
# (not their ipa-object-uuid). The syncuuid is derived from nsUniqueId in 389-ds.
# This is chosen oven DN because DN's can change with modrdn where nsUniqueId is

View file

@ -32,6 +32,15 @@ ldap_sync_base_dn = "dc=ldap,dc=dev,dc=kanidm,dc=com"
ldap_filter = "(|(objectclass=person)(objectclass=posixgroup))"
# ldap_filter = "(cn=\"my value\")"
# By default Kanidm seperates the primary account password and credentials from
# the unix credential. This allows the unix password to be isolated from the
# account password so that compromise of one doesn't compromise the other. However
# this can be surprising for new users during a migration. This boolean allows the
# user password to be set as the unix password during the migration for consistency
# and then after the migration they are "unlinked".
#
# sync_password_as_unix_password = false
# The objectclass used to identify persons to import to Kanidm.
#
# If not set, defaults to "person"

View file

@ -172,6 +172,7 @@ pub const ATTR_UID: &str = "uid";
pub const ATTR_UIDNUMBER: &str = "uidnumber";
pub const ATTR_UNIQUE: &str = "unique";
pub const ATTR_UNIX_PASSWORD: &str = "unix_password";
pub const ATTR_UNIX_PASSWORD_IMPORT: &str = "unix_password_import";
pub const ATTR_USER_AUTH_TOKEN_SESSION: &str = "user_auth_token_session";
pub const ATTR_USERID: &str = "userid";
pub const ATTR_USERPASSWORD: &str = "userpassword";

View file

@ -10,7 +10,8 @@ use scim_proto::*;
use crate::constants::{
ATTR_ACCOUNT_EXPIRE, ATTR_ACCOUNT_VALID_FROM, ATTR_DESCRIPTION, ATTR_DISPLAYNAME,
ATTR_GIDNUMBER, ATTR_LOGINSHELL, ATTR_MAIL, ATTR_MEMBER, ATTR_NAME, ATTR_SSH_PUBLICKEY,
ATTR_GIDNUMBER, ATTR_LOGINSHELL, ATTR_MAIL, ATTR_MEMBER, ATTR_NAME, ATTR_PASSWORD_IMPORT,
ATTR_SSH_PUBLICKEY, ATTR_UNIX_PASSWORD_IMPORT, ATTR_TOTP_IMPORT
};
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, ToSchema)]
@ -138,6 +139,7 @@ pub struct ScimSyncPerson {
pub display_name: String,
pub gidnumber: Option<u32>,
pub password_import: Option<String>,
pub unix_password_import: Option<String>,
pub totp_import: Vec<ScimTotp>,
pub login_shell: Option<String>,
pub mail: Vec<MultiValueAttr>,
@ -158,6 +160,7 @@ impl Into<ScimEntry> for ScimSyncPerson {
display_name,
gidnumber,
password_import,
unix_password_import,
totp_import,
login_shell,
mail,
@ -184,8 +187,9 @@ impl Into<ScimEntry> for ScimSyncPerson {
set_string!(attrs, ATTR_NAME, user_name);
set_string!(attrs, ATTR_DISPLAYNAME, display_name);
set_option_u32!(attrs, ATTR_GIDNUMBER, gidnumber);
set_option_string!(attrs, "password_import", password_import);
set_multi_complex!(attrs, "totp_import", totp_import);
set_option_string!(attrs, ATTR_PASSWORD_IMPORT, password_import);
set_option_string!(attrs, ATTR_UNIX_PASSWORD_IMPORT, unix_password_import);
set_multi_complex!(attrs, ATTR_TOTP_IMPORT, totp_import);
set_option_string!(attrs, ATTR_LOGINSHELL, login_shell);
set_multi_complex!(attrs, ATTR_MAIL, mail);
set_multi_complex!(attrs, ATTR_SSH_PUBLICKEY, ssh_publickey); // with the underscore

View file

@ -169,6 +169,7 @@ pub enum Attribute {
UidNumber,
Unique,
UnixPassword,
UnixPasswordImport,
UserAuthTokenSession,
UserId,
UserPassword,
@ -351,6 +352,7 @@ impl TryFrom<String> for Attribute {
ATTR_UIDNUMBER => Attribute::UidNumber,
ATTR_UNIQUE => Attribute::Unique,
ATTR_UNIX_PASSWORD => Attribute::UnixPassword,
ATTR_UNIX_PASSWORD_IMPORT => Attribute::UnixPasswordImport,
ATTR_USER_AUTH_TOKEN_SESSION => Attribute::UserAuthTokenSession,
ATTR_USERID => Attribute::UserId,
ATTR_USERPASSWORD => Attribute::UserPassword,
@ -511,6 +513,7 @@ impl From<Attribute> for &'static str {
Attribute::UidNumber => ATTR_UIDNUMBER,
Attribute::Unique => ATTR_UNIQUE,
Attribute::UnixPassword => ATTR_UNIX_PASSWORD,
Attribute::UnixPasswordImport => ATTR_UNIX_PASSWORD_IMPORT,
Attribute::UserAuthTokenSession => ATTR_USER_AUTH_TOKEN_SESSION,
Attribute::UserId => ATTR_USERID,
Attribute::UserPassword => ATTR_USERPASSWORD,

View file

@ -268,6 +268,8 @@ pub const UUID_SCHEMA_CLASS_ACCESS_CONTROL_RECEIVER_ENTRY_MANAGER: Uuid =
pub const UUID_SCHEMA_CLASS_ACCESS_CONTROL_TARGET_SCOPE: Uuid =
uuid!("00000000-0000-0000-0000-ffff00000155");
pub const UUID_SCHEMA_ATTR_ENTRY_MANAGED_BY: Uuid = uuid!("00000000-0000-0000-0000-ffff00000156");
pub const UUID_SCHEMA_ATTR_UNIX_PASSWORD_IMPORT: Uuid =
uuid!("00000000-0000-0000-0000-ffff00000157");
// System and domain infos
// I'd like to strongly criticise william of the past for making poor choices about these allocations.

View file

@ -820,13 +820,11 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
false,
ScimAttr::SingleSimple(ScimSimpleAttr::String(value)),
) => Ok(vec![Value::new_utf8(value.clone())]),
(
SyntaxType::Utf8StringInsensitive,
false,
ScimAttr::SingleSimple(ScimSimpleAttr::String(value)),
) => Ok(vec![Value::new_iutf8(value)]),
(
SyntaxType::Uint32,
false,
@ -848,7 +846,6 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
})
})
.map(|value| vec![Value::Uint32(value)]),
(SyntaxType::ReferenceUuid, true, ScimAttr::MultiComplex(values)) => {
// In this case, because it's a reference uuid only, despite the multicomplex structure, it's a list of
// "external_id" to external_ids. These *might* also be uuids. So we need to use sync_external_id_to_uuid
@ -3191,6 +3188,7 @@ mod tests {
"value": "sk-ecdsa-sha2-nistp256@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBENubZikrb8hu+HeVRdZ0pp/VAk2qv4JDbuJhvD0yNdWDL2e3cBbERiDeNPkWx58Q4rVnxkbV1fa8E2waRtT91wAAAAEc3NoOg== testuser@fidokey"
}
],
"unix_password_import": "ipaNTHash: iEb36u6PsRetBr3YMLdYbA",
"password_import": "ipaNTHash: iEb36u6PsRetBr3YMLdYbA"
},
{
@ -3302,6 +3300,7 @@ mod tests {
"loginshell": "/bin/sh",
"name": "testuser",
"password_import": "ipaNTHash: iEb36u6PsRetBr3YMLdYbA",
"unix_password_import": "ipaNTHash: iEb36u6PsRetBr3YMLdYbA",
"account_valid_from": "2021-11-28T04:57:55Z",
"account_expire": "2023-11-28T04:57:55Z"
},

View file

@ -73,7 +73,7 @@ impl CredImport {
let hint = im_pw.split_at(len).0;
let id = e.get_display_id();
error!(%hint, entry_id = %id, "password_import was unable to convert hash format");
error!(%hint, entry_id = %id, "{} was unable to convert hash format", Attribute::PasswordImport);
OperationError::Plugin(PluginError::CredImport(
"password_import was unable to convert hash format".to_string(),
@ -126,6 +126,40 @@ impl CredImport {
}
}
// UNIX PASSWORD IMPORT
if let Some(vs) = e.pop_ava(Attribute::UnixPasswordImport) {
// if there are multiple, fail.
let im_pw = vs.to_utf8_single().ok_or_else(|| {
OperationError::Plugin(PluginError::CredImport(
format!("{} has incorrect value type - should be a single utf8 string", Attribute::UnixPasswordImport),
))
})?;
// convert the import_password_string to a password
let pw = Password::try_from(im_pw).map_err(|_| {
let len = if im_pw.len() > 5 {
4
} else {
im_pw.len() - 1
};
let hint = im_pw.split_at(len).0;
let id = e.get_display_id();
error!(%hint, entry_id = %id, "{} was unable to convert hash format", Attribute::UnixPasswordImport);
OperationError::Plugin(PluginError::CredImport(
"unix_password_import was unable to convert hash format".to_string(),
))
})?;
// Unix pw's aren't like primary, we can just splat them here.
let c = Credential::new_from_password(pw);
e.set_ava(
Attribute::UnixPassword,
once(Value::new_credential("primary", c)),
);
};
Ok(())
})
}
@ -147,17 +181,29 @@ mod tests {
fn test_pre_create_password_import_1() {
let preload: Vec<Entry<EntryInit, EntryNew>> = Vec::new();
let e: Entry<EntryInit, EntryNew> = Entry::unsafe_from_entry_str(
r#"{
"attrs": {
"class": ["person", "account"],
"name": ["testperson"],
"description": ["testperson"],
"displayname": ["testperson"],
"uuid": ["d2b496bd-8493-47b7-8142-f568b5cf47ee"],
"password_import": ["pbkdf2_sha256$36000$xIEozuZVAoYm$uW1b35DUKyhvQAf1mBqMvoBDcqSD06juzyO/nmyV0+w="]
}
}"#,
let e = entry_init!(
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Name, Value::new_iname("testperson")),
(
Attribute::Description,
Value::Utf8("testperson".to_string())
),
(
Attribute::DisplayName,
Value::Utf8("testperson".to_string())
),
(
Attribute::Uuid,
Value::Uuid(uuid!("d2b496bd-8493-47b7-8142-f568b5cf47ee"))
),
(
Attribute::PasswordImport,
Value::Utf8(
"pbkdf2_sha256$36000$xIEozuZVAoYm$uW1b35DUKyhvQAf1mBqMvoBDcqSD06juzyO/nmyV0+w="
.into()
)
)
);
let create = vec![e];
@ -167,17 +213,22 @@ mod tests {
#[test]
fn test_modify_password_import_1() {
// Add another uuid to a type
let ea: Entry<EntryInit, EntryNew> = Entry::unsafe_from_entry_str(
r#"{
"attrs": {
"class": ["account", "person"],
"name": ["testperson"],
"description": ["testperson"],
"displayname": ["testperson"],
"uuid": ["d2b496bd-8493-47b7-8142-f568b5cf47ee"]
}
}"#,
let ea = entry_init!(
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Name, Value::new_iname("testperson")),
(
Attribute::Description,
Value::Utf8("testperson".to_string())
),
(
Attribute::DisplayName,
Value::Utf8("testperson".to_string())
),
(
Attribute::Uuid,
Value::Uuid(uuid!("d2b496bd-8493-47b7-8142-f568b5cf47ee"))
)
);
let preload = vec![ea];
@ -198,17 +249,22 @@ mod tests {
#[test]
fn test_modify_password_import_2() {
// Add another uuid to a type
let mut ea: Entry<EntryInit, EntryNew> = Entry::unsafe_from_entry_str(
r#"{
"attrs": {
"class": ["account", "person"],
"name": ["testperson"],
"description": ["testperson"],
"displayname": ["testperson"],
"uuid": ["d2b496bd-8493-47b7-8142-f568b5cf47ee"]
}
}"#,
let mut ea = entry_init!(
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Name, Value::new_iname("testperson")),
(
Attribute::Description,
Value::Utf8("testperson".to_string())
),
(
Attribute::DisplayName,
Value::Utf8("testperson".to_string())
),
(
Attribute::Uuid,
Value::Uuid(uuid!("d2b496bd-8493-47b7-8142-f568b5cf47ee"))
)
);
let p = CryptoPolicy::minimum();
@ -236,17 +292,22 @@ mod tests {
#[test]
fn test_modify_password_import_3_totp() {
// Add another uuid to a type
let mut ea: Entry<EntryInit, EntryNew> = Entry::unsafe_from_entry_str(
r#"{
"attrs": {
"class": ["account", "person"],
"name": ["testperson"],
"description": ["testperson"],
"displayname": ["testperson"],
"uuid": ["d2b496bd-8493-47b7-8142-f568b5cf47ee"]
}
}"#,
let mut ea = entry_init!(
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Name, Value::new_iname("testperson")),
(
Attribute::Description,
Value::Utf8("testperson".to_string())
),
(
Attribute::DisplayName,
Value::Utf8("testperson".to_string())
),
(
Attribute::Uuid,
Value::Uuid(uuid!("d2b496bd-8493-47b7-8142-f568b5cf47ee"))
)
);
let totp = Totp::generate_secure(TOTP_DEFAULT_STEP);
@ -393,4 +454,50 @@ mod tests {
|_| {}
);
}
#[test]
fn test_modify_unix_password_import() {
let ea = entry_init!(
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::PosixAccount.to_value()),
(Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Name, Value::new_iname("testperson")),
(
Attribute::Description,
Value::Utf8("testperson".to_string())
),
(
Attribute::DisplayName,
Value::Utf8("testperson".to_string())
),
(
Attribute::Uuid,
Value::Uuid(uuid!("d2b496bd-8493-47b7-8142-f568b5cf47ee"))
)
);
let preload = vec![ea];
run_modify_test!(
Ok(()),
preload,
filter!(f_eq(Attribute::Name, PartialValue::new_iutf8("testperson"))),
ModifyList::new_list(vec![Modify::Present(
Attribute::UnixPasswordImport.into(),
Value::from(IMPORT_HASH)
)]),
None,
|_| {},
|qs: &mut QueryServerWriteTransaction| {
let e = qs
.internal_search_uuid(uuid!("d2b496bd-8493-47b7-8142-f568b5cf47ee"))
.expect("failed to get entry");
let c = e
.get_ava_single_credential(Attribute::UnixPassword)
.expect("failed to get unix cred.");
assert!(matches!(&c.type_, CredentialType::Password(_pw)));
}
);
}
}

View file

@ -1588,6 +1588,24 @@ impl<'a> SchemaWriteTransaction<'a> {
},
);
self.attributes.insert(
Attribute::UnixPasswordImport.into(),
SchemaAttribute {
name: Attribute::UnixPasswordImport.into(),
uuid: UUID_SCHEMA_ATTR_UNIX_PASSWORD_IMPORT,
description: String::from(
"An imported unix password hash from an external system.",
),
multivalue: false,
unique: false,
phantom: true,
sync_allowed: true,
replicated: false,
index: vec![],
syntax: SyntaxType::Utf8String,
},
);
self.attributes.insert(
Attribute::TotpImport.into(),
SchemaAttribute {

View file

@ -14,6 +14,8 @@ pub struct Config {
pub ipa_sync_pw: String,
pub ipa_sync_base_dn: String,
pub sync_password_as_unix_password: Option<bool>,
// pub entry: Option<Vec<EntryConfig>>,
#[serde(flatten)]
pub entry_map: BTreeMap<Uuid, EntryConfig>,

View file

@ -456,6 +456,9 @@ async fn run_sync(
entries,
&sync_config.entry_map,
is_initialise,
sync_config
.sync_password_as_unix_password
.unwrap_or_default(),
)
.await
{
@ -521,6 +524,7 @@ async fn process_ipa_sync_result(
ldap_entries: Vec<LdapSyncReplEntry>,
entry_config_map: &BTreeMap<Uuid, EntryConfig>,
is_initialise: bool,
sync_password_as_unix_password: bool,
) -> Result<Vec<ScimEntry>, ()> {
// Because of how TOTP works with freeipa it's a soft referral from
// the totp toward the user. This means if a TOTP is added or removed
@ -758,7 +762,7 @@ async fn process_ipa_sync_result(
let totp = totp_entries.get(&dn).unwrap_or(&empty_slice);
match ipa_to_scim_entry(e, &e_config, totp) {
match ipa_to_scim_entry(e, &e_config, totp, sync_password_as_unix_password) {
Ok(Some(e)) => Some(Ok(e)),
Ok(None) => None,
Err(()) => Some(Err(())),
@ -767,12 +771,11 @@ async fn process_ipa_sync_result(
.collect::<Result<Vec<_>, _>>()
}
// TODO: Allow re-map of uuid -> uuid
fn ipa_to_scim_entry(
sync_entry: LdapSyncReplEntry,
entry_config: &EntryConfig,
totp: &[LdapSyncReplEntry],
sync_password_as_unix_password: bool,
) -> Result<Option<ScimEntry>, ()> {
debug!("{:#?}", sync_entry);
@ -857,6 +860,12 @@ fn ipa_to_scim_entry(
// pw hash formats in 389-ds we don't support!
.or_else(|| entry.remove_ava_single(Attribute::UserPassword.as_ref()));
let unix_password_import = if sync_password_as_unix_password {
password_import.clone()
} else {
None
};
let mail: Vec<_> = entry
.remove_ava(Attribute::Mail.as_ref())
.map(|set| {
@ -928,6 +937,7 @@ fn ipa_to_scim_entry(
display_name,
gidnumber,
password_import,
unix_password_import,
totp_import,
login_shell,
mail,

View file

@ -72,6 +72,8 @@ pub struct Config {
pub ldap_filter: LdapFilter,
pub sync_password_as_unix_password: Option<bool>,
#[serde(default = "person_objectclass")]
pub person_objectclass: String,
#[serde(default = "person_attr_user_name")]

View file

@ -553,6 +553,15 @@ fn ldap_to_scim_entry(
password_import
};
let unix_password_import = if sync_config
.sync_password_as_unix_password
.unwrap_or_default()
{
password_import.clone()
} else {
None
};
let mail: Vec<_> = entry
.remove_ava(&sync_config.person_attr_mail)
.map(|set| {
@ -610,6 +619,7 @@ fn ldap_to_scim_entry(
display_name,
gidnumber,
password_import,
unix_password_import,
totp_import,
login_shell,
mail,