From 4f261c906e777dc320d8e2af8ecc8fc757c29d4d Mon Sep 17 00:00:00 2001 From: keerthi <keerthi.sree2105@gmail.com> Date: Tue, 22 Apr 2025 12:41:20 +0530 Subject: [PATCH 1/2] fix(web): Preserve SSH key content on form validation error --- server/core/src/https/views/reset.rs | 4 ++++ .../credential_update_add_ssh_publickey_partial.html | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/server/core/src/https/views/reset.rs b/server/core/src/https/views/reset.rs index 195e042a6..c6450ff3a 100644 --- a/server/core/src/https/views/reset.rs +++ b/server/core/src/https/views/reset.rs @@ -121,6 +121,7 @@ struct SetUnixCredPartial { struct AddSshPublicKeyPartial { title_error: Option<String>, key_error: Option<String>, + key_value: Option<String>, } #[derive(Serialize, Deserialize, Debug)] @@ -902,6 +903,7 @@ pub(crate) async fn view_add_ssh_publickey( return Ok((AddSshPublicKeyPartial { title_error: None, key_error: None, + key_value: None, },) .into_response()); } @@ -920,6 +922,7 @@ pub(crate) async fn view_add_ssh_publickey( return Ok((AddSshPublicKeyPartial { title_error: None, key_error: Some("Key cannot be parsed".to_string()), + key_value: Some(new_key.key), },) .into_response()); } @@ -965,6 +968,7 @@ pub(crate) async fn view_add_ssh_publickey( AddSshPublicKeyPartial { title_error, key_error, + key_value: Some(new_key.key), }, ) .into_response()) diff --git a/server/core/templates/credential_update_add_ssh_publickey_partial.html b/server/core/templates/credential_update_add_ssh_publickey_partial.html index 141e19554..31e7c71a9 100644 --- a/server/core/templates/credential_update_add_ssh_publickey_partial.html +++ b/server/core/templates/credential_update_add_ssh_publickey_partial.html @@ -16,7 +16,7 @@ <textarea class="form-control(% if let Some(_) = key_error %) is-invalid(% endif %)" id="key-content" rows="5" name="key" aria-describedby="key-validation-feedback" placeholder="Begins with 'ssh-rsa', 'ecdsa-sha2-nistp256', 'ecdsa-sha2-nistp384', 'ecdsa-sha2-nistp521', 'ssh-ed25519', 'sk-ecdsa-sha2-nistp256@openssh.com', or 'sk-ssh-ed25519@openssh.com'" - ></textarea> + >(% if let Some(key_value) = key_value %)(( key_value ))(% endif %)</textarea> <div id="key-validation-feedback" class="invalid-feedback"> (% if let Some(key_error) = key_error %)(( key_error ))(% endif %) </div> From 70b49e6968c90a8e1d6cfd0542fab17a22f0bf29 Mon Sep 17 00:00:00 2001 From: keerthi <keerthi.sree2105@gmail.com> Date: Thu, 24 Apr 2025 15:04:12 +0530 Subject: [PATCH 2/2] feat(cli): Allow clearing legalname field via empty string --- libs/client/src/person.rs | 16 ++++++ tools/cli/src/cli/person.rs | 101 +++++++++++++++++++++++++++++++++--- tools/cli/src/opt/kanidm.rs | 3 +- 3 files changed, 111 insertions(+), 9 deletions(-) diff --git a/libs/client/src/person.rs b/libs/client/src/person.rs index a5fdbf71b..5734ac174 100644 --- a/libs/client/src/person.rs +++ b/libs/client/src/person.rs @@ -6,6 +6,10 @@ use kanidm_proto::v1::{AccountUnixExtend, Entry, SingleStringRequest, UatStatus} use uuid::Uuid; use crate::{ClientError, KanidmClient}; +use kanidm_proto::scim_v1::PatchRequest; +use serde_json::Value; + +use tracing::trace; impl KanidmClient { pub async fn idm_person_account_list(&self) -> Result<Vec<Entry>, ClientError> { @@ -286,4 +290,16 @@ impl KanidmClient { self.perform_post_request(format!("/v1/person/{}/_certificate", id).as_str(), new_cert) .await } + + pub async fn idm_person_account_patch( + &self, + id: &str, + patch_request: PatchRequest, + ) -> Result<(), ClientError> { + if patch_request.operations.is_empty() { + return Ok(()); + } + self.perform_patch_request_scim(format!("/v1/person/{}", id).as_str(), patch_request) + .await + } } diff --git a/tools/cli/src/cli/person.rs b/tools/cli/src/cli/person.rs index 87c2ad765..1ec2fb358 100644 --- a/tools/cli/src/cli/person.rs +++ b/tools/cli/src/cli/person.rs @@ -30,6 +30,36 @@ use crate::{ AccountSsh, AccountUserAuthToken, AccountValidity, OutputMode, PersonOpt, PersonPosix, }; +use kanidm_client::ClientError; +use kanidm_client::OutputFormat; +use kanidm_client::SSHKEY_ATTEST_FEATURE; +use kanidm_proto::identity::v1::{ + IdentityGetReply, KeyAttestationVerifyRequest, KeyType as SSHKeyAttestationType, +}; +use kanidm_proto::identity::{self}; +use kanidm_proto::internal::{ + CUCredState, CURegState, CURegWarning, CURequest, CUSessionToken, CUStatus, CredentialDetail, + PasskeyDetail, SshPublicKey, TotpSecret, +}; +use kanidm_proto::scim_v1::{ + client::{ScimSshPublicKeys, PATCHOP_SCHEMA_URI}, + PatchOp, PatchOperation, PatchRequest, ScimEntryGetQuery, + ATTR_DISPLAYNAME, ATTR_GIDNUMBER, ATTR_LEGALNAME, ATTR_MAIL, ATTR_NAME, ATTR_PASSWORD, ATTR_POSIXACCOUNTS, + ATTR_SSH_PUBLICKEY, +}; +use kanidm_proto::MessageError; +use serde_json::json; +use sshkey_attest::{Error as AttestationError, RegisterChallengeResponse}; +use sshkeys::PublicKey as ActualSshPublicKey; +use std::collections::BTreeMap; +use std::fmt; +use std::path::PathBuf; +use std::str::FromStr; + +use dialoguer::Input; +use tracing::{debug, error, info, trace, warn}; +use uuid::Uuid; + impl PersonOpt { pub fn debug(&self) -> bool { match self { @@ -321,14 +351,71 @@ impl PersonOpt { } PersonOpt::Update(aopt) => { let client = aopt.copt.to_client(OpType::Write).await; + + let mut patch_ops = Vec::new(); + + // Helper to create a replace operation + let replace_op = |path: &str, value: &str| -> PatchOperation { + PatchOperation { + op: PatchOp::Replace, + path: Some(path.to_string()), + value: Some(json!(value)), + } + }; + + // Helper to create a replace operation for Vec<String> + let replace_op_vec = |path: &str, value: &[String]| -> PatchOperation { + PatchOperation { + op: PatchOp::Replace, + path: Some(path.to_string()), + value: Some(json!(value)), + } + }; + + // Helper to create a remove operation + let remove_op = |path: &str| -> PatchOperation { + PatchOperation { + op: PatchOp::Remove, + path: Some(path.to_string()), + value: None, + } + }; + + if let Some(newname) = &aopt.newname { + patch_ops.push(replace_op(ATTR_NAME, newname)); + } + if let Some(displayname) = &aopt.displayname { + patch_ops.push(replace_op(ATTR_DISPLAYNAME, displayname)); + } + if let Some(legalname) = &aopt.legalname { + if legalname.is_empty() { + // Empty string means clear the attribute + patch_ops.push(remove_op(ATTR_LEGALNAME)); + } else { + patch_ops.push(replace_op(ATTR_LEGALNAME, legalname)); + } + } + if let Some(mail) = &aopt.mail { + if mail.is_empty() { + // Empty list means clear the attribute + patch_ops.push(remove_op(ATTR_MAIL)); + } else { + patch_ops.push(replace_op_vec(ATTR_MAIL, mail)); + } + } + + if patch_ops.is_empty() { + println!("No changes specified."); + return; + } + + let patch_request = PatchRequest { + schemas: vec![PATCHOP_SCHEMA_URI.to_string()], + operations: patch_ops, + }; + match client - .idm_person_account_update( - aopt.aopts.account_id.as_str(), - aopt.newname.as_deref(), - aopt.displayname.as_deref(), - aopt.legalname.as_deref(), - aopt.mail.as_deref(), - ) + .idm_person_account_patch(aopt.aopts.account_id.as_str(), patch_request) .await { Ok(()) => println!("Success"), diff --git a/tools/cli/src/opt/kanidm.rs b/tools/cli/src/opt/kanidm.rs index 5e02a10ba..9b197504e 100644 --- a/tools/cli/src/opt/kanidm.rs +++ b/tools/cli/src/opt/kanidm.rs @@ -594,8 +594,7 @@ pub enum ServiceAccountPosix { pub struct PersonUpdateOpt { #[clap(flatten)] aopts: AccountCommonOpt, - #[clap(long, short, help = "Set the legal name for the person.", - value_parser = clap::builder::NonEmptyStringValueParser::new())] + #[clap(long, short, help = "Set the legal name for the person. Use '' to clear.")] legalname: Option<String>, #[clap(long, short, help = "Set the account name for the person.", value_parser = clap::builder::NonEmptyStringValueParser::new())]