mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 12:37:00 +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]]
|
[[package]]
|
||||||
name = "base64urlsafedata"
|
name = "base64urlsafedata"
|
||||||
version = "0.1.3"
|
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 = [
|
dependencies = [
|
||||||
"base64 0.21.4",
|
"base64 0.21.4",
|
||||||
|
"paste 1.0.14",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -3135,7 +3135,7 @@ dependencies = [
|
||||||
"sketching",
|
"sketching",
|
||||||
"smartstring",
|
"smartstring",
|
||||||
"smolset",
|
"smolset",
|
||||||
"sshkeys",
|
"sshkey-attest",
|
||||||
"svg",
|
"svg",
|
||||||
"time",
|
"time",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
@ -5109,14 +5109,30 @@ version = "0.3.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3b9b39299b249ad65f3b7e96443bad61c02ca5cd3589f46cb6d610a0fd6c0d6a"
|
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]]
|
[[package]]
|
||||||
name = "sshkeys"
|
name = "sshkeys"
|
||||||
version = "0.3.2"
|
version = "0.3.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "git+https://github.com/dnaeon/rust-sshkeys.git?rev=fa5bd02dd6e90ee724fdb981253c1e7726a7f534#fa5bd02dd6e90ee724fdb981253c1e7726a7f534"
|
||||||
checksum = "c926cb006a77964474a13a86aa0135ea82c9fd43e6793a1151cc54143db6637c"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.12.3",
|
"base64 0.12.3",
|
||||||
"byteorder",
|
"byteorder",
|
||||||
|
"serde",
|
||||||
"sha2 0.8.2",
|
"sha2 0.8.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -5932,10 +5948,22 @@ dependencies = [
|
||||||
"wasm-bindgen",
|
"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]]
|
[[package]]
|
||||||
name = "webauthn-authenticator-rs"
|
name = "webauthn-authenticator-rs"
|
||||||
version = "0.5.0-dev"
|
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 = [
|
dependencies = [
|
||||||
"async-stream",
|
"async-stream",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
|
@ -5967,7 +5995,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "webauthn-rs"
|
name = "webauthn-rs"
|
||||||
version = "0.5.0-dev"
|
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 = [
|
dependencies = [
|
||||||
"base64urlsafedata",
|
"base64urlsafedata",
|
||||||
"serde",
|
"serde",
|
||||||
|
@ -5980,7 +6008,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "webauthn-rs-core"
|
name = "webauthn-rs-core"
|
||||||
version = "0.5.0-dev"
|
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 = [
|
dependencies = [
|
||||||
"base64 0.21.4",
|
"base64 0.21.4",
|
||||||
"base64urlsafedata",
|
"base64urlsafedata",
|
||||||
|
@ -5996,6 +6024,7 @@ dependencies = [
|
||||||
"tracing",
|
"tracing",
|
||||||
"url",
|
"url",
|
||||||
"uuid",
|
"uuid",
|
||||||
|
"webauthn-attestation-ca",
|
||||||
"webauthn-rs-proto",
|
"webauthn-rs-proto",
|
||||||
"x509-parser",
|
"x509-parser",
|
||||||
]
|
]
|
||||||
|
@ -6003,7 +6032,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "webauthn-rs-proto"
|
name = "webauthn-rs-proto"
|
||||||
version = "0.5.0-dev"
|
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 = [
|
dependencies = [
|
||||||
"base64urlsafedata",
|
"base64urlsafedata",
|
||||||
"js-sys",
|
"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 = { path = "../scim/proto" }
|
||||||
# scim_proto = { git = "https://github.com/kanidm/scim.git" }
|
# scim_proto = { git = "https://github.com/kanidm/scim.git" }
|
||||||
|
|
||||||
base64urlsafedata = { 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 = "429662e34d6e760af8cff68760567c6b56dbb2d5" }
|
webauthn-authenticator-rs = { git = "https://github.com/kanidm/webauthn-rs.git", rev = "2218d2055c0c900ef57b398423eee5e8d5521f4c" }
|
||||||
webauthn-rs = { git = "https://github.com/kanidm/webauthn-rs.git", rev = "429662e34d6e760af8cff68760567c6b56dbb2d5" }
|
webauthn-rs = { git = "https://github.com/kanidm/webauthn-rs.git", rev = "2218d2055c0c900ef57b398423eee5e8d5521f4c" }
|
||||||
webauthn-rs-core = { git = "https://github.com/kanidm/webauthn-rs.git", rev = "429662e34d6e760af8cff68760567c6b56dbb2d5" }
|
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 = "429662e34d6e760af8cff68760567c6b56dbb2d5" }
|
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" }
|
# base64urlsafedata = { path = "../webauthn-rs/base64urlsafedata" }
|
||||||
# webauthn-authenticator-rs = { path = "../webauthn-rs/webauthn-authenticator-rs" }
|
# webauthn-authenticator-rs = { path = "../webauthn-rs/webauthn-authenticator-rs" }
|
||||||
# webauthn-rs = { path = "../webauthn-rs/webauthn-rs" }
|
# webauthn-rs = { path = "../webauthn-rs/webauthn-rs" }
|
||||||
# webauthn-rs-core = { path = "../webauthn-rs/webauthn-rs-core" }
|
# webauthn-rs-core = { path = "../webauthn-rs/webauthn-rs-core" }
|
||||||
# webauthn-rs-proto = { path = "../webauthn-rs/webauthn-rs-proto" }
|
# webauthn-rs-proto = { path = "../webauthn-rs/webauthn-rs-proto" }
|
||||||
|
# sshkey-attest = { path = "../webauthn-rs/sshkey-attest" }
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
kanidmd_core = { path = "./server/core" }
|
kanidmd_core = { path = "./server/core" }
|
||||||
|
@ -180,7 +182,7 @@ shellexpand = "^2.1.2"
|
||||||
sketching = { path = "./libs/sketching" }
|
sketching = { path = "./libs/sketching" }
|
||||||
smartstring = "^1.0.1"
|
smartstring = "^1.0.1"
|
||||||
smolset = "^1.3.1"
|
smolset = "^1.3.1"
|
||||||
sshkeys = "^0.3.1"
|
sshkey-attest = "^0.5.0-dev"
|
||||||
svg = "0.13.1"
|
svg = "0.13.1"
|
||||||
syn = { version = "2.0.38", features = ["full"] }
|
syn = { version = "2.0.38", features = ["full"] }
|
||||||
tempfile = "3.8.0"
|
tempfile = "3.8.0"
|
||||||
|
|
|
@ -279,6 +279,12 @@ pub enum OperationError {
|
||||||
GidOverlapsSystemMin(u32),
|
GidOverlapsSystemMin(u32),
|
||||||
/// When a name is denied by the system config
|
/// When a name is denied by the system config
|
||||||
ValueDenyName,
|
ValueDenyName,
|
||||||
|
// What about something like this for unique errors?
|
||||||
|
// ValueSet errors
|
||||||
|
VS0001IncomingReplSshPublicKey,
|
||||||
|
// Value Errors
|
||||||
|
VL0001ValueSshPublicKeyString,
|
||||||
|
SC0001IncomingSshPublicKey,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialEq for OperationError {
|
impl PartialEq for OperationError {
|
||||||
|
|
|
@ -784,7 +784,7 @@ impl QueryServerReadV1 {
|
||||||
.and_then(|e| {
|
.and_then(|e| {
|
||||||
// From the entry, turn it into the value
|
// From the entry, turn it into the value
|
||||||
e.get_ava_iter_sshpubkeys(Attribute::SshPublicKey)
|
e.get_ava_iter_sshpubkeys(Attribute::SshPublicKey)
|
||||||
.map(|i| i.map(|s| s.to_string()).collect())
|
.map(|i| i.collect())
|
||||||
})
|
})
|
||||||
.unwrap_or_else(|| {
|
.unwrap_or_else(|| {
|
||||||
// No matching entry? Return none.
|
// No matching entry? Return none.
|
||||||
|
@ -848,7 +848,7 @@ impl QueryServerReadV1 {
|
||||||
// From the entry, turn it into the value
|
// From the entry, turn it into the value
|
||||||
e.get_ava_set(Attribute::SshPublicKey).and_then(|vs| {
|
e.get_ava_set(Attribute::SshPublicKey).and_then(|vs| {
|
||||||
// Get the one tagged value
|
// 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(|| {
|
.unwrap_or_else(|| {
|
||||||
|
|
|
@ -1023,21 +1023,23 @@ impl QueryServerWriteV1 {
|
||||||
#[instrument(
|
#[instrument(
|
||||||
level = "info",
|
level = "info",
|
||||||
name = "ssh_key_create",
|
name = "ssh_key_create",
|
||||||
skip(self, uat, uuid_or_name, tag, key, filter, eventid)
|
skip_all,
|
||||||
fields(uuid = ?eventid)
|
fields(uuid = ?eventid)
|
||||||
)]
|
)]
|
||||||
pub async fn handle_sshkeycreate(
|
pub async fn handle_sshkeycreate(
|
||||||
&self,
|
&self,
|
||||||
uat: Option<String>,
|
uat: Option<String>,
|
||||||
uuid_or_name: String,
|
uuid_or_name: String,
|
||||||
tag: String,
|
tag: &str,
|
||||||
key: String,
|
key: &str,
|
||||||
filter: Filter<FilterInvalid>,
|
filter: Filter<FilterInvalid>,
|
||||||
eventid: Uuid,
|
eventid: Uuid,
|
||||||
) -> Result<(), OperationError> {
|
) -> Result<(), OperationError> {
|
||||||
|
let v_sk = Value::new_sshkey_str(tag, key)?;
|
||||||
|
|
||||||
// Because this is from internal, we can generate a real modlist, rather
|
// Because this is from internal, we can generate a real modlist, rather
|
||||||
// than relying on the proto ones.
|
// 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)
|
self.modify_from_internal_parts(uat, &uuid_or_name, &ml, filter)
|
||||||
.await
|
.await
|
||||||
|
@ -1046,7 +1048,7 @@ impl QueryServerWriteV1 {
|
||||||
#[instrument(
|
#[instrument(
|
||||||
level = "info",
|
level = "info",
|
||||||
name = "idm_account_unix_extend",
|
name = "idm_account_unix_extend",
|
||||||
skip(self, uat, uuid_or_name, ux, eventid)
|
skip_all,
|
||||||
fields(uuid = ?eventid)
|
fields(uuid = ?eventid)
|
||||||
)]
|
)]
|
||||||
pub async fn handle_idmaccountunixextend(
|
pub async fn handle_idmaccountunixextend(
|
||||||
|
|
|
@ -28,7 +28,10 @@ impl IntoResponses for DefaultApiResponse {
|
||||||
.description("Ok"),
|
.description("Ok"),
|
||||||
)
|
)
|
||||||
.response("400", ResponseBuilder::new().description("Invalid Request"))
|
.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"))
|
.response("403", ResponseBuilder::new().description("Not Authorized"))
|
||||||
.build()
|
.build()
|
||||||
.into()
|
.into()
|
||||||
|
@ -47,10 +50,12 @@ impl IntoResponses for ApiResponseWithout200 {
|
||||||
fn responses() -> BTreeMap<String, RefOr<Response>> {
|
fn responses() -> BTreeMap<String, RefOr<Response>> {
|
||||||
ResponsesBuilder::new()
|
ResponsesBuilder::new()
|
||||||
.response("400", ResponseBuilder::new().description("Invalid Request"))
|
.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"))
|
.response("403", ResponseBuilder::new().description("Not Authorized"))
|
||||||
.build()
|
.build()
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -120,14 +120,16 @@ pub fn get_js_files(role: ServerRole) -> Vec<JavaScriptFile> {
|
||||||
env!("KANIDM_WEB_UI_PKG_PATH").to_owned(),
|
env!("KANIDM_WEB_UI_PKG_PATH").to_owned(),
|
||||||
filepath,
|
filepath,
|
||||||
)) {
|
)) {
|
||||||
Ok(hash) =>
|
Ok(hash) => js_files.push(JavaScriptFile {
|
||||||
js_files.push(JavaScriptFile {
|
|
||||||
filepath,
|
filepath,
|
||||||
hash,
|
hash,
|
||||||
filetype: None,
|
filetype: None,
|
||||||
}),
|
}),
|
||||||
Err(err) => {
|
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 http::{HeaderMap, HeaderValue, StatusCode};
|
||||||
use hyper::Body;
|
use hyper::Body;
|
||||||
use kanidm_proto::constants::APPLICATION_JSON;
|
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::{
|
use kanidmd_lib::idm::oauth2::{
|
||||||
AccessTokenIntrospectRequest, AccessTokenRequest, AuthorisationRequest, AuthorisePermitSuccess,
|
AccessTokenIntrospectRequest, AccessTokenRequest, AuthorisationRequest, AuthorisePermitSuccess,
|
||||||
AuthoriseResponse, ErrorResponse, Oauth2Error, TokenRevokeRequest,
|
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
|
// TODO: we should handle the session-based auth bit here I think maybe possibly there's no tests
|
||||||
let client_authz = match kopid.uat {
|
let client_authz = match kopid.uat {
|
||||||
Some(val) => val,
|
Some(val) => val,
|
||||||
None =>
|
None => {
|
||||||
{
|
|
||||||
return (
|
return (
|
||||||
StatusCode::UNAUTHORIZED,
|
StatusCode::UNAUTHORIZED,
|
||||||
[(ACCESS_CONTROL_ALLOW_ORIGIN, "*")],
|
[(ACCESS_CONTROL_ALLOW_ORIGIN, "*")],
|
||||||
""
|
"",
|
||||||
)
|
)
|
||||||
.into_response();
|
.into_response();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -676,19 +675,15 @@ pub async fn oauth2_token_revoke_post(
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
match res {
|
match res {
|
||||||
Ok(()) =>
|
Ok(()) => (StatusCode::OK, [(ACCESS_CONTROL_ALLOW_ORIGIN, "*")], "").into_response(),
|
||||||
{
|
|
||||||
(StatusCode::OK,
|
|
||||||
[(ACCESS_CONTROL_ALLOW_ORIGIN, "*")],
|
|
||||||
""
|
|
||||||
).into_response()
|
|
||||||
}
|
|
||||||
Err(Oauth2Error::AuthenticationRequired) => {
|
Err(Oauth2Error::AuthenticationRequired) => {
|
||||||
// This will trigger our ui to auth and retry.
|
// This will trigger our ui to auth and retry.
|
||||||
(StatusCode::UNAUTHORIZED,
|
(
|
||||||
[(ACCESS_CONTROL_ALLOW_ORIGIN, "*"),],
|
StatusCode::UNAUTHORIZED,
|
||||||
""
|
[(ACCESS_CONTROL_ALLOW_ORIGIN, "*")],
|
||||||
).into_response()
|
"",
|
||||||
|
)
|
||||||
|
.into_response()
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
// https://datatracker.ietf.org/doc/html/rfc6749#section-5.2
|
// 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(),
|
error: e.to_string(),
|
||||||
..Default::default()
|
..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()),
|
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"),
|
(ACCESS_CONTROL_ALLOW_HEADERS, "Authorization"),
|
||||||
],
|
],
|
||||||
String::new(),
|
String::new(),
|
||||||
).into_response()
|
)
|
||||||
|
.into_response()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn route_setup(state: ServerState) -> Router<ServerState> {
|
pub fn route_setup(state: ServerState) -> Router<ServerState> {
|
||||||
|
|
|
@ -1444,7 +1444,7 @@ pub async fn person_id_ssh_pubkeys_post(
|
||||||
// Add a msg here
|
// Add a msg here
|
||||||
state
|
state
|
||||||
.qe_w_ref
|
.qe_w_ref
|
||||||
.handle_sshkeycreate(kopid.uat, id, tag, key, filter, kopid.eventid)
|
.handle_sshkeycreate(kopid.uat, id, &tag, &key, filter, kopid.eventid)
|
||||||
.await
|
.await
|
||||||
.map(Json::from)
|
.map(Json::from)
|
||||||
.map_err(WebError::from)
|
.map_err(WebError::from)
|
||||||
|
@ -1473,7 +1473,7 @@ pub async fn service_account_id_ssh_pubkeys_post(
|
||||||
// Add a msg here
|
// Add a msg here
|
||||||
state
|
state
|
||||||
.qe_w_ref
|
.qe_w_ref
|
||||||
.handle_sshkeycreate(kopid.uat, id, tag, key, filter, kopid.eventid)
|
.handle_sshkeycreate(kopid.uat, id, &tag, &key, filter, kopid.eventid)
|
||||||
.await
|
.await
|
||||||
.map(Json::from)
|
.map(Json::from)
|
||||||
.map_err(WebError::from)
|
.map_err(WebError::from)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use super::apidocs::path_schema;
|
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::errors::WebError;
|
||||||
use super::middleware::KOpId;
|
use super::middleware::KOpId;
|
||||||
use super::oauth2::oauth2_id;
|
use super::oauth2::oauth2_id;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use super::apidocs::path_schema;
|
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::errors::WebError;
|
||||||
use super::middleware::KOpId;
|
use super::middleware::KOpId;
|
||||||
use super::v1::{
|
use super::v1::{
|
||||||
|
|
|
@ -25,7 +25,7 @@ extern crate tracing;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate kanidmd_lib;
|
extern crate kanidmd_lib;
|
||||||
|
|
||||||
pub mod actors;
|
mod actors;
|
||||||
pub mod admin;
|
pub mod admin;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
mod crypto;
|
mod crypto;
|
||||||
|
|
|
@ -61,7 +61,7 @@ serde_json = { workspace = true }
|
||||||
sketching = { workspace = true }
|
sketching = { workspace = true }
|
||||||
smartstring = { workspace = true, features = ["serde"] }
|
smartstring = { workspace = true, features = ["serde"] }
|
||||||
smolset = { workspace = true }
|
smolset = { workspace = true }
|
||||||
sshkeys = { workspace = true }
|
sshkey-attest = { workspace = true }
|
||||||
time = { workspace = true, features = ["serde", "std"] }
|
time = { workspace = true, features = ["serde", "std"] }
|
||||||
tokio = { workspace = true, features = ["net", "sync", "time", "rt"] }
|
tokio = { workspace = true, features = ["net", "sync", "time", "rt"] }
|
||||||
tokio-util = { workspace = true, features = ["codec"] }
|
tokio-util = { workspace = true, features = ["codec"] }
|
||||||
|
|
|
@ -59,6 +59,10 @@ pub enum DbValueIntentTokenStateV1 {
|
||||||
primary_can_edit: bool,
|
primary_can_edit: bool,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
passkeys_can_edit: bool,
|
passkeys_can_edit: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
unixcred_can_edit: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
sshpubkey_can_edit: bool,
|
||||||
},
|
},
|
||||||
#[serde(rename = "p")]
|
#[serde(rename = "p")]
|
||||||
InProgress {
|
InProgress {
|
||||||
|
@ -71,6 +75,10 @@ pub enum DbValueIntentTokenStateV1 {
|
||||||
primary_can_edit: bool,
|
primary_can_edit: bool,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
passkeys_can_edit: bool,
|
passkeys_can_edit: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
unixcred_can_edit: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
sshpubkey_can_edit: bool,
|
||||||
},
|
},
|
||||||
#[serde(rename = "c")]
|
#[serde(rename = "c")]
|
||||||
Consumed { max_ttl: Duration },
|
Consumed { max_ttl: Duration },
|
||||||
|
|
|
@ -2638,9 +2638,12 @@ impl<VALID, STATE> Entry<VALID, STATE> {
|
||||||
|
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
/// If possible, return an iterator over the set of ssh key values transformed into a `&str`.
|
/// 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)
|
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
|
// 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 crate::value::{IntentTokenState, PartialValue, SessionState, Value};
|
||||||
use kanidm_lib_crypto::CryptoPolicy;
|
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 {
|
macro_rules! try_from_entry {
|
||||||
($value:expr, $groups:expr) => {{
|
($value:expr, $groups:expr) => {{
|
||||||
// Check the classes
|
// Check the classes
|
||||||
|
@ -115,12 +158,44 @@ macro_rules! try_from_entry {
|
||||||
ui_hints.insert(UiHint::SynchronisedAccount);
|
ui_hints.insert(UiHint::SynchronisedAccount);
|
||||||
}
|
}
|
||||||
|
|
||||||
if $value.attribute_equality(
|
let unix_extn = if $value.attribute_equality(
|
||||||
Attribute::Class,
|
Attribute::Class,
|
||||||
&EntryClass::PosixAccount.to_partialvalue(),
|
&EntryClass::PosixAccount.to_partialvalue(),
|
||||||
) {
|
) {
|
||||||
ui_hints.insert(UiHint::PosixAccount);
|
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 {
|
Ok(Account {
|
||||||
uuid,
|
uuid,
|
||||||
|
@ -139,41 +214,16 @@ macro_rules! try_from_entry {
|
||||||
mail_primary,
|
mail_primary,
|
||||||
mail,
|
mail,
|
||||||
credential_update_intent_tokens,
|
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 {
|
impl Account {
|
||||||
|
pub(crate) fn unix_extn(&self) -> Option<&UnixExtensions> {
|
||||||
|
self.unix_extn.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
#[instrument(level = "trace", skip_all)]
|
#[instrument(level = "trace", skip_all)]
|
||||||
pub(crate) fn try_from_entry_ro(
|
pub(crate) fn try_from_entry_ro(
|
||||||
value: &Entry<EntrySealed, EntryCommitted>,
|
value: &Entry<EntrySealed, EntryCommitted>,
|
||||||
|
|
|
@ -4,6 +4,8 @@ use std::fmt;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use sshkey_attest::proto::PublicKey as SshPublicKey;
|
||||||
|
|
||||||
use hashbrown::HashSet;
|
use hashbrown::HashSet;
|
||||||
use kanidm_proto::v1::{
|
use kanidm_proto::v1::{
|
||||||
CUExtPortal, CURegState, CUStatus, CredentialDetail, PasskeyDetail, PasswordFeedback,
|
CUExtPortal, CURegState, CUStatus, CredentialDetail, PasskeyDetail, PasswordFeedback,
|
||||||
|
@ -87,7 +89,6 @@ pub(crate) struct CredentialUpdateSession {
|
||||||
account: Account,
|
account: Account,
|
||||||
// What intent was used to initiate this session.
|
// What intent was used to initiate this session.
|
||||||
intent_token_id: Option<String>,
|
intent_token_id: Option<String>,
|
||||||
// Acc policy
|
|
||||||
|
|
||||||
// Is there an extertal credential portal?
|
// Is there an extertal credential portal?
|
||||||
ext_cred_portal: CUExtPortal,
|
ext_cred_portal: CUExtPortal,
|
||||||
|
@ -96,6 +97,14 @@ pub(crate) struct CredentialUpdateSession {
|
||||||
primary: Option<Credential>,
|
primary: Option<Credential>,
|
||||||
primary_can_edit: bool,
|
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 that have been configured.
|
||||||
passkeys: BTreeMap<Uuid, (String, PasskeyV4)>,
|
passkeys: BTreeMap<Uuid, (String, PasskeyV4)>,
|
||||||
passkeys_can_edit: bool,
|
passkeys_can_edit: bool,
|
||||||
|
@ -121,6 +130,7 @@ impl fmt::Debug for CredentialUpdateSession {
|
||||||
.collect();
|
.collect();
|
||||||
f.debug_struct("CredentialUpdateSession")
|
f.debug_struct("CredentialUpdateSession")
|
||||||
.field("account.spn", &self.account.spn)
|
.field("account.spn", &self.account.spn)
|
||||||
|
.field("account.unix", &self.account.unix_extn().is_some())
|
||||||
.field("intent_token_id", &self.intent_token_id)
|
.field("intent_token_id", &self.intent_token_id)
|
||||||
.field("primary.detail()", &primary)
|
.field("primary.detail()", &primary)
|
||||||
.field("passkeys.list()", &passkeys)
|
.field("passkeys.list()", &passkeys)
|
||||||
|
@ -355,13 +365,15 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
||||||
ident,
|
ident,
|
||||||
Some(btreeset![
|
Some(btreeset![
|
||||||
Attribute::PrimaryCredential.into(),
|
Attribute::PrimaryCredential.into(),
|
||||||
Attribute::PassKeys.into()
|
Attribute::PassKeys.into(),
|
||||||
|
Attribute::UnixPassword.into(),
|
||||||
|
Attribute::SshPublicKey.into()
|
||||||
]),
|
]),
|
||||||
&[entry],
|
&[entry],
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let eperm = effective_perms.get(0).ok_or_else(|| {
|
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
|
OperationError::InvalidState
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
@ -369,7 +381,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
||||||
// the current status of it's authentication?
|
// the current status of it's authentication?
|
||||||
|
|
||||||
if eperm.target != account.uuid {
|
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);
|
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 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 {
|
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.
|
// 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)?;
|
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.
|
// 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.");
|
error!("Unable to proceed with credential update intent - at least one type of credential must be modifiable or visible.");
|
||||||
Err(OperationError::NotAuthorised)
|
Err(OperationError::NotAuthorised)
|
||||||
} else {
|
} 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((
|
Ok((
|
||||||
account,
|
account,
|
||||||
CredUpdateSessionPerms {
|
CredUpdateSessionPerms {
|
||||||
ext_cred_portal_can_view,
|
ext_cred_portal_can_view,
|
||||||
passkeys_can_edit,
|
passkeys_can_edit,
|
||||||
primary_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 ext_cred_portal_can_view = perms.ext_cred_portal_can_view;
|
||||||
let primary_can_edit = perms.primary_can_edit;
|
let primary_can_edit = perms.primary_can_edit;
|
||||||
let passkeys_can_edit = perms.passkeys_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
|
// - stash the current state of all associated credentials
|
||||||
let primary = if primary_can_edit {
|
let primary = if primary_can_edit {
|
||||||
|
@ -483,6 +550,21 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
||||||
BTreeMap::default()
|
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 = account.devicekeys.clone();
|
||||||
let devicekeys = BTreeMap::default();
|
let devicekeys = BTreeMap::default();
|
||||||
|
|
||||||
|
@ -511,6 +593,10 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
||||||
ext_cred_portal,
|
ext_cred_portal,
|
||||||
primary,
|
primary,
|
||||||
primary_can_edit,
|
primary_can_edit,
|
||||||
|
unixcred,
|
||||||
|
unixcred_can_edit,
|
||||||
|
sshkeys,
|
||||||
|
sshpubkey_can_edit,
|
||||||
passkeys,
|
passkeys,
|
||||||
passkeys_can_edit,
|
passkeys_can_edit,
|
||||||
_devicekeys: devicekeys,
|
_devicekeys: devicekeys,
|
||||||
|
@ -952,15 +1038,10 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
||||||
};
|
};
|
||||||
|
|
||||||
if session.primary_can_edit {
|
if session.primary_can_edit {
|
||||||
match &session.primary {
|
modlist.push_mod(Modify::Purged(Attribute::PrimaryCredential.into()));
|
||||||
Some(ncred) => {
|
if let Some(ncred) = &session.primary {
|
||||||
modlist.push_mod(Modify::Purged(Attribute::PrimaryCredential.into()));
|
let vcred = Value::new_credential("primary", ncred.clone());
|
||||||
let vcred = Value::new_credential("primary", ncred.clone());
|
modlist.push_mod(Modify::Present(Attribute::PrimaryCredential.into(), vcred));
|
||||||
modlist.push_mod(Modify::Present(Attribute::PrimaryCredential.into(), vcred));
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
modlist.push_mod(Modify::Purged(Attribute::PrimaryCredential.into()));
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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!
|
// Apply to the account!
|
||||||
trace!(?modlist, "processing change");
|
trace!(?modlist, "processing change");
|
||||||
|
|
||||||
|
@ -1142,7 +1239,6 @@ impl<'a> IdmServerCredUpdateTransaction<'a> {
|
||||||
) -> Result<(), PasswordQuality> {
|
) -> Result<(), PasswordQuality> {
|
||||||
// password strength and badlisting is always global, rather than per-pw-policy.
|
// 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.
|
// pw-policy as check on the account is about requirements for mfa for example.
|
||||||
//
|
|
||||||
|
|
||||||
// is the password at least 10 char?
|
// is the password at least 10 char?
|
||||||
if cleartext.len() < PW_MIN_LENGTH {
|
if cleartext.len() < PW_MIN_LENGTH {
|
||||||
|
|
|
@ -831,7 +831,7 @@ mod tests {
|
||||||
(Attribute::LoginShell, Value::new_iutf8("/bin/zsh")),
|
(Attribute::LoginShell, Value::new_iutf8("/bin/zsh")),
|
||||||
(
|
(
|
||||||
Attribute::SshPublicKey,
|
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::value::ApiToken;
|
||||||
|
|
||||||
use crate::schema::{SchemaClass, SchemaTransaction};
|
use crate::schema::{SchemaClass, SchemaTransaction};
|
||||||
|
use sshkey_attest::proto::PublicKey as SshPublicKey;
|
||||||
|
|
||||||
// Internals of a Scim Sync token
|
// Internals of a Scim Sync token
|
||||||
|
|
||||||
|
@ -1094,7 +1095,11 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
||||||
))
|
))
|
||||||
})
|
})
|
||||||
.and_then(|external_id| match external_id {
|
.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");
|
error!("Invalid value attribute - must be scim simple string");
|
||||||
Err(OperationError::InvalidAttribute(format!(
|
Err(OperationError::InvalidAttribute(format!(
|
||||||
|
@ -2641,7 +2646,8 @@ mod tests {
|
||||||
let mut ssh_keyiter = testuser
|
let mut ssh_keyiter = testuser
|
||||||
.get_ava_iter_sshpubkeys(Attribute::SshPublicKey)
|
.get_ava_iter_sshpubkeys(Attribute::SshPublicKey)
|
||||||
.expect("Failed to access ssh pubkeys");
|
.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);
|
assert_eq!(ssh_keyiter.next(), None);
|
||||||
|
|
||||||
// Check memberof works.
|
// Check memberof works.
|
||||||
|
|
|
@ -20,16 +20,17 @@ pub(crate) struct UnixUserAccount {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub spn: String,
|
pub spn: String,
|
||||||
pub displayname: String,
|
pub displayname: String,
|
||||||
pub gidnumber: u32,
|
|
||||||
pub uuid: Uuid,
|
pub uuid: Uuid,
|
||||||
pub shell: Option<String>,
|
|
||||||
pub sshkeys: Vec<String>,
|
|
||||||
pub groups: Vec<UnixGroup>,
|
|
||||||
cred: Option<Credential>,
|
|
||||||
pub valid_from: Option<OffsetDateTime>,
|
pub valid_from: Option<OffsetDateTime>,
|
||||||
pub expire: Option<OffsetDateTime>,
|
pub expire: Option<OffsetDateTime>,
|
||||||
pub radius_secret: Option<String>,
|
pub radius_secret: Option<String>,
|
||||||
pub mail: Vec<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 {
|
macro_rules! try_from_entry {
|
||||||
|
@ -150,16 +151,6 @@ impl UnixUserAccount {
|
||||||
try_from_entry!(value, groups)
|
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> {
|
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: Result<Vec<_>, _> = self.groups.iter().map(|g| g.to_unixgrouptoken()).collect();
|
||||||
let groups = groups?;
|
let groups = groups?;
|
||||||
|
|
|
@ -155,6 +155,10 @@ pub enum ReplIntentTokenV1 {
|
||||||
primary_can_edit: bool,
|
primary_can_edit: bool,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
passkeys_can_edit: bool,
|
passkeys_can_edit: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
unixcred_can_edit: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
sshpubkey_can_edit: bool,
|
||||||
},
|
},
|
||||||
InProgress {
|
InProgress {
|
||||||
token_id: String,
|
token_id: String,
|
||||||
|
@ -167,6 +171,10 @@ pub enum ReplIntentTokenV1 {
|
||||||
primary_can_edit: bool,
|
primary_can_edit: bool,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
passkeys_can_edit: bool,
|
passkeys_can_edit: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
unixcred_can_edit: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
sshpubkey_can_edit: bool,
|
||||||
},
|
},
|
||||||
Consumed {
|
Consumed {
|
||||||
token_id: String,
|
token_id: String,
|
||||||
|
|
|
@ -757,9 +757,13 @@ pub trait QueryServerTransaction<'a> {
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
v
|
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() {
|
} else if let Some(k_set) = value.as_sshkey_map() {
|
||||||
let v: Vec<_> = k_set.values().cloned().map(|s| s.into_bytes()).collect();
|
let v: Vec<_> = k_set.values().cloned().map(|s| s.into_bytes()).collect();
|
||||||
Ok(v)
|
Ok(v)
|
||||||
|
*/
|
||||||
} else {
|
} else {
|
||||||
let v: Vec<_> = value
|
let v: Vec<_> = value
|
||||||
.to_proto_string_clone_iter()
|
.to_proto_string_clone_iter()
|
||||||
|
|
|
@ -23,7 +23,7 @@ use openssl::ec::EcKey;
|
||||||
use openssl::pkey::Private;
|
use openssl::pkey::Private;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sshkeys::PublicKey as SshPublicKey;
|
use sshkey_attest::proto::PublicKey as SshPublicKey;
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
@ -124,6 +124,8 @@ pub struct CredUpdateSessionPerms {
|
||||||
pub ext_cred_portal_can_view: bool,
|
pub ext_cred_portal_can_view: bool,
|
||||||
pub primary_can_edit: bool,
|
pub primary_can_edit: bool,
|
||||||
pub passkeys_can_edit: bool,
|
pub passkeys_can_edit: bool,
|
||||||
|
pub unixcred_can_edit: bool,
|
||||||
|
pub sshpubkey_can_edit: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
@ -932,7 +934,7 @@ pub enum Value {
|
||||||
Refer(Uuid),
|
Refer(Uuid),
|
||||||
JsonFilt(ProtoFilter),
|
JsonFilt(ProtoFilter),
|
||||||
Cred(String, Credential),
|
Cred(String, Credential),
|
||||||
SshKey(String, String),
|
SshKey(String, SshPublicKey),
|
||||||
SecretValue(String),
|
SecretValue(String),
|
||||||
Spn(String, String),
|
Spn(String, String),
|
||||||
Uint32(u32),
|
Uint32(u32),
|
||||||
|
@ -1268,21 +1270,22 @@ impl Value {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new_sshkey_str(tag: &str, key: &str) -> Self {
|
pub fn new_sshkey_str(tag: &str, key: &str) -> Result<Self, OperationError> {
|
||||||
Value::SshKey(tag.to_string(), key.to_string())
|
SshPublicKey::from_string(key)
|
||||||
}
|
.map(|pk| Value::SshKey(tag.to_string(), pk))
|
||||||
|
.map_err(|err| {
|
||||||
pub fn new_sshkey(tag: String, key: String) -> Self {
|
error!(?err, "value sshkey failed to parse string");
|
||||||
Value::SshKey(tag, key)
|
OperationError::VL0001ValueSshPublicKeyString
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_sshkey(&self) -> bool {
|
pub fn is_sshkey(&self) -> bool {
|
||||||
matches!(&self, Value::SshKey(_, _))
|
matches!(&self, Value::SshKey(_, _))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_sshkey(&self) -> Option<&str> {
|
pub fn get_sshkey(&self) -> Option<String> {
|
||||||
match &self {
|
match &self {
|
||||||
Value::SshKey(_, key) => Some(key.as_str()),
|
Value::SshKey(_, key) => Some(key.to_string()),
|
||||||
_ => None,
|
_ => 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 {
|
match self {
|
||||||
Value::SshKey(tag, k) => Some((tag, k)),
|
Value::SshKey(tag, k) => Some((tag, k)),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
pub fn to_spn(self) -> Option<(String, String)> {
|
pub fn to_spn(self) -> Option<(String, String)> {
|
||||||
match self {
|
match self {
|
||||||
|
@ -1690,18 +1695,7 @@ impl Value {
|
||||||
Value::Iname(s) => s.clone(),
|
Value::Iname(s) => s.clone(),
|
||||||
Value::Uuid(u) => u.as_hyphenated().to_string(),
|
Value::Uuid(u) => u.as_hyphenated().to_string(),
|
||||||
// We display the tag and fingerprint.
|
// We display the tag and fingerprint.
|
||||||
Value::SshKey(tag, key) =>
|
Value::SshKey(tag, key) => format!("{}: {}", tag, key.to_string()),
|
||||||
// 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::Spn(n, r) => format!("{n}@{r}"),
|
Value::Spn(n, r) => format!("{n}@{r}"),
|
||||||
_ => unreachable!(
|
_ => unreachable!(
|
||||||
"You've specified the wrong type for the attribute, got: {:?}",
|
"You've specified the wrong type for the attribute, got: {:?}",
|
||||||
|
@ -1740,9 +1734,9 @@ impl Value {
|
||||||
&& Value::validate_singleline(s)
|
&& Value::validate_singleline(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
Value::SshKey(s, key) => {
|
Value::SshKey(s, _key) => {
|
||||||
SshPublicKey::from_string(key).is_ok()
|
Value::validate_str_escapes(s)
|
||||||
&& Value::validate_str_escapes(s)
|
// && Value::validate_iname(s)
|
||||||
&& Value::validate_singleline(s)
|
&& Value::validate_singleline(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1884,30 +1878,30 @@ mod tests {
|
||||||
"QK1JSAQqVfGhA8lLbJHmnQ/b/KMl2lzzp7SXej0wPUfvI/IP3NGb8irLzq8+JssAzXGJ+HMql+mNHiSuPaktbFzZ6y",
|
"QK1JSAQqVfGhA8lLbJHmnQ/b/KMl2lzzp7SXej0wPUfvI/IP3NGb8irLzq8+JssAzXGJ+HMql+mNHiSuPaktbFzZ6y",
|
||||||
"ikMR6Rx/psU07nAkxKZDEYpNVv william@amethyst");
|
"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());
|
assert!(sk1.validate());
|
||||||
// to proto them
|
// to proto them
|
||||||
let psk1 = sk1.to_proto_string_clone();
|
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());
|
assert!(sk2.validate());
|
||||||
let psk2 = sk2.to_proto_string_clone();
|
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());
|
assert!(sk3.validate());
|
||||||
let psk3 = sk3.to_proto_string_clone();
|
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");
|
let sk4 = Value::new_sshkey_str("tag", "ntaouhtnhtnuehtnuhotnuhtneouhtneouh");
|
||||||
assert!(!sk4.validate());
|
assert!(sk4.is_err());
|
||||||
|
|
||||||
let sk5 = Value::new_sshkey_str(
|
let sk5 = Value::new_sshkey_str(
|
||||||
"tag",
|
"tag",
|
||||||
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAeGW1P6Pc2rPq0XqbRaDKBcXZUPRklo",
|
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAeGW1P6Pc2rPq0XqbRaDKBcXZUPRklo",
|
||||||
);
|
);
|
||||||
assert!(!sk5.validate());
|
assert!(sk5.is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
@ -221,12 +221,16 @@ 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,
|
||||||
|
sshpubkey_can_edit,
|
||||||
} => IntentTokenState::Valid {
|
} => IntentTokenState::Valid {
|
||||||
max_ttl,
|
max_ttl,
|
||||||
perms: CredUpdateSessionPerms {
|
perms: CredUpdateSessionPerms {
|
||||||
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,
|
||||||
|
sshpubkey_can_edit,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
DbValueIntentTokenStateV1::InProgress {
|
DbValueIntentTokenStateV1::InProgress {
|
||||||
|
@ -236,6 +240,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,
|
||||||
|
sshpubkey_can_edit,
|
||||||
} => IntentTokenState::InProgress {
|
} => IntentTokenState::InProgress {
|
||||||
max_ttl,
|
max_ttl,
|
||||||
session_id,
|
session_id,
|
||||||
|
@ -244,6 +250,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,
|
||||||
|
sshpubkey_can_edit,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
DbValueIntentTokenStateV1::Consumed { max_ttl } => {
|
DbValueIntentTokenStateV1::Consumed { max_ttl } => {
|
||||||
|
@ -266,6 +274,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,
|
||||||
|
sshpubkey_can_edit,
|
||||||
} => (
|
} => (
|
||||||
token_id.clone(),
|
token_id.clone(),
|
||||||
IntentTokenState::Valid {
|
IntentTokenState::Valid {
|
||||||
|
@ -274,6 +284,8 @@ impl ValueSetIntentToken {
|
||||||
ext_cred_portal_can_view: *ext_cred_portal_can_view,
|
ext_cred_portal_can_view: *ext_cred_portal_can_view,
|
||||||
primary_can_edit: *primary_can_edit,
|
primary_can_edit: *primary_can_edit,
|
||||||
passkeys_can_edit: *passkeys_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,
|
ext_cred_portal_can_view,
|
||||||
primary_can_edit,
|
primary_can_edit,
|
||||||
passkeys_can_edit,
|
passkeys_can_edit,
|
||||||
|
unixcred_can_edit,
|
||||||
|
sshpubkey_can_edit,
|
||||||
} => (
|
} => (
|
||||||
token_id.clone(),
|
token_id.clone(),
|
||||||
IntentTokenState::InProgress {
|
IntentTokenState::InProgress {
|
||||||
|
@ -295,6 +309,8 @@ impl ValueSetIntentToken {
|
||||||
ext_cred_portal_can_view: *ext_cred_portal_can_view,
|
ext_cred_portal_can_view: *ext_cred_portal_can_view,
|
||||||
primary_can_edit: *primary_can_edit,
|
primary_can_edit: *primary_can_edit,
|
||||||
passkeys_can_edit: *passkeys_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,
|
ext_cred_portal_can_view,
|
||||||
primary_can_edit,
|
primary_can_edit,
|
||||||
passkeys_can_edit,
|
passkeys_can_edit,
|
||||||
|
unixcred_can_edit,
|
||||||
|
sshpubkey_can_edit,
|
||||||
},
|
},
|
||||||
} => DbValueIntentTokenStateV1::Valid {
|
} => DbValueIntentTokenStateV1::Valid {
|
||||||
max_ttl: *max_ttl,
|
max_ttl: *max_ttl,
|
||||||
ext_cred_portal_can_view: *ext_cred_portal_can_view,
|
ext_cred_portal_can_view: *ext_cred_portal_can_view,
|
||||||
primary_can_edit: *primary_can_edit,
|
primary_can_edit: *primary_can_edit,
|
||||||
passkeys_can_edit: *passkeys_can_edit,
|
passkeys_can_edit: *passkeys_can_edit,
|
||||||
|
unixcred_can_edit: *unixcred_can_edit,
|
||||||
|
sshpubkey_can_edit: *sshpubkey_can_edit,
|
||||||
},
|
},
|
||||||
IntentTokenState::InProgress {
|
IntentTokenState::InProgress {
|
||||||
max_ttl,
|
max_ttl,
|
||||||
|
@ -418,6 +438,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,
|
||||||
|
sshpubkey_can_edit,
|
||||||
},
|
},
|
||||||
} => DbValueIntentTokenStateV1::InProgress {
|
} => DbValueIntentTokenStateV1::InProgress {
|
||||||
max_ttl: *max_ttl,
|
max_ttl: *max_ttl,
|
||||||
|
@ -426,6 +448,8 @@ impl ValueSetT for ValueSetIntentToken {
|
||||||
ext_cred_portal_can_view: *ext_cred_portal_can_view,
|
ext_cred_portal_can_view: *ext_cred_portal_can_view,
|
||||||
primary_can_edit: *primary_can_edit,
|
primary_can_edit: *primary_can_edit,
|
||||||
passkeys_can_edit: *passkeys_can_edit,
|
passkeys_can_edit: *passkeys_can_edit,
|
||||||
|
unixcred_can_edit: *unixcred_can_edit,
|
||||||
|
sshpubkey_can_edit: *sshpubkey_can_edit,
|
||||||
},
|
},
|
||||||
IntentTokenState::Consumed { max_ttl } => {
|
IntentTokenState::Consumed { max_ttl } => {
|
||||||
DbValueIntentTokenStateV1::Consumed { max_ttl: *max_ttl }
|
DbValueIntentTokenStateV1::Consumed { max_ttl: *max_ttl }
|
||||||
|
@ -450,6 +474,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,
|
||||||
|
sshpubkey_can_edit,
|
||||||
},
|
},
|
||||||
} => ReplIntentTokenV1::Valid {
|
} => ReplIntentTokenV1::Valid {
|
||||||
token_id: u.clone(),
|
token_id: u.clone(),
|
||||||
|
@ -457,6 +483,8 @@ impl ValueSetT for ValueSetIntentToken {
|
||||||
ext_cred_portal_can_view: *ext_cred_portal_can_view,
|
ext_cred_portal_can_view: *ext_cred_portal_can_view,
|
||||||
primary_can_edit: *primary_can_edit,
|
primary_can_edit: *primary_can_edit,
|
||||||
passkeys_can_edit: *passkeys_can_edit,
|
passkeys_can_edit: *passkeys_can_edit,
|
||||||
|
unixcred_can_edit: *unixcred_can_edit,
|
||||||
|
sshpubkey_can_edit: *sshpubkey_can_edit,
|
||||||
},
|
},
|
||||||
IntentTokenState::InProgress {
|
IntentTokenState::InProgress {
|
||||||
max_ttl,
|
max_ttl,
|
||||||
|
@ -467,6 +495,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,
|
||||||
|
sshpubkey_can_edit,
|
||||||
},
|
},
|
||||||
} => ReplIntentTokenV1::InProgress {
|
} => ReplIntentTokenV1::InProgress {
|
||||||
token_id: u.clone(),
|
token_id: u.clone(),
|
||||||
|
@ -476,6 +506,8 @@ impl ValueSetT for ValueSetIntentToken {
|
||||||
ext_cred_portal_can_view: *ext_cred_portal_can_view,
|
ext_cred_portal_can_view: *ext_cred_portal_can_view,
|
||||||
primary_can_edit: *primary_can_edit,
|
primary_can_edit: *primary_can_edit,
|
||||||
passkeys_can_edit: *passkeys_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 {
|
IntentTokenState::Consumed { max_ttl } => ReplIntentTokenV1::Consumed {
|
||||||
token_id: u.clone(),
|
token_id: u.clone(),
|
||||||
|
|
|
@ -10,6 +10,7 @@ use openssl::pkey::Public;
|
||||||
use smolset::SmolSet;
|
use smolset::SmolSet;
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
// use std::fmt::Debug;
|
// use std::fmt::Debug;
|
||||||
|
use sshkey_attest::proto::PublicKey as SshPublicKey;
|
||||||
use webauthn_rs::prelude::AttestedPasskey as DeviceKeyV4;
|
use webauthn_rs::prelude::AttestedPasskey as DeviceKeyV4;
|
||||||
use webauthn_rs::prelude::Passkey as PasskeyV4;
|
use webauthn_rs::prelude::Passkey as PasskeyV4;
|
||||||
|
|
||||||
|
@ -147,7 +148,7 @@ pub trait ValueSetT: std::fmt::Debug + DynClone {
|
||||||
Err(OperationError::InvalidValueState)
|
Err(OperationError::InvalidValueState)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_ssh_tag(&self, _tag: &str) -> Option<&str> {
|
fn get_ssh_tag(&self, _tag: &str) -> Option<&SshPublicKey> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -197,7 +198,7 @@ pub trait ValueSetT: std::fmt::Debug + DynClone {
|
||||||
None
|
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
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -318,7 +319,7 @@ pub trait ValueSetT: std::fmt::Debug + DynClone {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn as_sshkey_map(&self) -> Option<&BTreeMap<String, String>> {
|
fn as_sshkey_map(&self) -> Option<&BTreeMap<String, SshPublicKey>> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,34 +7,52 @@ use crate::repl::proto::ReplAttrV1;
|
||||||
use crate::schema::SchemaAttribute;
|
use crate::schema::SchemaAttribute;
|
||||||
use crate::valueset::{DbValueSetV2, ValueSet};
|
use crate::valueset::{DbValueSetV2, ValueSet};
|
||||||
|
|
||||||
use sshkeys::PublicKey as SshPublicKey;
|
use sshkey_attest::proto::PublicKey as SshPublicKey;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ValueSetSshKey {
|
pub struct ValueSetSshKey {
|
||||||
map: BTreeMap<String, String>,
|
map: BTreeMap<String, SshPublicKey>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ValueSetSshKey {
|
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();
|
let mut map = BTreeMap::new();
|
||||||
map.insert(t, k);
|
map.insert(t, k);
|
||||||
Box::new(ValueSetSshKey { map })
|
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()
|
self.map.insert(t, k).is_none()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_dbvs2(data: Vec<DbValueTaggedStringV1>) -> Result<ValueSet, OperationError> {
|
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 }))
|
Ok(Box::new(ValueSetSshKey { map }))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_repl_v1(data: &[(String, String)]) -> Result<ValueSet, OperationError> {
|
pub fn from_repl_v1(data: &[(String, String)]) -> Result<ValueSet, OperationError> {
|
||||||
let map = data
|
let map = data
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(tag, data)| (tag.clone(), data.clone()))
|
.map(|(tag, data)| {
|
||||||
.collect();
|
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 }))
|
Ok(Box::new(ValueSetSshKey { map }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,7 +61,7 @@ impl ValueSetSshKey {
|
||||||
#[allow(clippy::should_implement_trait)]
|
#[allow(clippy::should_implement_trait)]
|
||||||
pub fn from_iter<T>(iter: T) -> Option<Box<Self>>
|
pub fn from_iter<T>(iter: T) -> Option<Box<Self>>
|
||||||
where
|
where
|
||||||
T: IntoIterator<Item = (String, String)>,
|
T: IntoIterator<Item = (String, SshPublicKey)>,
|
||||||
{
|
{
|
||||||
let map = iter.into_iter().collect();
|
let map = iter.into_iter().collect();
|
||||||
Some(Box::new(ValueSetSshKey { map }))
|
Some(Box::new(ValueSetSshKey { map }))
|
||||||
|
@ -104,15 +122,15 @@ impl ValueSetT for ValueSetSshKey {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate(&self, _schema_attr: &SchemaAttribute) -> bool {
|
fn validate(&self, _schema_attr: &SchemaAttribute) -> bool {
|
||||||
self.map.iter().all(|(s, key)| {
|
self.map.iter().all(|(s, _key)| {
|
||||||
SshPublicKey::from_string(key).is_ok()
|
Value::validate_str_escapes(s)
|
||||||
&& Value::validate_str_escapes(s)
|
// && Value::validate_iname(s)
|
||||||
&& Value::validate_singleline(s)
|
&& Value::validate_singleline(s)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_proto_string_clone_iter(&self) -> Box<dyn Iterator<Item = String> + '_> {
|
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 {
|
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
|
||||||
|
@ -121,7 +139,7 @@ impl ValueSetT for ValueSetSshKey {
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(tag, key)| DbValueTaggedStringV1 {
|
.map(|(tag, key)| DbValueTaggedStringV1 {
|
||||||
tag: tag.clone(),
|
tag: tag.clone(),
|
||||||
data: key.clone(),
|
data: key.to_string(),
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
)
|
)
|
||||||
|
@ -132,7 +150,7 @@ impl ValueSetT for ValueSetSshKey {
|
||||||
set: self
|
set: self
|
||||||
.map
|
.map
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(tag, key)| (tag.clone(), key.clone()))
|
.map(|(tag, key)| (tag.clone(), key.to_string()))
|
||||||
.collect(),
|
.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)
|
Some(&self.map)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_ssh_tag(&self, tag: &str) -> Option<&str> {
|
fn get_ssh_tag(&self, tag: &str) -> Option<&SshPublicKey> {
|
||||||
self.map.get(tag).map(|s| s.as_str())
|
self.map.get(tag)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn as_sshpubkey_str_iter(&self) -> Option<Box<dyn Iterator<Item = &str> + '_>> {
|
fn as_sshpubkey_string_iter(&self) -> Option<Box<dyn Iterator<Item = String> + '_>> {
|
||||||
Some(Box::new(self.map.values().map(|s| s.as_str())))
|
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
|
// check that the CSP headers are coming back
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"csp headers: {:#?}",
|
"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
|
// test that the proper content type comes back
|
||||||
let url = rsclient.make_url("/scim/v1/Sink");
|
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,
|
Ok(value) => value,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
panic!(
|
panic!("Failed to query {:?} : {:#?}", url, error);
|
||||||
"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();
|
let content_type = response.headers().get(http::header::CONTENT_TYPE).unwrap();
|
||||||
assert!(content_type.to_str().unwrap().contains("text/html"));
|
assert!(content_type.to_str().unwrap().contains("text/html"));
|
||||||
assert!(response.text().await.unwrap().contains("Sink"));
|
assert!(response.text().await.unwrap().contains("Sink"));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue