20231014 account policy (#2218)

* Start to prep for unix+ssh keys in credupdate session
This commit is contained in:
Firstyear 2023-10-19 11:40:06 +10:00 committed by GitHub
parent d88c8d2921
commit 6ff9082fd2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 445 additions and 188 deletions

47
Cargo.lock generated
View file

@ -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",

View file

@ -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"

View file

@ -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 {

View file

@ -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(|| {

View file

@ -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<String>,
uuid_or_name: String,
tag: String,
key: String,
tag: &str,
key: &str,
filter: Filter<FilterInvalid>,
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(

View file

@ -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<String, RefOr<Response>> {
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()
}
}

View file

@ -120,14 +120,16 @@ pub fn get_js_files(role: ServerRole) -> Vec<JavaScriptFile> {
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"
)
}
}
}

View file

@ -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<ServerState> {

View file

@ -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)

View file

@ -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;

View file

@ -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::{

View file

@ -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;

View file

@ -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"] }

View file

@ -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 },

View file

@ -2638,9 +2638,12 @@ impl<VALID, STATE> Entry<VALID, STATE> {
#[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<impl Iterator<Item = &str>> {
pub fn get_ava_iter_sshpubkeys(
&self,
attr: Attribute,
) -> Option<impl Iterator<Item = String> + '_> {
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

View file

@ -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<Credential>,
_shell: Option<String>,
sshkeys: BTreeMap<String, SshPublicKey>,
_gidnumber: u32,
}
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)]
pub struct Account {
// To make this self-referential, we'll need to likely make Entry Pin<Arc<_>>
// 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<Uuid>,
pub groups: Vec<Group>,
pub primary: Option<Credential>,
pub passkeys: BTreeMap<Uuid, (String, PasskeyV4)>,
pub devicekeys: BTreeMap<Uuid, (String, DeviceKeyV4)>,
pub valid_from: Option<OffsetDateTime>,
pub expire: Option<OffsetDateTime>,
pub radius_secret: Option<String>,
pub ui_hints: BTreeSet<UiHint>,
pub mail_primary: Option<String>,
pub mail: Vec<String>,
pub credential_update_intent_tokens: BTreeMap<String, IntentTokenState>,
pub(crate) unix_extn: Option<UnixExtensions>,
}
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<Uuid>,
// We want to allow this so that in the future we can populate this into oauth2 tokens
#[allow(dead_code)]
pub groups: Vec<Group>,
pub primary: Option<Credential>,
pub passkeys: BTreeMap<Uuid, (String, PasskeyV4)>,
pub devicekeys: BTreeMap<Uuid, (String, DeviceKeyV4)>,
pub valid_from: Option<OffsetDateTime>,
pub expire: Option<OffsetDateTime>,
pub radius_secret: Option<String>,
pub spn: String,
pub ui_hints: BTreeSet<UiHint>,
// TODO #256: When you add mail, you should update the check to zxcvbn
// to include these.
pub mail_primary: Option<String>,
pub mail: Vec<String>,
pub credential_update_intent_tokens: BTreeMap<String, IntentTokenState>,
}
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<EntrySealed, EntryCommitted>,

View file

@ -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<String>,
// Acc policy
// Is there an extertal credential portal?
ext_cred_portal: CUExtPortal,
@ -96,6 +97,14 @@ pub(crate) struct CredentialUpdateSession {
primary: Option<Credential>,
primary_can_edit: bool,
// Unix / Sudo PW
unixcred: Option<Credential>,
unixcred_can_edit: bool,
// Ssh Keys
sshkeys: BTreeMap<String, SshPublicKey>,
sshpubkey_can_edit: bool,
// Passkeys that have been configured.
passkeys: BTreeMap<Uuid, (String, PasskeyV4)>,
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<Credential> = 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 {

View file

@ -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")
)
);

View file

@ -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.

View file

@ -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<String>,
pub sshkeys: Vec<String>,
pub groups: Vec<UnixGroup>,
cred: Option<Credential>,
pub valid_from: Option<OffsetDateTime>,
pub expire: Option<OffsetDateTime>,
pub radius_secret: Option<String>,
pub mail: Vec<String>,
cred: Option<Credential>,
pub shell: Option<String>,
pub sshkeys: Vec<String>,
pub gidnumber: u32,
pub groups: Vec<UnixGroup>,
}
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<EntryReduced, EntryCommitted>,
qs: &mut QueryServerReadTransaction,
) -> Result<Self, OperationError> {
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<UnixUserToken, OperationError> {
let groups: Result<Vec<_>, _> = self.groups.iter().map(|g| g.to_unixgrouptoken()).collect();
let groups = groups?;

View file

@ -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,

View file

@ -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()

View file

@ -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<Self, OperationError> {
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<String> {
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());
}
/*

View file

@ -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(),

View file

@ -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<Box<dyn Iterator<Item = &str> + '_>> {
fn as_sshpubkey_string_iter(&self) -> Option<Box<dyn Iterator<Item = String> + '_>> {
None
}
@ -318,7 +319,7 @@ pub trait ValueSetT: std::fmt::Debug + DynClone {
None
}
fn as_sshkey_map(&self) -> Option<&BTreeMap<String, String>> {
fn as_sshkey_map(&self) -> Option<&BTreeMap<String, SshPublicKey>> {
None
}

View file

@ -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<String, String>,
map: BTreeMap<String, SshPublicKey>,
}
impl ValueSetSshKey {
pub fn new(t: String, k: String) -> Box<Self> {
pub fn new(t: String, k: SshPublicKey) -> Box<Self> {
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<DbValueTaggedStringV1>) -> Result<ValueSet, OperationError> {
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<ValueSet, OperationError> {
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::<Result<BTreeMap<_, _>, _>>()?;
Ok(Box::new(ValueSetSshKey { map }))
}
@ -43,7 +61,7 @@ impl ValueSetSshKey {
#[allow(clippy::should_implement_trait)]
pub fn from_iter<T>(iter: T) -> Option<Box<Self>>
where
T: IntoIterator<Item = (String, String)>,
T: IntoIterator<Item = (String, SshPublicKey)>,
{
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<dyn Iterator<Item = String> + '_> {
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<String, String>> {
fn as_sshkey_map(&self) -> Option<&BTreeMap<String, SshPublicKey>> {
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<Box<dyn Iterator<Item = &str> + '_>> {
Some(Box::new(self.map.values().map(|s| s.as_str())))
fn as_sshpubkey_string_iter(&self) -> Option<Box<dyn Iterator<Item = String> + '_>> {
Some(Box::new(self.map.values().map(|pk| pk.to_string())))
}
}

View file

@ -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"));
}