diff --git a/Cargo.lock b/Cargo.lock index a5a7f082a..0bb95bb7a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3298,6 +3298,7 @@ dependencies = [ "serde_json", "serde_with", "smartstring", + "sshkey-attest", "time", "tracing", "url", diff --git a/libs/client/src/lib.rs b/libs/client/src/lib.rs index 16a82ecc2..929c77afc 100644 --- a/libs/client/src/lib.rs +++ b/libs/client/src/lib.rs @@ -1875,6 +1875,46 @@ impl KanidmClient { .await } + pub async fn idm_account_credential_update_set_unix_password( + &self, + session_token: &CUSessionToken, + pw: &str, + ) -> Result { + 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 { + 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 { + 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 { + 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, diff --git a/proto/Cargo.toml b/proto/Cargo.toml index 53a625056..cff492cf3 100644 --- a/proto/Cargo.toml +++ b/proto/Cargo.toml @@ -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 } diff --git a/proto/src/internal/credupdate.rs b/proto/src/internal/credupdate.rs index 9dd684213..c695573ac 100644 --- a/proto/src/internal/credupdate.rs +++ b/proto/src/internal/credupdate.rs @@ -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, pub attested_passkeys_state: CUCredState, pub attested_passkeys_allowed_devices: Vec, + + pub unixcred: Option, + pub unixcred_state: CUCredState, + + pub sshkeys: BTreeMap, + pub sshkeys_state: CUCredState, } #[derive(Debug, Serialize, Deserialize, Clone, ToSchema)] diff --git a/proto/src/internal/error.rs b/proto/src/internal/error.rs index ca39c7814..7e4e79c5f 100644 --- a/proto/src/internal/error.rs +++ b/proto/src/internal/error.rs @@ -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, diff --git a/proto/src/scim_v1/server.rs b/proto/src/scim_v1/server.rs index c75ad3cb0..87571d8f8 100644 --- a/proto/src/scim_v1/server.rs +++ b/proto/src/scim_v1/server.rs @@ -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)] diff --git a/proto/src/v1/unix.rs b/proto/src/v1/unix.rs index a8721c84e..bb073ee8c 100644 --- a/proto/src/v1/unix.rs +++ b/proto/src/v1/unix.rs @@ -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, pub groups: Vec, - pub sshkeys: Vec, + pub sshkeys: Vec, // The default value of bool is false. #[serde(default)] pub valid: bool, diff --git a/server/core/src/actors/v1_read.rs b/server/core/src/actors/v1_read.rs index 40007cfe0..3d2a2eb13 100644 --- a/server/core/src/actors/v1_read.rs +++ b/server/core/src/actors/v1_read.rs @@ -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()) } diff --git a/server/lib/src/constants/acp.rs b/server/lib/src/constants/acp.rs index 3e8185995..347200625 100644 --- a/server/lib/src/constants/acp.rs +++ b/server/lib/src/constants/acp.rs @@ -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() }; diff --git a/server/lib/src/idm/account.rs b/server/lib/src/idm/account.rs index a4d871402..bd62bb475 100644 --- a/server/lib/src/idm/account.rs +++ b/server/lib/src/idm/account.rs @@ -35,7 +35,6 @@ use sshkey_attest::proto::PublicKey as SshPublicKey; pub struct UnixExtensions { ucred: Option, shell: Option, - sshkeys: BTreeMap, gidnumber: u32, groups: Vec, } @@ -44,10 +43,6 @@ impl UnixExtensions { pub(crate) fn ucred(&self) -> Option<&Credential> { self.ucred.as_ref() } - - pub(crate) fn sshkeys(&self) -> &BTreeMap { - &self.sshkeys - } } #[derive(Default, Debug, Clone)] @@ -71,6 +66,7 @@ pub struct Account { pub mail: Vec, pub credential_update_intent_tokens: BTreeMap, pub(crate) unix_extn: Option, + pub(crate) sshkeys: BTreeMap, pub apps_pwds: BTreeMap>, } @@ -156,18 +152,18 @@ macro_rules! try_from_entry { ui_hints.insert(UiHint::SynchronisedAccount); } + 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 sshkeys = $value - .get_ava_set(Attribute::SshPublicKey) - .and_then(|vs| vs.as_sshkey_map()) - .cloned() - .unwrap_or_default(); - 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 { + &self.sshkeys + } + #[instrument(level = "trace", skip_all)] pub(crate) fn try_from_entry_ro( value: &Entry, @@ -799,7 +799,7 @@ impl Account { pub(crate) fn to_unixusertoken(&self, ct: Duration) -> Result { let (gidnumber, shell, sshkeys, groups) = match &self.unix_extn { Some(ue) => { - let sshkeys: Vec = ue.sshkeys.keys().cloned().collect(); + let sshkeys: Vec<_> = self.sshkeys.values().cloned().collect(); (ue.gidnumber, ue.shell.clone(), sshkeys, ue.groups.clone()) } None => { diff --git a/server/lib/src/idm/credupdatesession.rs b/server/lib/src/idm/credupdatesession.rs index 7abfc0e1e..cd5572257 100644 --- a/server/lib/src/idm/credupdatesession.rs +++ b/server/lib/src/idm/credupdatesession.rs @@ -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, - unixcred_can_edit: bool, + unixcred_state: CredentialState, // Ssh Keys sshkeys: BTreeMap, - sshpubkey_can_edit: bool, + sshkeys_state: CredentialState, // Passkeys that have been configured. passkeys: BTreeMap, @@ -325,6 +325,12 @@ pub struct CredentialUpdateSessionStatus { attested_passkeys: Vec, attested_passkeys_state: CredentialState, attested_passkeys_allowed_devices: Vec, + + unixcred: Option, + unixcred_state: CredentialState, + + sshkeys: BTreeMap, + sshkeys_state: CredentialState, } impl CredentialUpdateSessionStatus { @@ -366,6 +372,10 @@ impl Into 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 = if unixcred_can_edit { + let unixcred: Option = 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 { - 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)); + 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 { - 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)); + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { - 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 { + 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; + } } diff --git a/server/lib/src/value.rs b/server/lib/src/value.rs index 13cf669c3..a2eedd4f7 100644 --- a/server/lib/src/value.rs +++ b/server/lib/src/value.rs @@ -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. diff --git a/server/lib/src/valueset/ssh.rs b/server/lib/src/valueset/ssh.rs index 77c2e977d..8e4afc938 100644 --- a/server/lib/src/valueset/ssh.rs +++ b/server/lib/src/valueset/ssh.rs @@ -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::>(), )) diff --git a/server/web_ui/user/src/credential/reset.rs b/server/web_ui/user/src/credential/reset.rs index c3bd2588f..698963d1b 100644 --- a/server/web_ui/user/src/credential/reset.rs +++ b/server/web_ui/user/src/credential/reset.rs @@ -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)); diff --git a/tools/cli/src/cli/person.rs b/tools/cli/src/cli/person.rs index 7a8d51e80..fb2006937 100644 --- a/tools/cli/src/cli/person.rs +++ b/tools/cli/src/cli/person.rs @@ -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; diff --git a/unix_integration/resolver/src/idprovider/kanidm.rs b/unix_integration/resolver/src/idprovider/kanidm.rs index 5dc40666b..ecb2a20c0 100644 --- a/unix_integration/resolver/src/idprovider/kanidm.rs +++ b/unix_integration/resolver/src/idprovider/kanidm.rs @@ -117,6 +117,8 @@ impl From for UserToken { valid, } = value; + let sshkeys = sshkeys.iter().map(|s| s.to_string()).collect(); + let groups = groups.into_iter().map(GroupToken::from).collect(); UserToken {