20240921 ssh keys and unix password in credential update session (#3056)

This commit is contained in:
Firstyear 2024-10-03 15:57:18 +10:00 committed by GitHub
parent 00ab55f2d6
commit 131ff80b32
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 780 additions and 92 deletions

1
Cargo.lock generated
View file

@ -3298,6 +3298,7 @@ dependencies = [
"serde_json",
"serde_with",
"smartstring",
"sshkey-attest",
"time",
"tracing",
"url",

View file

@ -1875,6 +1875,46 @@ impl KanidmClient {
.await
}
pub async fn idm_account_credential_update_set_unix_password(
&self,
session_token: &CUSessionToken,
pw: &str,
) -> Result<CUStatus, ClientError> {
let scr = CURequest::UnixPassword(pw.to_string());
self.perform_simple_post_request("/v1/credential/_update", &(scr, &session_token))
.await
}
pub async fn idm_account_credential_update_unix_remove(
&self,
session_token: &CUSessionToken,
) -> Result<CUStatus, ClientError> {
let scr = CURequest::UnixPasswordRemove;
self.perform_simple_post_request("/v1/credential/_update", &(scr, &session_token))
.await
}
pub async fn idm_account_credential_update_sshkey_add(
&self,
session_token: &CUSessionToken,
label: String,
key: SshPublicKey,
) -> Result<CUStatus, ClientError> {
let scr = CURequest::SshPublicKey(label, key);
self.perform_simple_post_request("/v1/credential/_update", &(scr, &session_token))
.await
}
pub async fn idm_account_credential_update_sshkey_remove(
&self,
session_token: &CUSessionToken,
label: String,
) -> Result<CUStatus, ClientError> {
let scr = CURequest::SshPublicKeyRemove(label);
self.perform_simple_post_request("/v1/credential/_update", &(scr, &session_token))
.await
}
pub async fn idm_account_credential_update_passkey_init(
&self,
session_token: &CUSessionToken,

View file

@ -35,6 +35,7 @@ urlencoding = { workspace = true }
utoipa = { workspace = true }
uuid = { workspace = true, features = ["serde"] }
webauthn-rs-proto = { workspace = true }
sshkey-attest = { workspace = true }
[dev-dependencies]
enum-iterator = { workspace = true }

View file

@ -1,4 +1,5 @@
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::fmt;
use url::Url;
use utoipa::ToSchema;
@ -7,6 +8,8 @@ use uuid::Uuid;
use webauthn_rs_proto::CreationChallengeResponse;
use webauthn_rs_proto::RegisterPublicKeyCredential;
pub use sshkey_attest::proto::PublicKey as SshPublicKey;
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "lowercase")]
pub enum TotpAlgo {
@ -86,6 +89,10 @@ pub enum CURequest {
AttestedPasskeyInit,
AttestedPasskeyFinish(String, RegisterPublicKeyCredential),
AttestedPasskeyRemove(Uuid),
UnixPasswordRemove,
UnixPassword(String),
SshPublicKey(String, SshPublicKey),
SshPublicKeyRemove(String),
}
impl fmt::Debug for CURequest {
@ -106,6 +113,10 @@ impl fmt::Debug for CURequest {
CURequest::AttestedPasskeyInit => "CURequest::AttestedPasskeyInit",
CURequest::AttestedPasskeyFinish(_, _) => "CURequest::AttestedPasskeyFinish",
CURequest::AttestedPasskeyRemove(_) => "CURequest::AttestedPasskeyRemove",
CURequest::UnixPassword(_) => "CURequest::UnixPassword",
CURequest::UnixPasswordRemove => "CURequest::UnixPasswordRemove",
CURequest::SshPublicKey(_, _) => "CURequest::SSHKeySubmit",
CURequest::SshPublicKeyRemove(_) => "CURequest::SSHKeyRemove",
};
writeln!(f, "{}", t)
}
@ -167,6 +178,12 @@ pub struct CUStatus {
pub attested_passkeys: Vec<PasskeyDetail>,
pub attested_passkeys_state: CUCredState,
pub attested_passkeys_allowed_devices: Vec<String>,
pub unixcred: Option<CredentialDetail>,
pub unixcred_state: CUCredState,
pub sshkeys: BTreeMap<String, SshPublicKey>,
pub sshkeys_state: CUCredState,
}
#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]

View file

@ -69,6 +69,8 @@ pub enum ConsistencyError {
pub enum OperationError {
// Logic errors, or "soft" errors.
SessionExpired,
DuplicateKey,
DuplicateLabel,
EmptyRequest,
Backend,
NoMatchingEntries,
@ -84,6 +86,7 @@ pub enum OperationError {
FilterUuidResolution,
InvalidAttributeName(String),
InvalidAttribute(String),
InvalidLabel,
InvalidDbState,
InvalidCacheState,
InvalidValueState,
@ -273,6 +276,9 @@ impl OperationError {
Self::FilterUuidResolution => None,
Self::InvalidAttributeName(_) => None,
Self::InvalidAttribute(_) => None,
Self::InvalidLabel => Some("The submitted label for this item is invalid.".into()),
Self::DuplicateLabel => Some("The submitted label for this item is already in use.".into()),
Self::DuplicateKey => Some("The submitted key already exists.".into()),
Self::InvalidDbState => None,
Self::InvalidCacheState => None,
Self::InvalidValueState => None,

View file

@ -2,6 +2,7 @@ use crate::attribute::Attribute;
use scim_proto::ScimEntryHeader;
use serde::Serialize;
use serde_with::{base64, formats, hex::Hex, serde_as, skip_serializing_none, StringWithSeparator};
use sshkey_attest::proto::PublicKey as SshPublicKey;
use std::collections::{BTreeMap, BTreeSet};
use time::format_description::well_known::Rfc3339;
use time::OffsetDateTime;
@ -78,7 +79,7 @@ pub struct ScimAuditString {
#[serde(rename_all = "camelCase")]
pub struct ScimSshPublicKey {
pub label: String,
pub value: String,
pub value: SshPublicKey,
}
#[derive(Serialize, Debug, Clone, ToSchema)]

View file

@ -1,4 +1,5 @@
use serde::{Deserialize, Serialize};
use sshkey_attest::proto::PublicKey as SshPublicKey;
use std::fmt;
use utoipa::ToSchema;
use uuid::Uuid;
@ -42,7 +43,7 @@ pub struct UnixUserToken {
pub uuid: Uuid,
pub shell: Option<String>,
pub groups: Vec<UnixGroupToken>,
pub sshkeys: Vec<String>,
pub sshkeys: Vec<SshPublicKey>,
// The default value of bool is false.
#[serde(default)]
pub valid: bool,

View file

@ -1293,7 +1293,7 @@ impl QueryServerReadV1 {
.map_err(|e| {
error!(
err = ?e,
"Failed to begin credential_passkey_remove",
"Failed to begin credential_passkey_remove"
);
e
}),
@ -1302,7 +1302,7 @@ impl QueryServerReadV1 {
.map_err(|e| {
error!(
err = ?e,
"Failed to begin credential_attested_passkey_init",
"Failed to begin credential_attested_passkey_init"
);
e
}),
@ -1311,7 +1311,7 @@ impl QueryServerReadV1 {
.map_err(|e| {
error!(
err = ?e,
"Failed to begin credential_attested_passkey_finish",
"Failed to begin credential_attested_passkey_finish"
);
e
}),
@ -1320,10 +1320,32 @@ impl QueryServerReadV1 {
.map_err(|e| {
error!(
err = ?e,
"Failed to begin credential_attested_passkey_remove",
"Failed to begin credential_attested_passkey_remove"
);
e
}),
CURequest::UnixPasswordRemove => idms_cred_update
.credential_unix_delete(&session_token, ct)
.inspect_err(|err| {
error!(?err, "Failed to begin credential_unix_delete");
}),
CURequest::UnixPassword(pw) => idms_cred_update
.credential_unix_set_password(&session_token, ct, &pw)
.inspect_err(|err| {
error!(?err, "Failed to begin credential_unix_set_password");
}),
CURequest::SshPublicKey(label, pubkey) => idms_cred_update
.credential_sshkey_add(&session_token, ct, label, pubkey)
.inspect_err(|err| {
error!(?err, "Failed to begin credential_sshkey_remove");
}),
CURequest::SshPublicKeyRemove(label) => idms_cred_update
.credential_sshkey_remove(&session_token, ct, &label)
.inspect_err(|err| {
error!(?err, "Failed to begin credential_sshkey_remove");
}),
}
.map(|sta| sta.into())
}

View file

@ -1291,6 +1291,8 @@ lazy_static! {
Attribute::PassKeys,
Attribute::AttestedPasskeys,
Attribute::ApplicationPassword,
Attribute::SshPublicKey,
Attribute::UnixPassword,
],
..Default::default()
};
@ -1398,7 +1400,6 @@ lazy_static! {
Attribute::PassKeys,
Attribute::AttestedPasskeys,
Attribute::ApplicationPassword,
Attribute::ApplicationPassword,
],
..Default::default()
};

View file

@ -35,7 +35,6 @@ use sshkey_attest::proto::PublicKey as SshPublicKey;
pub struct UnixExtensions {
ucred: Option<Credential>,
shell: Option<String>,
sshkeys: BTreeMap<String, SshPublicKey>,
gidnumber: u32,
groups: Vec<UnixGroup>,
}
@ -44,10 +43,6 @@ impl UnixExtensions {
pub(crate) fn ucred(&self) -> Option<&Credential> {
self.ucred.as_ref()
}
pub(crate) fn sshkeys(&self) -> &BTreeMap<String, SshPublicKey> {
&self.sshkeys
}
}
#[derive(Default, Debug, Clone)]
@ -71,6 +66,7 @@ pub struct Account {
pub mail: Vec<String>,
pub credential_update_intent_tokens: BTreeMap<String, IntentTokenState>,
pub(crate) unix_extn: Option<UnixExtensions>,
pub(crate) sshkeys: BTreeMap<String, SshPublicKey>,
pub apps_pwds: BTreeMap<Uuid, Vec<ApplicationPassword>>,
}
@ -156,18 +152,18 @@ macro_rules! try_from_entry {
ui_hints.insert(UiHint::SynchronisedAccount);
}
let unix_extn = if $value.attribute_equality(
Attribute::Class,
&EntryClass::PosixAccount.to_partialvalue(),
) {
ui_hints.insert(UiHint::PosixAccount);
let sshkeys = $value
.get_ava_set(Attribute::SshPublicKey)
.and_then(|vs| vs.as_sshkey_map())
.cloned()
.unwrap_or_default();
let unix_extn = if $value.attribute_equality(
Attribute::Class,
&EntryClass::PosixAccount.to_partialvalue(),
) {
ui_hints.insert(UiHint::PosixAccount);
let ucred = $value
.get_ava_single_credential(Attribute::UnixPassword)
.cloned();
@ -185,7 +181,6 @@ macro_rules! try_from_entry {
Some(UnixExtensions {
ucred,
shell,
sshkeys,
gidnumber,
groups,
})
@ -216,6 +211,7 @@ macro_rules! try_from_entry {
mail,
credential_update_intent_tokens,
unix_extn,
sshkeys,
apps_pwds,
})
}};
@ -230,6 +226,10 @@ impl Account {
self.primary.as_ref()
}
pub(crate) fn sshkeys(&self) -> &BTreeMap<String, SshPublicKey> {
&self.sshkeys
}
#[instrument(level = "trace", skip_all)]
pub(crate) fn try_from_entry_ro(
value: &Entry<EntrySealed, EntryCommitted>,
@ -799,7 +799,7 @@ impl Account {
pub(crate) fn to_unixusertoken(&self, ct: Duration) -> Result<UnixUserToken, OperationError> {
let (gidnumber, shell, sshkeys, groups) = match &self.unix_extn {
Some(ue) => {
let sshkeys: Vec<String> = ue.sshkeys.keys().cloned().collect();
let sshkeys: Vec<_> = self.sshkeys.values().cloned().collect();
(ue.gidnumber, ue.shell.clone(), sshkeys, ue.groups.clone())
}
None => {

View file

@ -25,7 +25,7 @@ use crate::idm::server::{IdmServerCredUpdateTransaction, IdmServerProxyWriteTran
use crate::prelude::*;
use crate::server::access::Access;
use crate::utils::{backup_code_from_random, readable_password_from_random, uuid_from_duration};
use crate::value::{CredUpdateSessionPerms, CredentialType, IntentTokenState};
use crate::value::{CredUpdateSessionPerms, CredentialType, IntentTokenState, LABEL_RE};
use compact_jwt::compact::JweCompact;
use compact_jwt::jwe::JweBuilder;
@ -130,11 +130,11 @@ pub(crate) struct CredentialUpdateSession {
// Unix / Sudo PW
unixcred: Option<Credential>,
unixcred_can_edit: bool,
unixcred_state: CredentialState,
// Ssh Keys
sshkeys: BTreeMap<String, SshPublicKey>,
sshpubkey_can_edit: bool,
sshkeys_state: CredentialState,
// Passkeys that have been configured.
passkeys: BTreeMap<Uuid, (String, PasskeyV4)>,
@ -325,6 +325,12 @@ pub struct CredentialUpdateSessionStatus {
attested_passkeys: Vec<PasskeyDetail>,
attested_passkeys_state: CredentialState,
attested_passkeys_allowed_devices: Vec<String>,
unixcred: Option<CredentialDetail>,
unixcred_state: CredentialState,
sshkeys: BTreeMap<String, SshPublicKey>,
sshkeys_state: CredentialState,
}
impl CredentialUpdateSessionStatus {
@ -366,6 +372,10 @@ impl Into<CUStatus> for CredentialUpdateSessionStatus {
attested_passkeys: self.attested_passkeys,
attested_passkeys_state: self.attested_passkeys_state.into(),
attested_passkeys_allowed_devices: self.attested_passkeys_allowed_devices,
unixcred: self.unixcred,
unixcred_state: self.unixcred_state.into(),
sshkeys: self.sshkeys,
sshkeys_state: self.sshkeys_state.into(),
}
}
}
@ -414,6 +424,13 @@ impl From<&CredentialUpdateSession> for CredentialUpdateSessionStatus {
.collect(),
attested_passkeys_state: session.attested_passkeys_state,
attested_passkeys_allowed_devices,
unixcred: session.unixcred.as_ref().map(|c| c.into()),
unixcred_state: session.unixcred_state,
sshkeys: session.sshkeys.clone(),
sshkeys_state: session.sshkeys_state,
mfaregstate: match &session.mfaregstate {
MfaRegState::None => MfaRegStateStatus::None,
MfaRegState::TotpInit(token) => MfaRegStateStatus::TotpCheck(
@ -713,8 +730,6 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
ct: Duration,
) -> Result<(CredentialUpdateSessionToken, CredentialUpdateSessionStatus), OperationError> {
let ext_cred_portal_can_view = perms.ext_cred_portal_can_view;
let unixcred_can_edit = perms.unixcred_can_edit;
let sshpubkey_can_edit = perms.sshpubkey_can_edit;
let cred_type_min = resolved_account_policy.credential_policy();
@ -755,6 +770,20 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
CredentialState::AccessDeny
};
let unixcred_state = if account.unix_extn().is_none() {
CredentialState::PolicyDeny
} else if perms.unixcred_can_edit {
CredentialState::Modifiable
} else {
CredentialState::AccessDeny
};
let sshkeys_state = if perms.sshpubkey_can_edit {
CredentialState::Modifiable
} else {
CredentialState::AccessDeny
};
// - stash the current state of all associated credentials
let primary = if matches!(primary_state, CredentialState::Modifiable) {
account.primary.clone()
@ -768,17 +797,15 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
BTreeMap::default()
};
let unixcred: Option<Credential> = if unixcred_can_edit {
let unixcred: Option<Credential> = if matches!(unixcred_state, CredentialState::Modifiable)
{
account.unix_extn().and_then(|uext| uext.ucred()).cloned()
} else {
None
};
let sshkeys = if sshpubkey_can_edit {
account
.unix_extn()
.map(|uext| uext.sshkeys().clone())
.unwrap_or_default()
let sshkeys = if matches!(sshkeys_state, CredentialState::Modifiable) {
account.sshkeys().clone()
} else {
BTreeMap::default()
};
@ -842,9 +869,9 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
primary,
primary_state,
unixcred,
unixcred_can_edit,
unixcred_state,
sshkeys,
sshpubkey_can_edit,
sshkeys_state,
passkeys,
passkeys_state,
attested_passkeys,
@ -1371,21 +1398,33 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
CredentialState::AccessDeny => {}
};
if session.unixcred_can_edit {
match session.unixcred_state {
CredentialState::DeleteOnly | CredentialState::Modifiable => {
modlist.push_mod(Modify::Purged(Attribute::UnixPassword));
if let Some(ncred) = &session.unixcred {
let vcred = Value::new_credential("unix", ncred.clone());
modlist.push_mod(Modify::Present(Attribute::UnixPassword, vcred));
}
}
CredentialState::PolicyDeny => {
modlist.push_mod(Modify::Purged(Attribute::UnixPassword));
}
CredentialState::AccessDeny => {}
};
if session.sshpubkey_can_edit {
match session.sshkeys_state {
CredentialState::DeleteOnly | CredentialState::Modifiable => {
modlist.push_mod(Modify::Purged(Attribute::SshPublicKey));
for (tag, pk) in &session.sshkeys {
let v_sk = Value::SshKey(tag.clone(), pk.clone());
modlist.push_mod(Modify::Present(Attribute::SshPublicKey, v_sk));
}
}
CredentialState::PolicyDeny => {
modlist.push_mod(Modify::Purged(Attribute::SshPublicKey));
}
CredentialState::AccessDeny => {}
};
// Apply to the account!
trace!(?modlist, "processing change");
@ -2033,6 +2072,29 @@ impl<'a> IdmServerCredUpdateTransaction<'a> {
Ok(session.deref().into())
}
pub fn credential_primary_delete(
&self,
cust: &CredentialUpdateSessionToken,
ct: Duration,
) -> Result<CredentialUpdateSessionStatus, OperationError> {
let session_handle = self.get_current_session(cust, ct)?;
let mut session = session_handle.try_lock().map_err(|_| {
admin_error!("Session already locked, unable to proceed.");
OperationError::InvalidState
})?;
trace!(?session);
if !(matches!(session.primary_state, CredentialState::Modifiable)
|| matches!(session.primary_state, CredentialState::DeleteOnly))
{
error!("Session does not have permission to modify primary credential");
return Err(OperationError::AccessDenied);
};
session.primary = None;
Ok(session.deref().into())
}
pub fn credential_passkey_init(
&self,
cust: &CredentialUpdateSessionToken,
@ -2282,6 +2344,150 @@ impl<'a> IdmServerCredUpdateTransaction<'a> {
Ok(session.deref().into())
}
#[instrument(level = "trace", skip(cust, self))]
pub fn credential_unix_set_password(
&self,
cust: &CredentialUpdateSessionToken,
ct: Duration,
pw: &str,
) -> Result<CredentialUpdateSessionStatus, OperationError> {
let session_handle = self.get_current_session(cust, ct)?;
let mut session = session_handle.try_lock().map_err(|_| {
admin_error!("Session already locked, unable to proceed.");
OperationError::InvalidState
})?;
trace!(?session);
if !matches!(session.unixcred_state, CredentialState::Modifiable) {
error!("Session does not have permission to modify unix credential");
return Err(OperationError::AccessDenied);
};
self.check_password_quality(
pw,
&session.resolved_account_policy,
session.account.related_inputs().as_slice(),
session.account.radius_secret.as_deref(),
)
.map_err(|e| match e {
PasswordQuality::TooShort(sz) => {
OperationError::PasswordQuality(vec![PasswordFeedback::TooShort(sz)])
}
PasswordQuality::BadListed => {
OperationError::PasswordQuality(vec![PasswordFeedback::BadListed])
}
PasswordQuality::DontReusePasswords => {
OperationError::PasswordQuality(vec![PasswordFeedback::DontReusePasswords])
}
PasswordQuality::Feedback(feedback) => OperationError::PasswordQuality(feedback),
})?;
let ncred = match &session.unixcred {
Some(unixcred) => {
// Is there a need to update the uuid of the cred re softlocks?
unixcred.set_password(self.crypto_policy, pw)?
}
None => Credential::new_password_only(self.crypto_policy, pw)?,
};
session.unixcred = Some(ncred);
Ok(session.deref().into())
}
pub fn credential_unix_delete(
&self,
cust: &CredentialUpdateSessionToken,
ct: Duration,
) -> Result<CredentialUpdateSessionStatus, OperationError> {
let session_handle = self.get_current_session(cust, ct)?;
let mut session = session_handle.try_lock().map_err(|_| {
admin_error!("Session already locked, unable to proceed.");
OperationError::InvalidState
})?;
trace!(?session);
if !(matches!(session.unixcred_state, CredentialState::Modifiable)
|| matches!(session.unixcred_state, CredentialState::DeleteOnly))
{
error!("Session does not have permission to modify unix credential");
return Err(OperationError::AccessDenied);
};
session.unixcred = None;
Ok(session.deref().into())
}
#[instrument(level = "trace", skip(cust, self))]
pub fn credential_sshkey_add(
&self,
cust: &CredentialUpdateSessionToken,
ct: Duration,
label: String,
sshpubkey: SshPublicKey,
) -> Result<CredentialUpdateSessionStatus, OperationError> {
let session_handle = self.get_current_session(cust, ct)?;
let mut session = session_handle.try_lock().map_err(|_| {
admin_error!("Session already locked, unable to proceed.");
OperationError::InvalidState
})?;
trace!(?session);
if !matches!(session.unixcred_state, CredentialState::Modifiable) {
error!("Session does not have permission to modify unix credential");
return Err(OperationError::AccessDenied);
};
// Check the label.
if !LABEL_RE.is_match(&label) {
error!("SSH Pubilc Key label invalid");
return Err(OperationError::InvalidLabel);
}
if session.sshkeys.contains_key(&label) {
error!("SSH Pubilc Key label duplicate");
return Err(OperationError::DuplicateLabel);
}
if session.sshkeys.values().any(|sk| *sk == sshpubkey) {
error!("SSH Pubilc Key duplicate");
return Err(OperationError::DuplicateKey);
}
session.sshkeys.insert(label, sshpubkey);
Ok(session.deref().into())
}
pub fn credential_sshkey_remove(
&self,
cust: &CredentialUpdateSessionToken,
ct: Duration,
label: &str,
) -> Result<CredentialUpdateSessionStatus, OperationError> {
let session_handle = self.get_current_session(cust, ct)?;
let mut session = session_handle.try_lock().map_err(|_| {
admin_error!("Session already locked, unable to proceed.");
OperationError::InvalidState
})?;
trace!(?session);
if !(matches!(session.sshkeys_state, CredentialState::Modifiable)
|| matches!(session.sshkeys_state, CredentialState::DeleteOnly))
{
error!("Session does not have permission to modify sshkeys");
return Err(OperationError::AccessDenied);
};
session.sshkeys.remove(label).ok_or_else(|| {
error!("No such key for label");
OperationError::NoMatchingEntries
})?;
// session.unixcred = None;
Ok(session.deref().into())
}
pub fn credential_update_cancel_mfareg(
&self,
cust: &CredentialUpdateSessionToken,
@ -2297,29 +2503,6 @@ impl<'a> IdmServerCredUpdateTransaction<'a> {
Ok(session.deref().into())
}
pub fn credential_primary_delete(
&self,
cust: &CredentialUpdateSessionToken,
ct: Duration,
) -> Result<CredentialUpdateSessionStatus, OperationError> {
let session_handle = self.get_current_session(cust, ct)?;
let mut session = session_handle.try_lock().map_err(|_| {
admin_error!("Session already locked, unable to proceed.");
OperationError::InvalidState
})?;
trace!(?session);
if !(matches!(session.primary_state, CredentialState::Modifiable)
|| matches!(session.primary_state, CredentialState::DeleteOnly))
{
error!("Session does not have permission to modify primary credential");
return Err(OperationError::AccessDenied);
};
session.primary = None;
Ok(session.deref().into())
}
// Generate password?
}
@ -2329,7 +2512,7 @@ mod tests {
use std::time::Duration;
use kanidm_proto::internal::{CUExtPortal, CredentialDetailType, PasswordFeedback};
use kanidm_proto::v1::{AuthAllowed, AuthIssueSession, AuthMech};
use kanidm_proto::v1::{AuthAllowed, AuthIssueSession, AuthMech, UnixUserToken};
use uuid::uuid;
use webauthn_authenticator_rs::softpasskey::SoftPasskey;
use webauthn_authenticator_rs::softtoken::{self, SoftToken};
@ -2345,18 +2528,25 @@ mod tests {
use crate::event::CreateEvent;
use crate::idm::audit::AuditEvent;
use crate::idm::delayed::DelayedAction;
use crate::idm::event::{AuthEvent, AuthResult, RegenerateRadiusSecretEvent};
use crate::idm::event::{
AuthEvent, AuthResult, RegenerateRadiusSecretEvent, UnixUserAuthEvent,
};
use crate::idm::server::{IdmServer, IdmServerCredUpdateTransaction, IdmServerDelayed};
use crate::idm::AuthState;
use crate::prelude::*;
use crate::utils::password_from_random_len;
use crate::value::CredentialType;
use sshkey_attest::proto::PublicKey as SshPublicKey;
const TEST_CURRENT_TIME: u64 = 6000;
const TESTPERSON_UUID: Uuid = uuid!("cf231fea-1a8f-4410-a520-fd9b1a379c86");
const SSHKEY_VALID_1: &str = "sk-ecdsa-sha2-nistp256@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBENubZikrb8hu+HeVRdZ0pp/VAk2qv4JDbuJhvD0yNdWDL2e3cBbERiDeNPkWx58Q4rVnxkbV1fa8E2waRtT91wAAAAEc3NoOg== testuser@fidokey";
const SSHKEY_VALID_2: &str = "sk-ecdsa-sha2-nistp256@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBIbkSsdGCRoW6v0nO/3vNYPhG20YhWU0wQPY7x52EOb4dmYhC4IJfzVDpEPg313BxWRKQglb5RQ1PPkou7JFyCUAAAAEc3NoOg== testuser@fidokey";
const SSHKEY_INVALID: &str = "sk-ecrsa-sha9000-nistp@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBIbkSsdGCRoW6v0nO/3vNYPhG20YhWU0wQPY7x52EOb4dmYhC4IJfzVDpEPg313BxWRKQglb5RQ1PPkou7JFyCUAAAAEc3NoOg== badkey@rejectme";
#[idm_test]
async fn test_idm_credential_update_session_init(
async fn credential_update_session_init(
idms: &IdmServer,
_idms_delayed: &mut IdmServerDelayed,
) {
@ -2378,6 +2568,7 @@ mod tests {
let e2 = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::PosixAccount.to_value()),
(Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Name, Value::new_iname("testperson")),
(Attribute::Uuid, Value::Uuid(TESTPERSON_UUID)),
@ -2490,6 +2681,7 @@ mod tests {
let e2 = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::PosixAccount.to_value()),
(Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Name, Value::new_iname("testperson")),
(Attribute::Uuid, Value::Uuid(TESTPERSON_UUID)),
@ -2609,6 +2801,22 @@ mod tests {
}
}
async fn check_testperson_unix_password(
idms: &IdmServer,
// idms_delayed: &mut IdmServerDelayed,
pw: &str,
ct: Duration,
) -> Option<UnixUserToken> {
let mut idms_auth = idms.auth().await.unwrap();
let auth_event = UnixUserAuthEvent::new_internal(TESTPERSON_UUID, pw);
idms_auth
.auth_unix(&auth_event, ct)
.await
.expect("Unable to perform unix authentication")
}
async fn check_testperson_password_totp(
idms: &IdmServer,
idms_delayed: &mut IdmServerDelayed,
@ -2819,7 +3027,7 @@ mod tests {
}
#[idm_test]
async fn test_idm_credential_update_session_cleanup(
async fn credential_update_session_cleanup(
idms: &IdmServer,
_idms_delayed: &mut IdmServerDelayed,
) {
@ -2847,7 +3055,7 @@ mod tests {
}
#[idm_test]
async fn test_idm_credential_update_onboarding_create_new_pw(
async fn credential_update_onboarding_create_new_pw(
idms: &IdmServer,
idms_delayed: &mut IdmServerDelayed,
) {
@ -2911,7 +3119,7 @@ mod tests {
}
#[idm_test]
async fn test_idm_credential_update_password_quality_checks(
async fn credential_update_password_quality_checks(
idms: &IdmServer,
_idms_delayed: &mut IdmServerDelayed,
) {
@ -3005,7 +3213,7 @@ mod tests {
}
#[idm_test]
async fn test_idm_credential_update_password_min_length_account_policy(
async fn credential_update_password_min_length_account_policy(
idms: &IdmServer,
_idms_delayed: &mut IdmServerDelayed,
) {
@ -3081,7 +3289,7 @@ mod tests {
// - setup TOTP
#[idm_test]
async fn test_idm_credential_update_onboarding_create_new_mfa_totp_basic(
async fn credential_update_onboarding_create_new_mfa_totp_basic(
idms: &IdmServer,
idms_delayed: &mut IdmServerDelayed,
) {
@ -3175,7 +3383,7 @@ mod tests {
// Check sha1 totp.
#[idm_test]
async fn test_idm_credential_update_onboarding_create_new_mfa_totp_sha1(
async fn credential_update_onboarding_create_new_mfa_totp_sha1(
idms: &IdmServer,
idms_delayed: &mut IdmServerDelayed,
) {
@ -3249,7 +3457,7 @@ mod tests {
}
#[idm_test]
async fn test_idm_credential_update_onboarding_create_new_mfa_totp_backup_codes(
async fn credential_update_onboarding_create_new_mfa_totp_backup_codes(
idms: &IdmServer,
idms_delayed: &mut IdmServerDelayed,
) {
@ -3386,7 +3594,7 @@ mod tests {
}
#[idm_test]
async fn test_idm_credential_update_onboarding_cancel_inprogress_totp(
async fn credential_update_onboarding_cancel_inprogress_totp(
idms: &IdmServer,
idms_delayed: &mut IdmServerDelayed,
) {
@ -3479,7 +3687,7 @@ mod tests {
}
#[idm_test]
async fn test_idm_credential_update_onboarding_create_new_passkey(
async fn credential_update_onboarding_create_new_passkey(
idms: &IdmServer,
idms_delayed: &mut IdmServerDelayed,
) {
@ -3536,7 +3744,7 @@ mod tests {
}
#[idm_test]
async fn test_idm_credential_update_access_denied(
async fn credential_update_access_denied(
idms: &IdmServer,
_idms_delayed: &mut IdmServerDelayed,
) {
@ -3564,6 +3772,7 @@ mod tests {
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::SyncObject.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::PosixAccount.to_value()),
(Attribute::Class, EntryClass::Person.to_value()),
(Attribute::SyncParentUuid, Value::Refer(sync_uuid)),
(Attribute::Name, Value::new_iname("testperson")),
@ -3608,6 +3817,10 @@ mod tests {
attested_passkeys: _,
attested_passkeys_state,
attested_passkeys_allowed_devices: _,
unixcred_state,
unixcred: _,
sshkeys: _,
sshkeys_state,
} = custatus;
assert!(matches!(ext_cred_portal, CUExtPortal::Hidden));
@ -3617,6 +3830,8 @@ mod tests {
attested_passkeys_state,
CredentialState::AccessDeny
));
assert!(matches!(unixcred_state, CredentialState::AccessDeny));
assert!(matches!(sshkeys_state, CredentialState::AccessDeny));
let cutxn = idms.cred_update_transaction().await.unwrap();
@ -3630,6 +3845,18 @@ mod tests {
.unwrap_err();
assert!(matches!(err, OperationError::AccessDenied));
let err = cutxn
.credential_unix_set_password(&cust, ct, "password")
.unwrap_err();
assert!(matches!(err, OperationError::AccessDenied));
let sshkey = SshPublicKey::from_string(SSHKEY_VALID_1).expect("Invalid SSHKEY_VALID_1");
let err = cutxn
.credential_sshkey_add(&cust, ct, "label".to_string(), sshkey)
.unwrap_err();
assert!(matches!(err, OperationError::AccessDenied));
// credential_primary_init_totp
let err = cutxn.credential_primary_init_totp(&cust, ct).unwrap_err();
assert!(matches!(err, OperationError::AccessDenied));
@ -3694,7 +3921,7 @@ mod tests {
// Assert we can't create "just" a password when mfa is required.
#[idm_test]
async fn test_idm_credential_update_account_policy_mfa_required(
async fn credential_update_account_policy_mfa_required(
idms: &IdmServer,
_idms_delayed: &mut IdmServerDelayed,
) {
@ -3816,7 +4043,7 @@ mod tests {
}
#[idm_test]
async fn test_idm_credential_update_account_policy_passkey_required(
async fn credential_update_account_policy_passkey_required(
idms: &IdmServer,
_idms_delayed: &mut IdmServerDelayed,
) {
@ -3876,7 +4103,7 @@ mod tests {
// Attested passkey types
#[idm_test]
async fn test_idm_credential_update_account_policy_attested_passkey_required(
async fn credential_update_account_policy_attested_passkey_required(
idms: &IdmServer,
idms_delayed: &mut IdmServerDelayed,
) {
@ -4070,7 +4297,7 @@ mod tests {
}
#[idm_test(audit = 1)]
async fn test_idm_credential_update_account_policy_attested_passkey_changed(
async fn credential_update_account_policy_attested_passkey_changed(
idms: &IdmServer,
idms_delayed: &mut IdmServerDelayed,
idms_audit: &mut IdmServerAudit,
@ -4216,7 +4443,7 @@ mod tests {
// Test that when attestation policy is removed, the apk downgrades to passkey and still works.
#[idm_test]
async fn test_idm_credential_update_account_policy_attested_passkey_downgrade(
async fn credential_update_account_policy_attested_passkey_downgrade(
idms: &IdmServer,
idms_delayed: &mut IdmServerDelayed,
) {
@ -4330,4 +4557,144 @@ mod tests {
drop(cutxn);
commit_session(idms, ct, cust).await;
}
#[idm_test]
async fn credential_update_unix_password(
idms: &IdmServer,
_idms_delayed: &mut IdmServerDelayed,
) {
let test_pw = "fo3EitierohF9AelaNgiem0Ei6vup4equo1Oogeevaetehah8Tobeengae3Ci0ooh0uki";
let ct = Duration::from_secs(TEST_CURRENT_TIME);
let (cust, _) = setup_test_session(idms, ct).await;
let cutxn = idms.cred_update_transaction().await.unwrap();
// Get the credential status - this should tell
// us the details of the credentials, as well as
// if they are ready and valid to commit?
let c_status = cutxn
.credential_update_status(&cust, ct)
.expect("Failed to get the current session status.");
trace!(?c_status);
assert!(c_status.unixcred.is_none());
// Test initially creating a credential.
// - pw first
let c_status = cutxn
.credential_unix_set_password(&cust, ct, test_pw)
.expect("Failed to update the unix cred password");
assert!(c_status.can_commit);
drop(cutxn);
commit_session(idms, ct, cust).await;
// Check it works!
assert!(check_testperson_unix_password(idms, test_pw, ct)
.await
.is_some());
// Test deleting the pw
let (cust, _) = renew_test_session(idms, ct).await;
let cutxn = idms.cred_update_transaction().await.unwrap();
let c_status = cutxn
.credential_update_status(&cust, ct)
.expect("Failed to get the current session status.");
trace!(?c_status);
assert!(c_status.unixcred.is_some());
let c_status = cutxn
.credential_unix_delete(&cust, ct)
.expect("Failed to delete the unix cred");
trace!(?c_status);
assert!(c_status.unixcred.is_none());
drop(cutxn);
commit_session(idms, ct, cust).await;
// Must fail now!
assert!(check_testperson_unix_password(idms, test_pw, ct)
.await
.is_none());
}
#[idm_test]
async fn credential_update_sshkeys(idms: &IdmServer, _idms_delayed: &mut IdmServerDelayed) {
let sshkey_valid_1 =
SshPublicKey::from_string(SSHKEY_VALID_1).expect("Invalid SSHKEY_VALID_1");
let sshkey_valid_2 =
SshPublicKey::from_string(SSHKEY_VALID_2).expect("Invalid SSHKEY_VALID_2");
assert!(SshPublicKey::from_string(SSHKEY_INVALID).is_err());
let ct = Duration::from_secs(TEST_CURRENT_TIME);
let (cust, _) = setup_test_session(idms, ct).await;
let cutxn = idms.cred_update_transaction().await.unwrap();
let c_status = cutxn
.credential_update_status(&cust, ct)
.expect("Failed to get the current session status.");
trace!(?c_status);
assert!(c_status.sshkeys.is_empty());
// Reject empty str key label
let result = cutxn.credential_sshkey_add(&cust, ct, "".to_string(), sshkey_valid_1.clone());
assert!(matches!(result, Err(OperationError::InvalidLabel)));
// Reject invalid name label.
let result =
cutxn.credential_sshkey_add(&cust, ct, "🚛".to_string(), sshkey_valid_1.clone());
assert!(matches!(result, Err(OperationError::InvalidLabel)));
// Remove non-existante
let result = cutxn.credential_sshkey_remove(&cust, ct, "key1");
assert!(matches!(result, Err(OperationError::NoMatchingEntries)));
// Add a valid key.
let c_status = cutxn
.credential_sshkey_add(&cust, ct, "key1".to_string(), sshkey_valid_1.clone())
.expect("Failed to add sshkey_valid_1");
trace!(?c_status);
assert_eq!(c_status.sshkeys.len(), 1);
assert!(c_status.sshkeys.contains_key("key1"));
// Add a second valid key.
let c_status = cutxn
.credential_sshkey_add(&cust, ct, "key2".to_string(), sshkey_valid_2.clone())
.expect("Failed to add sshkey_valid_2");
trace!(?c_status);
assert_eq!(c_status.sshkeys.len(), 2);
assert!(c_status.sshkeys.contains_key("key1"));
assert!(c_status.sshkeys.contains_key("key2"));
// Remove a key (check second key untouched)
let c_status = cutxn
.credential_sshkey_remove(&cust, ct, "key2")
.expect("Failed to remove sshkey_valid_2");
trace!(?c_status);
assert_eq!(c_status.sshkeys.len(), 1);
assert!(c_status.sshkeys.contains_key("key1"));
// Reject duplicate key label
let result =
cutxn.credential_sshkey_add(&cust, ct, "key1".to_string(), sshkey_valid_2.clone());
assert!(matches!(result, Err(OperationError::DuplicateLabel)));
// Reject duplicate key
let result =
cutxn.credential_sshkey_add(&cust, ct, "key2".to_string(), sshkey_valid_1.clone());
assert!(matches!(result, Err(OperationError::DuplicateKey)));
drop(cutxn);
commit_session(idms, ct, cust).await;
}
}

View file

@ -64,7 +64,13 @@ lazy_static! {
/// Only lowercase+numbers, with limited chars.
pub static ref INAME_RE: Regex = {
#[allow(clippy::expect_used)]
Regex::new("^[a-z][a-z0-9-_\\.]*$").expect("Invalid Iname regex found")
Regex::new("^[a-z][a-z0-9-_\\.]{0,63}$").expect("Invalid Iname regex found")
};
/// Only alpha-numeric with limited special chars and space
pub static ref LABEL_RE: Regex = {
#[allow(clippy::expect_used)]
Regex::new("^[a-zA-Z0-9][ a-zA-Z0-9-_\\.@]{0,63}$").expect("Invalid Iname regex found")
};
/// Only lowercase+numbers, with limited chars.

View file

@ -141,9 +141,9 @@ impl ValueSetT for ValueSetSshKey {
Some(ScimValueKanidm::from(
self.map
.iter()
.map(|(label, pk)| ScimSshPublicKey {
.map(|(label, value)| ScimSshPublicKey {
label: label.clone(),
value: pk.to_string(),
value: value.clone(),
})
.collect::<Vec<_>>(),
))

View file

@ -375,6 +375,10 @@ impl CredentialResetApp {
attested_passkeys,
attested_passkeys_state,
attested_passkeys_allowed_devices,
unixcred: _,
unixcred_state: _,
sshkeys: _,
sshkeys_state: _,
} = status;
let (username, domain) = spn.split_once('@').unwrap_or(("", spn));

View file

@ -9,10 +9,12 @@ use kanidm_client::KanidmClient;
use kanidm_proto::constants::{
ATTR_ACCOUNT_EXPIRE, ATTR_ACCOUNT_VALID_FROM, ATTR_GIDNUMBER, ATTR_SSH_PUBLICKEY,
};
use kanidm_proto::internal::OperationError::PasswordQuality;
use kanidm_proto::internal::OperationError::{
DuplicateKey, DuplicateLabel, InvalidLabel, NoMatchingEntries, PasswordQuality,
};
use kanidm_proto::internal::{
CUCredState, CUExtPortal, CUIntentToken, CURegState, CURegWarning, CUSessionToken, CUStatus,
TotpSecret,
SshPublicKey, TotpSecret,
};
use kanidm_proto::internal::{CredentialDetail, CredentialDetailType};
use kanidm_proto::messages::{AccountChangeMessage, ConsoleOutputMode, MessageStatus};
@ -708,6 +710,10 @@ enum CUAction {
PasskeyRemove,
AttestedPasskey,
AttestedPasskeyRemove,
UnixPassword,
UnixPasswordRemove,
SshPublicKey,
SshPublicKeyRemove,
End,
Commit,
}
@ -732,7 +738,13 @@ passkey (pk) - Add a new Passkey
passkey remove (passkey rm, pkrm) - Remove a Passkey
-- Attested Passkeys
attested-passkey (apk) - Add a new Attested Passkey
attested-passkey-remove (attested-passkey rm, apkrm) - Remove an Attested Passkey
attested-passkey remove (attested-passkey rm, apkrm) - Remove an Attested Passkey
-- Unix (sudo) Password
unix-password (upasswd, upass, upw) - Set a new unix/sudo password
unix-password remove (upassrm upwrm) - Remove the accounts unix password
-- SSH Public Keys
ssh-pub-key (ssh, spk) - Add a new ssh public key
ssh-pub-key remove (sshrm, spkrm) - Remove an ssh public key
"#
)
}
@ -759,6 +771,12 @@ impl FromStr for CUAction {
"attested-passkey remove" | "attested-passkey rm" | "apkrm" => {
Ok(CUAction::AttestedPasskeyRemove)
}
"unix-password" | "upasswd" | "upass" | "upw" => Ok(CUAction::UnixPassword),
"unix-password remove" | "upassrm" | "upwrm" => Ok(CUAction::UnixPasswordRemove),
"ssh-pub-key" | "ssh" | "spk" => Ok(CUAction::SshPublicKey),
"ssh-pub-key remove" | "sshrm" | "spkrm" => Ok(CUAction::SshPublicKeyRemove),
_ => Err(()),
}
}
@ -1119,6 +1137,100 @@ async fn passkey_remove_prompt(
}
}
async fn sshkey_add_prompt(session_token: &CUSessionToken, client: &KanidmClient) {
// Get the key.
let ssh_pub_key_str: String = Input::new()
.with_prompt("\nEnter the SSH Public Key (blank to stop) # ")
.validate_with(|input: &String| -> Result<(), &str> {
if input.is_empty() || SshPublicKey::from_string(input).is_ok() {
Ok(())
} else {
Err("This is not a valid SSH Public Key")
}
})
.allow_empty(true)
.interact_text()
.expect("Failed to interact with interactive session");
if ssh_pub_key_str.is_empty() {
println!("SSH Public Key was not added");
return;
}
let ssh_pub_key = match SshPublicKey::from_string(&ssh_pub_key_str) {
Ok(spk) => spk,
Err(_err) => {
eprintln!("Failed to parse ssh public key that previously parsed correctly.");
return;
}
};
let default_label = ssh_pub_key
.comment
.clone()
.unwrap_or_else(|| ssh_pub_key.fingerprint().hash);
loop {
// Get the label
let label: String = Input::new()
.with_prompt("\nEnter the label of the new SSH Public Key")
.default(default_label.clone())
.interact_text()
.expect("Failed to interact with interactive session");
if let Err(err) = client
.idm_account_credential_update_sshkey_add(session_token, label, ssh_pub_key.clone())
.await
{
match err {
ClientErrorHttp(_, Some(InvalidLabel), _) => {
eprintln!("Invalid SSH Public Key label - must only contain letters, numbers, and the characters '@' or '.'");
continue;
}
ClientErrorHttp(_, Some(DuplicateLabel), _) => {
eprintln!("SSH Public Key label already exists - choose another");
continue;
}
ClientErrorHttp(_, Some(DuplicateKey), _) => {
eprintln!("SSH Public Key already exists in this account");
}
_ => eprintln!("An error occured -> {:?}", err),
}
break;
} else {
println!("Successfully added SSH Public Key");
break;
}
}
}
async fn sshkey_remove_prompt(session_token: &CUSessionToken, client: &KanidmClient) {
let label: String = Input::new()
.with_prompt("\nEnter the label of the new SSH Public Key (blank to stop) # ")
.allow_empty(true)
.interact_text()
.expect("Failed to interact with interactive session");
if label.is_empty() {
println!("SSH Public Key was NOT removed");
return;
}
if let Err(err) = client
.idm_account_credential_update_sshkey_remove(session_token, label)
.await
{
match err {
ClientErrorHttp(_, Some(NoMatchingEntries), _) => {
eprintln!("SSH Public Key does not exist. Keys were NOT removed.");
}
_ => eprintln!("An error occured -> {:?}", err),
}
} else {
println!("Successfully removed SSH Public Key");
}
}
fn display_warnings(warnings: &[CURegWarning]) {
if !warnings.is_empty() {
println!("Warnings:");
@ -1163,6 +1275,10 @@ fn display_status(status: CUStatus) {
attested_passkeys,
attested_passkeys_state,
attested_passkeys_allowed_devices,
unixcred,
unixcred_state,
sshkeys,
sshkeys_state,
} = status;
println!("spn: {}", spn);
@ -1267,6 +1383,58 @@ fn display_status(status: CUStatus) {
}
}
println!("Unix (sudo) Password:");
match unixcred_state {
CUCredState::Modifiable => {
if let Some(cred_detail) = &unixcred {
print!("{}", cred_detail);
} else {
println!(" not set");
}
}
CUCredState::DeleteOnly => {
if let Some(cred_detail) = &unixcred {
print!("{}", cred_detail);
} else {
println!(" unable to modify - access denied");
}
}
CUCredState::AccessDeny => {
println!(" unable to modify - access denied");
}
CUCredState::PolicyDeny => {
println!(" unable to modify - account does not have posix attributes");
}
}
println!("SSH Public Keys:");
match sshkeys_state {
CUCredState::Modifiable => {
if sshkeys.is_empty() {
println!(" not set");
} else {
for (label, sk) in sshkeys {
println!(" {}: {}", label, sk);
}
}
}
CUCredState::DeleteOnly => {
if sshkeys.is_empty() {
println!(" unable to modify - access denied");
} else {
for (label, sk) in sshkeys {
println!(" {}: {}", label, sk);
}
}
}
CUCredState::AccessDeny => {
println!(" unable to modify - access denied");
}
CUCredState::PolicyDeny => {
println!(" unable to modify - account policy denied");
}
}
// We may need to be able to display if there are dangling
// curegstates, but the cli ui statemachine can match the
// server so it may not be needed?
@ -1458,6 +1626,57 @@ async fn credential_update_exec(
CUAction::AttestedPasskeyRemove => {
passkey_remove_prompt(&session_token, &client, PasskeyClass::Attested).await
}
CUAction::UnixPassword => {
let password_a = Password::new()
.with_prompt("New Unix Password")
.interact()
.expect("Failed to interact with interactive session");
let password_b = Password::new()
.with_prompt("Confirm password")
.interact()
.expect("Failed to interact with interactive session");
if password_a != password_b {
eprintln!("Passwords do not match");
} else if let Err(e) = client
.idm_account_credential_update_set_unix_password(&session_token, &password_a)
.await
{
match e {
ClientErrorHttp(_, Some(PasswordQuality(feedback)), _) => {
eprintln!("Password was not secure enough, please consider the following suggestions:");
for fb_item in feedback.iter() {
eprintln!(" - {}", fb_item)
}
}
_ => eprintln!("An error occurred -> {:?}", e),
}
} else {
println!("Successfully reset unix password.");
}
}
CUAction::UnixPasswordRemove => {
if Confirm::new()
.with_prompt("Do you want to remove your unix password?")
.interact()
.expect("Failed to interact with interactive session")
{
if let Err(e) = client
.idm_account_credential_update_unix_remove(&session_token)
.await
{
eprintln!("An error occurred -> {:?}", e);
} else {
println!("success");
}
} else {
println!("unix password was NOT removed");
}
}
CUAction::SshPublicKey => sshkey_add_prompt(&session_token, &client).await,
CUAction::SshPublicKeyRemove => sshkey_remove_prompt(&session_token, &client).await,
CUAction::End => {
println!("Changes were NOT saved.");
break;

View file

@ -117,6 +117,8 @@ impl From<UnixUserToken> for UserToken {
valid,
} = value;
let sshkeys = sshkeys.iter().map(|s| s.to_string()).collect();
let groups = groups.into_iter().map(GroupToken::from).collect();
UserToken {