mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 04:27:02 +01:00
SSH Keys in Credentials Update (#3027)
This commit is contained in:
parent
ad3cf8828f
commit
0ce1bbeddc
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -3175,6 +3175,8 @@ dependencies = [
|
|||
"serde_json",
|
||||
"serde_with",
|
||||
"sketching",
|
||||
"sshkey-attest",
|
||||
"sshkeys",
|
||||
"tempfile",
|
||||
"time",
|
||||
"tokio",
|
||||
|
|
|
@ -52,6 +52,8 @@ serde = { workspace = true, features = ["derive"] }
|
|||
serde_json = { workspace = true }
|
||||
serde_with = { workspace = true }
|
||||
sketching = { workspace = true }
|
||||
sshkeys = { workspace = true }
|
||||
sshkey-attest = { workspace = true }
|
||||
time = { workspace = true, features = ["serde", "std", "local-offset"] }
|
||||
tokio = { workspace = true, features = ["net", "sync", "io-util", "macros"] }
|
||||
tokio-openssl = { workspace = true }
|
||||
|
|
|
@ -103,6 +103,10 @@ pub fn view_router() -> Router<ServerState> {
|
|||
.route("/reset/change_password", post(reset::view_new_pwd))
|
||||
.route("/reset/add_passkey", post(reset::view_new_passkey))
|
||||
.route("/reset/set_unixcred", post(reset::view_set_unixcred))
|
||||
.route(
|
||||
"/reset/add_ssh_publickey",
|
||||
post(reset::view_add_ssh_publickey),
|
||||
)
|
||||
.route("/api/delete_alt_creds", post(reset::remove_alt_creds))
|
||||
.route("/api/delete_unixcred", post(reset::remove_unixcred))
|
||||
.route("/api/add_totp", post(reset::add_totp))
|
||||
|
@ -110,6 +114,10 @@ pub fn view_router() -> Router<ServerState> {
|
|||
.route("/api/remove_passkey", post(reset::remove_passkey))
|
||||
.route("/api/finish_passkey", post(reset::finish_passkey))
|
||||
.route("/api/cancel_mfareg", post(reset::cancel_mfareg))
|
||||
.route(
|
||||
"/api/remove_ssh_publickey",
|
||||
post(reset::remove_ssh_publickey),
|
||||
)
|
||||
.route("/api/cu_cancel", post(reset::cancel_cred_update))
|
||||
.route("/api/cu_commit", post(reset::commit))
|
||||
.layer(HxRequestGuardLayer::new("/ui"));
|
||||
|
|
|
@ -14,11 +14,15 @@ use qrcode::render::svg;
|
|||
use qrcode::QrCode;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_with::skip_serializing_none;
|
||||
use std::collections::BTreeMap;
|
||||
use std::fmt;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::str::FromStr;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub use sshkey_attest::proto::PublicKey as SshPublicKey;
|
||||
pub use sshkeys::KeyType;
|
||||
|
||||
use kanidm_proto::internal::{
|
||||
CUCredState, CUExtPortal, CURegState, CURegWarning, CURequest, CUSessionToken, CUStatus,
|
||||
CredentialDetail, OperationError, PasskeyDetail, PasswordFeedback, TotpAlgo, UserAuthToken,
|
||||
|
@ -69,6 +73,12 @@ struct CredStatusView {
|
|||
credentials_update_partial: CredResetPartialView,
|
||||
}
|
||||
|
||||
struct SshKey {
|
||||
key_type: KeyType,
|
||||
key: String,
|
||||
comment: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "credentials_update_partial.html")]
|
||||
struct CredResetPartialView {
|
||||
|
@ -83,6 +93,8 @@ struct CredResetPartialView {
|
|||
primary: Option<CredentialDetail>,
|
||||
unixcred_state: CUCredState,
|
||||
unixcred: Option<CredentialDetail>,
|
||||
sshkeys_state: CUCredState,
|
||||
sshkeys: BTreeMap<String, SshKey>,
|
||||
}
|
||||
|
||||
#[skip_serializing_none]
|
||||
|
@ -104,6 +116,13 @@ struct SetUnixCredPartial {
|
|||
check_res: PwdCheckResult,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "credential_update_add_ssh_publickey_partial.html")]
|
||||
struct AddSshPublicKeyPartial {
|
||||
title_error: Option<String>,
|
||||
key_error: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
enum PwdCheckResult {
|
||||
Success,
|
||||
|
@ -120,6 +139,17 @@ pub(crate) struct NewPassword {
|
|||
new_password_check: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub(crate) struct NewPublicKey {
|
||||
title: String,
|
||||
key: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub(crate) struct PublicKeyRemoveData {
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub(crate) struct NewTotp {
|
||||
name: String,
|
||||
|
@ -341,6 +371,30 @@ pub(crate) async fn remove_unixcred(
|
|||
Ok(get_cu_partial_response(cu_status))
|
||||
}
|
||||
|
||||
pub(crate) async fn remove_ssh_publickey(
|
||||
State(state): State<ServerState>,
|
||||
Extension(kopid): Extension<KOpId>,
|
||||
HxRequest(_hx_request): HxRequest,
|
||||
VerifiedClientInformation(_client_auth_info): VerifiedClientInformation,
|
||||
DomainInfo(domain_info): DomainInfo,
|
||||
jar: CookieJar,
|
||||
Form(publickey): Form<PublicKeyRemoveData>,
|
||||
) -> axum::response::Result<Response> {
|
||||
let cu_session_token: CUSessionToken = get_cu_session(&jar).await?;
|
||||
|
||||
let cu_status = state
|
||||
.qe_r_ref
|
||||
.handle_idmcredentialupdate(
|
||||
cu_session_token,
|
||||
CURequest::SshPublicKeyRemove(publickey.name),
|
||||
kopid.eventid,
|
||||
)
|
||||
.map_err(|op_err| HtmxError::new(&kopid, op_err, domain_info))
|
||||
.await?;
|
||||
|
||||
Ok(get_cu_partial_response(cu_status))
|
||||
}
|
||||
|
||||
pub(crate) async fn remove_totp(
|
||||
State(state): State<ServerState>,
|
||||
Extension(kopid): Extension<KOpId>,
|
||||
|
@ -805,6 +859,95 @@ pub(crate) async fn view_set_unixcred(
|
|||
.into_response())
|
||||
}
|
||||
|
||||
struct AddSshPublicKeyError {
|
||||
key: Option<String>,
|
||||
title: Option<String>,
|
||||
}
|
||||
|
||||
pub(crate) async fn view_add_ssh_publickey(
|
||||
State(state): State<ServerState>,
|
||||
Extension(kopid): Extension<KOpId>,
|
||||
HxRequest(_hx_request): HxRequest,
|
||||
VerifiedClientInformation(_client_auth_info): VerifiedClientInformation,
|
||||
DomainInfo(domain_info): DomainInfo,
|
||||
jar: CookieJar,
|
||||
opt_form: Option<Form<NewPublicKey>>,
|
||||
) -> axum::response::Result<Response> {
|
||||
let cu_session_token: CUSessionToken = get_cu_session(&jar).await?;
|
||||
|
||||
let new_key = match opt_form {
|
||||
None => {
|
||||
return Ok((AddSshPublicKeyPartial {
|
||||
title_error: None,
|
||||
key_error: None,
|
||||
},)
|
||||
.into_response());
|
||||
}
|
||||
Some(Form(new_key)) => new_key,
|
||||
};
|
||||
|
||||
let (
|
||||
AddSshPublicKeyError {
|
||||
key: key_error,
|
||||
title: title_error,
|
||||
},
|
||||
status,
|
||||
) = {
|
||||
let publickey = match SshPublicKey::from_string(&new_key.key) {
|
||||
Err(_) => {
|
||||
return Ok((AddSshPublicKeyPartial {
|
||||
title_error: None,
|
||||
key_error: Some("Key cannot be parsed".to_string()),
|
||||
},)
|
||||
.into_response());
|
||||
}
|
||||
Ok(publickey) => publickey,
|
||||
};
|
||||
let res = state
|
||||
.qe_r_ref
|
||||
.handle_idmcredentialupdate(
|
||||
cu_session_token,
|
||||
CURequest::SshPublicKey(new_key.title, publickey),
|
||||
kopid.eventid,
|
||||
)
|
||||
.await;
|
||||
match res {
|
||||
Ok(cu_status) => return Ok(get_cu_partial_response(cu_status)),
|
||||
Err(e @ (OperationError::InvalidLabel | OperationError::DuplicateLabel)) => (
|
||||
AddSshPublicKeyError {
|
||||
title: Some(e.to_string()),
|
||||
key: None,
|
||||
},
|
||||
StatusCode::UNPROCESSABLE_ENTITY,
|
||||
),
|
||||
Err(e @ OperationError::DuplicateKey) => (
|
||||
AddSshPublicKeyError {
|
||||
key: Some(e.to_string()),
|
||||
title: None,
|
||||
},
|
||||
StatusCode::UNPROCESSABLE_ENTITY,
|
||||
),
|
||||
Err(operr) => {
|
||||
return Err(ErrorResponse::from(HtmxError::new(
|
||||
&kopid,
|
||||
operr,
|
||||
domain_info,
|
||||
)))
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok((
|
||||
status,
|
||||
HxPushUrl(Uri::from_static("/ui/reset/add_ssh_publickey")),
|
||||
AddSshPublicKeyPartial {
|
||||
title_error,
|
||||
key_error,
|
||||
},
|
||||
)
|
||||
.into_response())
|
||||
}
|
||||
|
||||
pub(crate) async fn view_reset_get(
|
||||
State(state): State<ServerState>,
|
||||
Extension(kopid): Extension<KOpId>,
|
||||
|
@ -910,9 +1053,25 @@ fn get_cu_partial(cu_status: CUStatus) -> CredResetPartialView {
|
|||
primary,
|
||||
unixcred_state,
|
||||
unixcred,
|
||||
sshkeys_state,
|
||||
sshkeys,
|
||||
..
|
||||
} = cu_status;
|
||||
|
||||
let sshkeyss: BTreeMap<String, SshKey> = sshkeys
|
||||
.iter()
|
||||
.map(|(k, v)| {
|
||||
(
|
||||
k.clone(),
|
||||
SshKey {
|
||||
key_type: v.clone().key_type,
|
||||
key: v.fingerprint().hash,
|
||||
comment: v.comment.clone(),
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
CredResetPartialView {
|
||||
ext_cred_portal,
|
||||
can_commit,
|
||||
|
@ -925,6 +1084,8 @@ fn get_cu_partial(cu_status: CUStatus) -> CredResetPartialView {
|
|||
primary,
|
||||
unixcred_state,
|
||||
unixcred,
|
||||
sshkeys_state,
|
||||
sshkeys: sshkeyss,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -190,3 +190,10 @@ footer {
|
|||
width: var(--icon-size);
|
||||
height: var(--icon-size);
|
||||
}
|
||||
|
||||
.ssh-list-icon {
|
||||
--icon-size: 32px;
|
||||
width: var(--icon-size);
|
||||
height: var(--icon-size);
|
||||
transform: rotate(35deg);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
<hr>
|
||||
<div class="d-flex flex-column row-gap-4">
|
||||
<h4>Add new SSH Key</h4>
|
||||
<form class="row-gap-3 d-flex flex-column needs-validation"
|
||||
hx-target="#credentialUpdateDynamicSection"
|
||||
hx-post="/ui/reset/add_ssh_publickey">
|
||||
<div>
|
||||
<label for="key-title" class="form-label">Title</label>
|
||||
<input type="text" class="form-control(% if let Some(_) = title_error %) is-invalid(% endif %)" id="key-title" name="title" aria-describedby="title-validation-feedback">
|
||||
<div id="title-validation-feedback" class="invalid-feedback">
|
||||
(% if let Some(title_error) = title_error %)(( title_error ))(% endif %)
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="key-content" class="form-label">Key</label>
|
||||
<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>
|
||||
<div id="key-validation-feedback" class="invalid-feedback">
|
||||
(% if let Some(key_error) = key_error %)(( key_error ))(% endif %)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column-gap-2 d-flex justify-content-end mt-2" hx-target="#credentialUpdateDynamicSection">
|
||||
<button type="button" class="btn btn-danger" hx-get=((Urls::CredReset)) hx-target="body">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
@ -85,12 +85,14 @@
|
|||
(% when CUCredState::Modifiable %)
|
||||
(% include "credentials_update_passkeys.html" %)
|
||||
<!-- Here we are modifiable so we can render the button to add passkeys -->
|
||||
<div class="mt-3">
|
||||
<button type="button" class="btn btn-primary"
|
||||
hx-post="/ui/reset/add_passkey"
|
||||
hx-vals='{"class": "Any"}'
|
||||
hx-target="#credentialUpdateDynamicSection">
|
||||
Add Passkey
|
||||
</button>
|
||||
</div>
|
||||
|
||||
(% when CUCredState::DeleteOnly %)
|
||||
(% if passkeys.len() > 0 %)
|
||||
|
@ -134,6 +136,55 @@
|
|||
(% when CUCredState::PolicyDeny %)
|
||||
(% endmatch %)
|
||||
|
||||
(% match sshkeys_state %)
|
||||
(% when CUCredState::Modifiable %)
|
||||
<hr class="my-4" />
|
||||
<h4>SSH Keys</h4>
|
||||
(% if sshkeys.len() > 0 %)
|
||||
<p>This is a list of SSH keys associated with your account.</p>
|
||||
<ul class="list-group">
|
||||
(% for (keyname, sshkey) in sshkeys %)
|
||||
<li class="list-group-item d-flex column-gap-3 py-3">
|
||||
<div>
|
||||
<img class="ssh-list-icon" src="/pkg/img/icons/key.svg" alt="" />
|
||||
</div>
|
||||
<div class="d-flex flex-column row-gap-2 flex-grow-1">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div class="fw-bold column-gap-2">
|
||||
(( keyname ))<span class="badge rounded-pill text-bg-dark ms-2">(( sshkey.key_type.short_name ))</span>
|
||||
</div>
|
||||
<button class="btn btn-tiny btn-danger"
|
||||
hx-post="/ui/api/remove_ssh_publickey"
|
||||
hx-vals='{"name": "(( keyname ))"}'
|
||||
hx-target="#credentialUpdateDynamicSection">
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
<div><span class="font-monospace text-break">SHA256:(( sshkey.key ))</span></div>
|
||||
(% if let Some(comment) = sshkey.comment %)
|
||||
<div class="rounded bg-body-tertiary border border-light-subtle text-body-secondary px-2 py-1 align-self-stretch">Comment: (( comment ))</div>
|
||||
(% endif %)
|
||||
</div>
|
||||
</li>
|
||||
(% endfor %)
|
||||
</ul>
|
||||
(% else %)
|
||||
<p>There are no SSH keys associated with your account.</p>
|
||||
(% endif %)
|
||||
<div class="mt-3">
|
||||
<button class="btn btn-primary" type="button"
|
||||
hx-post="/ui/reset/add_ssh_publickey"
|
||||
hx-target="#credentialUpdateDynamicSection">
|
||||
Add SSH Key
|
||||
</button>
|
||||
</div>
|
||||
|
||||
(% when CUCredState::DeleteOnly %)
|
||||
(% when CUCredState::AccessDeny %)
|
||||
(% when CUCredState::PolicyDeny %)
|
||||
(% endmatch %)
|
||||
|
||||
|
||||
<hr class="my-4" />
|
||||
<div id="cred-update-commit-bar" class="toast bs-emphasis-color bs-secondary-bg">
|
||||
<div class="toast-body">
|
||||
|
@ -148,7 +199,7 @@
|
|||
</svg>
|
||||
<b>Careful</b> - Unsaved changes will be lost</div>
|
||||
</span>
|
||||
<div class="mt-2 pt-2 border-top">
|
||||
<div class="mt-3 d-flex column-gap-2">
|
||||
<button class="btn btn-danger"
|
||||
hx-post="/ui/api/cu_cancel"
|
||||
hx-boost="false"
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarCollapse">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-md-0">
|
||||
<ul class="navbar-nav">
|
||||
<li>
|
||||
<a class="nav-link" href=((Urls::Apps))>
|
||||
<span data-feather="file"></span>Applications</a>
|
||||
|
@ -31,7 +31,7 @@
|
|||
<span data-feather="file"></span>Profile</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="navbar-nav me-auto mb-2 mb-md-0 ms-md-auto">
|
||||
<ul class="navbar-nav ms-md-auto">
|
||||
<li>
|
||||
<a class="nav-link" href="#" data-bs-toggle="modal"
|
||||
data-bs-target="#signoutModal">Sign out</a>
|
||||
|
|
Loading…
Reference in a new issue