mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 20:47:01 +01:00
1121 SCIM import totp freeipa (#1328)
This commit is contained in:
parent
84fc7d0bac
commit
00cf5f4e15
|
@ -14,15 +14,16 @@
|
||||||
mod config;
|
mod config;
|
||||||
mod error;
|
mod error;
|
||||||
|
|
||||||
#[cfg(test)]
|
// #[cfg(test)]
|
||||||
mod tests;
|
// mod tests;
|
||||||
|
|
||||||
use crate::config::{Config, EntryConfig};
|
use crate::config::{Config, EntryConfig};
|
||||||
use crate::error::SyncError;
|
use crate::error::SyncError;
|
||||||
|
use base64urlsafedata::Base64UrlSafeData;
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use cron::Schedule;
|
use cron::Schedule;
|
||||||
use std::collections::HashMap;
|
use std::collections::{BTreeMap, HashMap};
|
||||||
use std::fs::metadata;
|
use std::fs::metadata;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
|
@ -48,13 +49,14 @@ use uuid::Uuid;
|
||||||
use kanidm_client::KanidmClientBuilder;
|
use kanidm_client::KanidmClientBuilder;
|
||||||
use kanidm_proto::scim_v1::{
|
use kanidm_proto::scim_v1::{
|
||||||
ScimEntry, ScimExternalMember, ScimSyncGroup, ScimSyncPerson, ScimSyncRequest, ScimSyncState,
|
ScimEntry, ScimExternalMember, ScimSyncGroup, ScimSyncPerson, ScimSyncRequest, ScimSyncState,
|
||||||
|
ScimTotp,
|
||||||
};
|
};
|
||||||
use kanidmd_lib::utils::file_permissions_readonly;
|
use kanidmd_lib::utils::file_permissions_readonly;
|
||||||
|
|
||||||
use users::{get_current_gid, get_current_uid, get_effective_gid, get_effective_uid};
|
use users::{get_current_gid, get_current_uid, get_effective_gid, get_effective_uid};
|
||||||
|
|
||||||
use ldap3_client::{
|
use ldap3_client::{
|
||||||
proto, proto::LdapFilter, LdapClientBuilder, LdapSyncRepl, LdapSyncReplEntry,
|
proto, proto::LdapFilter, LdapClient, LdapClientBuilder, LdapSyncRepl, LdapSyncReplEntry,
|
||||||
LdapSyncStateValue,
|
LdapSyncStateValue,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -331,6 +333,8 @@ async fn run_sync(
|
||||||
ScimSyncState::Active { cookie } => Some(cookie.0.clone()),
|
ScimSyncState::Active { cookie } => Some(cookie.0.clone()),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let is_initialise = cookie.is_none();
|
||||||
|
|
||||||
let filter = LdapFilter::Or(vec![
|
let filter = LdapFilter::Or(vec![
|
||||||
// LdapFilter::Equality("objectclass".to_string(), "domain".to_string()),
|
// LdapFilter::Equality("objectclass".to_string(), "domain".to_string()),
|
||||||
LdapFilter::And(vec![
|
LdapFilter::And(vec![
|
||||||
|
@ -341,6 +345,7 @@ async fn run_sync(
|
||||||
LdapFilter::And(vec![
|
LdapFilter::And(vec![
|
||||||
LdapFilter::Equality("objectclass".to_string(), "groupofnames".to_string()),
|
LdapFilter::Equality("objectclass".to_string(), "groupofnames".to_string()),
|
||||||
LdapFilter::Equality("objectclass".to_string(), "ipausergroup".to_string()),
|
LdapFilter::Equality("objectclass".to_string(), "ipausergroup".to_string()),
|
||||||
|
// Ignore user private groups, kani generates these internally.
|
||||||
LdapFilter::Not(Box::new(LdapFilter::Equality(
|
LdapFilter::Not(Box::new(LdapFilter::Equality(
|
||||||
"objectclass".to_string(),
|
"objectclass".to_string(),
|
||||||
"mepmanagedentry".to_string(),
|
"mepmanagedentry".to_string(),
|
||||||
|
@ -356,6 +361,7 @@ async fn run_sync(
|
||||||
"ipausers".to_string(),
|
"ipausers".to_string(),
|
||||||
))),
|
))),
|
||||||
]),
|
]),
|
||||||
|
// Fetch TOTP's so we know when/if they change.
|
||||||
LdapFilter::And(vec![
|
LdapFilter::And(vec![
|
||||||
LdapFilter::Equality("objectclass".to_string(), "ipatoken".to_string()),
|
LdapFilter::Equality("objectclass".to_string(), "ipatoken".to_string()),
|
||||||
LdapFilter::Equality("objectclass".to_string(), "ipatokentotp".to_string()),
|
LdapFilter::Equality("objectclass".to_string(), "ipatokentotp".to_string()),
|
||||||
|
@ -381,17 +387,69 @@ async fn run_sync(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// pre-process the entries.
|
// Convert the ldap sync repl result to a scim equivalent
|
||||||
// - > fn so we can test.
|
let scim_sync_request = match sync_result {
|
||||||
let scim_sync_request = match process_ipa_sync_result(
|
LdapSyncRepl::Success {
|
||||||
scim_sync_status,
|
cookie,
|
||||||
sync_result,
|
refresh_deletes,
|
||||||
&sync_config.entry_map,
|
entries,
|
||||||
)
|
delete_uuids,
|
||||||
.await
|
present_uuids,
|
||||||
{
|
} => {
|
||||||
Ok(ssr) => ssr,
|
if refresh_deletes {
|
||||||
Err(()) => return Err(SyncError::Preprocess),
|
error!("Unsure how to handle refreshDeletes=True");
|
||||||
|
return Err(SyncError::Preprocess);
|
||||||
|
}
|
||||||
|
|
||||||
|
if !present_uuids.is_empty() {
|
||||||
|
error!("Unsure how to handle presentUuids > 0");
|
||||||
|
return Err(SyncError::Preprocess);
|
||||||
|
}
|
||||||
|
|
||||||
|
let to_state = cookie
|
||||||
|
.map(|cookie| {
|
||||||
|
ScimSyncState::Active { cookie }
|
||||||
|
})
|
||||||
|
.ok_or_else(|| {
|
||||||
|
error!("Invalid state, ldap sync repl did not provide a valid state cookie in response.");
|
||||||
|
|
||||||
|
SyncError::Preprocess
|
||||||
|
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// process the entries to scim.
|
||||||
|
let entries = match process_ipa_sync_result(
|
||||||
|
ipa_client,
|
||||||
|
entries,
|
||||||
|
&sync_config.entry_map,
|
||||||
|
is_initialise,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(ssr) => ssr,
|
||||||
|
Err(()) => {
|
||||||
|
error!("Failed to process IPA entries to SCIM");
|
||||||
|
return Err(SyncError::Preprocess);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ScimSyncRequest {
|
||||||
|
from_state: scim_sync_status,
|
||||||
|
to_state,
|
||||||
|
entries,
|
||||||
|
delete_uuids,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LdapSyncRepl::RefreshRequired => {
|
||||||
|
let to_state = ScimSyncState::Refresh;
|
||||||
|
|
||||||
|
ScimSyncRequest {
|
||||||
|
from_state: scim_sync_status,
|
||||||
|
to_state,
|
||||||
|
entries: Vec::new(),
|
||||||
|
delete_uuids: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if opt.proto_dump {
|
if opt.proto_dump {
|
||||||
|
@ -421,78 +479,186 @@ async fn run_sync(
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn process_ipa_sync_result(
|
async fn process_ipa_sync_result(
|
||||||
from_state: ScimSyncState,
|
_ipa_client: LdapClient,
|
||||||
sync_result: LdapSyncRepl,
|
ldap_entries: Vec<LdapSyncReplEntry>,
|
||||||
entry_config_map: &HashMap<Uuid, EntryConfig>,
|
entry_config_map: &HashMap<Uuid, EntryConfig>,
|
||||||
) -> Result<ScimSyncRequest, ()> {
|
is_initialise: bool,
|
||||||
match sync_result {
|
) -> Result<Vec<ScimEntry>, ()> {
|
||||||
LdapSyncRepl::Success {
|
// Because of how TOTP works with freeipa it's a soft referral from
|
||||||
cookie,
|
// the totp toward the user. This means if a TOTP is added or removed
|
||||||
refresh_deletes,
|
// we see those as unique entries in the syncrepl but we are missing
|
||||||
entries,
|
// the user entry that actually needs the update since Kanidm makes TOTP
|
||||||
delete_uuids,
|
// part of the entry itself.
|
||||||
present_uuids,
|
//
|
||||||
} => {
|
// This *also* means that when a user is updated that we also need to
|
||||||
if refresh_deletes {
|
// fetch their TOTP's that are related so we can assert them on the
|
||||||
error!("Unsure how to handle refreshDeletes=True");
|
// submission.
|
||||||
return Err(());
|
//
|
||||||
}
|
// Because of this, we have to do some client-side processing here to
|
||||||
|
// work out what "entries we are missing" and do a second search to
|
||||||
|
// fetch them. Sadly, this means that we need to do a second search
|
||||||
|
// and since ldap is NOT transactional there is a possibility of a
|
||||||
|
// desync between the sync-repl and the results of the second search.
|
||||||
|
//
|
||||||
|
// There are 5 possibilities - note one of TOTP or USER must be in syncrepl
|
||||||
|
// state else we wouldn't proceed.
|
||||||
|
// TOTP USER OUTCOME
|
||||||
|
// SyncRepl SyncRepl No ext detail needed, proceed
|
||||||
|
// SyncRepl Add/Mod Update user, won't change on next syncrepl
|
||||||
|
// SyncRepl Del Ignore this TOTP -> will be deleted on next syncrepl
|
||||||
|
// Add/Mod SyncRepl Add the new TOTP, won't change on next syncrepl
|
||||||
|
// Del SyncRepl Remove TOTP, won't change on next syncrepl
|
||||||
|
//
|
||||||
|
// The big challenge is to transform our data in a way that we can actually work
|
||||||
|
// with it here meaning we have to disassemble and "index" the content of our
|
||||||
|
// sync result.
|
||||||
|
|
||||||
if !present_uuids.is_empty() {
|
// Hash entries by DN -> Split TOTP's to their own set.
|
||||||
error!("Unsure how to handle presentUuids > 0");
|
// make a list of updated TOTP's and what DN's they require.
|
||||||
return Err(());
|
// make a list of updated Users and what TOTP's they require.
|
||||||
}
|
|
||||||
|
|
||||||
let to_state = cookie
|
let mut entries = BTreeMap::default();
|
||||||
.map(|cookie| {
|
let mut user_dns = Vec::default();
|
||||||
ScimSyncState::Active { cookie }
|
let mut totp_entries: BTreeMap<String, Vec<_>> = BTreeMap::default();
|
||||||
})
|
|
||||||
.ok_or_else(|| {
|
|
||||||
error!("Invalid state, ldap sync repl did not provide a valid state cookie in response.");
|
|
||||||
})?;
|
|
||||||
|
|
||||||
// Future - make this par-map
|
for lentry in ldap_entries.into_iter() {
|
||||||
let entries = entries
|
if lentry
|
||||||
.into_iter()
|
.entry
|
||||||
.filter_map(|e| {
|
.attrs
|
||||||
let e_config = entry_config_map
|
.get("objectclass")
|
||||||
.get(&e.entry_uuid)
|
.map(|oc| oc.contains("ipatokentotp"))
|
||||||
.cloned()
|
.unwrap_or_default()
|
||||||
.unwrap_or_default();
|
{
|
||||||
match ipa_to_scim_entry(e, &e_config) {
|
// It's an otp. Lets see ...
|
||||||
Ok(Some(e)) => Some(Ok(e)),
|
let token_owner_dn = if let Some(todn) = lentry
|
||||||
Ok(None) => None,
|
.entry
|
||||||
Err(()) => Some(Err(())),
|
.attrs
|
||||||
}
|
.get("ipatokenowner")
|
||||||
})
|
.and_then(|attr| if attr.len() != 1 { None } else { attr.first() })
|
||||||
.collect::<Result<Vec<_>, _>>();
|
{
|
||||||
|
debug!("totp with owner {}", todn);
|
||||||
let entries = match entries {
|
todn.clone()
|
||||||
Ok(e) => e,
|
} else {
|
||||||
Err(()) => {
|
warn!("totp with invalid ownership will be ignored");
|
||||||
error!("Failed to process IPA entries to SCIM");
|
continue;
|
||||||
return Err(());
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(ScimSyncRequest {
|
if !totp_entries.contains_key(&token_owner_dn) {
|
||||||
from_state,
|
totp_entries.insert(token_owner_dn.clone(), Vec::default());
|
||||||
to_state,
|
}
|
||||||
entries,
|
|
||||||
delete_uuids,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
LdapSyncRepl::RefreshRequired => {
|
|
||||||
let to_state = ScimSyncState::Refresh;
|
|
||||||
|
|
||||||
Ok(ScimSyncRequest {
|
if let Some(v) = totp_entries.get_mut(&token_owner_dn) {
|
||||||
from_state,
|
v.push(lentry)
|
||||||
to_state,
|
}
|
||||||
entries: Vec::new(),
|
} else {
|
||||||
delete_uuids: Vec::new(),
|
let dn = lentry.entry.dn.clone();
|
||||||
})
|
|
||||||
|
if lentry
|
||||||
|
.entry
|
||||||
|
.attrs
|
||||||
|
.get("objectclass")
|
||||||
|
.map(|oc| oc.contains("person"))
|
||||||
|
.unwrap_or_default()
|
||||||
|
{
|
||||||
|
user_dns.push(dn.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
entries.insert(dn, lentry);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Now, we have to invert the totp set so that it's defined by entry dn instead.
|
||||||
|
debug!("te, {}, e {}", totp_entries.len(), entries.len());
|
||||||
|
|
||||||
|
// If this is an INIT we have the full state already - no extra search is needed.
|
||||||
|
|
||||||
|
// On a refresh, we need to search and fix up to make sure TOTP/USER sets are
|
||||||
|
// consistent.
|
||||||
|
if !is_initialise {
|
||||||
|
// If the totp's related user is NOT in our sync repl, we need to fetch them.
|
||||||
|
let fetch_user: Vec<&str> = totp_entries
|
||||||
|
.keys()
|
||||||
|
.map(|k| k.as_str())
|
||||||
|
.filter(|k| !entries.contains_key(*k))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// For every user in our fetch_user *and* entries set, we need to fetch their
|
||||||
|
// related TOTP's.
|
||||||
|
let fetch_totps_for: Vec<&str> = fetch_user
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.chain(user_dns.iter().map(|s| s.as_str()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Create filter (could hit a limit, may need to split this search).
|
||||||
|
|
||||||
|
let totp_conditions: Vec<_> = fetch_totps_for
|
||||||
|
.iter()
|
||||||
|
.map(|dn| LdapFilter::Equality("ipatokenowner".to_string(), dn.to_string()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let user_conditions = fetch_user
|
||||||
|
.iter()
|
||||||
|
.filter_map(|dn| {
|
||||||
|
// We have to split the DN to it's RDN because lol.
|
||||||
|
dn.split_once(',')
|
||||||
|
.and_then(|(rdn, _)| rdn.split_once('='))
|
||||||
|
.map(|(_, uid)| LdapFilter::Equality("uid".to_string(), uid.to_string()))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let filter = LdapFilter::Or(vec![
|
||||||
|
LdapFilter::And(vec![
|
||||||
|
LdapFilter::Equality("objectclass".to_string(), "ipatoken".to_string()),
|
||||||
|
LdapFilter::Equality("objectclass".to_string(), "ipatokentotp".to_string()),
|
||||||
|
LdapFilter::Or(totp_conditions),
|
||||||
|
]),
|
||||||
|
LdapFilter::And(vec![
|
||||||
|
LdapFilter::Equality("objectclass".to_string(), "person".to_string()),
|
||||||
|
LdapFilter::Equality("objectclass".to_string(), "ipantuserattrs".to_string()),
|
||||||
|
LdapFilter::Equality("objectclass".to_string(), "posixaccount".to_string()),
|
||||||
|
LdapFilter::Or(user_conditions),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
debug!(?filter);
|
||||||
|
|
||||||
|
// Search
|
||||||
|
// 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.
|
||||||
|
}
|
||||||
|
|
||||||
|
// For each updated TOTP -> If it's related DN is not in Hash -> remove from map
|
||||||
|
totp_entries.retain(|k, _| {
|
||||||
|
let x = entries.contains_key(k);
|
||||||
|
if !x {
|
||||||
|
warn!("Removing totp with no valid owner {}", k);
|
||||||
|
}
|
||||||
|
x
|
||||||
|
});
|
||||||
|
|
||||||
|
let empty_slice = Vec::default();
|
||||||
|
|
||||||
|
// Future - make this par-map
|
||||||
|
let entries = entries
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|(dn, e)| {
|
||||||
|
let e_config = entry_config_map
|
||||||
|
.get(&e.entry_uuid)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let totp = totp_entries.get(&dn).unwrap_or(&empty_slice);
|
||||||
|
|
||||||
|
match ipa_to_scim_entry(e, &e_config, totp) {
|
||||||
|
Ok(Some(e)) => Some(Ok(e)),
|
||||||
|
Ok(None) => None,
|
||||||
|
Err(()) => Some(Err(())),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>, _>>();
|
||||||
|
|
||||||
|
entries
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Allow re-map of uuid -> uuid
|
// TODO: Allow re-map of uuid -> uuid
|
||||||
|
@ -500,6 +666,7 @@ async fn process_ipa_sync_result(
|
||||||
fn ipa_to_scim_entry(
|
fn ipa_to_scim_entry(
|
||||||
sync_entry: LdapSyncReplEntry,
|
sync_entry: LdapSyncReplEntry,
|
||||||
entry_config: &EntryConfig,
|
entry_config: &EntryConfig,
|
||||||
|
totp: &[LdapSyncReplEntry],
|
||||||
) -> Result<Option<ScimEntry>, ()> {
|
) -> Result<Option<ScimEntry>, ()> {
|
||||||
debug!("{:#?}", sync_entry);
|
debug!("{:#?}", sync_entry);
|
||||||
|
|
||||||
|
@ -555,6 +722,10 @@ fn ipa_to_scim_entry(
|
||||||
let password_import = entry
|
let password_import = entry
|
||||||
.remove_ava_single("ipanthash")
|
.remove_ava_single("ipanthash")
|
||||||
.map(|s| format!("ipaNTHash: {}", s));
|
.map(|s| format!("ipaNTHash: {}", s));
|
||||||
|
|
||||||
|
// If there are TOTP's, convert them to something sensible.
|
||||||
|
let totp_import = totp.iter().filter_map(ipa_to_totp).collect();
|
||||||
|
|
||||||
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);
|
||||||
|
|
||||||
|
@ -566,6 +737,7 @@ fn ipa_to_scim_entry(
|
||||||
display_name,
|
display_name,
|
||||||
gidnumber,
|
gidnumber,
|
||||||
password_import,
|
password_import,
|
||||||
|
totp_import,
|
||||||
login_shell,
|
login_shell,
|
||||||
}
|
}
|
||||||
.into(),
|
.into(),
|
||||||
|
@ -631,6 +803,74 @@ fn ipa_to_scim_entry(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn ipa_to_totp(sync_entry: &LdapSyncReplEntry) -> Option<ScimTotp> {
|
||||||
|
let external_id = sync_entry
|
||||||
|
.entry
|
||||||
|
.attrs
|
||||||
|
.get("ipatokenuniqueid")
|
||||||
|
.and_then(|v| v.first().cloned())
|
||||||
|
.or_else(|| {
|
||||||
|
warn!("Invalid ipatokenuniqueid");
|
||||||
|
None
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let secret = sync_entry
|
||||||
|
.entry
|
||||||
|
.attrs
|
||||||
|
.get("ipatokenotpkey")
|
||||||
|
.and_then(|v| v.first())
|
||||||
|
.and_then(|s| {
|
||||||
|
// Decode, and then make it urlsafe.
|
||||||
|
Base64UrlSafeData::try_from(s.as_str())
|
||||||
|
.ok()
|
||||||
|
.map(|b| b.to_string())
|
||||||
|
})
|
||||||
|
.or_else(|| {
|
||||||
|
warn!("Invalid ipatokenotpkey");
|
||||||
|
None
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let algo = sync_entry
|
||||||
|
.entry
|
||||||
|
.attrs
|
||||||
|
.get("ipatokenotpalgorithm")
|
||||||
|
.and_then(|v| v.first().cloned())
|
||||||
|
.or_else(|| {
|
||||||
|
warn!("Invalid ipatokenotpalgorithm");
|
||||||
|
None
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let step = sync_entry
|
||||||
|
.entry
|
||||||
|
.attrs
|
||||||
|
.get("ipatokentotptimestep")
|
||||||
|
.and_then(|v| v.first())
|
||||||
|
.and_then(|d| u32::from_str(d).ok())
|
||||||
|
.or_else(|| {
|
||||||
|
warn!("Invalid ipatokentotptimestep");
|
||||||
|
None
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let digits = sync_entry
|
||||||
|
.entry
|
||||||
|
.attrs
|
||||||
|
.get("ipatokenotpdigits")
|
||||||
|
.and_then(|v| v.first())
|
||||||
|
.and_then(|d| u32::from_str(d).ok())
|
||||||
|
.or_else(|| {
|
||||||
|
warn!("Invalid ipatokenotpdigits");
|
||||||
|
None
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Some(ScimTotp {
|
||||||
|
external_id,
|
||||||
|
secret,
|
||||||
|
algo,
|
||||||
|
step,
|
||||||
|
digits,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn config_security_checks(cfg_path: &Path) -> bool {
|
fn config_security_checks(cfg_path: &Path) -> bool {
|
||||||
let cfg_path_str = cfg_path.to_string_lossy();
|
let cfg_path_str = cfg_path.to_string_lossy();
|
||||||
|
|
||||||
|
|
|
@ -40,6 +40,47 @@ pub const SCIM_SCHEMA_SYNC_ACCOUNT: &str = "urn:ietf:params:scim:schemas:kanidm:
|
||||||
pub const SCIM_SCHEMA_SYNC_POSIXACCOUNT: &str =
|
pub const SCIM_SCHEMA_SYNC_POSIXACCOUNT: &str =
|
||||||
"urn:ietf:params:scim:schemas:kanidm:1.0:posixaccount";
|
"urn:ietf:params:scim:schemas:kanidm:1.0:posixaccount";
|
||||||
|
|
||||||
|
#[derive(Serialize, Debug, Clone)]
|
||||||
|
pub struct ScimTotp {
|
||||||
|
/// maps to "label" in kanidm.
|
||||||
|
pub external_id: String,
|
||||||
|
pub secret: String,
|
||||||
|
pub algo: String,
|
||||||
|
pub step: u32,
|
||||||
|
pub digits: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Need to allow this because clippy is broken and doesn't realise scimentry is out of crate
|
||||||
|
// so this can't be fulfilled
|
||||||
|
#[allow(clippy::from_over_into)]
|
||||||
|
impl Into<ScimComplexAttr> for ScimTotp {
|
||||||
|
fn into(self) -> ScimComplexAttr {
|
||||||
|
let ScimTotp {
|
||||||
|
external_id,
|
||||||
|
secret,
|
||||||
|
algo,
|
||||||
|
step,
|
||||||
|
digits,
|
||||||
|
} = self;
|
||||||
|
let mut attrs = BTreeMap::default();
|
||||||
|
|
||||||
|
attrs.insert(
|
||||||
|
"external_id".to_string(),
|
||||||
|
ScimSimpleAttr::String(external_id),
|
||||||
|
);
|
||||||
|
|
||||||
|
attrs.insert("secret".to_string(), ScimSimpleAttr::String(secret));
|
||||||
|
|
||||||
|
attrs.insert("algo".to_string(), ScimSimpleAttr::String(algo));
|
||||||
|
|
||||||
|
attrs.insert("step".to_string(), ScimSimpleAttr::Number(step.into()));
|
||||||
|
|
||||||
|
attrs.insert("digits".to_string(), ScimSimpleAttr::Number(digits.into()));
|
||||||
|
|
||||||
|
ScimComplexAttr { attrs }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Debug, Clone)]
|
#[derive(Serialize, Debug, Clone)]
|
||||||
#[serde(into = "ScimEntry")]
|
#[serde(into = "ScimEntry")]
|
||||||
pub struct ScimSyncPerson {
|
pub struct ScimSyncPerson {
|
||||||
|
@ -49,6 +90,7 @@ pub struct ScimSyncPerson {
|
||||||
pub display_name: String,
|
pub display_name: String,
|
||||||
pub gidnumber: Option<u32>,
|
pub gidnumber: Option<u32>,
|
||||||
pub password_import: Option<String>,
|
pub password_import: Option<String>,
|
||||||
|
pub totp_import: Vec<ScimTotp>,
|
||||||
pub login_shell: Option<String>,
|
pub login_shell: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,6 +106,7 @@ impl Into<ScimEntry> for ScimSyncPerson {
|
||||||
display_name,
|
display_name,
|
||||||
gidnumber,
|
gidnumber,
|
||||||
password_import,
|
password_import,
|
||||||
|
totp_import,
|
||||||
login_shell,
|
login_shell,
|
||||||
} = self;
|
} = self;
|
||||||
|
|
||||||
|
@ -86,6 +129,7 @@ impl Into<ScimEntry> for ScimSyncPerson {
|
||||||
set_string!(attrs, "displayname", display_name);
|
set_string!(attrs, "displayname", display_name);
|
||||||
set_option_u32!(attrs, "gidnumber", gidnumber);
|
set_option_u32!(attrs, "gidnumber", gidnumber);
|
||||||
set_option_string!(attrs, "password_import", password_import);
|
set_option_string!(attrs, "password_import", password_import);
|
||||||
|
set_multi_complex!(attrs, "totp_import", totp_import);
|
||||||
set_option_string!(attrs, "loginshell", login_shell);
|
set_option_string!(attrs, "loginshell", login_shell);
|
||||||
|
|
||||||
ScimEntry {
|
ScimEntry {
|
||||||
|
|
|
@ -38,7 +38,7 @@ pub enum PluginError {
|
||||||
AttrUnique(String),
|
AttrUnique(String),
|
||||||
Base(String),
|
Base(String),
|
||||||
ReferentialIntegrity(String),
|
ReferentialIntegrity(String),
|
||||||
PasswordImport(String),
|
CredImport(String),
|
||||||
Oauth2Secrets,
|
Oauth2Secrets,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1076,6 +1076,7 @@ pub struct TotpSecret {
|
||||||
pub secret: Vec<u8>,
|
pub secret: Vec<u8>,
|
||||||
pub algo: TotpAlgo,
|
pub algo: TotpAlgo,
|
||||||
pub step: u64,
|
pub step: u64,
|
||||||
|
pub digits: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TotpSecret {
|
impl TotpSecret {
|
||||||
|
@ -1087,10 +1088,11 @@ impl TotpSecret {
|
||||||
let algo = self.algo.to_string();
|
let algo = self.algo.to_string();
|
||||||
let secret = self.get_secret();
|
let secret = self.get_secret();
|
||||||
let period = self.step;
|
let period = self.step;
|
||||||
|
let digits = self.digits;
|
||||||
|
|
||||||
format!(
|
format!(
|
||||||
"otpauth://totp/{}?secret={}&issuer={}&algorithm={}&digits=6&period={}",
|
"otpauth://totp/{}?secret={}&issuer={}&algorithm={}&digits={}&period={}",
|
||||||
label, secret, issuer, algo, period
|
label, secret, issuer, algo, digits, period
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1224,6 +1226,7 @@ mod tests {
|
||||||
secret: vec![0xaa, 0xbb, 0xcc, 0xdd],
|
secret: vec![0xaa, 0xbb, 0xcc, 0xdd],
|
||||||
step: 30,
|
step: 30,
|
||||||
algo: TotpAlgo::Sha256,
|
algo: TotpAlgo::Sha256,
|
||||||
|
digits: 6,
|
||||||
};
|
};
|
||||||
let s = totp.to_uri();
|
let s = totp.to_uri();
|
||||||
assert!(s == "otpauth://totp/blackhats:william?secret=VK54ZXI&issuer=blackhats&algorithm=SHA256&digits=6&period=30");
|
assert!(s == "otpauth://totp/blackhats:william?secret=VK54ZXI&issuer=blackhats&algorithm=SHA256&digits=6&period=30");
|
||||||
|
@ -1235,6 +1238,7 @@ mod tests {
|
||||||
secret: vec![0xaa, 0xbb, 0xcc, 0xdd],
|
secret: vec![0xaa, 0xbb, 0xcc, 0xdd],
|
||||||
step: 30,
|
step: 30,
|
||||||
algo: TotpAlgo::Sha256,
|
algo: TotpAlgo::Sha256,
|
||||||
|
digits: 6,
|
||||||
};
|
};
|
||||||
let s = totp.to_uri();
|
let s = totp.to_uri();
|
||||||
println!("{}", s);
|
println!("{}", s);
|
||||||
|
|
|
@ -73,6 +73,8 @@ pub struct DbTotpV1 {
|
||||||
pub step: u64,
|
pub step: u64,
|
||||||
#[serde(rename = "a")]
|
#[serde(rename = "a")]
|
||||||
pub algo: DbTotpAlgoV1,
|
pub algo: DbTotpAlgoV1,
|
||||||
|
#[serde(rename = "d", default)]
|
||||||
|
pub digits: Option<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Debug for DbTotpV1 {
|
impl std::fmt::Debug for DbTotpV1 {
|
||||||
|
@ -576,6 +578,8 @@ pub enum DbValueSetV2 {
|
||||||
Oauth2Session(Vec<DbValueOauth2Session>),
|
Oauth2Session(Vec<DbValueOauth2Session>),
|
||||||
#[serde(rename = "UH")]
|
#[serde(rename = "UH")]
|
||||||
UiHint(Vec<u16>),
|
UiHint(Vec<u16>),
|
||||||
|
#[serde(rename = "TO")]
|
||||||
|
TotpSecret(Vec<(String, DbTotpV1)>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DbValueSetV2 {
|
impl DbValueSetV2 {
|
||||||
|
@ -616,6 +620,7 @@ impl DbValueSetV2 {
|
||||||
DbValueSetV2::JwsKeyEs256(set) => set.len(),
|
DbValueSetV2::JwsKeyEs256(set) => set.len(),
|
||||||
DbValueSetV2::JwsKeyRs256(set) => set.len(),
|
DbValueSetV2::JwsKeyRs256(set) => set.len(),
|
||||||
DbValueSetV2::UiHint(set) => set.len(),
|
DbValueSetV2::UiHint(set) => set.len(),
|
||||||
|
DbValueSetV2::TotpSecret(set) => set.len(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -61,8 +61,8 @@ impl Default for Limits {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Limits {
|
Limits {
|
||||||
unindexed_allow: false,
|
unindexed_allow: false,
|
||||||
search_max_results: 128,
|
search_max_results: 256,
|
||||||
search_max_filter_test: 256,
|
search_max_filter_test: 512,
|
||||||
filter_max_elements: 32,
|
filter_max_elements: 32,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -222,6 +222,7 @@ pub const UUID_SCHEMA_ATTR_SYNC_ALLOWED: Uuid = uuid!("00000000-0000-0000-0000-f
|
||||||
|
|
||||||
pub const UUID_SCHEMA_ATTR_EMAILPRIMARY: Uuid = uuid!("00000000-0000-0000-0000-ffff00000126");
|
pub const UUID_SCHEMA_ATTR_EMAILPRIMARY: Uuid = uuid!("00000000-0000-0000-0000-ffff00000126");
|
||||||
pub const UUID_SCHEMA_ATTR_EMAILALTERNATIVE: Uuid = uuid!("00000000-0000-0000-0000-ffff00000127");
|
pub const UUID_SCHEMA_ATTR_EMAILALTERNATIVE: Uuid = uuid!("00000000-0000-0000-0000-ffff00000127");
|
||||||
|
pub const UUID_SCHEMA_ATTR_TOTP_IMPORT: Uuid = uuid!("00000000-0000-0000-0000-ffff00000128");
|
||||||
|
|
||||||
// System and domain infos
|
// System and domain infos
|
||||||
// I'd like to strongly criticise william of the past for making poor choices about these allocations.
|
// I'd like to strongly criticise william of the past for making poor choices about these allocations.
|
||||||
|
|
|
@ -20,6 +20,35 @@ pub enum TotpError {
|
||||||
TimeError,
|
TimeError,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[repr(u32)]
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum TotpDigits {
|
||||||
|
Six = 1_000_000,
|
||||||
|
Eight = 100_000_000,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<u8> for TotpDigits {
|
||||||
|
type Error = ();
|
||||||
|
|
||||||
|
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||||
|
match value {
|
||||||
|
6 => Ok(TotpDigits::Six),
|
||||||
|
8 => Ok(TotpDigits::Six),
|
||||||
|
_ => Err(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::from_over_into)]
|
||||||
|
impl Into<u8> for TotpDigits {
|
||||||
|
fn into(self) -> u8 {
|
||||||
|
match self {
|
||||||
|
TotpDigits::Six => 6,
|
||||||
|
TotpDigits::Eight => 8,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum TotpAlgo {
|
pub enum TotpAlgo {
|
||||||
Sha1,
|
Sha1,
|
||||||
|
@ -62,6 +91,7 @@ pub struct Totp {
|
||||||
secret: Vec<u8>,
|
secret: Vec<u8>,
|
||||||
pub(crate) step: u64,
|
pub(crate) step: u64,
|
||||||
algo: TotpAlgo,
|
algo: TotpAlgo,
|
||||||
|
digits: TotpDigits,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<DbTotpV1> for Totp {
|
impl TryFrom<DbTotpV1> for Totp {
|
||||||
|
@ -73,17 +103,23 @@ impl TryFrom<DbTotpV1> for Totp {
|
||||||
DbTotpAlgoV1::S256 => TotpAlgo::Sha256,
|
DbTotpAlgoV1::S256 => TotpAlgo::Sha256,
|
||||||
DbTotpAlgoV1::S512 => TotpAlgo::Sha512,
|
DbTotpAlgoV1::S512 => TotpAlgo::Sha512,
|
||||||
};
|
};
|
||||||
|
// Default.
|
||||||
|
let digits = TotpDigits::try_from(value.digits.unwrap_or(6))?;
|
||||||
|
|
||||||
Ok(Totp {
|
Ok(Totp {
|
||||||
secret: value.key,
|
secret: value.key,
|
||||||
step: value.step,
|
step: value.step,
|
||||||
algo,
|
algo,
|
||||||
|
digits,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<ProtoTotp> for Totp {
|
impl TryFrom<ProtoTotp> for Totp {
|
||||||
fn from(value: ProtoTotp) -> Self {
|
type Error = ();
|
||||||
Totp {
|
|
||||||
|
fn try_from(value: ProtoTotp) -> Result<Self, Self::Error> {
|
||||||
|
Ok(Totp {
|
||||||
secret: value.secret,
|
secret: value.secret,
|
||||||
algo: match value.algo {
|
algo: match value.algo {
|
||||||
ProtoTotpAlgo::Sha1 => TotpAlgo::Sha1,
|
ProtoTotpAlgo::Sha1 => TotpAlgo::Sha1,
|
||||||
|
@ -91,13 +127,19 @@ impl From<ProtoTotp> for Totp {
|
||||||
ProtoTotpAlgo::Sha512 => TotpAlgo::Sha512,
|
ProtoTotpAlgo::Sha512 => TotpAlgo::Sha512,
|
||||||
},
|
},
|
||||||
step: value.step,
|
step: value.step,
|
||||||
}
|
digits: TotpDigits::try_from(value.digits)?,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Totp {
|
impl Totp {
|
||||||
pub fn new(secret: Vec<u8>, step: u64, algo: TotpAlgo) -> Self {
|
pub fn new(secret: Vec<u8>, step: u64, algo: TotpAlgo, digits: TotpDigits) -> Self {
|
||||||
Totp { secret, step, algo }
|
Totp {
|
||||||
|
secret,
|
||||||
|
step,
|
||||||
|
algo,
|
||||||
|
digits,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a new token with secure key and algo.
|
// Create a new token with secure key and algo.
|
||||||
|
@ -105,7 +147,13 @@ impl Totp {
|
||||||
let mut rng = rand::thread_rng();
|
let mut rng = rand::thread_rng();
|
||||||
let secret: Vec<u8> = (0..SECRET_SIZE_BYTES).map(|_| rng.gen()).collect();
|
let secret: Vec<u8> = (0..SECRET_SIZE_BYTES).map(|_| rng.gen()).collect();
|
||||||
let algo = TotpAlgo::Sha256;
|
let algo = TotpAlgo::Sha256;
|
||||||
Totp { secret, step, algo }
|
let digits = TotpDigits::Six;
|
||||||
|
Totp {
|
||||||
|
secret,
|
||||||
|
step,
|
||||||
|
algo,
|
||||||
|
digits,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn to_dbtotpv1(&self) -> DbTotpV1 {
|
pub(crate) fn to_dbtotpv1(&self) -> DbTotpV1 {
|
||||||
|
@ -118,6 +166,7 @@ impl Totp {
|
||||||
TotpAlgo::Sha256 => DbTotpAlgoV1::S256,
|
TotpAlgo::Sha256 => DbTotpAlgoV1::S256,
|
||||||
TotpAlgo::Sha512 => DbTotpAlgoV1::S512,
|
TotpAlgo::Sha512 => DbTotpAlgoV1::S512,
|
||||||
},
|
},
|
||||||
|
digits: Some(self.digits.into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -134,7 +183,12 @@ impl Totp {
|
||||||
.map_err(|_| TotpError::HmacError)?;
|
.map_err(|_| TotpError::HmacError)?;
|
||||||
|
|
||||||
let otp = u32::from_be_bytes(bytes);
|
let otp = u32::from_be_bytes(bytes);
|
||||||
Ok((otp & 0x7fff_ffff) % 1_000_000)
|
// Treat as a u31, this masks the first bit.
|
||||||
|
// then modulo based on the number of digits requested.
|
||||||
|
// * For 6 digits modulo 1_000_000
|
||||||
|
// * For 8 digits modulo 100_000_000
|
||||||
|
// Based on this 9 is max digits.
|
||||||
|
Ok((otp & 0x7fff_ffff) % (self.digits as u32))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn do_totp_duration_from_epoch(&self, time: &Duration) -> Result<u32, TotpError> {
|
pub fn do_totp_duration_from_epoch(&self, time: &Duration) -> Result<u32, TotpError> {
|
||||||
|
@ -173,6 +227,7 @@ impl Totp {
|
||||||
TotpAlgo::Sha256 => ProtoTotpAlgo::Sha256,
|
TotpAlgo::Sha256 => ProtoTotpAlgo::Sha256,
|
||||||
TotpAlgo::Sha512 => ProtoTotpAlgo::Sha512,
|
TotpAlgo::Sha512 => ProtoTotpAlgo::Sha512,
|
||||||
},
|
},
|
||||||
|
digits: self.digits.into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -185,6 +240,7 @@ impl Totp {
|
||||||
secret: self.secret,
|
secret: self.secret,
|
||||||
step: self.step,
|
step: self.step,
|
||||||
algo: TotpAlgo::Sha1,
|
algo: TotpAlgo::Sha1,
|
||||||
|
digits: self.digits,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -193,20 +249,27 @@ impl Totp {
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use crate::credential::totp::{Totp, TotpAlgo, TotpError, TOTP_DEFAULT_STEP};
|
use crate::credential::totp::{Totp, TotpAlgo, TotpDigits, TotpError, TOTP_DEFAULT_STEP};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn hotp_basic() {
|
fn hotp_basic() {
|
||||||
let otp_sha1 = Totp::new(vec![0], 30, TotpAlgo::Sha1);
|
let otp_sha1 = Totp::new(vec![0], 30, TotpAlgo::Sha1, TotpDigits::Six);
|
||||||
assert!(otp_sha1.digest(0) == Ok(328482));
|
assert!(otp_sha1.digest(0) == Ok(328482));
|
||||||
let otp_sha256 = Totp::new(vec![0], 30, TotpAlgo::Sha256);
|
let otp_sha256 = Totp::new(vec![0], 30, TotpAlgo::Sha256, TotpDigits::Six);
|
||||||
assert!(otp_sha256.digest(0) == Ok(356306));
|
assert!(otp_sha256.digest(0) == Ok(356306));
|
||||||
let otp_sha512 = Totp::new(vec![0], 30, TotpAlgo::Sha512);
|
let otp_sha512 = Totp::new(vec![0], 30, TotpAlgo::Sha512, TotpDigits::Six);
|
||||||
assert!(otp_sha512.digest(0) == Ok(674061));
|
assert!(otp_sha512.digest(0) == Ok(674061));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn do_test(key: Vec<u8>, algo: TotpAlgo, secs: u64, step: u64, expect: Result<u32, TotpError>) {
|
fn do_test(
|
||||||
let otp = Totp::new(key.clone(), step, algo.clone());
|
key: Vec<u8>,
|
||||||
|
algo: TotpAlgo,
|
||||||
|
secs: u64,
|
||||||
|
step: u64,
|
||||||
|
digits: TotpDigits,
|
||||||
|
expect: Result<u32, TotpError>,
|
||||||
|
) {
|
||||||
|
let otp = Totp::new(key.clone(), step, algo.clone(), digits);
|
||||||
let d = Duration::from_secs(secs);
|
let d = Duration::from_secs(secs);
|
||||||
let r = otp.do_totp_duration_from_epoch(&d);
|
let r = otp.do_totp_duration_from_epoch(&d);
|
||||||
debug!(
|
debug!(
|
||||||
|
@ -223,13 +286,23 @@ mod tests {
|
||||||
TotpAlgo::Sha1,
|
TotpAlgo::Sha1,
|
||||||
1585368920,
|
1585368920,
|
||||||
TOTP_DEFAULT_STEP,
|
TOTP_DEFAULT_STEP,
|
||||||
|
TotpDigits::Six,
|
||||||
Ok(728926),
|
Ok(728926),
|
||||||
);
|
);
|
||||||
|
do_test(
|
||||||
|
vec![0x00, 0x00, 0x00, 0x00],
|
||||||
|
TotpAlgo::Sha1,
|
||||||
|
1585368920,
|
||||||
|
TOTP_DEFAULT_STEP,
|
||||||
|
TotpDigits::Eight,
|
||||||
|
Ok(74728926),
|
||||||
|
);
|
||||||
do_test(
|
do_test(
|
||||||
vec![0x00, 0xaa, 0xbb, 0xcc],
|
vec![0x00, 0xaa, 0xbb, 0xcc],
|
||||||
TotpAlgo::Sha1,
|
TotpAlgo::Sha1,
|
||||||
1585369498,
|
1585369498,
|
||||||
TOTP_DEFAULT_STEP,
|
TOTP_DEFAULT_STEP,
|
||||||
|
TotpDigits::Six,
|
||||||
Ok(985074),
|
Ok(985074),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -241,13 +314,23 @@ mod tests {
|
||||||
TotpAlgo::Sha256,
|
TotpAlgo::Sha256,
|
||||||
1585369682,
|
1585369682,
|
||||||
TOTP_DEFAULT_STEP,
|
TOTP_DEFAULT_STEP,
|
||||||
|
TotpDigits::Six,
|
||||||
Ok(795483),
|
Ok(795483),
|
||||||
);
|
);
|
||||||
|
do_test(
|
||||||
|
vec![0x00, 0x00, 0x00, 0x00],
|
||||||
|
TotpAlgo::Sha256,
|
||||||
|
1585369682,
|
||||||
|
TOTP_DEFAULT_STEP,
|
||||||
|
TotpDigits::Eight,
|
||||||
|
Ok(11795483),
|
||||||
|
);
|
||||||
do_test(
|
do_test(
|
||||||
vec![0x00, 0xaa, 0xbb, 0xcc],
|
vec![0x00, 0xaa, 0xbb, 0xcc],
|
||||||
TotpAlgo::Sha256,
|
TotpAlgo::Sha256,
|
||||||
1585369689,
|
1585369689,
|
||||||
TOTP_DEFAULT_STEP,
|
TOTP_DEFAULT_STEP,
|
||||||
|
TotpDigits::Six,
|
||||||
Ok(728402),
|
Ok(728402),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -259,13 +342,23 @@ mod tests {
|
||||||
TotpAlgo::Sha512,
|
TotpAlgo::Sha512,
|
||||||
1585369775,
|
1585369775,
|
||||||
TOTP_DEFAULT_STEP,
|
TOTP_DEFAULT_STEP,
|
||||||
|
TotpDigits::Six,
|
||||||
Ok(587735),
|
Ok(587735),
|
||||||
);
|
);
|
||||||
|
do_test(
|
||||||
|
vec![0x00, 0x00, 0x00, 0x00],
|
||||||
|
TotpAlgo::Sha512,
|
||||||
|
1585369775,
|
||||||
|
TOTP_DEFAULT_STEP,
|
||||||
|
TotpDigits::Eight,
|
||||||
|
Ok(14587735),
|
||||||
|
);
|
||||||
do_test(
|
do_test(
|
||||||
vec![0x00, 0xaa, 0xbb, 0xcc],
|
vec![0x00, 0xaa, 0xbb, 0xcc],
|
||||||
TotpAlgo::Sha512,
|
TotpAlgo::Sha512,
|
||||||
1585369780,
|
1585369780,
|
||||||
TOTP_DEFAULT_STEP,
|
TOTP_DEFAULT_STEP,
|
||||||
|
TotpDigits::Six,
|
||||||
Ok(952181),
|
Ok(952181),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -274,7 +367,12 @@ mod tests {
|
||||||
fn totp_allow_one_previous() {
|
fn totp_allow_one_previous() {
|
||||||
let key = vec![0x00, 0xaa, 0xbb, 0xcc];
|
let key = vec![0x00, 0xaa, 0xbb, 0xcc];
|
||||||
let secs = 1585369780;
|
let secs = 1585369780;
|
||||||
let otp = Totp::new(key.clone(), TOTP_DEFAULT_STEP, TotpAlgo::Sha512);
|
let otp = Totp::new(
|
||||||
|
key.clone(),
|
||||||
|
TOTP_DEFAULT_STEP,
|
||||||
|
TotpAlgo::Sha512,
|
||||||
|
TotpDigits::Six,
|
||||||
|
);
|
||||||
let d = Duration::from_secs(secs);
|
let d = Duration::from_secs(secs);
|
||||||
// Step
|
// Step
|
||||||
assert!(otp.verify(952181, &d));
|
assert!(otp.verify(952181, &d));
|
||||||
|
|
|
@ -2114,7 +2114,7 @@ mod tests {
|
||||||
|
|
||||||
// Check the status has the token.
|
// Check the status has the token.
|
||||||
let totp_token: Totp = match c_status.mfaregstate {
|
let totp_token: Totp = match c_status.mfaregstate {
|
||||||
MfaRegStateStatus::TotpCheck(secret) => Some(secret.into()),
|
MfaRegStateStatus::TotpCheck(secret) => Some(secret.try_into().unwrap()),
|
||||||
|
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
|
@ -2208,7 +2208,7 @@ mod tests {
|
||||||
|
|
||||||
// Check the status has the token.
|
// Check the status has the token.
|
||||||
let totp_token: Totp = match c_status.mfaregstate {
|
let totp_token: Totp = match c_status.mfaregstate {
|
||||||
MfaRegStateStatus::TotpCheck(secret) => Some(secret.into()),
|
MfaRegStateStatus::TotpCheck(secret) => Some(secret.try_into().unwrap()),
|
||||||
|
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
|
@ -2283,7 +2283,7 @@ mod tests {
|
||||||
.expect("Failed to update the primary cred password");
|
.expect("Failed to update the primary cred password");
|
||||||
|
|
||||||
let totp_token: Totp = match c_status.mfaregstate {
|
let totp_token: Totp = match c_status.mfaregstate {
|
||||||
MfaRegStateStatus::TotpCheck(secret) => Some(secret.into()),
|
MfaRegStateStatus::TotpCheck(secret) => Some(secret.try_into().unwrap()),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
.expect("Unable to retrieve totp token, invalid state.");
|
.expect("Unable to retrieve totp token, invalid state.");
|
||||||
|
|
|
@ -8,6 +8,7 @@ use kanidm_proto::v1::ApiTokenPurpose;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::{BTreeMap, BTreeSet};
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
|
|
||||||
|
use crate::credential::totp::{Totp, TotpAlgo, TotpDigits};
|
||||||
use crate::idm::server::{IdmServerProxyReadTransaction, IdmServerProxyWriteTransaction};
|
use crate::idm::server::{IdmServerProxyReadTransaction, IdmServerProxyWriteTransaction};
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::value::Session;
|
use crate::value::Session;
|
||||||
|
@ -876,6 +877,159 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
||||||
}
|
}
|
||||||
Ok(vs)
|
Ok(vs)
|
||||||
}
|
}
|
||||||
|
(SyntaxType::TotpSecret, true, ScimAttr::MultiComplex(values)) => {
|
||||||
|
// We have to break down each complex value into a totp.
|
||||||
|
let mut vs = Vec::with_capacity(values.len());
|
||||||
|
for complex in values.iter() {
|
||||||
|
let external_id = complex
|
||||||
|
.attrs
|
||||||
|
.get("external_id")
|
||||||
|
.ok_or_else(|| {
|
||||||
|
error!("Invalid scim complex attr - missing required key external_id");
|
||||||
|
OperationError::InvalidAttribute(format!(
|
||||||
|
"missing required key external_id - {}",
|
||||||
|
scim_attr_name
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.and_then(|external_id| match external_id {
|
||||||
|
ScimSimpleAttr::String(value) => Ok(value.clone()),
|
||||||
|
_ => {
|
||||||
|
error!(
|
||||||
|
"Invalid external_id attribute - must be scim simple string"
|
||||||
|
);
|
||||||
|
Err(OperationError::InvalidAttribute(format!(
|
||||||
|
"external_id must be scim simple string - {}",
|
||||||
|
scim_attr_name
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let secret = complex
|
||||||
|
.attrs
|
||||||
|
.get("secret")
|
||||||
|
.ok_or_else(|| {
|
||||||
|
error!("Invalid scim complex attr - missing required key secret");
|
||||||
|
OperationError::InvalidAttribute(format!(
|
||||||
|
"missing required key secret - {}",
|
||||||
|
scim_attr_name
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.and_then(|secret| match secret {
|
||||||
|
ScimSimpleAttr::String(value) => {
|
||||||
|
Base64UrlSafeData::try_from(value.as_str())
|
||||||
|
.map(|b| b.into())
|
||||||
|
.map_err(|_| {
|
||||||
|
error!("Invalid secret attribute - must be base64 string");
|
||||||
|
OperationError::InvalidAttribute(format!(
|
||||||
|
"secret must be base64 string - {}",
|
||||||
|
scim_attr_name
|
||||||
|
))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
error!("Invalid secret attribute - must be scim simple string");
|
||||||
|
Err(OperationError::InvalidAttribute(format!(
|
||||||
|
"secret must be scim simple string - {}",
|
||||||
|
scim_attr_name
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let algo = complex.attrs.get("algo")
|
||||||
|
.ok_or_else(|| {
|
||||||
|
error!("Invalid scim complex attr - missing required key algo");
|
||||||
|
OperationError::InvalidAttribute(format!(
|
||||||
|
"missing required key algo - {}",
|
||||||
|
scim_attr_name
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.and_then(|algo_str| {
|
||||||
|
match algo_str {
|
||||||
|
ScimSimpleAttr::String(value) => {
|
||||||
|
match value.as_str() {
|
||||||
|
"sha1" => Ok(TotpAlgo::Sha1),
|
||||||
|
"sha256" => Ok(TotpAlgo::Sha256),
|
||||||
|
"sha512" => Ok(TotpAlgo::Sha512),
|
||||||
|
_ => {
|
||||||
|
error!("Invalid algo attribute - must be one of sha1, sha256 or sha512");
|
||||||
|
Err(OperationError::InvalidAttribute(format!(
|
||||||
|
"algo must be one of sha1, sha256 or sha512 - {}",
|
||||||
|
scim_attr_name
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
error!("Invalid algo attribute - must be scim simple string");
|
||||||
|
Err(OperationError::InvalidAttribute(format!(
|
||||||
|
"algo must be scim simple string - {}",
|
||||||
|
scim_attr_name
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let step = complex.attrs.get("step").ok_or_else(|| {
|
||||||
|
error!("Invalid scim complex attr - missing required key step");
|
||||||
|
OperationError::InvalidAttribute(format!(
|
||||||
|
"missing required key step - {}",
|
||||||
|
scim_attr_name
|
||||||
|
))
|
||||||
|
}).and_then(|step| {
|
||||||
|
match step {
|
||||||
|
ScimSimpleAttr::Number(value) => {
|
||||||
|
match value.as_u64() {
|
||||||
|
Some(s) if s >= 30 => Ok(s),
|
||||||
|
_ =>
|
||||||
|
Err(OperationError::InvalidAttribute(format!(
|
||||||
|
"step must be a positive integer value equal to or greater than 30 - {}",
|
||||||
|
scim_attr_name
|
||||||
|
))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
error!("Invalid step attribute - must be scim simple number");
|
||||||
|
Err(OperationError::InvalidAttribute(format!(
|
||||||
|
"step must be scim simple number - {}",
|
||||||
|
scim_attr_name
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let digits = complex
|
||||||
|
.attrs
|
||||||
|
.get("digits")
|
||||||
|
.ok_or_else(|| {
|
||||||
|
error!("Invalid scim complex attr - missing required key digits");
|
||||||
|
OperationError::InvalidAttribute(format!(
|
||||||
|
"missing required key digits - {}",
|
||||||
|
scim_attr_name
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.and_then(|digits| match digits {
|
||||||
|
ScimSimpleAttr::Number(value) => match value.as_u64() {
|
||||||
|
Some(6) => Ok(TotpDigits::Six),
|
||||||
|
Some(8) => Ok(TotpDigits::Eight),
|
||||||
|
_ => Err(OperationError::InvalidAttribute(format!(
|
||||||
|
"digits must be a positive integer value of 6 OR 8 - {}",
|
||||||
|
scim_attr_name
|
||||||
|
))),
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
error!("Invalid digits attribute - must be scim simple number");
|
||||||
|
Err(OperationError::InvalidAttribute(format!(
|
||||||
|
"digits must be scim simple number - {}",
|
||||||
|
scim_attr_name
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let totp = Totp::new(secret, step, algo, digits);
|
||||||
|
vs.push(Value::TotpSecret(external_id, totp))
|
||||||
|
}
|
||||||
|
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!(
|
||||||
|
|
|
@ -9,9 +9,9 @@ use crate::event::{CreateEvent, ModifyEvent};
|
||||||
use crate::plugins::Plugin;
|
use crate::plugins::Plugin;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
|
||||||
pub struct PasswordImport {}
|
pub struct CredImport {}
|
||||||
|
|
||||||
impl Plugin for PasswordImport {
|
impl Plugin for CredImport {
|
||||||
fn id() -> &'static str {
|
fn id() -> &'static str {
|
||||||
"plugin_password_import"
|
"plugin_password_import"
|
||||||
}
|
}
|
||||||
|
@ -26,44 +26,6 @@ impl Plugin for PasswordImport {
|
||||||
cand: &mut Vec<Entry<EntryInvalid, EntryNew>>,
|
cand: &mut Vec<Entry<EntryInvalid, EntryNew>>,
|
||||||
_ce: &CreateEvent,
|
_ce: &CreateEvent,
|
||||||
) -> Result<(), OperationError> {
|
) -> Result<(), OperationError> {
|
||||||
/*
|
|
||||||
cand.iter_mut()
|
|
||||||
.try_for_each(|e| {
|
|
||||||
// is there a password we are trying to import?
|
|
||||||
let vs = match e.pop_ava("password_import") {
|
|
||||||
Some(vs) => vs,
|
|
||||||
None => return Ok(()),
|
|
||||||
};
|
|
||||||
// if there are multiple, fail.
|
|
||||||
if vs.len() > 1 {
|
|
||||||
return Err(OperationError::Plugin(PluginError::PasswordImport("multiple password_imports specified".to_string())))
|
|
||||||
}
|
|
||||||
|
|
||||||
let im_pw = vs.to_utf8_single()
|
|
||||||
.ok_or_else(|| OperationError::Plugin(PluginError::PasswordImport("password_import has incorrect value type".to_string())))?;
|
|
||||||
|
|
||||||
// convert the import_password to a cred
|
|
||||||
let pw = Password::try_from(im_pw)
|
|
||||||
.map_err(|_| OperationError::Plugin(PluginError::PasswordImport("password_import was unable to convert hash format".to_string())))?;
|
|
||||||
|
|
||||||
// does the entry have a primary cred?
|
|
||||||
match e.get_ava_single_credential("primary_credential") {
|
|
||||||
Some(_c) => {
|
|
||||||
Err(
|
|
||||||
OperationError::Plugin(PluginError::PasswordImport(
|
|
||||||
"password_import - impossible state, how did you get a credential into a create!?".to_string()))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
// just set it then!
|
|
||||||
let c = Credential::new_from_password(pw);
|
|
||||||
e.set_ava("primary_credential",
|
|
||||||
once(Value::new_credential("primary", c)));
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
*/
|
|
||||||
Self::modify_inner(cand)
|
Self::modify_inner(cand)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,55 +52,74 @@ impl Plugin for PasswordImport {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PasswordImport {
|
impl CredImport {
|
||||||
fn modify_inner<T: Clone>(cand: &mut [Entry<EntryInvalid, T>]) -> Result<(), OperationError> {
|
fn modify_inner<T: Clone>(cand: &mut [Entry<EntryInvalid, T>]) -> Result<(), OperationError> {
|
||||||
cand.iter_mut().try_for_each(|e| {
|
cand.iter_mut().try_for_each(|e| {
|
||||||
// is there a password we are trying to import?
|
// PASSWORD IMPORT
|
||||||
let vs = match e.pop_ava("password_import") {
|
if let Some(vs) = e.pop_ava("password_import") {
|
||||||
Some(vs) => vs,
|
// if there are multiple, fail.
|
||||||
None => return Ok(()),
|
let im_pw = vs.to_utf8_single().ok_or_else(|| {
|
||||||
|
OperationError::Plugin(PluginError::CredImport(
|
||||||
|
"password_import has incorrect value type - should be a single utf8 string"
|
||||||
|
.to_string(),
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// convert the import_password_string to a password
|
||||||
|
let pw = Password::try_from(im_pw).map_err(|_| {
|
||||||
|
OperationError::Plugin(PluginError::CredImport(
|
||||||
|
"password_import was unable to convert hash format".to_string(),
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// does the entry have a primary cred?
|
||||||
|
match e.get_ava_single_credential("primary_credential") {
|
||||||
|
Some(c) => {
|
||||||
|
// This is the major diff to create, we can update in place!
|
||||||
|
let c = c.update_password(pw);
|
||||||
|
e.set_ava(
|
||||||
|
"primary_credential",
|
||||||
|
once(Value::new_credential("primary", c)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
// just set it then!
|
||||||
|
let c = Credential::new_from_password(pw);
|
||||||
|
e.set_ava(
|
||||||
|
"primary_credential",
|
||||||
|
once(Value::new_credential("primary", c)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
// if there are multiple, fail.
|
|
||||||
if vs.len() > 1 {
|
|
||||||
return Err(OperationError::Plugin(PluginError::PasswordImport(
|
|
||||||
"multiple password_imports specified".to_string(),
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
let im_pw = vs.to_utf8_single().ok_or_else(|| {
|
// TOTP IMPORT
|
||||||
OperationError::Plugin(PluginError::PasswordImport(
|
if let Some(vs) = e.pop_ava("totp_import") {
|
||||||
"password_import has incorrect value type".to_string(),
|
// Get the map.
|
||||||
))
|
let totps = vs.as_totp_map().ok_or_else(|| {
|
||||||
})?;
|
OperationError::Plugin(PluginError::CredImport(
|
||||||
|
"totp_import has incorrect value type - should be a map of totp"
|
||||||
|
.to_string(),
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
|
||||||
// convert the import_password to a cred
|
if let Some(c) = e.get_ava_single_credential("primary_credential") {
|
||||||
let pw = Password::try_from(im_pw).map_err(|_| {
|
let c = totps.iter().fold(c.clone(), |acc, (label, totp)| {
|
||||||
OperationError::Plugin(PluginError::PasswordImport(
|
acc.append_totp(label.clone(), totp.clone())
|
||||||
"password_import was unable to convert hash format".to_string(),
|
});
|
||||||
))
|
|
||||||
})?;
|
|
||||||
|
|
||||||
// does the entry have a primary cred?
|
|
||||||
match e.get_ava_single_credential("primary_credential") {
|
|
||||||
Some(c) => {
|
|
||||||
// This is the major diff to create, we can update in place!
|
|
||||||
let c = c.update_password(pw);
|
|
||||||
e.set_ava(
|
e.set_ava(
|
||||||
"primary_credential",
|
"primary_credential",
|
||||||
once(Value::new_credential("primary", c)),
|
once(Value::new_credential("primary", c)),
|
||||||
);
|
);
|
||||||
Ok(())
|
} else {
|
||||||
}
|
return Err(OperationError::Plugin(PluginError::CredImport(
|
||||||
None => {
|
"totp_import can not be used if primary_credential (password) is missing"
|
||||||
// just set it then!
|
.to_string(),
|
||||||
let c = Credential::new_from_password(pw);
|
)));
|
||||||
e.set_ava(
|
|
||||||
"primary_credential",
|
|
||||||
once(Value::new_credential("primary", c)),
|
|
||||||
);
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -149,6 +130,7 @@ mod tests {
|
||||||
use crate::credential::totp::{Totp, TOTP_DEFAULT_STEP};
|
use crate::credential::totp::{Totp, TOTP_DEFAULT_STEP};
|
||||||
use crate::credential::{Credential, CredentialType};
|
use crate::credential::{Credential, CredentialType};
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
use kanidm_proto::v1::PluginError;
|
||||||
|
|
||||||
const IMPORT_HASH: &'static str =
|
const IMPORT_HASH: &'static str =
|
||||||
"pbkdf2_sha256$36000$xIEozuZVAoYm$uW1b35DUKyhvQAf1mBqMvoBDcqSD06juzyO/nmyV0+w=";
|
"pbkdf2_sha256$36000$xIEozuZVAoYm$uW1b35DUKyhvQAf1mBqMvoBDcqSD06juzyO/nmyV0+w=";
|
||||||
|
@ -285,7 +267,7 @@ mod tests {
|
||||||
.expect("failed to get primary cred.");
|
.expect("failed to get primary cred.");
|
||||||
match &c.type_ {
|
match &c.type_ {
|
||||||
CredentialType::PasswordMfa(_pw, totp, webauthn, backup_code) => {
|
CredentialType::PasswordMfa(_pw, totp, webauthn, backup_code) => {
|
||||||
assert!(!totp.is_empty());
|
assert!(totp.len() == 1);
|
||||||
assert!(webauthn.is_empty());
|
assert!(webauthn.is_empty());
|
||||||
assert!(backup_code.is_none());
|
assert!(backup_code.is_none());
|
||||||
}
|
}
|
||||||
|
@ -294,4 +276,96 @@ mod tests {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_modify_cred_import_pw_and_multi_totp() {
|
||||||
|
let euuid = Uuid::new_v4();
|
||||||
|
|
||||||
|
let ea = entry_init!(
|
||||||
|
("class", Value::new_class("account")),
|
||||||
|
("class", Value::new_class("person")),
|
||||||
|
("name", Value::new_iname("testperson")),
|
||||||
|
("description", Value::Utf8("testperson".to_string())),
|
||||||
|
("displayname", Value::Utf8("testperson".to_string())),
|
||||||
|
("uuid", Value::Uuid(euuid))
|
||||||
|
);
|
||||||
|
|
||||||
|
let preload = vec![ea];
|
||||||
|
|
||||||
|
let totp_a = Totp::generate_secure(TOTP_DEFAULT_STEP);
|
||||||
|
let totp_b = Totp::generate_secure(TOTP_DEFAULT_STEP);
|
||||||
|
|
||||||
|
run_modify_test!(
|
||||||
|
Ok(()),
|
||||||
|
preload,
|
||||||
|
filter!(f_eq("name", PartialValue::new_iutf8("testperson"))),
|
||||||
|
ModifyList::new_list(vec![
|
||||||
|
Modify::Present(
|
||||||
|
AttrString::from("password_import"),
|
||||||
|
Value::Utf8(IMPORT_HASH.to_string())
|
||||||
|
),
|
||||||
|
Modify::Present(
|
||||||
|
AttrString::from("totp_import"),
|
||||||
|
Value::TotpSecret("a".to_string(), totp_a.clone())
|
||||||
|
),
|
||||||
|
Modify::Present(
|
||||||
|
AttrString::from("totp_import"),
|
||||||
|
Value::TotpSecret("b".to_string(), totp_b.clone())
|
||||||
|
)
|
||||||
|
]),
|
||||||
|
None,
|
||||||
|
|_| {},
|
||||||
|
|qs: &mut QueryServerWriteTransaction| {
|
||||||
|
let e = qs.internal_search_uuid(euuid).expect("failed to get entry");
|
||||||
|
let c = e
|
||||||
|
.get_ava_single_credential("primary_credential")
|
||||||
|
.expect("failed to get primary cred.");
|
||||||
|
match &c.type_ {
|
||||||
|
CredentialType::PasswordMfa(_pw, totp, webauthn, backup_code) => {
|
||||||
|
assert!(totp.len() == 2);
|
||||||
|
assert!(webauthn.is_empty());
|
||||||
|
assert!(backup_code.is_none());
|
||||||
|
|
||||||
|
assert!(totp.get("a") == Some(&totp_a));
|
||||||
|
assert!(totp.get("b") == Some(&totp_b));
|
||||||
|
}
|
||||||
|
_ => assert!(false),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_modify_cred_import_pw_missing_with_totp() {
|
||||||
|
let euuid = Uuid::new_v4();
|
||||||
|
|
||||||
|
let ea = entry_init!(
|
||||||
|
("class", Value::new_class("account")),
|
||||||
|
("class", Value::new_class("person")),
|
||||||
|
("name", Value::new_iname("testperson")),
|
||||||
|
("description", Value::Utf8("testperson".to_string())),
|
||||||
|
("displayname", Value::Utf8("testperson".to_string())),
|
||||||
|
("uuid", Value::Uuid(euuid))
|
||||||
|
);
|
||||||
|
|
||||||
|
let preload = vec![ea];
|
||||||
|
|
||||||
|
let totp_a = Totp::generate_secure(TOTP_DEFAULT_STEP);
|
||||||
|
|
||||||
|
run_modify_test!(
|
||||||
|
Err(OperationError::Plugin(PluginError::CredImport(
|
||||||
|
"totp_import can not be used if primary_credential (password) is missing"
|
||||||
|
.to_string()
|
||||||
|
))),
|
||||||
|
preload,
|
||||||
|
filter!(f_eq("name", PartialValue::new_iutf8("testperson"))),
|
||||||
|
ModifyList::new_list(vec![Modify::Present(
|
||||||
|
AttrString::from("totp_import"),
|
||||||
|
Value::TotpSecret("a".to_string(), totp_a.clone())
|
||||||
|
)]),
|
||||||
|
None,
|
||||||
|
|_| {},
|
||||||
|
|_| {}
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -13,12 +13,12 @@ use crate::prelude::*;
|
||||||
|
|
||||||
mod attrunique;
|
mod attrunique;
|
||||||
mod base;
|
mod base;
|
||||||
|
mod cred_import;
|
||||||
mod domain;
|
mod domain;
|
||||||
pub(crate) mod dyngroup;
|
pub(crate) mod dyngroup;
|
||||||
mod gidnumber;
|
mod gidnumber;
|
||||||
mod jwskeygen;
|
mod jwskeygen;
|
||||||
mod memberof;
|
mod memberof;
|
||||||
mod password_import;
|
|
||||||
mod protected;
|
mod protected;
|
||||||
mod refint;
|
mod refint;
|
||||||
mod session;
|
mod session;
|
||||||
|
@ -151,7 +151,7 @@ impl Plugins {
|
||||||
ce: &CreateEvent,
|
ce: &CreateEvent,
|
||||||
) -> Result<(), OperationError> {
|
) -> Result<(), OperationError> {
|
||||||
base::Base::pre_create_transform(qs, cand, ce)
|
base::Base::pre_create_transform(qs, cand, ce)
|
||||||
.and_then(|_| password_import::PasswordImport::pre_create_transform(qs, cand, ce))
|
.and_then(|_| cred_import::CredImport::pre_create_transform(qs, cand, ce))
|
||||||
.and_then(|_| jwskeygen::JwsKeygen::pre_create_transform(qs, cand, ce))
|
.and_then(|_| jwskeygen::JwsKeygen::pre_create_transform(qs, cand, ce))
|
||||||
.and_then(|_| gidnumber::GidNumber::pre_create_transform(qs, cand, ce))
|
.and_then(|_| gidnumber::GidNumber::pre_create_transform(qs, cand, ce))
|
||||||
.and_then(|_| domain::Domain::pre_create_transform(qs, cand, ce))
|
.and_then(|_| domain::Domain::pre_create_transform(qs, cand, ce))
|
||||||
|
@ -187,7 +187,7 @@ impl Plugins {
|
||||||
) -> Result<(), OperationError> {
|
) -> Result<(), OperationError> {
|
||||||
protected::Protected::pre_modify(qs, cand, me)
|
protected::Protected::pre_modify(qs, cand, me)
|
||||||
.and_then(|_| base::Base::pre_modify(qs, cand, me))
|
.and_then(|_| base::Base::pre_modify(qs, cand, me))
|
||||||
.and_then(|_| password_import::PasswordImport::pre_modify(qs, cand, me))
|
.and_then(|_| cred_import::CredImport::pre_modify(qs, cand, me))
|
||||||
.and_then(|_| jwskeygen::JwsKeygen::pre_modify(qs, cand, me))
|
.and_then(|_| jwskeygen::JwsKeygen::pre_modify(qs, cand, me))
|
||||||
.and_then(|_| gidnumber::GidNumber::pre_modify(qs, cand, me))
|
.and_then(|_| gidnumber::GidNumber::pre_modify(qs, cand, me))
|
||||||
.and_then(|_| domain::Domain::pre_modify(qs, cand, me))
|
.and_then(|_| domain::Domain::pre_modify(qs, cand, me))
|
||||||
|
@ -217,7 +217,7 @@ impl Plugins {
|
||||||
) -> Result<(), OperationError> {
|
) -> Result<(), OperationError> {
|
||||||
protected::Protected::pre_batch_modify(qs, cand, me)
|
protected::Protected::pre_batch_modify(qs, cand, me)
|
||||||
.and_then(|_| base::Base::pre_batch_modify(qs, cand, me))
|
.and_then(|_| base::Base::pre_batch_modify(qs, cand, me))
|
||||||
.and_then(|_| password_import::PasswordImport::pre_batch_modify(qs, cand, me))
|
.and_then(|_| cred_import::CredImport::pre_batch_modify(qs, cand, me))
|
||||||
.and_then(|_| jwskeygen::JwsKeygen::pre_batch_modify(qs, cand, me))
|
.and_then(|_| jwskeygen::JwsKeygen::pre_batch_modify(qs, cand, me))
|
||||||
.and_then(|_| gidnumber::GidNumber::pre_batch_modify(qs, cand, me))
|
.and_then(|_| gidnumber::GidNumber::pre_batch_modify(qs, cand, me))
|
||||||
.and_then(|_| domain::Domain::pre_batch_modify(qs, cand, me))
|
.and_then(|_| domain::Domain::pre_batch_modify(qs, cand, me))
|
||||||
|
|
|
@ -203,6 +203,8 @@ impl SchemaAttribute {
|
||||||
SyntaxType::JwsKeyEs256 => matches!(v, PartialValue::Iutf8(_)),
|
SyntaxType::JwsKeyEs256 => matches!(v, PartialValue::Iutf8(_)),
|
||||||
SyntaxType::JwsKeyRs256 => matches!(v, PartialValue::Iutf8(_)),
|
SyntaxType::JwsKeyRs256 => matches!(v, PartialValue::Iutf8(_)),
|
||||||
SyntaxType::UiHint => matches!(v, PartialValue::UiHint(_)),
|
SyntaxType::UiHint => matches!(v, PartialValue::UiHint(_)),
|
||||||
|
// Comparing on the label.
|
||||||
|
SyntaxType::TotpSecret => matches!(v, PartialValue::Utf8(_)),
|
||||||
};
|
};
|
||||||
if r {
|
if r {
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -250,6 +252,7 @@ impl SchemaAttribute {
|
||||||
SyntaxType::JwsKeyEs256 => matches!(v, Value::JwsKeyEs256(_)),
|
SyntaxType::JwsKeyEs256 => matches!(v, Value::JwsKeyEs256(_)),
|
||||||
SyntaxType::JwsKeyRs256 => matches!(v, Value::JwsKeyRs256(_)),
|
SyntaxType::JwsKeyRs256 => matches!(v, Value::JwsKeyRs256(_)),
|
||||||
SyntaxType::UiHint => matches!(v, Value::UiHint(_)),
|
SyntaxType::UiHint => matches!(v, Value::UiHint(_)),
|
||||||
|
SyntaxType::TotpSecret => matches!(v, Value::TotpSecret(_, _)),
|
||||||
};
|
};
|
||||||
if r {
|
if r {
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -1317,6 +1320,21 @@ impl<'a> SchemaWriteTransaction<'a> {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
self.attributes.insert(
|
||||||
|
AttrString::from("totp_import"),
|
||||||
|
SchemaAttribute {
|
||||||
|
name: AttrString::from("totp_import"),
|
||||||
|
uuid: UUID_SCHEMA_ATTR_TOTP_IMPORT,
|
||||||
|
description: String::from("An imported totp secret from an external system."),
|
||||||
|
multivalue: true,
|
||||||
|
unique: false,
|
||||||
|
phantom: true,
|
||||||
|
sync_allowed: true,
|
||||||
|
index: vec![],
|
||||||
|
syntax: SyntaxType::TotpSecret,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// LDAP Masking Phantoms
|
// LDAP Masking Phantoms
|
||||||
self.attributes.insert(
|
self.attributes.insert(
|
||||||
AttrString::from("dn"),
|
AttrString::from("dn"),
|
||||||
|
|
|
@ -499,6 +499,7 @@ pub trait QueryServerTransaction<'a> {
|
||||||
SyntaxType::UiHint => UiHint::from_str(value)
|
SyntaxType::UiHint => UiHint::from_str(value)
|
||||||
.map(Value::UiHint)
|
.map(Value::UiHint)
|
||||||
.map_err(|()| OperationError::InvalidAttribute("Invalid uihint syntax".to_string())),
|
.map_err(|()| OperationError::InvalidAttribute("Invalid uihint syntax".to_string())),
|
||||||
|
SyntaxType::TotpSecret => Err(OperationError::InvalidAttribute("TotpSecret Values can not be supplied through modification".to_string())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
|
@ -520,7 +521,9 @@ pub trait QueryServerTransaction<'a> {
|
||||||
match schema.get_attributes().get(attr) {
|
match schema.get_attributes().get(attr) {
|
||||||
Some(schema_a) => {
|
Some(schema_a) => {
|
||||||
match schema_a.syntax {
|
match schema_a.syntax {
|
||||||
SyntaxType::Utf8String => Ok(PartialValue::new_utf8(value.to_string())),
|
SyntaxType::Utf8String | SyntaxType::TotpSecret => {
|
||||||
|
Ok(PartialValue::new_utf8(value.to_string()))
|
||||||
|
}
|
||||||
SyntaxType::Utf8StringInsensitive
|
SyntaxType::Utf8StringInsensitive
|
||||||
| SyntaxType::JwsKeyEs256
|
| SyntaxType::JwsKeyEs256
|
||||||
| SyntaxType::JwsKeyRs256 => Ok(PartialValue::new_iutf8(value)),
|
| SyntaxType::JwsKeyRs256 => Ok(PartialValue::new_iutf8(value)),
|
||||||
|
|
|
@ -23,7 +23,7 @@ use uuid::Uuid;
|
||||||
use webauthn_rs::prelude::{DeviceKey as DeviceKeyV4, Passkey as PasskeyV4};
|
use webauthn_rs::prelude::{DeviceKey as DeviceKeyV4, Passkey as PasskeyV4};
|
||||||
|
|
||||||
use crate::be::dbentry::DbIdentSpn;
|
use crate::be::dbentry::DbIdentSpn;
|
||||||
use crate::credential::Credential;
|
use crate::credential::{totp::Totp, Credential};
|
||||||
use crate::repl::cid::Cid;
|
use crate::repl::cid::Cid;
|
||||||
use crate::server::identity::{AccessScope, IdentityId};
|
use crate::server::identity::{AccessScope, IdentityId};
|
||||||
|
|
||||||
|
@ -198,6 +198,7 @@ pub enum SyntaxType {
|
||||||
JwsKeyRs256 = 27,
|
JwsKeyRs256 = 27,
|
||||||
Oauth2Session = 28,
|
Oauth2Session = 28,
|
||||||
UiHint = 29,
|
UiHint = 29,
|
||||||
|
TotpSecret = 30,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<&str> for SyntaxType {
|
impl TryFrom<&str> for SyntaxType {
|
||||||
|
@ -237,6 +238,7 @@ impl TryFrom<&str> for SyntaxType {
|
||||||
"JWS_KEY_RS256" => Ok(SyntaxType::JwsKeyRs256),
|
"JWS_KEY_RS256" => Ok(SyntaxType::JwsKeyRs256),
|
||||||
"OAUTH2SESSION" => Ok(SyntaxType::Oauth2Session),
|
"OAUTH2SESSION" => Ok(SyntaxType::Oauth2Session),
|
||||||
"UIHINT" => Ok(SyntaxType::UiHint),
|
"UIHINT" => Ok(SyntaxType::UiHint),
|
||||||
|
"TOTPSECRET" => Ok(SyntaxType::TotpSecret),
|
||||||
_ => Err(()),
|
_ => Err(()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -275,6 +277,7 @@ impl fmt::Display for SyntaxType {
|
||||||
SyntaxType::JwsKeyRs256 => "JWS_KEY_RS256",
|
SyntaxType::JwsKeyRs256 => "JWS_KEY_RS256",
|
||||||
SyntaxType::Oauth2Session => "OAUTH2SESSION",
|
SyntaxType::Oauth2Session => "OAUTH2SESSION",
|
||||||
SyntaxType::UiHint => "UIHINT",
|
SyntaxType::UiHint => "UIHINT",
|
||||||
|
SyntaxType::TotpSecret => "TOTPSECRET",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -321,12 +324,11 @@ pub enum PartialValue {
|
||||||
RestrictedString(String),
|
RestrictedString(String),
|
||||||
IntentToken(String),
|
IntentToken(String),
|
||||||
UiHint(UiHint),
|
UiHint(UiHint),
|
||||||
|
|
||||||
Passkey(Uuid),
|
Passkey(Uuid),
|
||||||
DeviceKey(Uuid),
|
DeviceKey(Uuid),
|
||||||
|
|
||||||
TrustedDeviceEnrollment(Uuid),
|
TrustedDeviceEnrollment(Uuid),
|
||||||
Session(Uuid),
|
Session(Uuid),
|
||||||
|
// The label, if any.
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<SyntaxType> for PartialValue {
|
impl From<SyntaxType> for PartialValue {
|
||||||
|
@ -773,6 +775,8 @@ pub enum Value {
|
||||||
JwsKeyEs256(JwsSigner),
|
JwsKeyEs256(JwsSigner),
|
||||||
JwsKeyRs256(JwsSigner),
|
JwsKeyRs256(JwsSigner),
|
||||||
UiHint(UiHint),
|
UiHint(UiHint),
|
||||||
|
|
||||||
|
TotpSecret(String, Totp),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialEq for Value {
|
impl PartialEq for Value {
|
||||||
|
|
|
@ -12,7 +12,7 @@ use webauthn_rs::prelude::DeviceKey as DeviceKeyV4;
|
||||||
use webauthn_rs::prelude::Passkey as PasskeyV4;
|
use webauthn_rs::prelude::Passkey as PasskeyV4;
|
||||||
|
|
||||||
use crate::be::dbvalue::DbValueSetV2;
|
use crate::be::dbvalue::DbValueSetV2;
|
||||||
use crate::credential::Credential;
|
use crate::credential::{totp::Totp, Credential};
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::repl::cid::Cid;
|
use crate::repl::cid::Cid;
|
||||||
use crate::schema::SchemaAttribute;
|
use crate::schema::SchemaAttribute;
|
||||||
|
@ -37,6 +37,7 @@ mod session;
|
||||||
mod spn;
|
mod spn;
|
||||||
mod ssh;
|
mod ssh;
|
||||||
mod syntax;
|
mod syntax;
|
||||||
|
mod totp;
|
||||||
mod uihint;
|
mod uihint;
|
||||||
mod uint32;
|
mod uint32;
|
||||||
mod url;
|
mod url;
|
||||||
|
@ -62,6 +63,7 @@ pub use self::session::{ValueSetOauth2Session, ValueSetSession};
|
||||||
pub use self::spn::ValueSetSpn;
|
pub use self::spn::ValueSetSpn;
|
||||||
pub use self::ssh::ValueSetSshKey;
|
pub use self::ssh::ValueSetSshKey;
|
||||||
pub use self::syntax::ValueSetSyntax;
|
pub use self::syntax::ValueSetSyntax;
|
||||||
|
pub use self::totp::ValueSetTotpSecret;
|
||||||
pub use self::uihint::ValueSetUiHint;
|
pub use self::uihint::ValueSetUiHint;
|
||||||
pub use self::uint32::ValueSetUint32;
|
pub use self::uint32::ValueSetUint32;
|
||||||
pub use self::url::ValueSetUrl;
|
pub use self::url::ValueSetUrl;
|
||||||
|
@ -283,6 +285,11 @@ pub trait ValueSetT: std::fmt::Debug + DynClone {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn as_totp_map(&self) -> Option<&BTreeMap<String, Totp>> {
|
||||||
|
debug_assert!(false);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
fn as_emailaddress_set(&self) -> Option<(&String, &BTreeSet<String>)> {
|
fn as_emailaddress_set(&self) -> Option<(&String, &BTreeSet<String>)> {
|
||||||
debug_assert!(false);
|
debug_assert!(false);
|
||||||
None
|
None
|
||||||
|
@ -563,6 +570,7 @@ pub fn from_result_value_iter(
|
||||||
Value::PhoneNumber(_, _)
|
Value::PhoneNumber(_, _)
|
||||||
| Value::Passkey(_, _, _)
|
| Value::Passkey(_, _, _)
|
||||||
| Value::DeviceKey(_, _, _)
|
| Value::DeviceKey(_, _, _)
|
||||||
|
| Value::TotpSecret(_, _)
|
||||||
| Value::TrustedDeviceEnrollment(_)
|
| Value::TrustedDeviceEnrollment(_)
|
||||||
| Value::Session(_, _)
|
| Value::Session(_, _)
|
||||||
| Value::Oauth2Session(_, _)
|
| Value::Oauth2Session(_, _)
|
||||||
|
@ -623,6 +631,7 @@ pub fn from_value_iter(mut iter: impl Iterator<Item = Value>) -> Result<ValueSet
|
||||||
Value::Session(u, m) => ValueSetSession::new(u, m),
|
Value::Session(u, m) => ValueSetSession::new(u, m),
|
||||||
Value::Oauth2Session(u, m) => ValueSetOauth2Session::new(u, m),
|
Value::Oauth2Session(u, m) => ValueSetOauth2Session::new(u, m),
|
||||||
Value::UiHint(u) => ValueSetUiHint::new(u),
|
Value::UiHint(u) => ValueSetUiHint::new(u),
|
||||||
|
Value::TotpSecret(l, t) => ValueSetTotpSecret::new(l, t),
|
||||||
Value::PhoneNumber(_, _) | Value::TrustedDeviceEnrollment(_) => {
|
Value::PhoneNumber(_, _) | Value::TrustedDeviceEnrollment(_) => {
|
||||||
debug_assert!(false);
|
debug_assert!(false);
|
||||||
return Err(OperationError::InvalidValueState);
|
return Err(OperationError::InvalidValueState);
|
||||||
|
@ -670,6 +679,7 @@ pub fn from_db_valueset_v2(dbvs: DbValueSetV2) -> Result<ValueSet, OperationErro
|
||||||
DbValueSetV2::JwsKeyEs256(set) => ValueSetJwsKeyEs256::from_dbvs2(&set),
|
DbValueSetV2::JwsKeyEs256(set) => ValueSetJwsKeyEs256::from_dbvs2(&set),
|
||||||
DbValueSetV2::JwsKeyRs256(set) => ValueSetJwsKeyEs256::from_dbvs2(&set),
|
DbValueSetV2::JwsKeyRs256(set) => ValueSetJwsKeyEs256::from_dbvs2(&set),
|
||||||
DbValueSetV2::UiHint(set) => ValueSetUiHint::from_dbvs2(set),
|
DbValueSetV2::UiHint(set) => ValueSetUiHint::from_dbvs2(set),
|
||||||
|
DbValueSetV2::TotpSecret(set) => ValueSetTotpSecret::from_dbvs2(set),
|
||||||
DbValueSetV2::PhoneNumber(_, _) | DbValueSetV2::TrustedDeviceEnrollment(_) => {
|
DbValueSetV2::PhoneNumber(_, _) | DbValueSetV2::TrustedDeviceEnrollment(_) => {
|
||||||
debug_assert!(false);
|
debug_assert!(false);
|
||||||
Err(OperationError::InvalidValueState)
|
Err(OperationError::InvalidValueState)
|
||||||
|
|
164
kanidmd/lib/src/valueset/totp.rs
Normal file
164
kanidmd/lib/src/valueset/totp.rs
Normal file
|
@ -0,0 +1,164 @@
|
||||||
|
use std::collections::btree_map::Entry as BTreeEntry;
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
|
use crate::credential::totp::Totp;
|
||||||
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
use crate::be::dbvalue::DbTotpV1;
|
||||||
|
use crate::schema::SchemaAttribute;
|
||||||
|
use crate::valueset::{DbValueSetV2, ValueSet};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ValueSetTotpSecret {
|
||||||
|
map: BTreeMap<String, Totp>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ValueSetTotpSecret {
|
||||||
|
pub fn new(l: String, t: Totp) -> Box<Self> {
|
||||||
|
let mut map = BTreeMap::new();
|
||||||
|
map.insert(l, t);
|
||||||
|
Box::new(ValueSetTotpSecret { map })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn push(&mut self, l: String, t: Totp) -> bool {
|
||||||
|
self.map.insert(l, t).is_none()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_dbvs2(data: Vec<(String, DbTotpV1)>) -> Result<ValueSet, OperationError> {
|
||||||
|
let map = data
|
||||||
|
.into_iter()
|
||||||
|
.map(|(l, data)| {
|
||||||
|
Totp::try_from(data)
|
||||||
|
.map_err(|()| OperationError::InvalidValueState)
|
||||||
|
.map(|t| (l, t))
|
||||||
|
})
|
||||||
|
.collect::<Result<_, _>>()?;
|
||||||
|
Ok(Box::new(ValueSetTotpSecret { map }))
|
||||||
|
}
|
||||||
|
|
||||||
|
// We need to allow this, because rust doesn't allow us to impl FromIterator on foreign
|
||||||
|
// types, and tuples are always foreign.
|
||||||
|
#[allow(clippy::should_implement_trait)]
|
||||||
|
pub fn from_iter<T>(iter: T) -> Option<Box<Self>>
|
||||||
|
where
|
||||||
|
T: IntoIterator<Item = (String, Totp)>,
|
||||||
|
{
|
||||||
|
let map = iter.into_iter().collect();
|
||||||
|
Some(Box::new(ValueSetTotpSecret { map }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ValueSetT for ValueSetTotpSecret {
|
||||||
|
fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError> {
|
||||||
|
match value {
|
||||||
|
Value::TotpSecret(l, t) => {
|
||||||
|
if let BTreeEntry::Vacant(e) = self.map.entry(l) {
|
||||||
|
e.insert(t);
|
||||||
|
Ok(true)
|
||||||
|
} else {
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => Err(OperationError::InvalidValueState),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clear(&mut self) {
|
||||||
|
self.map.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove(&mut self, pv: &PartialValue) -> bool {
|
||||||
|
match pv {
|
||||||
|
PartialValue::Utf8(l) => self.map.remove(l.as_str()).is_some(),
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn contains(&self, pv: &PartialValue) -> bool {
|
||||||
|
match pv {
|
||||||
|
PartialValue::Utf8(l) => self.map.contains_key(l.as_str()),
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn substring(&self, _pv: &PartialValue) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lessthan(&self, _pv: &PartialValue) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn len(&self) -> usize {
|
||||||
|
self.map.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_idx_eq_keys(&self) -> Vec<String> {
|
||||||
|
self.map.keys().cloned().collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn syntax(&self) -> SyntaxType {
|
||||||
|
SyntaxType::TotpSecret
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate(&self, _schema_attr: &SchemaAttribute) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_proto_string_clone_iter(&self) -> Box<dyn Iterator<Item = String> + '_> {
|
||||||
|
Box::new(self.map.keys().cloned())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
|
||||||
|
DbValueSetV2::TotpSecret(
|
||||||
|
self.map
|
||||||
|
.iter()
|
||||||
|
.map(|(label, totp)| (label.clone(), totp.to_dbtotpv1()))
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_partialvalue_iter(&self) -> Box<dyn Iterator<Item = PartialValue> + '_> {
|
||||||
|
Box::new(self.map.keys().cloned().map(PartialValue::Utf8))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_value_iter(&self) -> Box<dyn Iterator<Item = Value> + '_> {
|
||||||
|
Box::new(
|
||||||
|
self.map
|
||||||
|
.iter()
|
||||||
|
.map(|(l, t)| Value::TotpSecret(l.clone(), t.clone())),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn equal(&self, _other: &ValueSet) -> bool {
|
||||||
|
// Looks like we may not need this?
|
||||||
|
/*
|
||||||
|
if let Some(other) = other.as_credential_map() {
|
||||||
|
&self.map == other
|
||||||
|
} else {
|
||||||
|
// debug_assert!(false);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
debug_assert!(false);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn merge(&mut self, _other: &ValueSet) -> Result<(), OperationError> {
|
||||||
|
/*
|
||||||
|
if let Some(b) = other.as_credential_map() {
|
||||||
|
mergemaps!(self.map, b)
|
||||||
|
} else {
|
||||||
|
debug_assert!(false);
|
||||||
|
Err(OperationError::InvalidValueState)
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
debug_assert!(false);
|
||||||
|
Err(OperationError::InvalidValueState)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_totp_map(&self) -> Option<&BTreeMap<String, Totp>> {
|
||||||
|
Some(&self.map)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1014,7 +1014,7 @@ async fn test_server_credential_update_session_totp_pw(rsclient: KanidmClient) {
|
||||||
|
|
||||||
// Extract the totp from the status, and set it back
|
// Extract the totp from the status, and set it back
|
||||||
let totp: Totp = match status.mfaregstate {
|
let totp: Totp = match status.mfaregstate {
|
||||||
CURegState::TotpCheck(totp_secret) => totp_secret.into(),
|
CURegState::TotpCheck(totp_secret) => totp_secret.try_into().unwrap(),
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue