mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 12:37:00 +01:00
20240921 ssh keys and unix password in credential update session (#3056)
This commit is contained in:
parent
00ab55f2d6
commit
131ff80b32
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -3298,6 +3298,7 @@ dependencies = [
|
|||
"serde_json",
|
||||
"serde_with",
|
||||
"smartstring",
|
||||
"sshkey-attest",
|
||||
"time",
|
||||
"tracing",
|
||||
"url",
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
};
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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<_>>(),
|
||||
))
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue