mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 12:37:00 +01:00
1121 SCIM import totp freeipa (#1328)
This commit is contained in:
parent
84fc7d0bac
commit
00cf5f4e15
|
@ -14,15 +14,16 @@
|
|||
mod config;
|
||||
mod error;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
// #[cfg(test)]
|
||||
// mod tests;
|
||||
|
||||
use crate::config::{Config, EntryConfig};
|
||||
use crate::error::SyncError;
|
||||
use base64urlsafedata::Base64UrlSafeData;
|
||||
use chrono::Utc;
|
||||
use clap::Parser;
|
||||
use cron::Schedule;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::fs::metadata;
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
|
@ -48,13 +49,14 @@ use uuid::Uuid;
|
|||
use kanidm_client::KanidmClientBuilder;
|
||||
use kanidm_proto::scim_v1::{
|
||||
ScimEntry, ScimExternalMember, ScimSyncGroup, ScimSyncPerson, ScimSyncRequest, ScimSyncState,
|
||||
ScimTotp,
|
||||
};
|
||||
use kanidmd_lib::utils::file_permissions_readonly;
|
||||
|
||||
use users::{get_current_gid, get_current_uid, get_effective_gid, get_effective_uid};
|
||||
|
||||
use ldap3_client::{
|
||||
proto, proto::LdapFilter, LdapClientBuilder, LdapSyncRepl, LdapSyncReplEntry,
|
||||
proto, proto::LdapFilter, LdapClient, LdapClientBuilder, LdapSyncRepl, LdapSyncReplEntry,
|
||||
LdapSyncStateValue,
|
||||
};
|
||||
|
||||
|
@ -331,6 +333,8 @@ async fn run_sync(
|
|||
ScimSyncState::Active { cookie } => Some(cookie.0.clone()),
|
||||
};
|
||||
|
||||
let is_initialise = cookie.is_none();
|
||||
|
||||
let filter = LdapFilter::Or(vec![
|
||||
// LdapFilter::Equality("objectclass".to_string(), "domain".to_string()),
|
||||
LdapFilter::And(vec![
|
||||
|
@ -341,6 +345,7 @@ async fn run_sync(
|
|||
LdapFilter::And(vec![
|
||||
LdapFilter::Equality("objectclass".to_string(), "groupofnames".to_string()),
|
||||
LdapFilter::Equality("objectclass".to_string(), "ipausergroup".to_string()),
|
||||
// Ignore user private groups, kani generates these internally.
|
||||
LdapFilter::Not(Box::new(LdapFilter::Equality(
|
||||
"objectclass".to_string(),
|
||||
"mepmanagedentry".to_string(),
|
||||
|
@ -356,6 +361,7 @@ async fn run_sync(
|
|||
"ipausers".to_string(),
|
||||
))),
|
||||
]),
|
||||
// Fetch TOTP's so we know when/if they change.
|
||||
LdapFilter::And(vec![
|
||||
LdapFilter::Equality("objectclass".to_string(), "ipatoken".to_string()),
|
||||
LdapFilter::Equality("objectclass".to_string(), "ipatokentotp".to_string()),
|
||||
|
@ -381,17 +387,69 @@ async fn run_sync(
|
|||
}
|
||||
}
|
||||
|
||||
// pre-process the entries.
|
||||
// - > fn so we can test.
|
||||
let scim_sync_request = match process_ipa_sync_result(
|
||||
scim_sync_status,
|
||||
sync_result,
|
||||
// Convert the ldap sync repl result to a scim equivalent
|
||||
let scim_sync_request = match sync_result {
|
||||
LdapSyncRepl::Success {
|
||||
cookie,
|
||||
refresh_deletes,
|
||||
entries,
|
||||
delete_uuids,
|
||||
present_uuids,
|
||||
} => {
|
||||
if refresh_deletes {
|
||||
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(()) => return Err(SyncError::Preprocess),
|
||||
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 {
|
||||
|
@ -421,45 +479,178 @@ async fn run_sync(
|
|||
}
|
||||
|
||||
async fn process_ipa_sync_result(
|
||||
from_state: ScimSyncState,
|
||||
sync_result: LdapSyncRepl,
|
||||
_ipa_client: LdapClient,
|
||||
ldap_entries: Vec<LdapSyncReplEntry>,
|
||||
entry_config_map: &HashMap<Uuid, EntryConfig>,
|
||||
) -> Result<ScimSyncRequest, ()> {
|
||||
match sync_result {
|
||||
LdapSyncRepl::Success {
|
||||
cookie,
|
||||
refresh_deletes,
|
||||
entries,
|
||||
delete_uuids,
|
||||
present_uuids,
|
||||
} => {
|
||||
if refresh_deletes {
|
||||
error!("Unsure how to handle refreshDeletes=True");
|
||||
return Err(());
|
||||
is_initialise: 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
|
||||
// we see those as unique entries in the syncrepl but we are missing
|
||||
// the user entry that actually needs the update since Kanidm makes TOTP
|
||||
// part of the entry itself.
|
||||
//
|
||||
// This *also* means that when a user is updated that we also need to
|
||||
// fetch their TOTP's that are related so we can assert them on the
|
||||
// submission.
|
||||
//
|
||||
// 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.
|
||||
|
||||
// Hash entries by DN -> Split TOTP's to their own set.
|
||||
// make a list of updated TOTP's and what DN's they require.
|
||||
// make a list of updated Users and what TOTP's they require.
|
||||
|
||||
let mut entries = BTreeMap::default();
|
||||
let mut user_dns = Vec::default();
|
||||
let mut totp_entries: BTreeMap<String, Vec<_>> = BTreeMap::default();
|
||||
|
||||
for lentry in ldap_entries.into_iter() {
|
||||
if lentry
|
||||
.entry
|
||||
.attrs
|
||||
.get("objectclass")
|
||||
.map(|oc| oc.contains("ipatokentotp"))
|
||||
.unwrap_or_default()
|
||||
{
|
||||
// It's an otp. Lets see ...
|
||||
let token_owner_dn = if let Some(todn) = lentry
|
||||
.entry
|
||||
.attrs
|
||||
.get("ipatokenowner")
|
||||
.and_then(|attr| if attr.len() != 1 { None } else { attr.first() })
|
||||
{
|
||||
debug!("totp with owner {}", todn);
|
||||
todn.clone()
|
||||
} else {
|
||||
warn!("totp with invalid ownership will be ignored");
|
||||
continue;
|
||||
};
|
||||
|
||||
if !totp_entries.contains_key(&token_owner_dn) {
|
||||
totp_entries.insert(token_owner_dn.clone(), Vec::default());
|
||||
}
|
||||
|
||||
if !present_uuids.is_empty() {
|
||||
error!("Unsure how to handle presentUuids > 0");
|
||||
return Err(());
|
||||
if let Some(v) = totp_entries.get_mut(&token_owner_dn) {
|
||||
v.push(lentry)
|
||||
}
|
||||
} else {
|
||||
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());
|
||||
}
|
||||
|
||||
let to_state = cookie
|
||||
.map(|cookie| {
|
||||
ScimSyncState::Active { cookie }
|
||||
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()))
|
||||
})
|
||||
.ok_or_else(|| {
|
||||
error!("Invalid state, ldap sync repl did not provide a valid state cookie in response.");
|
||||
})?;
|
||||
.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(|e| {
|
||||
.filter_map(|(dn, e)| {
|
||||
let e_config = entry_config_map
|
||||
.get(&e.entry_uuid)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
match ipa_to_scim_entry(e, &e_config) {
|
||||
|
||||
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(())),
|
||||
|
@ -467,32 +658,7 @@ async fn process_ipa_sync_result(
|
|||
})
|
||||
.collect::<Result<Vec<_>, _>>();
|
||||
|
||||
let entries = match entries {
|
||||
Ok(e) => e,
|
||||
Err(()) => {
|
||||
error!("Failed to process IPA entries to SCIM");
|
||||
return Err(());
|
||||
}
|
||||
};
|
||||
|
||||
Ok(ScimSyncRequest {
|
||||
from_state,
|
||||
to_state,
|
||||
entries,
|
||||
delete_uuids,
|
||||
})
|
||||
}
|
||||
LdapSyncRepl::RefreshRequired => {
|
||||
let to_state = ScimSyncState::Refresh;
|
||||
|
||||
Ok(ScimSyncRequest {
|
||||
from_state,
|
||||
to_state,
|
||||
entries: Vec::new(),
|
||||
delete_uuids: Vec::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
entries
|
||||
}
|
||||
|
||||
// TODO: Allow re-map of uuid -> uuid
|
||||
|
@ -500,6 +666,7 @@ async fn process_ipa_sync_result(
|
|||
fn ipa_to_scim_entry(
|
||||
sync_entry: LdapSyncReplEntry,
|
||||
entry_config: &EntryConfig,
|
||||
totp: &[LdapSyncReplEntry],
|
||||
) -> Result<Option<ScimEntry>, ()> {
|
||||
debug!("{:#?}", sync_entry);
|
||||
|
||||
|
@ -555,6 +722,10 @@ fn ipa_to_scim_entry(
|
|||
let password_import = entry
|
||||
.remove_ava_single("ipanthash")
|
||||
.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 external_id = Some(entry.dn);
|
||||
|
||||
|
@ -566,6 +737,7 @@ fn ipa_to_scim_entry(
|
|||
display_name,
|
||||
gidnumber,
|
||||
password_import,
|
||||
totp_import,
|
||||
login_shell,
|
||||
}
|
||||
.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 {
|
||||
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 =
|
||||
"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)]
|
||||
#[serde(into = "ScimEntry")]
|
||||
pub struct ScimSyncPerson {
|
||||
|
@ -49,6 +90,7 @@ pub struct ScimSyncPerson {
|
|||
pub display_name: String,
|
||||
pub gidnumber: Option<u32>,
|
||||
pub password_import: Option<String>,
|
||||
pub totp_import: Vec<ScimTotp>,
|
||||
pub login_shell: Option<String>,
|
||||
}
|
||||
|
||||
|
@ -64,6 +106,7 @@ impl Into<ScimEntry> for ScimSyncPerson {
|
|||
display_name,
|
||||
gidnumber,
|
||||
password_import,
|
||||
totp_import,
|
||||
login_shell,
|
||||
} = self;
|
||||
|
||||
|
@ -86,6 +129,7 @@ impl Into<ScimEntry> for ScimSyncPerson {
|
|||
set_string!(attrs, "displayname", display_name);
|
||||
set_option_u32!(attrs, "gidnumber", gidnumber);
|
||||
set_option_string!(attrs, "password_import", password_import);
|
||||
set_multi_complex!(attrs, "totp_import", totp_import);
|
||||
set_option_string!(attrs, "loginshell", login_shell);
|
||||
|
||||
ScimEntry {
|
||||
|
|
|
@ -38,7 +38,7 @@ pub enum PluginError {
|
|||
AttrUnique(String),
|
||||
Base(String),
|
||||
ReferentialIntegrity(String),
|
||||
PasswordImport(String),
|
||||
CredImport(String),
|
||||
Oauth2Secrets,
|
||||
}
|
||||
|
||||
|
@ -1076,6 +1076,7 @@ pub struct TotpSecret {
|
|||
pub secret: Vec<u8>,
|
||||
pub algo: TotpAlgo,
|
||||
pub step: u64,
|
||||
pub digits: u8,
|
||||
}
|
||||
|
||||
impl TotpSecret {
|
||||
|
@ -1087,10 +1088,11 @@ impl TotpSecret {
|
|||
let algo = self.algo.to_string();
|
||||
let secret = self.get_secret();
|
||||
let period = self.step;
|
||||
let digits = self.digits;
|
||||
|
||||
format!(
|
||||
"otpauth://totp/{}?secret={}&issuer={}&algorithm={}&digits=6&period={}",
|
||||
label, secret, issuer, algo, period
|
||||
"otpauth://totp/{}?secret={}&issuer={}&algorithm={}&digits={}&period={}",
|
||||
label, secret, issuer, algo, digits, period
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1224,6 +1226,7 @@ mod tests {
|
|||
secret: vec![0xaa, 0xbb, 0xcc, 0xdd],
|
||||
step: 30,
|
||||
algo: TotpAlgo::Sha256,
|
||||
digits: 6,
|
||||
};
|
||||
let s = totp.to_uri();
|
||||
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],
|
||||
step: 30,
|
||||
algo: TotpAlgo::Sha256,
|
||||
digits: 6,
|
||||
};
|
||||
let s = totp.to_uri();
|
||||
println!("{}", s);
|
||||
|
|
|
@ -73,6 +73,8 @@ pub struct DbTotpV1 {
|
|||
pub step: u64,
|
||||
#[serde(rename = "a")]
|
||||
pub algo: DbTotpAlgoV1,
|
||||
#[serde(rename = "d", default)]
|
||||
pub digits: Option<u8>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for DbTotpV1 {
|
||||
|
@ -576,6 +578,8 @@ pub enum DbValueSetV2 {
|
|||
Oauth2Session(Vec<DbValueOauth2Session>),
|
||||
#[serde(rename = "UH")]
|
||||
UiHint(Vec<u16>),
|
||||
#[serde(rename = "TO")]
|
||||
TotpSecret(Vec<(String, DbTotpV1)>),
|
||||
}
|
||||
|
||||
impl DbValueSetV2 {
|
||||
|
@ -616,6 +620,7 @@ impl DbValueSetV2 {
|
|||
DbValueSetV2::JwsKeyEs256(set) => set.len(),
|
||||
DbValueSetV2::JwsKeyRs256(set) => set.len(),
|
||||
DbValueSetV2::UiHint(set) => set.len(),
|
||||
DbValueSetV2::TotpSecret(set) => set.len(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -61,8 +61,8 @@ impl Default for Limits {
|
|||
fn default() -> Self {
|
||||
Limits {
|
||||
unindexed_allow: false,
|
||||
search_max_results: 128,
|
||||
search_max_filter_test: 256,
|
||||
search_max_results: 256,
|
||||
search_max_filter_test: 512,
|
||||
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_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
|
||||
// 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,
|
||||
}
|
||||
|
||||
#[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)]
|
||||
pub enum TotpAlgo {
|
||||
Sha1,
|
||||
|
@ -62,6 +91,7 @@ pub struct Totp {
|
|||
secret: Vec<u8>,
|
||||
pub(crate) step: u64,
|
||||
algo: TotpAlgo,
|
||||
digits: TotpDigits,
|
||||
}
|
||||
|
||||
impl TryFrom<DbTotpV1> for Totp {
|
||||
|
@ -73,17 +103,23 @@ impl TryFrom<DbTotpV1> for Totp {
|
|||
DbTotpAlgoV1::S256 => TotpAlgo::Sha256,
|
||||
DbTotpAlgoV1::S512 => TotpAlgo::Sha512,
|
||||
};
|
||||
// Default.
|
||||
let digits = TotpDigits::try_from(value.digits.unwrap_or(6))?;
|
||||
|
||||
Ok(Totp {
|
||||
secret: value.key,
|
||||
step: value.step,
|
||||
algo,
|
||||
digits,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ProtoTotp> for Totp {
|
||||
fn from(value: ProtoTotp) -> Self {
|
||||
Totp {
|
||||
impl TryFrom<ProtoTotp> for Totp {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: ProtoTotp) -> Result<Self, Self::Error> {
|
||||
Ok(Totp {
|
||||
secret: value.secret,
|
||||
algo: match value.algo {
|
||||
ProtoTotpAlgo::Sha1 => TotpAlgo::Sha1,
|
||||
|
@ -91,13 +127,19 @@ impl From<ProtoTotp> for Totp {
|
|||
ProtoTotpAlgo::Sha512 => TotpAlgo::Sha512,
|
||||
},
|
||||
step: value.step,
|
||||
}
|
||||
digits: TotpDigits::try_from(value.digits)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Totp {
|
||||
pub fn new(secret: Vec<u8>, step: u64, algo: TotpAlgo) -> Self {
|
||||
Totp { secret, step, algo }
|
||||
pub fn new(secret: Vec<u8>, step: u64, algo: TotpAlgo, digits: TotpDigits) -> Self {
|
||||
Totp {
|
||||
secret,
|
||||
step,
|
||||
algo,
|
||||
digits,
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new token with secure key and algo.
|
||||
|
@ -105,7 +147,13 @@ impl Totp {
|
|||
let mut rng = rand::thread_rng();
|
||||
let secret: Vec<u8> = (0..SECRET_SIZE_BYTES).map(|_| rng.gen()).collect();
|
||||
let algo = TotpAlgo::Sha256;
|
||||
Totp { secret, step, algo }
|
||||
let digits = TotpDigits::Six;
|
||||
Totp {
|
||||
secret,
|
||||
step,
|
||||
algo,
|
||||
digits,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn to_dbtotpv1(&self) -> DbTotpV1 {
|
||||
|
@ -118,6 +166,7 @@ impl Totp {
|
|||
TotpAlgo::Sha256 => DbTotpAlgoV1::S256,
|
||||
TotpAlgo::Sha512 => DbTotpAlgoV1::S512,
|
||||
},
|
||||
digits: Some(self.digits.into()),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -134,7 +183,12 @@ impl Totp {
|
|||
.map_err(|_| TotpError::HmacError)?;
|
||||
|
||||
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> {
|
||||
|
@ -173,6 +227,7 @@ impl Totp {
|
|||
TotpAlgo::Sha256 => ProtoTotpAlgo::Sha256,
|
||||
TotpAlgo::Sha512 => ProtoTotpAlgo::Sha512,
|
||||
},
|
||||
digits: self.digits.into(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -185,6 +240,7 @@ impl Totp {
|
|||
secret: self.secret,
|
||||
step: self.step,
|
||||
algo: TotpAlgo::Sha1,
|
||||
digits: self.digits,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -193,20 +249,27 @@ impl Totp {
|
|||
mod tests {
|
||||
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]
|
||||
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));
|
||||
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));
|
||||
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));
|
||||
}
|
||||
|
||||
fn do_test(key: Vec<u8>, algo: TotpAlgo, secs: u64, step: u64, expect: Result<u32, TotpError>) {
|
||||
let otp = Totp::new(key.clone(), step, algo.clone());
|
||||
fn do_test(
|
||||
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 r = otp.do_totp_duration_from_epoch(&d);
|
||||
debug!(
|
||||
|
@ -223,13 +286,23 @@ mod tests {
|
|||
TotpAlgo::Sha1,
|
||||
1585368920,
|
||||
TOTP_DEFAULT_STEP,
|
||||
TotpDigits::Six,
|
||||
Ok(728926),
|
||||
);
|
||||
do_test(
|
||||
vec![0x00, 0x00, 0x00, 0x00],
|
||||
TotpAlgo::Sha1,
|
||||
1585368920,
|
||||
TOTP_DEFAULT_STEP,
|
||||
TotpDigits::Eight,
|
||||
Ok(74728926),
|
||||
);
|
||||
do_test(
|
||||
vec![0x00, 0xaa, 0xbb, 0xcc],
|
||||
TotpAlgo::Sha1,
|
||||
1585369498,
|
||||
TOTP_DEFAULT_STEP,
|
||||
TotpDigits::Six,
|
||||
Ok(985074),
|
||||
);
|
||||
}
|
||||
|
@ -241,13 +314,23 @@ mod tests {
|
|||
TotpAlgo::Sha256,
|
||||
1585369682,
|
||||
TOTP_DEFAULT_STEP,
|
||||
TotpDigits::Six,
|
||||
Ok(795483),
|
||||
);
|
||||
do_test(
|
||||
vec![0x00, 0x00, 0x00, 0x00],
|
||||
TotpAlgo::Sha256,
|
||||
1585369682,
|
||||
TOTP_DEFAULT_STEP,
|
||||
TotpDigits::Eight,
|
||||
Ok(11795483),
|
||||
);
|
||||
do_test(
|
||||
vec![0x00, 0xaa, 0xbb, 0xcc],
|
||||
TotpAlgo::Sha256,
|
||||
1585369689,
|
||||
TOTP_DEFAULT_STEP,
|
||||
TotpDigits::Six,
|
||||
Ok(728402),
|
||||
);
|
||||
}
|
||||
|
@ -259,13 +342,23 @@ mod tests {
|
|||
TotpAlgo::Sha512,
|
||||
1585369775,
|
||||
TOTP_DEFAULT_STEP,
|
||||
TotpDigits::Six,
|
||||
Ok(587735),
|
||||
);
|
||||
do_test(
|
||||
vec![0x00, 0x00, 0x00, 0x00],
|
||||
TotpAlgo::Sha512,
|
||||
1585369775,
|
||||
TOTP_DEFAULT_STEP,
|
||||
TotpDigits::Eight,
|
||||
Ok(14587735),
|
||||
);
|
||||
do_test(
|
||||
vec![0x00, 0xaa, 0xbb, 0xcc],
|
||||
TotpAlgo::Sha512,
|
||||
1585369780,
|
||||
TOTP_DEFAULT_STEP,
|
||||
TotpDigits::Six,
|
||||
Ok(952181),
|
||||
);
|
||||
}
|
||||
|
@ -274,7 +367,12 @@ mod tests {
|
|||
fn totp_allow_one_previous() {
|
||||
let key = vec![0x00, 0xaa, 0xbb, 0xcc];
|
||||
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);
|
||||
// Step
|
||||
assert!(otp.verify(952181, &d));
|
||||
|
|
|
@ -2114,7 +2114,7 @@ mod tests {
|
|||
|
||||
// Check the status has the token.
|
||||
let totp_token: Totp = match c_status.mfaregstate {
|
||||
MfaRegStateStatus::TotpCheck(secret) => Some(secret.into()),
|
||||
MfaRegStateStatus::TotpCheck(secret) => Some(secret.try_into().unwrap()),
|
||||
|
||||
_ => None,
|
||||
}
|
||||
|
@ -2208,7 +2208,7 @@ mod tests {
|
|||
|
||||
// Check the status has the token.
|
||||
let totp_token: Totp = match c_status.mfaregstate {
|
||||
MfaRegStateStatus::TotpCheck(secret) => Some(secret.into()),
|
||||
MfaRegStateStatus::TotpCheck(secret) => Some(secret.try_into().unwrap()),
|
||||
|
||||
_ => None,
|
||||
}
|
||||
|
@ -2283,7 +2283,7 @@ mod tests {
|
|||
.expect("Failed to update the primary cred password");
|
||||
|
||||
let totp_token: Totp = match c_status.mfaregstate {
|
||||
MfaRegStateStatus::TotpCheck(secret) => Some(secret.into()),
|
||||
MfaRegStateStatus::TotpCheck(secret) => Some(secret.try_into().unwrap()),
|
||||
_ => None,
|
||||
}
|
||||
.expect("Unable to retrieve totp token, invalid state.");
|
||||
|
|
|
@ -8,6 +8,7 @@ use kanidm_proto::v1::ApiTokenPurpose;
|
|||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use crate::credential::totp::{Totp, TotpAlgo, TotpDigits};
|
||||
use crate::idm::server::{IdmServerProxyReadTransaction, IdmServerProxyWriteTransaction};
|
||||
use crate::prelude::*;
|
||||
use crate::value::Session;
|
||||
|
@ -876,6 +877,159 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
|||
}
|
||||
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) => {
|
||||
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!(
|
||||
|
|
|
@ -9,9 +9,9 @@ use crate::event::{CreateEvent, ModifyEvent};
|
|||
use crate::plugins::Plugin;
|
||||
use crate::prelude::*;
|
||||
|
||||
pub struct PasswordImport {}
|
||||
pub struct CredImport {}
|
||||
|
||||
impl Plugin for PasswordImport {
|
||||
impl Plugin for CredImport {
|
||||
fn id() -> &'static str {
|
||||
"plugin_password_import"
|
||||
}
|
||||
|
@ -26,44 +26,6 @@ impl Plugin for PasswordImport {
|
|||
cand: &mut Vec<Entry<EntryInvalid, EntryNew>>,
|
||||
_ce: &CreateEvent,
|
||||
) -> 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)
|
||||
}
|
||||
|
||||
|
@ -90,30 +52,22 @@ impl Plugin for PasswordImport {
|
|||
}
|
||||
}
|
||||
|
||||
impl PasswordImport {
|
||||
impl CredImport {
|
||||
fn modify_inner<T: Clone>(cand: &mut [Entry<EntryInvalid, T>]) -> 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(()),
|
||||
};
|
||||
// PASSWORD IMPORT
|
||||
if let Some(vs) = e.pop_ava("password_import") {
|
||||
// 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(),
|
||||
OperationError::Plugin(PluginError::CredImport(
|
||||
"password_import has incorrect value type - should be a single utf8 string"
|
||||
.to_string(),
|
||||
))
|
||||
})?;
|
||||
|
||||
// convert the import_password to a cred
|
||||
// convert the import_password_string to a password
|
||||
let pw = Password::try_from(im_pw).map_err(|_| {
|
||||
OperationError::Plugin(PluginError::PasswordImport(
|
||||
OperationError::Plugin(PluginError::CredImport(
|
||||
"password_import was unable to convert hash format".to_string(),
|
||||
))
|
||||
})?;
|
||||
|
@ -127,7 +81,6 @@ impl PasswordImport {
|
|||
"primary_credential",
|
||||
once(Value::new_credential("primary", c)),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
None => {
|
||||
// just set it then!
|
||||
|
@ -136,9 +89,37 @@ impl PasswordImport {
|
|||
"primary_credential",
|
||||
once(Value::new_credential("primary", c)),
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// TOTP IMPORT
|
||||
if let Some(vs) = e.pop_ava("totp_import") {
|
||||
// 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(),
|
||||
))
|
||||
})?;
|
||||
|
||||
if let Some(c) = e.get_ava_single_credential("primary_credential") {
|
||||
let c = totps.iter().fold(c.clone(), |acc, (label, totp)| {
|
||||
acc.append_totp(label.clone(), totp.clone())
|
||||
});
|
||||
e.set_ava(
|
||||
"primary_credential",
|
||||
once(Value::new_credential("primary", c)),
|
||||
);
|
||||
} else {
|
||||
return Err(OperationError::Plugin(PluginError::CredImport(
|
||||
"totp_import can not be used if primary_credential (password) is missing"
|
||||
.to_string(),
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -149,6 +130,7 @@ mod tests {
|
|||
use crate::credential::totp::{Totp, TOTP_DEFAULT_STEP};
|
||||
use crate::credential::{Credential, CredentialType};
|
||||
use crate::prelude::*;
|
||||
use kanidm_proto::v1::PluginError;
|
||||
|
||||
const IMPORT_HASH: &'static str =
|
||||
"pbkdf2_sha256$36000$xIEozuZVAoYm$uW1b35DUKyhvQAf1mBqMvoBDcqSD06juzyO/nmyV0+w=";
|
||||
|
@ -285,7 +267,7 @@ mod tests {
|
|||
.expect("failed to get primary cred.");
|
||||
match &c.type_ {
|
||||
CredentialType::PasswordMfa(_pw, totp, webauthn, backup_code) => {
|
||||
assert!(!totp.is_empty());
|
||||
assert!(totp.len() == 1);
|
||||
assert!(webauthn.is_empty());
|
||||
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 base;
|
||||
mod cred_import;
|
||||
mod domain;
|
||||
pub(crate) mod dyngroup;
|
||||
mod gidnumber;
|
||||
mod jwskeygen;
|
||||
mod memberof;
|
||||
mod password_import;
|
||||
mod protected;
|
||||
mod refint;
|
||||
mod session;
|
||||
|
@ -151,7 +151,7 @@ impl Plugins {
|
|||
ce: &CreateEvent,
|
||||
) -> Result<(), OperationError> {
|
||||
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(|_| gidnumber::GidNumber::pre_create_transform(qs, cand, ce))
|
||||
.and_then(|_| domain::Domain::pre_create_transform(qs, cand, ce))
|
||||
|
@ -187,7 +187,7 @@ impl Plugins {
|
|||
) -> Result<(), OperationError> {
|
||||
protected::Protected::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(|_| gidnumber::GidNumber::pre_modify(qs, cand, me))
|
||||
.and_then(|_| domain::Domain::pre_modify(qs, cand, me))
|
||||
|
@ -217,7 +217,7 @@ impl Plugins {
|
|||
) -> Result<(), OperationError> {
|
||||
protected::Protected::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(|_| gidnumber::GidNumber::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::JwsKeyRs256 => matches!(v, PartialValue::Iutf8(_)),
|
||||
SyntaxType::UiHint => matches!(v, PartialValue::UiHint(_)),
|
||||
// Comparing on the label.
|
||||
SyntaxType::TotpSecret => matches!(v, PartialValue::Utf8(_)),
|
||||
};
|
||||
if r {
|
||||
Ok(())
|
||||
|
@ -250,6 +252,7 @@ impl SchemaAttribute {
|
|||
SyntaxType::JwsKeyEs256 => matches!(v, Value::JwsKeyEs256(_)),
|
||||
SyntaxType::JwsKeyRs256 => matches!(v, Value::JwsKeyRs256(_)),
|
||||
SyntaxType::UiHint => matches!(v, Value::UiHint(_)),
|
||||
SyntaxType::TotpSecret => matches!(v, Value::TotpSecret(_, _)),
|
||||
};
|
||||
if r {
|
||||
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
|
||||
self.attributes.insert(
|
||||
AttrString::from("dn"),
|
||||
|
|
|
@ -499,6 +499,7 @@ pub trait QueryServerTransaction<'a> {
|
|||
SyntaxType::UiHint => UiHint::from_str(value)
|
||||
.map(Value::UiHint)
|
||||
.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 => {
|
||||
|
@ -520,7 +521,9 @@ pub trait QueryServerTransaction<'a> {
|
|||
match schema.get_attributes().get(attr) {
|
||||
Some(schema_a) => {
|
||||
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::JwsKeyEs256
|
||||
| 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 crate::be::dbentry::DbIdentSpn;
|
||||
use crate::credential::Credential;
|
||||
use crate::credential::{totp::Totp, Credential};
|
||||
use crate::repl::cid::Cid;
|
||||
use crate::server::identity::{AccessScope, IdentityId};
|
||||
|
||||
|
@ -198,6 +198,7 @@ pub enum SyntaxType {
|
|||
JwsKeyRs256 = 27,
|
||||
Oauth2Session = 28,
|
||||
UiHint = 29,
|
||||
TotpSecret = 30,
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for SyntaxType {
|
||||
|
@ -237,6 +238,7 @@ impl TryFrom<&str> for SyntaxType {
|
|||
"JWS_KEY_RS256" => Ok(SyntaxType::JwsKeyRs256),
|
||||
"OAUTH2SESSION" => Ok(SyntaxType::Oauth2Session),
|
||||
"UIHINT" => Ok(SyntaxType::UiHint),
|
||||
"TOTPSECRET" => Ok(SyntaxType::TotpSecret),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
|
@ -275,6 +277,7 @@ impl fmt::Display for SyntaxType {
|
|||
SyntaxType::JwsKeyRs256 => "JWS_KEY_RS256",
|
||||
SyntaxType::Oauth2Session => "OAUTH2SESSION",
|
||||
SyntaxType::UiHint => "UIHINT",
|
||||
SyntaxType::TotpSecret => "TOTPSECRET",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -321,12 +324,11 @@ pub enum PartialValue {
|
|||
RestrictedString(String),
|
||||
IntentToken(String),
|
||||
UiHint(UiHint),
|
||||
|
||||
Passkey(Uuid),
|
||||
DeviceKey(Uuid),
|
||||
|
||||
TrustedDeviceEnrollment(Uuid),
|
||||
Session(Uuid),
|
||||
// The label, if any.
|
||||
}
|
||||
|
||||
impl From<SyntaxType> for PartialValue {
|
||||
|
@ -773,6 +775,8 @@ pub enum Value {
|
|||
JwsKeyEs256(JwsSigner),
|
||||
JwsKeyRs256(JwsSigner),
|
||||
UiHint(UiHint),
|
||||
|
||||
TotpSecret(String, Totp),
|
||||
}
|
||||
|
||||
impl PartialEq for Value {
|
||||
|
|
|
@ -12,7 +12,7 @@ use webauthn_rs::prelude::DeviceKey as DeviceKeyV4;
|
|||
use webauthn_rs::prelude::Passkey as PasskeyV4;
|
||||
|
||||
use crate::be::dbvalue::DbValueSetV2;
|
||||
use crate::credential::Credential;
|
||||
use crate::credential::{totp::Totp, Credential};
|
||||
use crate::prelude::*;
|
||||
use crate::repl::cid::Cid;
|
||||
use crate::schema::SchemaAttribute;
|
||||
|
@ -37,6 +37,7 @@ mod session;
|
|||
mod spn;
|
||||
mod ssh;
|
||||
mod syntax;
|
||||
mod totp;
|
||||
mod uihint;
|
||||
mod uint32;
|
||||
mod url;
|
||||
|
@ -62,6 +63,7 @@ pub use self::session::{ValueSetOauth2Session, ValueSetSession};
|
|||
pub use self::spn::ValueSetSpn;
|
||||
pub use self::ssh::ValueSetSshKey;
|
||||
pub use self::syntax::ValueSetSyntax;
|
||||
pub use self::totp::ValueSetTotpSecret;
|
||||
pub use self::uihint::ValueSetUiHint;
|
||||
pub use self::uint32::ValueSetUint32;
|
||||
pub use self::url::ValueSetUrl;
|
||||
|
@ -283,6 +285,11 @@ pub trait ValueSetT: std::fmt::Debug + DynClone {
|
|||
None
|
||||
}
|
||||
|
||||
fn as_totp_map(&self) -> Option<&BTreeMap<String, Totp>> {
|
||||
debug_assert!(false);
|
||||
None
|
||||
}
|
||||
|
||||
fn as_emailaddress_set(&self) -> Option<(&String, &BTreeSet<String>)> {
|
||||
debug_assert!(false);
|
||||
None
|
||||
|
@ -563,6 +570,7 @@ pub fn from_result_value_iter(
|
|||
Value::PhoneNumber(_, _)
|
||||
| Value::Passkey(_, _, _)
|
||||
| Value::DeviceKey(_, _, _)
|
||||
| Value::TotpSecret(_, _)
|
||||
| Value::TrustedDeviceEnrollment(_)
|
||||
| Value::Session(_, _)
|
||||
| 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::Oauth2Session(u, m) => ValueSetOauth2Session::new(u, m),
|
||||
Value::UiHint(u) => ValueSetUiHint::new(u),
|
||||
Value::TotpSecret(l, t) => ValueSetTotpSecret::new(l, t),
|
||||
Value::PhoneNumber(_, _) | Value::TrustedDeviceEnrollment(_) => {
|
||||
debug_assert!(false);
|
||||
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::JwsKeyRs256(set) => ValueSetJwsKeyEs256::from_dbvs2(&set),
|
||||
DbValueSetV2::UiHint(set) => ValueSetUiHint::from_dbvs2(set),
|
||||
DbValueSetV2::TotpSecret(set) => ValueSetTotpSecret::from_dbvs2(set),
|
||||
DbValueSetV2::PhoneNumber(_, _) | DbValueSetV2::TrustedDeviceEnrollment(_) => {
|
||||
debug_assert!(false);
|
||||
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
|
||||
let totp: Totp = match status.mfaregstate {
|
||||
CURegState::TotpCheck(totp_secret) => totp_secret.into(),
|
||||
CURegState::TotpCheck(totp_secret) => totp_secret.try_into().unwrap(),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in a new issue