From 6ff9082fd272e2e0862cc2fd08575c6a094f291a Mon Sep 17 00:00:00 2001 From: Firstyear Date: Thu, 19 Oct 2023 11:40:06 +1000 Subject: [PATCH] 20231014 account policy (#2218) * Start to prep for unix+ssh keys in credupdate session --- Cargo.lock | 47 +++++-- Cargo.toml | 14 +- proto/src/v1.rs | 6 + server/core/src/actors/v1_read.rs | 4 +- server/core/src/actors/v1_write.rs | 12 +- .../core/src/https/apidocs/response_schema.rs | 11 +- server/core/src/https/mod.rs | 8 +- server/core/src/https/oauth2.rs | 38 +++--- server/core/src/https/v1.rs | 4 +- server/core/src/https/v1_oauth2.rs | 2 +- server/core/src/https/v1_scim.rs | 2 +- server/core/src/lib.rs | 2 +- server/lib/Cargo.toml | 2 +- server/lib/src/be/dbvalue.rs | 8 ++ server/lib/src/entry.rs | 7 +- server/lib/src/idm/account.rs | 114 +++++++++++----- server/lib/src/idm/credupdatesession.rs | 128 +++++++++++++++--- server/lib/src/idm/ldap.rs | 2 +- server/lib/src/idm/scim.rs | 10 +- server/lib/src/idm/unix.rs | 21 +-- server/lib/src/repl/proto.rs | 8 ++ server/lib/src/server/mod.rs | 4 + server/lib/src/value.rs | 62 ++++----- server/lib/src/valueset/cred.rs | 32 +++++ server/lib/src/valueset/mod.rs | 7 +- server/lib/src/valueset/ssh.rs | 56 +++++--- server/testkit/tests/scim_test.rs | 22 +-- 27 files changed, 445 insertions(+), 188 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a9fe2092a..66b13564e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -436,11 +436,11 @@ checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" [[package]] name = "base64urlsafedata" version = "0.1.3" -source = "git+https://github.com/kanidm/webauthn-rs.git?rev=429662e34d6e760af8cff68760567c6b56dbb2d5#429662e34d6e760af8cff68760567c6b56dbb2d5" +source = "git+https://github.com/kanidm/webauthn-rs.git?rev=2218d2055c0c900ef57b398423eee5e8d5521f4c#2218d2055c0c900ef57b398423eee5e8d5521f4c" dependencies = [ "base64 0.21.4", + "paste 1.0.14", "serde", - "serde_json", ] [[package]] @@ -3135,7 +3135,7 @@ dependencies = [ "sketching", "smartstring", "smolset", - "sshkeys", + "sshkey-attest", "svg", "time", "tokio", @@ -5109,14 +5109,30 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b9b39299b249ad65f3b7e96443bad61c02ca5cd3589f46cb6d610a0fd6c0d6a" +[[package]] +name = "sshkey-attest" +version = "0.5.0-dev" +source = "git+https://github.com/kanidm/webauthn-rs.git?rev=2218d2055c0c900ef57b398423eee5e8d5521f4c#2218d2055c0c900ef57b398423eee5e8d5521f4c" +dependencies = [ + "base64urlsafedata", + "nom", + "openssl", + "serde", + "serde_cbor_2", + "sshkeys", + "tracing", + "uuid", + "webauthn-rs-core", +] + [[package]] name = "sshkeys" version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c926cb006a77964474a13a86aa0135ea82c9fd43e6793a1151cc54143db6637c" +source = "git+https://github.com/dnaeon/rust-sshkeys.git?rev=fa5bd02dd6e90ee724fdb981253c1e7726a7f534#fa5bd02dd6e90ee724fdb981253c1e7726a7f534" dependencies = [ "base64 0.12.3", "byteorder", + "serde", "sha2 0.8.2", ] @@ -5932,10 +5948,22 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webauthn-attestation-ca" +version = "0.1.0" +source = "git+https://github.com/kanidm/webauthn-rs.git?rev=2218d2055c0c900ef57b398423eee5e8d5521f4c#2218d2055c0c900ef57b398423eee5e8d5521f4c" +dependencies = [ + "base64urlsafedata", + "openssl", + "serde", + "tracing", + "uuid", +] + [[package]] name = "webauthn-authenticator-rs" version = "0.5.0-dev" -source = "git+https://github.com/kanidm/webauthn-rs.git?rev=429662e34d6e760af8cff68760567c6b56dbb2d5#429662e34d6e760af8cff68760567c6b56dbb2d5" +source = "git+https://github.com/kanidm/webauthn-rs.git?rev=2218d2055c0c900ef57b398423eee5e8d5521f4c#2218d2055c0c900ef57b398423eee5e8d5521f4c" dependencies = [ "async-stream", "async-trait", @@ -5967,7 +5995,7 @@ dependencies = [ [[package]] name = "webauthn-rs" version = "0.5.0-dev" -source = "git+https://github.com/kanidm/webauthn-rs.git?rev=429662e34d6e760af8cff68760567c6b56dbb2d5#429662e34d6e760af8cff68760567c6b56dbb2d5" +source = "git+https://github.com/kanidm/webauthn-rs.git?rev=2218d2055c0c900ef57b398423eee5e8d5521f4c#2218d2055c0c900ef57b398423eee5e8d5521f4c" dependencies = [ "base64urlsafedata", "serde", @@ -5980,7 +6008,7 @@ dependencies = [ [[package]] name = "webauthn-rs-core" version = "0.5.0-dev" -source = "git+https://github.com/kanidm/webauthn-rs.git?rev=429662e34d6e760af8cff68760567c6b56dbb2d5#429662e34d6e760af8cff68760567c6b56dbb2d5" +source = "git+https://github.com/kanidm/webauthn-rs.git?rev=2218d2055c0c900ef57b398423eee5e8d5521f4c#2218d2055c0c900ef57b398423eee5e8d5521f4c" dependencies = [ "base64 0.21.4", "base64urlsafedata", @@ -5996,6 +6024,7 @@ dependencies = [ "tracing", "url", "uuid", + "webauthn-attestation-ca", "webauthn-rs-proto", "x509-parser", ] @@ -6003,7 +6032,7 @@ dependencies = [ [[package]] name = "webauthn-rs-proto" version = "0.5.0-dev" -source = "git+https://github.com/kanidm/webauthn-rs.git?rev=429662e34d6e760af8cff68760567c6b56dbb2d5#429662e34d6e760af8cff68760567c6b56dbb2d5" +source = "git+https://github.com/kanidm/webauthn-rs.git?rev=2218d2055c0c900ef57b398423eee5e8d5521f4c#2218d2055c0c900ef57b398423eee5e8d5521f4c" dependencies = [ "base64urlsafedata", "js-sys", diff --git a/Cargo.toml b/Cargo.toml index 630992564..6de0daa7e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,17 +58,19 @@ repository = "https://github.com/kanidm/kanidm/" # scim_proto = { path = "../scim/proto" } # scim_proto = { git = "https://github.com/kanidm/scim.git" } -base64urlsafedata = { git = "https://github.com/kanidm/webauthn-rs.git", rev = "429662e34d6e760af8cff68760567c6b56dbb2d5" } -webauthn-authenticator-rs = { git = "https://github.com/kanidm/webauthn-rs.git", rev = "429662e34d6e760af8cff68760567c6b56dbb2d5" } -webauthn-rs = { git = "https://github.com/kanidm/webauthn-rs.git", rev = "429662e34d6e760af8cff68760567c6b56dbb2d5" } -webauthn-rs-core = { git = "https://github.com/kanidm/webauthn-rs.git", rev = "429662e34d6e760af8cff68760567c6b56dbb2d5" } -webauthn-rs-proto = { git = "https://github.com/kanidm/webauthn-rs.git", rev = "429662e34d6e760af8cff68760567c6b56dbb2d5" } +base64urlsafedata = { git = "https://github.com/kanidm/webauthn-rs.git", rev = "2218d2055c0c900ef57b398423eee5e8d5521f4c" } +webauthn-authenticator-rs = { git = "https://github.com/kanidm/webauthn-rs.git", rev = "2218d2055c0c900ef57b398423eee5e8d5521f4c" } +webauthn-rs = { git = "https://github.com/kanidm/webauthn-rs.git", rev = "2218d2055c0c900ef57b398423eee5e8d5521f4c" } +webauthn-rs-core = { git = "https://github.com/kanidm/webauthn-rs.git", rev = "2218d2055c0c900ef57b398423eee5e8d5521f4c" } +webauthn-rs-proto = { git = "https://github.com/kanidm/webauthn-rs.git", rev = "2218d2055c0c900ef57b398423eee5e8d5521f4c" } +sshkey-attest = { git = "https://github.com/kanidm/webauthn-rs.git", rev = "2218d2055c0c900ef57b398423eee5e8d5521f4c" } # base64urlsafedata = { path = "../webauthn-rs/base64urlsafedata" } # webauthn-authenticator-rs = { path = "../webauthn-rs/webauthn-authenticator-rs" } # webauthn-rs = { path = "../webauthn-rs/webauthn-rs" } # webauthn-rs-core = { path = "../webauthn-rs/webauthn-rs-core" } # webauthn-rs-proto = { path = "../webauthn-rs/webauthn-rs-proto" } +# sshkey-attest = { path = "../webauthn-rs/sshkey-attest" } [workspace.dependencies] kanidmd_core = { path = "./server/core" } @@ -180,7 +182,7 @@ shellexpand = "^2.1.2" sketching = { path = "./libs/sketching" } smartstring = "^1.0.1" smolset = "^1.3.1" -sshkeys = "^0.3.1" +sshkey-attest = "^0.5.0-dev" svg = "0.13.1" syn = { version = "2.0.38", features = ["full"] } tempfile = "3.8.0" diff --git a/proto/src/v1.rs b/proto/src/v1.rs index df31c73f4..b47a7864e 100644 --- a/proto/src/v1.rs +++ b/proto/src/v1.rs @@ -279,6 +279,12 @@ pub enum OperationError { GidOverlapsSystemMin(u32), /// When a name is denied by the system config ValueDenyName, + // What about something like this for unique errors? + // ValueSet errors + VS0001IncomingReplSshPublicKey, + // Value Errors + VL0001ValueSshPublicKeyString, + SC0001IncomingSshPublicKey, } impl PartialEq for OperationError { diff --git a/server/core/src/actors/v1_read.rs b/server/core/src/actors/v1_read.rs index 4d37ca358..99b82ffea 100644 --- a/server/core/src/actors/v1_read.rs +++ b/server/core/src/actors/v1_read.rs @@ -784,7 +784,7 @@ impl QueryServerReadV1 { .and_then(|e| { // From the entry, turn it into the value e.get_ava_iter_sshpubkeys(Attribute::SshPublicKey) - .map(|i| i.map(|s| s.to_string()).collect()) + .map(|i| i.collect()) }) .unwrap_or_else(|| { // No matching entry? Return none. @@ -848,7 +848,7 @@ impl QueryServerReadV1 { // From the entry, turn it into the value e.get_ava_set(Attribute::SshPublicKey).and_then(|vs| { // Get the one tagged value - vs.get_ssh_tag(&tag).map(str::to_string) + vs.get_ssh_tag(&tag).map(|pk| pk.to_string()) }) }) .unwrap_or_else(|| { diff --git a/server/core/src/actors/v1_write.rs b/server/core/src/actors/v1_write.rs index a8120780b..e24ecb38e 100644 --- a/server/core/src/actors/v1_write.rs +++ b/server/core/src/actors/v1_write.rs @@ -1023,21 +1023,23 @@ impl QueryServerWriteV1 { #[instrument( level = "info", name = "ssh_key_create", - skip(self, uat, uuid_or_name, tag, key, filter, eventid) + skip_all, fields(uuid = ?eventid) )] pub async fn handle_sshkeycreate( &self, uat: Option, uuid_or_name: String, - tag: String, - key: String, + tag: &str, + key: &str, filter: Filter, eventid: Uuid, ) -> Result<(), OperationError> { + let v_sk = Value::new_sshkey_str(tag, key)?; + // Because this is from internal, we can generate a real modlist, rather // than relying on the proto ones. - let ml = ModifyList::new_append(Attribute::SshPublicKey, Value::new_sshkey(tag, key)); + let ml = ModifyList::new_append(Attribute::SshPublicKey, v_sk); self.modify_from_internal_parts(uat, &uuid_or_name, &ml, filter) .await @@ -1046,7 +1048,7 @@ impl QueryServerWriteV1 { #[instrument( level = "info", name = "idm_account_unix_extend", - skip(self, uat, uuid_or_name, ux, eventid) + skip_all, fields(uuid = ?eventid) )] pub async fn handle_idmaccountunixextend( diff --git a/server/core/src/https/apidocs/response_schema.rs b/server/core/src/https/apidocs/response_schema.rs index 8d0ba9357..e71949dff 100644 --- a/server/core/src/https/apidocs/response_schema.rs +++ b/server/core/src/https/apidocs/response_schema.rs @@ -28,7 +28,10 @@ impl IntoResponses for DefaultApiResponse { .description("Ok"), ) .response("400", ResponseBuilder::new().description("Invalid Request")) - .response("401", ResponseBuilder::new().description("Authorization required")) + .response( + "401", + ResponseBuilder::new().description("Authorization required"), + ) .response("403", ResponseBuilder::new().description("Not Authorized")) .build() .into() @@ -47,10 +50,12 @@ impl IntoResponses for ApiResponseWithout200 { fn responses() -> BTreeMap> { ResponsesBuilder::new() .response("400", ResponseBuilder::new().description("Invalid Request")) - .response("401", ResponseBuilder::new().description("Authorization required")) + .response( + "401", + ResponseBuilder::new().description("Authorization required"), + ) .response("403", ResponseBuilder::new().description("Not Authorized")) .build() .into() } } - diff --git a/server/core/src/https/mod.rs b/server/core/src/https/mod.rs index 0d962bbc6..20193271e 100644 --- a/server/core/src/https/mod.rs +++ b/server/core/src/https/mod.rs @@ -120,14 +120,16 @@ pub fn get_js_files(role: ServerRole) -> Vec { env!("KANIDM_WEB_UI_PKG_PATH").to_owned(), filepath, )) { - Ok(hash) => - js_files.push(JavaScriptFile { + Ok(hash) => js_files.push(JavaScriptFile { filepath, hash, filetype: None, }), Err(err) => { - admin_error!(?err, "Failed to generate integrity hash for bootstrap.bundle.min.js") + admin_error!( + ?err, + "Failed to generate integrity hash for bootstrap.bundle.min.js" + ) } } } diff --git a/server/core/src/https/oauth2.rs b/server/core/src/https/oauth2.rs index 3d63f3673..b3324e73d 100644 --- a/server/core/src/https/oauth2.rs +++ b/server/core/src/https/oauth2.rs @@ -15,7 +15,7 @@ use http::header::{ use http::{HeaderMap, HeaderValue, StatusCode}; use hyper::Body; use kanidm_proto::constants::APPLICATION_JSON; -use kanidm_proto::oauth2::{AuthorisationResponse, OidcDiscoveryResponse, AccessTokenResponse}; +use kanidm_proto::oauth2::{AccessTokenResponse, AuthorisationResponse, OidcDiscoveryResponse}; use kanidmd_lib::idm::oauth2::{ AccessTokenIntrospectRequest, AccessTokenRequest, AuthorisationRequest, AuthorisePermitSuccess, AuthoriseResponse, ErrorResponse, Oauth2Error, TokenRevokeRequest, @@ -657,14 +657,13 @@ pub async fn oauth2_token_revoke_post( // TODO: we should handle the session-based auth bit here I think maybe possibly there's no tests let client_authz = match kopid.uat { Some(val) => val, - None => - { + None => { return ( StatusCode::UNAUTHORIZED, [(ACCESS_CONTROL_ALLOW_ORIGIN, "*")], - "" + "", ) - .into_response(); + .into_response(); } }; @@ -676,19 +675,15 @@ pub async fn oauth2_token_revoke_post( .await; match res { - Ok(()) => - { - (StatusCode::OK, - [(ACCESS_CONTROL_ALLOW_ORIGIN, "*")], - "" - ).into_response() - } + Ok(()) => (StatusCode::OK, [(ACCESS_CONTROL_ALLOW_ORIGIN, "*")], "").into_response(), Err(Oauth2Error::AuthenticationRequired) => { // This will trigger our ui to auth and retry. - (StatusCode::UNAUTHORIZED, - [(ACCESS_CONTROL_ALLOW_ORIGIN, "*"),], - "" - ).into_response() + ( + StatusCode::UNAUTHORIZED, + [(ACCESS_CONTROL_ALLOW_ORIGIN, "*")], + "", + ) + .into_response() } Err(e) => { // https://datatracker.ietf.org/doc/html/rfc6749#section-5.2 @@ -696,10 +691,12 @@ pub async fn oauth2_token_revoke_post( error: e.to_string(), ..Default::default() }; - (StatusCode::BAD_REQUEST, - [(ACCESS_CONTROL_ALLOW_ORIGIN, "*"),], + ( + StatusCode::BAD_REQUEST, + [(ACCESS_CONTROL_ALLOW_ORIGIN, "*")], serde_json::to_string(&err).unwrap_or("".to_string()), - ).into_response() + ) + .into_response() } } } @@ -713,7 +710,8 @@ pub async fn oauth2_preflight_options() -> Response { (ACCESS_CONTROL_ALLOW_HEADERS, "Authorization"), ], String::new(), - ).into_response() + ) + .into_response() } pub fn route_setup(state: ServerState) -> Router { diff --git a/server/core/src/https/v1.rs b/server/core/src/https/v1.rs index 8b6e91d21..01ccc6a13 100644 --- a/server/core/src/https/v1.rs +++ b/server/core/src/https/v1.rs @@ -1444,7 +1444,7 @@ pub async fn person_id_ssh_pubkeys_post( // Add a msg here state .qe_w_ref - .handle_sshkeycreate(kopid.uat, id, tag, key, filter, kopid.eventid) + .handle_sshkeycreate(kopid.uat, id, &tag, &key, filter, kopid.eventid) .await .map(Json::from) .map_err(WebError::from) @@ -1473,7 +1473,7 @@ pub async fn service_account_id_ssh_pubkeys_post( // Add a msg here state .qe_w_ref - .handle_sshkeycreate(kopid.uat, id, tag, key, filter, kopid.eventid) + .handle_sshkeycreate(kopid.uat, id, &tag, &key, filter, kopid.eventid) .await .map(Json::from) .map_err(WebError::from) diff --git a/server/core/src/https/v1_oauth2.rs b/server/core/src/https/v1_oauth2.rs index 1f43eb963..96e2bad3c 100644 --- a/server/core/src/https/v1_oauth2.rs +++ b/server/core/src/https/v1_oauth2.rs @@ -1,5 +1,5 @@ use super::apidocs::path_schema; -use super::apidocs::response_schema::{DefaultApiResponse, ApiResponseWithout200}; +use super::apidocs::response_schema::{ApiResponseWithout200, DefaultApiResponse}; use super::errors::WebError; use super::middleware::KOpId; use super::oauth2::oauth2_id; diff --git a/server/core/src/https/v1_scim.rs b/server/core/src/https/v1_scim.rs index cec84d653..c5e725196 100644 --- a/server/core/src/https/v1_scim.rs +++ b/server/core/src/https/v1_scim.rs @@ -1,5 +1,5 @@ use super::apidocs::path_schema; -use super::apidocs::response_schema::{ApiResponseWithout200,DefaultApiResponse}; +use super::apidocs::response_schema::{ApiResponseWithout200, DefaultApiResponse}; use super::errors::WebError; use super::middleware::KOpId; use super::v1::{ diff --git a/server/core/src/lib.rs b/server/core/src/lib.rs index 6e7ed91c7..0bbbd5f91 100644 --- a/server/core/src/lib.rs +++ b/server/core/src/lib.rs @@ -25,7 +25,7 @@ extern crate tracing; #[macro_use] extern crate kanidmd_lib; -pub mod actors; +mod actors; pub mod admin; pub mod config; mod crypto; diff --git a/server/lib/Cargo.toml b/server/lib/Cargo.toml index 1b7a7dbbc..0e65db9a4 100644 --- a/server/lib/Cargo.toml +++ b/server/lib/Cargo.toml @@ -61,7 +61,7 @@ serde_json = { workspace = true } sketching = { workspace = true } smartstring = { workspace = true, features = ["serde"] } smolset = { workspace = true } -sshkeys = { workspace = true } +sshkey-attest = { workspace = true } time = { workspace = true, features = ["serde", "std"] } tokio = { workspace = true, features = ["net", "sync", "time", "rt"] } tokio-util = { workspace = true, features = ["codec"] } diff --git a/server/lib/src/be/dbvalue.rs b/server/lib/src/be/dbvalue.rs index 204c6cd8b..fc5a45b3d 100644 --- a/server/lib/src/be/dbvalue.rs +++ b/server/lib/src/be/dbvalue.rs @@ -59,6 +59,10 @@ pub enum DbValueIntentTokenStateV1 { primary_can_edit: bool, #[serde(default)] passkeys_can_edit: bool, + #[serde(default)] + unixcred_can_edit: bool, + #[serde(default)] + sshpubkey_can_edit: bool, }, #[serde(rename = "p")] InProgress { @@ -71,6 +75,10 @@ pub enum DbValueIntentTokenStateV1 { primary_can_edit: bool, #[serde(default)] passkeys_can_edit: bool, + #[serde(default)] + unixcred_can_edit: bool, + #[serde(default)] + sshpubkey_can_edit: bool, }, #[serde(rename = "c")] Consumed { max_ttl: Duration }, diff --git a/server/lib/src/entry.rs b/server/lib/src/entry.rs index 64311a4db..02466f7bf 100644 --- a/server/lib/src/entry.rs +++ b/server/lib/src/entry.rs @@ -2638,9 +2638,12 @@ impl Entry { #[inline(always)] /// If possible, return an iterator over the set of ssh key values transformed into a `&str`. - pub fn get_ava_iter_sshpubkeys(&self, attr: Attribute) -> Option> { + pub fn get_ava_iter_sshpubkeys( + &self, + attr: Attribute, + ) -> Option + '_> { self.get_ava_set(attr) - .and_then(|vs| vs.as_sshpubkey_str_iter()) + .and_then(|vs| vs.as_sshpubkey_string_iter()) } // These are special types to allow returning typed values from diff --git a/server/lib/src/idm/account.rs b/server/lib/src/idm/account.rs index b58733eb2..51376dcce 100644 --- a/server/lib/src/idm/account.rs +++ b/server/lib/src/idm/account.rs @@ -24,6 +24,49 @@ use crate::schema::SchemaTransaction; use crate::value::{IntentTokenState, PartialValue, SessionState, Value}; use kanidm_lib_crypto::CryptoPolicy; +use sshkey_attest::proto::PublicKey as SshPublicKey; + +#[derive(Debug, Clone)] +pub struct UnixExtensions { + ucred: Option, + _shell: Option, + sshkeys: BTreeMap, + _gidnumber: u32, +} + +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)] +pub struct Account { + // To make this self-referential, we'll need to likely make Entry Pin> + // so that we can make the references work. + pub name: String, + pub spn: String, + pub displayname: String, + pub uuid: Uuid, + pub sync_parent_uuid: Option, + pub groups: Vec, + pub primary: Option, + pub passkeys: BTreeMap, + pub devicekeys: BTreeMap, + pub valid_from: Option, + pub expire: Option, + pub radius_secret: Option, + pub ui_hints: BTreeSet, + pub mail_primary: Option, + pub mail: Vec, + pub credential_update_intent_tokens: BTreeMap, + pub(crate) unix_extn: Option, +} + macro_rules! try_from_entry { ($value:expr, $groups:expr) => {{ // Check the classes @@ -115,12 +158,44 @@ macro_rules! try_from_entry { ui_hints.insert(UiHint::SynchronisedAccount); } - if $value.attribute_equality( + 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) + .map(|v| v.clone()); + + let _shell = $value + .get_ava_single_iutf8(Attribute::LoginShell) + .map(|s| s.to_string()); + + let _gidnumber = $value + .get_ava_single_uint32(Attribute::GidNumber) + .ok_or_else(|| { + OperationError::InvalidAccountState(format!( + "Missing attribute: {}", + Attribute::GidNumber + )) + })?; + + Some(UnixExtensions { + ucred, + _shell, + sshkeys, + _gidnumber, + }) + } else { + None + }; Ok(Account { uuid, @@ -139,41 +214,16 @@ macro_rules! try_from_entry { mail_primary, mail, credential_update_intent_tokens, + unix_extn, }) }}; } -#[derive(Default, Debug, Clone)] -pub struct Account { - // Later these could be &str if we cache entry here too ... - // They can't because if we mod the entry, we'll lose the ref. - // - // We do need to decide if we'll cache the entry, or if we just "work out" - // what the ops should be based on the values we cache here ... That's a future - // william problem I think :) - pub name: String, - pub displayname: String, - pub uuid: Uuid, - pub sync_parent_uuid: Option, - // We want to allow this so that in the future we can populate this into oauth2 tokens - #[allow(dead_code)] - pub groups: Vec, - pub primary: Option, - pub passkeys: BTreeMap, - pub devicekeys: BTreeMap, - pub valid_from: Option, - pub expire: Option, - pub radius_secret: Option, - pub spn: String, - pub ui_hints: BTreeSet, - // TODO #256: When you add mail, you should update the check to zxcvbn - // to include these. - pub mail_primary: Option, - pub mail: Vec, - pub credential_update_intent_tokens: BTreeMap, -} - impl Account { + pub(crate) fn unix_extn(&self) -> Option<&UnixExtensions> { + self.unix_extn.as_ref() + } + #[instrument(level = "trace", skip_all)] pub(crate) fn try_from_entry_ro( value: &Entry, diff --git a/server/lib/src/idm/credupdatesession.rs b/server/lib/src/idm/credupdatesession.rs index 77480989f..f67faaec3 100644 --- a/server/lib/src/idm/credupdatesession.rs +++ b/server/lib/src/idm/credupdatesession.rs @@ -4,6 +4,8 @@ use std::fmt; use std::sync::{Arc, Mutex}; use std::time::Duration; +use sshkey_attest::proto::PublicKey as SshPublicKey; + use hashbrown::HashSet; use kanidm_proto::v1::{ CUExtPortal, CURegState, CUStatus, CredentialDetail, PasskeyDetail, PasswordFeedback, @@ -87,7 +89,6 @@ pub(crate) struct CredentialUpdateSession { account: Account, // What intent was used to initiate this session. intent_token_id: Option, - // Acc policy // Is there an extertal credential portal? ext_cred_portal: CUExtPortal, @@ -96,6 +97,14 @@ pub(crate) struct CredentialUpdateSession { primary: Option, primary_can_edit: bool, + // Unix / Sudo PW + unixcred: Option, + unixcred_can_edit: bool, + + // Ssh Keys + sshkeys: BTreeMap, + sshpubkey_can_edit: bool, + // Passkeys that have been configured. passkeys: BTreeMap, passkeys_can_edit: bool, @@ -121,6 +130,7 @@ impl fmt::Debug for CredentialUpdateSession { .collect(); f.debug_struct("CredentialUpdateSession") .field("account.spn", &self.account.spn) + .field("account.unix", &self.account.unix_extn().is_some()) .field("intent_token_id", &self.intent_token_id) .field("primary.detail()", &primary) .field("passkeys.list()", &passkeys) @@ -355,13 +365,15 @@ impl<'a> IdmServerProxyWriteTransaction<'a> { ident, Some(btreeset![ Attribute::PrimaryCredential.into(), - Attribute::PassKeys.into() + Attribute::PassKeys.into(), + Attribute::UnixPassword.into(), + Attribute::SshPublicKey.into() ]), &[entry], )?; let eperm = effective_perms.get(0).ok_or_else(|| { - admin_error!("Effective Permission check returned no results"); + error!("Effective Permission check returned no results"); OperationError::InvalidState })?; @@ -369,7 +381,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> { // the current status of it's authentication? if eperm.target != account.uuid { - admin_error!("Effective Permission check target differs from requested entry uuid"); + error!("Effective Permission check target differs from requested entry uuid"); return Err(OperationError::InvalidEntryState); } @@ -414,6 +426,52 @@ impl<'a> IdmServerProxyWriteTransaction<'a> { let passkeys_can_edit = eperm_search_passkeys && eperm_mod_passkeys && eperm_rem_passkeys; + let eperm_search_unixcred = match &eperm.search { + Access::Denied => false, + Access::Grant => true, + Access::Allow(attrs) => attrs.contains(Attribute::UnixPassword.as_ref()), + }; + + let eperm_mod_unixcred = match &eperm.modify_pres { + Access::Denied => false, + Access::Grant => true, + Access::Allow(attrs) => attrs.contains(Attribute::UnixPassword.as_ref()), + }; + + let eperm_rem_unixcred = match &eperm.modify_rem { + Access::Denied => false, + Access::Grant => true, + Access::Allow(attrs) => attrs.contains(Attribute::UnixPassword.as_ref()), + }; + + let unixcred_can_edit = account.unix_extn().is_some() + && eperm_search_unixcred + && eperm_mod_unixcred + && eperm_rem_unixcred; + + let eperm_search_sshpubkey = match &eperm.search { + Access::Denied => false, + Access::Grant => true, + Access::Allow(attrs) => attrs.contains(Attribute::SshPublicKey.as_ref()), + }; + + let eperm_mod_sshpubkey = match &eperm.modify_pres { + Access::Denied => false, + Access::Grant => true, + Access::Allow(attrs) => attrs.contains(Attribute::SshPublicKey.as_ref()), + }; + + let eperm_rem_sshpubkey = match &eperm.modify_rem { + Access::Denied => false, + Access::Grant => true, + Access::Allow(attrs) => attrs.contains(Attribute::SshPublicKey.as_ref()), + }; + + let sshpubkey_can_edit = account.unix_extn().is_some() + && eperm_search_sshpubkey + && eperm_mod_sshpubkey + && eperm_rem_sshpubkey; + let ext_cred_portal_can_view = if let Some(sync_parent_uuid) = account.sync_parent_uuid { // In theory this is always granted due to how access controls work, but we check anyway. let entry = self.qs_write.internal_search_uuid(sync_parent_uuid)?; @@ -442,17 +500,24 @@ impl<'a> IdmServerProxyWriteTransaction<'a> { }; // At lease *one* must be modifiable OR visible. - if !(primary_can_edit || passkeys_can_edit || ext_cred_portal_can_view) { + if !(primary_can_edit + || passkeys_can_edit + || ext_cred_portal_can_view + || sshpubkey_can_edit + || unixcred_can_edit) + { error!("Unable to proceed with credential update intent - at least one type of credential must be modifiable or visible."); Err(OperationError::NotAuthorised) } else { - security_info!(%primary_can_edit, %passkeys_can_edit, %ext_cred_portal_can_view, "Proceeding"); + security_info!(%primary_can_edit, %passkeys_can_edit, %unixcred_can_edit, %sshpubkey_can_edit, %ext_cred_portal_can_view, "Proceeding"); Ok(( account, CredUpdateSessionPerms { ext_cred_portal_can_view, passkeys_can_edit, primary_can_edit, + unixcred_can_edit, + sshpubkey_can_edit, }, )) } @@ -469,6 +534,8 @@ impl<'a> IdmServerProxyWriteTransaction<'a> { let ext_cred_portal_can_view = perms.ext_cred_portal_can_view; let primary_can_edit = perms.primary_can_edit; let passkeys_can_edit = perms.passkeys_can_edit; + let unixcred_can_edit = perms.unixcred_can_edit; + let sshpubkey_can_edit = perms.sshpubkey_can_edit; // - stash the current state of all associated credentials let primary = if primary_can_edit { @@ -483,6 +550,21 @@ impl<'a> IdmServerProxyWriteTransaction<'a> { BTreeMap::default() }; + let unixcred: Option = if unixcred_can_edit { + 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() + } else { + BTreeMap::default() + }; + // let devicekeys = account.devicekeys.clone(); let devicekeys = BTreeMap::default(); @@ -511,6 +593,10 @@ impl<'a> IdmServerProxyWriteTransaction<'a> { ext_cred_portal, primary, primary_can_edit, + unixcred, + unixcred_can_edit, + sshkeys, + sshpubkey_can_edit, passkeys, passkeys_can_edit, _devicekeys: devicekeys, @@ -952,15 +1038,10 @@ impl<'a> IdmServerProxyWriteTransaction<'a> { }; if session.primary_can_edit { - match &session.primary { - Some(ncred) => { - modlist.push_mod(Modify::Purged(Attribute::PrimaryCredential.into())); - let vcred = Value::new_credential("primary", ncred.clone()); - modlist.push_mod(Modify::Present(Attribute::PrimaryCredential.into(), vcred)); - } - None => { - modlist.push_mod(Modify::Purged(Attribute::PrimaryCredential.into())); - } + modlist.push_mod(Modify::Purged(Attribute::PrimaryCredential.into())); + if let Some(ncred) = &session.primary { + let vcred = Value::new_credential("primary", ncred.clone()); + modlist.push_mod(Modify::Present(Attribute::PrimaryCredential.into(), vcred)); }; }; @@ -975,6 +1056,22 @@ impl<'a> IdmServerProxyWriteTransaction<'a> { }); }; + if session.unixcred_can_edit { + modlist.push_mod(Modify::Purged(Attribute::UnixPassword.into())); + if let Some(ncred) = &session.unixcred { + let vcred = Value::new_credential("unix", ncred.clone()); + modlist.push_mod(Modify::Present(Attribute::UnixPassword.into(), vcred)); + } + } + + if session.sshpubkey_can_edit { + modlist.push_mod(Modify::Purged(Attribute::SshPublicKey.into())); + for (tag, pk) in &session.sshkeys { + let v_sk = Value::SshKey(tag.clone(), pk.clone()); + modlist.push_mod(Modify::Present(Attribute::SshPublicKey.into(), v_sk)); + } + } + // Apply to the account! trace!(?modlist, "processing change"); @@ -1142,7 +1239,6 @@ impl<'a> IdmServerCredUpdateTransaction<'a> { ) -> Result<(), PasswordQuality> { // password strength and badlisting is always global, rather than per-pw-policy. // pw-policy as check on the account is about requirements for mfa for example. - // // is the password at least 10 char? if cleartext.len() < PW_MIN_LENGTH { diff --git a/server/lib/src/idm/ldap.rs b/server/lib/src/idm/ldap.rs index edc646117..48a4d931b 100644 --- a/server/lib/src/idm/ldap.rs +++ b/server/lib/src/idm/ldap.rs @@ -831,7 +831,7 @@ mod tests { (Attribute::LoginShell, Value::new_iutf8("/bin/zsh")), ( Attribute::SshPublicKey, - Value::new_sshkey_str("test", ssh_ed25519) + Value::new_sshkey_str("test", ssh_ed25519).expect("Invalid ssh key") ) ); diff --git a/server/lib/src/idm/scim.rs b/server/lib/src/idm/scim.rs index 6ef5c03a2..edffb93bf 100644 --- a/server/lib/src/idm/scim.rs +++ b/server/lib/src/idm/scim.rs @@ -14,6 +14,7 @@ use crate::prelude::*; use crate::value::ApiToken; use crate::schema::{SchemaClass, SchemaTransaction}; +use sshkey_attest::proto::PublicKey as SshPublicKey; // Internals of a Scim Sync token @@ -1094,7 +1095,11 @@ impl<'a> IdmServerProxyWriteTransaction<'a> { )) }) .and_then(|external_id| match external_id { - ScimSimpleAttr::String(value) => Ok(value.clone()), + ScimSimpleAttr::String(value) => SshPublicKey::from_string(value) + .map_err(|err| { + error!(?err, "Invalid ssh key provided via scim"); + OperationError::SC0001IncomingSshPublicKey + }), _ => { error!("Invalid value attribute - must be scim simple string"); Err(OperationError::InvalidAttribute(format!( @@ -2641,7 +2646,8 @@ mod tests { let mut ssh_keyiter = testuser .get_ava_iter_sshpubkeys(Attribute::SshPublicKey) .expect("Failed to access ssh pubkeys"); - assert_eq!(ssh_keyiter.next(), Some("sk-ecdsa-sha2-nistp256@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBENubZikrb8hu+HeVRdZ0pp/VAk2qv4JDbuJhvD0yNdWDL2e3cBbERiDeNPkWx58Q4rVnxkbV1fa8E2waRtT91wAAAAEc3NoOg== testuser@fidokey")); + + assert_eq!(ssh_keyiter.next(), Some("sk-ecdsa-sha2-nistp256@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBENubZikrb8hu+HeVRdZ0pp/VAk2qv4JDbuJhvD0yNdWDL2e3cBbERiDeNPkWx58Q4rVnxkbV1fa8E2waRtT91wAAAAEc3NoOg== testuser@fidokey".to_string())); assert_eq!(ssh_keyiter.next(), None); // Check memberof works. diff --git a/server/lib/src/idm/unix.rs b/server/lib/src/idm/unix.rs index ba21353b4..890fa0291 100644 --- a/server/lib/src/idm/unix.rs +++ b/server/lib/src/idm/unix.rs @@ -20,16 +20,17 @@ pub(crate) struct UnixUserAccount { pub name: String, pub spn: String, pub displayname: String, - pub gidnumber: u32, pub uuid: Uuid, - pub shell: Option, - pub sshkeys: Vec, - pub groups: Vec, - cred: Option, pub valid_from: Option, pub expire: Option, pub radius_secret: Option, pub mail: Vec, + + cred: Option, + pub shell: Option, + pub sshkeys: Vec, + pub gidnumber: u32, + pub groups: Vec, } macro_rules! try_from_entry { @@ -150,16 +151,6 @@ impl UnixUserAccount { try_from_entry!(value, groups) } - /* - pub(crate) fn try_from_entry_reduced( - value: &Entry, - qs: &mut QueryServerReadTransaction, - ) -> Result { - let groups = UnixGroup::try_from_account_entry_red_ro(au, value, qs)?; - try_from_entry!(value, groups) - } - */ - pub(crate) fn to_unixusertoken(&self, ct: Duration) -> Result { let groups: Result, _> = self.groups.iter().map(|g| g.to_unixgrouptoken()).collect(); let groups = groups?; diff --git a/server/lib/src/repl/proto.rs b/server/lib/src/repl/proto.rs index b5dd96ee6..74f093226 100644 --- a/server/lib/src/repl/proto.rs +++ b/server/lib/src/repl/proto.rs @@ -155,6 +155,10 @@ pub enum ReplIntentTokenV1 { primary_can_edit: bool, #[serde(default)] passkeys_can_edit: bool, + #[serde(default)] + unixcred_can_edit: bool, + #[serde(default)] + sshpubkey_can_edit: bool, }, InProgress { token_id: String, @@ -167,6 +171,10 @@ pub enum ReplIntentTokenV1 { primary_can_edit: bool, #[serde(default)] passkeys_can_edit: bool, + #[serde(default)] + unixcred_can_edit: bool, + #[serde(default)] + sshpubkey_can_edit: bool, }, Consumed { token_id: String, diff --git a/server/lib/src/server/mod.rs b/server/lib/src/server/mod.rs index 7a45c15f1..37fceceb6 100644 --- a/server/lib/src/server/mod.rs +++ b/server/lib/src/server/mod.rs @@ -757,9 +757,13 @@ pub trait QueryServerTransaction<'a> { }) .collect(); v + /* + // We previously special cased sshkeys here, but proto string now yields + // these as the proper string keys that ldap expects. } else if let Some(k_set) = value.as_sshkey_map() { let v: Vec<_> = k_set.values().cloned().map(|s| s.into_bytes()).collect(); Ok(v) + */ } else { let v: Vec<_> = value .to_proto_string_clone_iter() diff --git a/server/lib/src/value.rs b/server/lib/src/value.rs index b3259f034..ca2d03b16 100644 --- a/server/lib/src/value.rs +++ b/server/lib/src/value.rs @@ -23,7 +23,7 @@ use openssl::ec::EcKey; use openssl::pkey::Private; use regex::Regex; use serde::{Deserialize, Serialize}; -use sshkeys::PublicKey as SshPublicKey; +use sshkey_attest::proto::PublicKey as SshPublicKey; use time::OffsetDateTime; use url::Url; use uuid::Uuid; @@ -124,6 +124,8 @@ pub struct CredUpdateSessionPerms { pub ext_cred_portal_can_view: bool, pub primary_can_edit: bool, pub passkeys_can_edit: bool, + pub unixcred_can_edit: bool, + pub sshpubkey_can_edit: bool, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -932,7 +934,7 @@ pub enum Value { Refer(Uuid), JsonFilt(ProtoFilter), Cred(String, Credential), - SshKey(String, String), + SshKey(String, SshPublicKey), SecretValue(String), Spn(String, String), Uint32(u32), @@ -1268,21 +1270,22 @@ impl Value { } } - pub fn new_sshkey_str(tag: &str, key: &str) -> Self { - Value::SshKey(tag.to_string(), key.to_string()) - } - - pub fn new_sshkey(tag: String, key: String) -> Self { - Value::SshKey(tag, key) + pub fn new_sshkey_str(tag: &str, key: &str) -> Result { + SshPublicKey::from_string(key) + .map(|pk| Value::SshKey(tag.to_string(), pk)) + .map_err(|err| { + error!(?err, "value sshkey failed to parse string"); + OperationError::VL0001ValueSshPublicKeyString + }) } pub fn is_sshkey(&self) -> bool { matches!(&self, Value::SshKey(_, _)) } - pub fn get_sshkey(&self) -> Option<&str> { + pub fn get_sshkey(&self) -> Option { match &self { - Value::SshKey(_, key) => Some(key.as_str()), + Value::SshKey(_, key) => Some(key.to_string()), _ => None, } } @@ -1585,12 +1588,14 @@ impl Value { } } - pub fn to_sshkey(self) -> Option<(String, String)> { + /* + pub(crate) fn to_sshkey(self) -> Option<(String, SshPublicKey)> { match self { Value::SshKey(tag, k) => Some((tag, k)), _ => None, } } + */ pub fn to_spn(self) -> Option<(String, String)> { match self { @@ -1690,18 +1695,7 @@ impl Value { Value::Iname(s) => s.clone(), Value::Uuid(u) => u.as_hyphenated().to_string(), // We display the tag and fingerprint. - Value::SshKey(tag, key) => - // Check it's really an sshkey in the - // supplemental data. - { - match SshPublicKey::from_string(key) { - Ok(spk) => { - let fp = spk.fingerprint(); - format!("{}: {}", tag, fp.hash) - } - Err(_) => format!("{tag}: corrupted ssh public key"), - } - } + Value::SshKey(tag, key) => format!("{}: {}", tag, key.to_string()), Value::Spn(n, r) => format!("{n}@{r}"), _ => unreachable!( "You've specified the wrong type for the attribute, got: {:?}", @@ -1740,9 +1734,9 @@ impl Value { && Value::validate_singleline(s) } - Value::SshKey(s, key) => { - SshPublicKey::from_string(key).is_ok() - && Value::validate_str_escapes(s) + Value::SshKey(s, _key) => { + Value::validate_str_escapes(s) + // && Value::validate_iname(s) && Value::validate_singleline(s) } @@ -1884,30 +1878,30 @@ mod tests { "QK1JSAQqVfGhA8lLbJHmnQ/b/KMl2lzzp7SXej0wPUfvI/IP3NGb8irLzq8+JssAzXGJ+HMql+mNHiSuPaktbFzZ6y", "ikMR6Rx/psU07nAkxKZDEYpNVv william@amethyst"); - let sk1 = Value::new_sshkey_str("tag", ecdsa); + let sk1 = Value::new_sshkey_str("tag", ecdsa).expect("Invalid ssh key"); assert!(sk1.validate()); // to proto them let psk1 = sk1.to_proto_string_clone(); - assert_eq!(psk1, "tag: oMh0SibdRGV2APapEdVojzSySx9PuhcklWny5LP0Mg4"); + assert_eq!(psk1, format!("tag: {}", ecdsa)); - let sk2 = Value::new_sshkey_str("tag", ed25519); + let sk2 = Value::new_sshkey_str("tag", ed25519).expect("Invalid ssh key"); assert!(sk2.validate()); let psk2 = sk2.to_proto_string_clone(); - assert_eq!(psk2, "tag: UR7mRCLLXmZNsun+F2lWO3hG3PORk/0JyjxPQxDUcdc"); + assert_eq!(psk2, format!("tag: {}", ed25519)); - let sk3 = Value::new_sshkey_str("tag", rsa); + let sk3 = Value::new_sshkey_str("tag", rsa).expect("Invalid ssh key"); assert!(sk3.validate()); let psk3 = sk3.to_proto_string_clone(); - assert_eq!(psk3, "tag: sWugDdWeE4LkmKer8hz7ERf+6VttYPIqD0ULXR3EUcU"); + assert_eq!(psk3, format!("tag: {}", rsa)); let sk4 = Value::new_sshkey_str("tag", "ntaouhtnhtnuehtnuhotnuhtneouhtneouh"); - assert!(!sk4.validate()); + assert!(sk4.is_err()); let sk5 = Value::new_sshkey_str( "tag", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAeGW1P6Pc2rPq0XqbRaDKBcXZUPRklo", ); - assert!(!sk5.validate()); + assert!(sk5.is_err()); } /* diff --git a/server/lib/src/valueset/cred.rs b/server/lib/src/valueset/cred.rs index c25492bca..b42e97ce4 100644 --- a/server/lib/src/valueset/cred.rs +++ b/server/lib/src/valueset/cred.rs @@ -221,12 +221,16 @@ impl ValueSetIntentToken { ext_cred_portal_can_view, primary_can_edit, passkeys_can_edit, + unixcred_can_edit, + sshpubkey_can_edit, } => IntentTokenState::Valid { max_ttl, perms: CredUpdateSessionPerms { ext_cred_portal_can_view, primary_can_edit, passkeys_can_edit, + unixcred_can_edit, + sshpubkey_can_edit, }, }, DbValueIntentTokenStateV1::InProgress { @@ -236,6 +240,8 @@ impl ValueSetIntentToken { ext_cred_portal_can_view, primary_can_edit, passkeys_can_edit, + unixcred_can_edit, + sshpubkey_can_edit, } => IntentTokenState::InProgress { max_ttl, session_id, @@ -244,6 +250,8 @@ impl ValueSetIntentToken { ext_cred_portal_can_view, primary_can_edit, passkeys_can_edit, + unixcred_can_edit, + sshpubkey_can_edit, }, }, DbValueIntentTokenStateV1::Consumed { max_ttl } => { @@ -266,6 +274,8 @@ impl ValueSetIntentToken { ext_cred_portal_can_view, primary_can_edit, passkeys_can_edit, + unixcred_can_edit, + sshpubkey_can_edit, } => ( token_id.clone(), IntentTokenState::Valid { @@ -274,6 +284,8 @@ impl ValueSetIntentToken { ext_cred_portal_can_view: *ext_cred_portal_can_view, primary_can_edit: *primary_can_edit, passkeys_can_edit: *passkeys_can_edit, + unixcred_can_edit: *unixcred_can_edit, + sshpubkey_can_edit: *sshpubkey_can_edit, }, }, ), @@ -285,6 +297,8 @@ impl ValueSetIntentToken { ext_cred_portal_can_view, primary_can_edit, passkeys_can_edit, + unixcred_can_edit, + sshpubkey_can_edit, } => ( token_id.clone(), IntentTokenState::InProgress { @@ -295,6 +309,8 @@ impl ValueSetIntentToken { ext_cred_portal_can_view: *ext_cred_portal_can_view, primary_can_edit: *primary_can_edit, passkeys_can_edit: *passkeys_can_edit, + unixcred_can_edit: *unixcred_can_edit, + sshpubkey_can_edit: *sshpubkey_can_edit, }, }, ), @@ -402,12 +418,16 @@ impl ValueSetT for ValueSetIntentToken { ext_cred_portal_can_view, primary_can_edit, passkeys_can_edit, + unixcred_can_edit, + sshpubkey_can_edit, }, } => DbValueIntentTokenStateV1::Valid { max_ttl: *max_ttl, ext_cred_portal_can_view: *ext_cred_portal_can_view, primary_can_edit: *primary_can_edit, passkeys_can_edit: *passkeys_can_edit, + unixcred_can_edit: *unixcred_can_edit, + sshpubkey_can_edit: *sshpubkey_can_edit, }, IntentTokenState::InProgress { max_ttl, @@ -418,6 +438,8 @@ impl ValueSetT for ValueSetIntentToken { ext_cred_portal_can_view, primary_can_edit, passkeys_can_edit, + unixcred_can_edit, + sshpubkey_can_edit, }, } => DbValueIntentTokenStateV1::InProgress { max_ttl: *max_ttl, @@ -426,6 +448,8 @@ impl ValueSetT for ValueSetIntentToken { ext_cred_portal_can_view: *ext_cred_portal_can_view, primary_can_edit: *primary_can_edit, passkeys_can_edit: *passkeys_can_edit, + unixcred_can_edit: *unixcred_can_edit, + sshpubkey_can_edit: *sshpubkey_can_edit, }, IntentTokenState::Consumed { max_ttl } => { DbValueIntentTokenStateV1::Consumed { max_ttl: *max_ttl } @@ -450,6 +474,8 @@ impl ValueSetT for ValueSetIntentToken { ext_cred_portal_can_view, primary_can_edit, passkeys_can_edit, + unixcred_can_edit, + sshpubkey_can_edit, }, } => ReplIntentTokenV1::Valid { token_id: u.clone(), @@ -457,6 +483,8 @@ impl ValueSetT for ValueSetIntentToken { ext_cred_portal_can_view: *ext_cred_portal_can_view, primary_can_edit: *primary_can_edit, passkeys_can_edit: *passkeys_can_edit, + unixcred_can_edit: *unixcred_can_edit, + sshpubkey_can_edit: *sshpubkey_can_edit, }, IntentTokenState::InProgress { max_ttl, @@ -467,6 +495,8 @@ impl ValueSetT for ValueSetIntentToken { ext_cred_portal_can_view, primary_can_edit, passkeys_can_edit, + unixcred_can_edit, + sshpubkey_can_edit, }, } => ReplIntentTokenV1::InProgress { token_id: u.clone(), @@ -476,6 +506,8 @@ impl ValueSetT for ValueSetIntentToken { ext_cred_portal_can_view: *ext_cred_portal_can_view, primary_can_edit: *primary_can_edit, passkeys_can_edit: *passkeys_can_edit, + unixcred_can_edit: *unixcred_can_edit, + sshpubkey_can_edit: *sshpubkey_can_edit, }, IntentTokenState::Consumed { max_ttl } => ReplIntentTokenV1::Consumed { token_id: u.clone(), diff --git a/server/lib/src/valueset/mod.rs b/server/lib/src/valueset/mod.rs index b1d4687b6..df1fe1813 100644 --- a/server/lib/src/valueset/mod.rs +++ b/server/lib/src/valueset/mod.rs @@ -10,6 +10,7 @@ use openssl::pkey::Public; use smolset::SmolSet; use time::OffsetDateTime; // use std::fmt::Debug; +use sshkey_attest::proto::PublicKey as SshPublicKey; use webauthn_rs::prelude::AttestedPasskey as DeviceKeyV4; use webauthn_rs::prelude::Passkey as PasskeyV4; @@ -147,7 +148,7 @@ pub trait ValueSetT: std::fmt::Debug + DynClone { Err(OperationError::InvalidValueState) } - fn get_ssh_tag(&self, _tag: &str) -> Option<&str> { + fn get_ssh_tag(&self, _tag: &str) -> Option<&SshPublicKey> { None } @@ -197,7 +198,7 @@ pub trait ValueSetT: std::fmt::Debug + DynClone { None } - fn as_sshpubkey_str_iter(&self) -> Option + '_>> { + fn as_sshpubkey_string_iter(&self) -> Option + '_>> { None } @@ -318,7 +319,7 @@ pub trait ValueSetT: std::fmt::Debug + DynClone { None } - fn as_sshkey_map(&self) -> Option<&BTreeMap> { + fn as_sshkey_map(&self) -> Option<&BTreeMap> { None } diff --git a/server/lib/src/valueset/ssh.rs b/server/lib/src/valueset/ssh.rs index ece2921e4..cc6759f15 100644 --- a/server/lib/src/valueset/ssh.rs +++ b/server/lib/src/valueset/ssh.rs @@ -7,34 +7,52 @@ use crate::repl::proto::ReplAttrV1; use crate::schema::SchemaAttribute; use crate::valueset::{DbValueSetV2, ValueSet}; -use sshkeys::PublicKey as SshPublicKey; +use sshkey_attest::proto::PublicKey as SshPublicKey; #[derive(Debug, Clone)] pub struct ValueSetSshKey { - map: BTreeMap, + map: BTreeMap, } impl ValueSetSshKey { - pub fn new(t: String, k: String) -> Box { + pub fn new(t: String, k: SshPublicKey) -> Box { let mut map = BTreeMap::new(); map.insert(t, k); Box::new(ValueSetSshKey { map }) } - pub fn push(&mut self, t: String, k: String) -> bool { + pub fn push(&mut self, t: String, k: SshPublicKey) -> bool { self.map.insert(t, k).is_none() } pub fn from_dbvs2(data: Vec) -> Result { - let map = data.into_iter().map(|dbv| (dbv.tag, dbv.data)).collect(); + let map = data + .into_iter() + .filter_map(|DbValueTaggedStringV1 { tag, data }| { + SshPublicKey::from_string(&data) + .map_err(|err| { + warn!(%tag, ?err, "discarding corrupted ssh public key"); + }) + .map(|pk| (tag, pk)) + .ok() + }) + .collect(); Ok(Box::new(ValueSetSshKey { map })) } pub fn from_repl_v1(data: &[(String, String)]) -> Result { let map = data .iter() - .map(|(tag, data)| (tag.clone(), data.clone())) - .collect(); + .map(|(tag, data)| { + SshPublicKey::from_string(&data) + .map_err(|err| { + warn!(%tag, ?err, "discarding corrupted ssh public key"); + OperationError::VS0001IncomingReplSshPublicKey + }) + .map(|pk| (tag.clone(), pk)) + }) + .collect::, _>>()?; + Ok(Box::new(ValueSetSshKey { map })) } @@ -43,7 +61,7 @@ impl ValueSetSshKey { #[allow(clippy::should_implement_trait)] pub fn from_iter(iter: T) -> Option> where - T: IntoIterator, + T: IntoIterator, { let map = iter.into_iter().collect(); Some(Box::new(ValueSetSshKey { map })) @@ -104,15 +122,15 @@ impl ValueSetT for ValueSetSshKey { } fn validate(&self, _schema_attr: &SchemaAttribute) -> bool { - self.map.iter().all(|(s, key)| { - SshPublicKey::from_string(key).is_ok() - && Value::validate_str_escapes(s) + self.map.iter().all(|(s, _key)| { + Value::validate_str_escapes(s) + // && Value::validate_iname(s) && Value::validate_singleline(s) }) } fn to_proto_string_clone_iter(&self) -> Box + '_> { - Box::new(self.map.keys().cloned()) + Box::new(self.map.values().map(|pk| pk.to_string())) } fn to_db_valueset_v2(&self) -> DbValueSetV2 { @@ -121,7 +139,7 @@ impl ValueSetT for ValueSetSshKey { .iter() .map(|(tag, key)| DbValueTaggedStringV1 { tag: tag.clone(), - data: key.clone(), + data: key.to_string(), }) .collect(), ) @@ -132,7 +150,7 @@ impl ValueSetT for ValueSetSshKey { set: self .map .iter() - .map(|(tag, key)| (tag.clone(), key.clone())) + .map(|(tag, key)| (tag.clone(), key.to_string())) .collect(), } } @@ -167,15 +185,15 @@ impl ValueSetT for ValueSetSshKey { } } - fn as_sshkey_map(&self) -> Option<&BTreeMap> { + fn as_sshkey_map(&self) -> Option<&BTreeMap> { Some(&self.map) } - fn get_ssh_tag(&self, tag: &str) -> Option<&str> { - self.map.get(tag).map(|s| s.as_str()) + fn get_ssh_tag(&self, tag: &str) -> Option<&SshPublicKey> { + self.map.get(tag) } - fn as_sshpubkey_str_iter(&self) -> Option + '_>> { - Some(Box::new(self.map.values().map(|s| s.as_str()))) + fn as_sshpubkey_string_iter(&self) -> Option + '_>> { + Some(Box::new(self.map.values().map(|pk| pk.to_string()))) } } diff --git a/server/testkit/tests/scim_test.rs b/server/testkit/tests/scim_test.rs index b4ff9ecee..6a3e2526b 100644 --- a/server/testkit/tests/scim_test.rs +++ b/server/testkit/tests/scim_test.rs @@ -123,25 +123,27 @@ async fn test_scim_sync_get(rsclient: KanidmClient) { // check that the CSP headers are coming back eprintln!( "csp headers: {:#?}", - response.headers().get(http::header::CONTENT_SECURITY_POLICY) + response + .headers() + .get(http::header::CONTENT_SECURITY_POLICY) + ); + assert_ne!( + response + .headers() + .get(http::header::CONTENT_SECURITY_POLICY), + None ); - assert_ne!(response.headers().get(http::header::CONTENT_SECURITY_POLICY), None); // test that the proper content type comes back let url = rsclient.make_url("/scim/v1/Sink"); - let response = match client.get(url.clone()).send().await { + let response = match client.get(url.clone()).send().await { Ok(value) => value, Err(error) => { - panic!( - "Failed to query {:?} : {:#?}", - url, - error - ); + panic!("Failed to query {:?} : {:#?}", url, error); } }; - assert!( response.status().is_success()); + assert!(response.status().is_success()); let content_type = response.headers().get(http::header::CONTENT_TYPE).unwrap(); assert!(content_type.to_str().unwrap().contains("text/html")); assert!(response.text().await.unwrap().contains("Sink")); - }