mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 04:27:02 +01:00
20231014 account policy (#2218)
* Start to prep for unix+ssh keys in credupdate session
This commit is contained in:
parent
d88c8d2921
commit
6ff9082fd2
47
Cargo.lock
generated
47
Cargo.lock
generated
|
@ -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",
|
||||
|
|
14
Cargo.toml
14
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"
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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(|| {
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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::{
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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"] }
|
||||
|
|
|
@ -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 },
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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")
|
||||
)
|
||||
);
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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?;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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())))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"));
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue