mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 20:47:01 +01:00
20240607 2417 piv (#2829)
Add some more ground work for future PIV/x509 authentication.
This commit is contained in:
parent
074646bcf3
commit
bd6d9284c0
90
Cargo.lock
generated
90
Cargo.lock
generated
|
@ -932,6 +932,12 @@ dependencies = [
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "const-oid"
|
||||||
|
version = "0.9.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cookie"
|
name = "cookie"
|
||||||
version = "0.16.2"
|
version = "0.16.2"
|
||||||
|
@ -1260,6 +1266,19 @@ version = "2.6.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2"
|
checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "der"
|
||||||
|
version = "0.7.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0"
|
||||||
|
dependencies = [
|
||||||
|
"const-oid",
|
||||||
|
"der_derive",
|
||||||
|
"flagset",
|
||||||
|
"pem-rfc7468",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "der-parser"
|
name = "der-parser"
|
||||||
version = "7.0.0"
|
version = "7.0.0"
|
||||||
|
@ -1274,6 +1293,17 @@ dependencies = [
|
||||||
"rusticata-macros",
|
"rusticata-macros",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "der_derive"
|
||||||
|
version = "0.7.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5fe87ce4529967e0ba1dcf8450bab64d97dfd5010a6256187ffe2e43e6f0e049"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.66",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "deranged"
|
name = "deranged"
|
||||||
version = "0.3.11"
|
version = "0.3.11"
|
||||||
|
@ -1611,6 +1641,12 @@ version = "0.4.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
|
checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "flagset"
|
||||||
|
version = "0.4.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cdeb3aa5e95cf9aabc17f060cfa0ced7b83f042390760ca53bf09df9968acaa1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "flate2"
|
name = "flate2"
|
||||||
version = "1.0.30"
|
version = "1.0.30"
|
||||||
|
@ -3156,9 +3192,11 @@ dependencies = [
|
||||||
"openssl-sys",
|
"openssl-sys",
|
||||||
"rand",
|
"rand",
|
||||||
"serde",
|
"serde",
|
||||||
|
"sha2",
|
||||||
"sketching",
|
"sketching",
|
||||||
"tracing",
|
"tracing",
|
||||||
"uuid",
|
"uuid",
|
||||||
|
"x509-cert",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -4469,6 +4507,15 @@ version = "0.8.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e3aeb8f54c078314c2065ee649a7241f46b9d8e418e1a9581ba0546657d7aa3a"
|
checksum = "e3aeb8f54c078314c2065ee649a7241f46b9d8e418e1a9581ba0546657d7aa3a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pem-rfc7468"
|
||||||
|
version = "0.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412"
|
||||||
|
dependencies = [
|
||||||
|
"base64ct",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "percent-encoding"
|
name = "percent-encoding"
|
||||||
version = "2.3.1"
|
version = "2.3.1"
|
||||||
|
@ -5520,6 +5567,16 @@ version = "0.9.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
|
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "spki"
|
||||||
|
version = "0.7.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
|
||||||
|
dependencies = [
|
||||||
|
"base64ct",
|
||||||
|
"der",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sptr"
|
name = "sptr"
|
||||||
version = "0.3.2"
|
version = "0.3.2"
|
||||||
|
@ -5753,6 +5810,27 @@ version = "0.1.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tls_codec"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b5e78c9c330f8c85b2bae7c8368f2739157db9991235123aa1b15ef9502bfb6a"
|
||||||
|
dependencies = [
|
||||||
|
"tls_codec_derive",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tls_codec_derive"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8d9ef545650e79f30233c0003bcc2504d7efac6dad25fca40744de773fe2049c"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.66",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.38.0"
|
version = "1.38.0"
|
||||||
|
@ -6834,6 +6912,18 @@ dependencies = [
|
||||||
"windows-sys 0.48.0",
|
"windows-sys 0.48.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "x509-cert"
|
||||||
|
version = "0.2.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1301e935010a701ae5f8655edc0ad17c44bad3ac5ce8c39185f75453b720ae94"
|
||||||
|
dependencies = [
|
||||||
|
"const-oid",
|
||||||
|
"der",
|
||||||
|
"spki",
|
||||||
|
"tls_codec",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "x509-parser"
|
name = "x509-parser"
|
||||||
version = "0.13.2"
|
version = "0.13.2"
|
||||||
|
|
|
@ -197,6 +197,7 @@ serde = "^1.0.197"
|
||||||
serde_cbor = { version = "0.12.0-dev", package = "serde_cbor_2" }
|
serde_cbor = { version = "0.12.0-dev", package = "serde_cbor_2" }
|
||||||
serde_json = "^1.0.114"
|
serde_json = "^1.0.114"
|
||||||
serde-wasm-bindgen = "0.5"
|
serde-wasm-bindgen = "0.5"
|
||||||
|
sha2 = "0.10.8"
|
||||||
shellexpand = "^2.1.2"
|
shellexpand = "^2.1.2"
|
||||||
smartstring = "^1.0.1"
|
smartstring = "^1.0.1"
|
||||||
smolset = "^1.3.1"
|
smolset = "^1.3.1"
|
||||||
|
@ -243,6 +244,8 @@ web-sys = "^0.3.69"
|
||||||
whoami = "^1.5.1"
|
whoami = "^1.5.1"
|
||||||
walkdir = "2"
|
walkdir = "2"
|
||||||
|
|
||||||
|
x509-cert = "0.2.5"
|
||||||
|
|
||||||
yew = "^0.20.0"
|
yew = "^0.20.0"
|
||||||
yew-router = "^0.17.0"
|
yew-router = "^0.17.0"
|
||||||
zxcvbn = "^2.2.2"
|
zxcvbn = "^2.2.2"
|
||||||
|
|
|
@ -5,15 +5,15 @@ db_fs_type = "zfs"
|
||||||
db_path = "/tmp/kanidm/kanidm.db"
|
db_path = "/tmp/kanidm/kanidm.db"
|
||||||
tls_chain = "/tmp/kanidm/chain.pem"
|
tls_chain = "/tmp/kanidm/chain.pem"
|
||||||
tls_key = "/tmp/kanidm/key.pem"
|
tls_key = "/tmp/kanidm/key.pem"
|
||||||
# tls_client_ca = "/tmp/kanidm/client_ca"
|
tls_client_ca = "/tmp/kanidm/client_ca"
|
||||||
|
|
||||||
# The log level of the server. May be one of info, debug, trace
|
# The log level of the server. May be one of info, debug, trace
|
||||||
#
|
#
|
||||||
# NOTE: this is overridden by KANIDM_LOG_LEVEL environment variable
|
# NOTE: this is overridden by KANIDM_LOG_LEVEL environment variable
|
||||||
# Defaults to "info"
|
# Defaults to "info"
|
||||||
#
|
#
|
||||||
log_level = "info"
|
# log_level = "info"
|
||||||
# log_level = "debug"
|
log_level = "debug"
|
||||||
# log_level = "trace"
|
# log_level = "trace"
|
||||||
|
|
||||||
# otel_grpc_url = "http://localhost:4317"
|
# otel_grpc_url = "http://localhost:4317"
|
||||||
|
|
|
@ -261,4 +261,24 @@ impl KanidmClient {
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn idm_person_certificate_list(&self, id: &str) -> Result<Vec<Entry>, ClientError> {
|
||||||
|
self.perform_get_request(format!("/v1/person/{}/_certificate", id).as_str())
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn idm_person_certificate_create(
|
||||||
|
&self,
|
||||||
|
id: &str,
|
||||||
|
pem_data: &str,
|
||||||
|
) -> Result<(), ClientError> {
|
||||||
|
let mut new_cert = Entry {
|
||||||
|
attrs: BTreeMap::new(),
|
||||||
|
};
|
||||||
|
new_cert
|
||||||
|
.attrs
|
||||||
|
.insert(ATTR_CERTIFICATE.to_string(), vec![pem_data.to_string()]);
|
||||||
|
self.perform_post_request(format!("/v1/person/{}/_certificate", id).as_str(), new_cert)
|
||||||
|
.await
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,9 +27,11 @@ kanidm-hsm-crypto = { workspace = true }
|
||||||
openssl-sys = { workspace = true }
|
openssl-sys = { workspace = true }
|
||||||
openssl = { workspace = true }
|
openssl = { workspace = true }
|
||||||
rand = { workspace = true }
|
rand = { workspace = true }
|
||||||
|
sha2 = { workspace = true }
|
||||||
serde = { workspace = true, features = ["derive"] }
|
serde = { workspace = true, features = ["derive"] }
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
uuid = { workspace = true }
|
uuid = { workspace = true }
|
||||||
|
x509-cert = { workspace = true, features = ["pem"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
sketching = { workspace = true }
|
sketching = { workspace = true }
|
||||||
|
|
|
@ -34,6 +34,12 @@ use kanidm_hsm_crypto::{HmacKey, Tpm};
|
||||||
pub mod mtls;
|
pub mod mtls;
|
||||||
pub mod prelude;
|
pub mod prelude;
|
||||||
pub mod serialise;
|
pub mod serialise;
|
||||||
|
pub mod x509_cert;
|
||||||
|
|
||||||
|
pub use sha2;
|
||||||
|
|
||||||
|
pub type Sha256Digest =
|
||||||
|
sha2::digest::generic_array::GenericArray<u8, sha2::digest::typenum::consts::U32>;
|
||||||
|
|
||||||
// NIST 800-63.b salt should be 112 bits -> 14 8u8.
|
// NIST 800-63.b salt should be 112 bits -> 14 8u8.
|
||||||
const PBKDF2_SALT_LEN: usize = 24;
|
const PBKDF2_SALT_LEN: usize = 24;
|
||||||
|
|
19
libs/crypto/src/x509_cert.rs
Normal file
19
libs/crypto/src/x509_cert.rs
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
use crate::Sha256Digest;
|
||||||
|
|
||||||
|
pub use ::x509_cert::der;
|
||||||
|
pub use ::x509_cert::der::pem;
|
||||||
|
pub use ::x509_cert::Certificate;
|
||||||
|
|
||||||
|
use ::sha2::{Digest, Sha256};
|
||||||
|
|
||||||
|
pub fn x509_public_key_s256(certificate: &Certificate) -> Option<Sha256Digest> {
|
||||||
|
let public_key_bytes = certificate
|
||||||
|
.tbs_certificate
|
||||||
|
.subject_public_key_info
|
||||||
|
.subject_public_key
|
||||||
|
.as_bytes()?;
|
||||||
|
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
hasher.update(public_key_bytes);
|
||||||
|
Some(hasher.finalize())
|
||||||
|
}
|
|
@ -63,6 +63,7 @@ pub const ATTR_ATTRIBUTETYPE: &str = "attributetype";
|
||||||
pub const ATTR_AUTH_SESSION_EXPIRY: &str = "authsession_expiry";
|
pub const ATTR_AUTH_SESSION_EXPIRY: &str = "authsession_expiry";
|
||||||
pub const ATTR_AUTH_PASSWORD_MINIMUM_LENGTH: &str = "auth_password_minimum_length";
|
pub const ATTR_AUTH_PASSWORD_MINIMUM_LENGTH: &str = "auth_password_minimum_length";
|
||||||
pub const ATTR_BADLIST_PASSWORD: &str = "badlist_password";
|
pub const ATTR_BADLIST_PASSWORD: &str = "badlist_password";
|
||||||
|
pub const ATTR_CERTIFICATE: &str = "certificate";
|
||||||
pub const ATTR_CLAIM: &str = "claim";
|
pub const ATTR_CLAIM: &str = "claim";
|
||||||
pub const ATTR_CLASS: &str = "class";
|
pub const ATTR_CLASS: &str = "class";
|
||||||
pub const ATTR_CLASSNAME: &str = "classname";
|
pub const ATTR_CLASSNAME: &str = "classname";
|
||||||
|
@ -159,6 +160,7 @@ pub const ATTR_PRIVILEGE_EXPIRY: &str = "privilege_expiry";
|
||||||
pub const ATTR_RADIUS_SECRET: &str = "radius_secret";
|
pub const ATTR_RADIUS_SECRET: &str = "radius_secret";
|
||||||
pub const ATTR_RECYCLED: &str = "recycled";
|
pub const ATTR_RECYCLED: &str = "recycled";
|
||||||
pub const ATTR_RECYCLEDDIRECTMEMBEROF: &str = "recycled_directmemberof";
|
pub const ATTR_RECYCLEDDIRECTMEMBEROF: &str = "recycled_directmemberof";
|
||||||
|
pub const ATTR_REFERS: &str = "refers";
|
||||||
pub const ATTR_REPLICATED: &str = "replicated";
|
pub const ATTR_REPLICATED: &str = "replicated";
|
||||||
pub const ATTR_RS256_PRIVATE_KEY_DER: &str = "rs256_private_key_der";
|
pub const ATTR_RS256_PRIVATE_KEY_DER: &str = "rs256_private_key_der";
|
||||||
pub const ATTR_SCOPE: &str = "scope";
|
pub const ATTR_SCOPE: &str = "scope";
|
||||||
|
|
|
@ -132,6 +132,10 @@ pub enum OperationError {
|
||||||
CU0003WebauthnUserNotVerified,
|
CU0003WebauthnUserNotVerified,
|
||||||
// ValueSet errors
|
// ValueSet errors
|
||||||
VS0001IncomingReplSshPublicKey,
|
VS0001IncomingReplSshPublicKey,
|
||||||
|
VS0002CertificatePublicKeyDigest,
|
||||||
|
VS0003CertificateDerDecode,
|
||||||
|
VS0004CertificatePublicKeyDigest,
|
||||||
|
VS0005CertificatePublicKeyDigest,
|
||||||
// Value Errors
|
// Value Errors
|
||||||
VL0001ValueSshPublicKeyString,
|
VL0001ValueSshPublicKeyString,
|
||||||
|
|
||||||
|
@ -226,120 +230,124 @@ impl OperationError {
|
||||||
/// Return the message associated with the error if there is one.
|
/// Return the message associated with the error if there is one.
|
||||||
fn message(&self) -> Option<&'static str> {
|
fn message(&self) -> Option<&'static str> {
|
||||||
match self {
|
match self {
|
||||||
OperationError::SessionExpired => None,
|
Self::SessionExpired => None,
|
||||||
OperationError::EmptyRequest => None,
|
Self::EmptyRequest => None,
|
||||||
OperationError::Backend => None,
|
Self::Backend => None,
|
||||||
OperationError::NoMatchingEntries => None,
|
Self::NoMatchingEntries => None,
|
||||||
OperationError::NoMatchingAttributes => None,
|
Self::NoMatchingAttributes => None,
|
||||||
OperationError::CorruptedEntry(_) => None,
|
Self::CorruptedEntry(_) => None,
|
||||||
OperationError::CorruptedIndex(_) => None,
|
Self::CorruptedIndex(_) => None,
|
||||||
OperationError::ConsistencyError(_) => None,
|
Self::ConsistencyError(_) => None,
|
||||||
OperationError::SchemaViolation(_) => None,
|
Self::SchemaViolation(_) => None,
|
||||||
OperationError::Plugin(_) => None,
|
Self::Plugin(_) => None,
|
||||||
OperationError::FilterGeneration => None,
|
Self::FilterGeneration => None,
|
||||||
OperationError::FilterUuidResolution => None,
|
Self::FilterUuidResolution => None,
|
||||||
OperationError::InvalidAttributeName(_) => None,
|
Self::InvalidAttributeName(_) => None,
|
||||||
OperationError::InvalidAttribute(_) => None,
|
Self::InvalidAttribute(_) => None,
|
||||||
OperationError::InvalidDbState => None,
|
Self::InvalidDbState => None,
|
||||||
OperationError::InvalidCacheState => None,
|
Self::InvalidCacheState => None,
|
||||||
OperationError::InvalidValueState => None,
|
Self::InvalidValueState => None,
|
||||||
OperationError::InvalidEntryId => None,
|
Self::InvalidEntryId => None,
|
||||||
OperationError::InvalidRequestState => None,
|
Self::InvalidRequestState => None,
|
||||||
OperationError::InvalidSyncState => None,
|
Self::InvalidSyncState => None,
|
||||||
OperationError::InvalidState => None,
|
Self::InvalidState => None,
|
||||||
OperationError::InvalidEntryState => None,
|
Self::InvalidEntryState => None,
|
||||||
OperationError::InvalidUuid => None,
|
Self::InvalidUuid => None,
|
||||||
OperationError::InvalidReplChangeId => None,
|
Self::InvalidReplChangeId => None,
|
||||||
OperationError::InvalidAcpState(_) => None,
|
Self::InvalidAcpState(_) => None,
|
||||||
OperationError::InvalidSchemaState(_) => None,
|
Self::InvalidSchemaState(_) => None,
|
||||||
OperationError::InvalidAccountState(_) => None,
|
Self::InvalidAccountState(_) => None,
|
||||||
OperationError::MissingEntries => None,
|
Self::MissingEntries => None,
|
||||||
OperationError::ModifyAssertionFailed => None,
|
Self::ModifyAssertionFailed => None,
|
||||||
OperationError::BackendEngine => None,
|
Self::BackendEngine => None,
|
||||||
OperationError::SqliteError => None,
|
Self::SqliteError => None,
|
||||||
OperationError::FsError => None,
|
Self::FsError => None,
|
||||||
OperationError::SerdeJsonError => None,
|
Self::SerdeJsonError => None,
|
||||||
OperationError::SerdeCborError => None,
|
Self::SerdeCborError => None,
|
||||||
OperationError::AccessDenied => None,
|
Self::AccessDenied => None,
|
||||||
OperationError::NotAuthenticated => None,
|
Self::NotAuthenticated => None,
|
||||||
OperationError::NotAuthorised => None,
|
Self::NotAuthorised => None,
|
||||||
OperationError::InvalidAuthState(_) => None,
|
Self::InvalidAuthState(_) => None,
|
||||||
OperationError::InvalidSessionState => None,
|
Self::InvalidSessionState => None,
|
||||||
OperationError::SystemProtectedObject => None,
|
Self::SystemProtectedObject => None,
|
||||||
OperationError::SystemProtectedAttribute => None,
|
Self::SystemProtectedAttribute => None,
|
||||||
OperationError::PasswordQuality(_) => None,
|
Self::PasswordQuality(_) => None,
|
||||||
OperationError::CryptographyError => None,
|
Self::CryptographyError => None,
|
||||||
OperationError::ResourceLimit => None,
|
Self::ResourceLimit => None,
|
||||||
OperationError::QueueDisconnected => None,
|
Self::QueueDisconnected => None,
|
||||||
OperationError::Webauthn => None,
|
Self::Webauthn => None,
|
||||||
OperationError::Wait(_) => None,
|
Self::Wait(_) => None,
|
||||||
OperationError::ReplReplayFailure => None,
|
Self::ReplReplayFailure => None,
|
||||||
OperationError::ReplEntryNotChanged => None,
|
Self::ReplEntryNotChanged => None,
|
||||||
OperationError::ReplInvalidRUVState => None,
|
Self::ReplInvalidRUVState => None,
|
||||||
OperationError::ReplDomainLevelUnsatisfiable => None,
|
Self::ReplDomainLevelUnsatisfiable => None,
|
||||||
OperationError::ReplDomainUuidMismatch => None,
|
Self::ReplDomainUuidMismatch => None,
|
||||||
OperationError::ReplServerUuidSplitDataState => None,
|
Self::ReplServerUuidSplitDataState => None,
|
||||||
OperationError::TransactionAlreadyCommitted => None,
|
Self::TransactionAlreadyCommitted => None,
|
||||||
OperationError::ValueDenyName => None,
|
Self::ValueDenyName => None,
|
||||||
OperationError::CU0002WebauthnRegistrationError => None,
|
Self::CU0002WebauthnRegistrationError => None,
|
||||||
OperationError::CU0003WebauthnUserNotVerified => Some("User Verification bit not set while registering credential, you may need to configure a PIN on this device."),
|
Self::CU0003WebauthnUserNotVerified => Some("User Verification bit not set while registering credential, you may need to configure a PIN on this device."),
|
||||||
OperationError::CU0001WebauthnAttestationNotTrusted => None,
|
Self::CU0001WebauthnAttestationNotTrusted => None,
|
||||||
OperationError::VS0001IncomingReplSshPublicKey => None,
|
Self::VS0001IncomingReplSshPublicKey => None,
|
||||||
OperationError::VL0001ValueSshPublicKeyString => None,
|
Self::VS0003CertificateDerDecode => Some("Decoding the stored certificate from DER failed."),
|
||||||
OperationError::SC0001IncomingSshPublicKey => None,
|
Self::VS0002CertificatePublicKeyDigest |
|
||||||
OperationError::MG0001InvalidReMigrationLevel => None,
|
Self::VS0004CertificatePublicKeyDigest |
|
||||||
OperationError::MG0002RaiseDomainLevelExceedsMaximum => None,
|
Self::VS0005CertificatePublicKeyDigest => Some("The certificates public key is unabled to be digested."),
|
||||||
OperationError::MG0003ServerPhaseInvalidForMigration => None,
|
Self::VL0001ValueSshPublicKeyString => None,
|
||||||
OperationError::DB0001MismatchedRestoreVersion => None,
|
Self::SC0001IncomingSshPublicKey => None,
|
||||||
OperationError::DB0002MismatchedRestoreVersion => None,
|
Self::MG0001InvalidReMigrationLevel => None,
|
||||||
OperationError::MG0004DomainLevelInDevelopment => None,
|
Self::MG0002RaiseDomainLevelExceedsMaximum => None,
|
||||||
OperationError::MG0005GidConstraintsNotMet => None,
|
Self::MG0003ServerPhaseInvalidForMigration => None,
|
||||||
OperationError::KP0001KeyProviderNotLoaded => None,
|
Self::DB0001MismatchedRestoreVersion => None,
|
||||||
OperationError::KP0002KeyProviderInvalidClass => None,
|
Self::DB0002MismatchedRestoreVersion => None,
|
||||||
OperationError::KP0003KeyProviderInvalidType => None,
|
Self::MG0004DomainLevelInDevelopment => None,
|
||||||
OperationError::KP0004KeyProviderMissingAttributeName => None,
|
Self::MG0005GidConstraintsNotMet => None,
|
||||||
OperationError::KP0005KeyProviderDuplicate => None,
|
Self::KP0001KeyProviderNotLoaded => None,
|
||||||
OperationError::KP0006KeyObjectJwtEs256Generation => None,
|
Self::KP0002KeyProviderInvalidClass => None,
|
||||||
OperationError::KP0007KeyProviderDefaultNotAvailable => None,
|
Self::KP0003KeyProviderInvalidType => None,
|
||||||
OperationError::KP0008KeyObjectMissingUuid => None,
|
Self::KP0004KeyProviderMissingAttributeName => None,
|
||||||
OperationError::KP0009KeyObjectPrivateToDer => None,
|
Self::KP0005KeyProviderDuplicate => None,
|
||||||
OperationError::KP0010KeyObjectSignerToVerifier => None,
|
Self::KP0006KeyObjectJwtEs256Generation => None,
|
||||||
OperationError::KP0011KeyObjectMissingClass => None,
|
Self::KP0007KeyProviderDefaultNotAvailable => None,
|
||||||
OperationError::KP0012KeyObjectMissingProvider => None,
|
Self::KP0008KeyObjectMissingUuid => None,
|
||||||
OperationError::KP0012KeyProviderNotLoaded => None,
|
Self::KP0009KeyObjectPrivateToDer => None,
|
||||||
OperationError::KP0013KeyObjectJwsEs256DerInvalid => None,
|
Self::KP0010KeyObjectSignerToVerifier => None,
|
||||||
OperationError::KP0014KeyObjectSignerToVerifier => None,
|
Self::KP0011KeyObjectMissingClass => None,
|
||||||
OperationError::KP0015KeyObjectJwsEs256DerInvalid => None,
|
Self::KP0012KeyObjectMissingProvider => None,
|
||||||
OperationError::KP0016KeyObjectJwsEs256DerInvalid => None,
|
Self::KP0012KeyProviderNotLoaded => None,
|
||||||
OperationError::KP0017KeyProviderNoSuchKey => None,
|
Self::KP0013KeyObjectJwsEs256DerInvalid => None,
|
||||||
OperationError::KP0018KeyProviderNoSuchKey => None,
|
Self::KP0014KeyObjectSignerToVerifier => None,
|
||||||
OperationError::KP0019KeyProviderUnsupportedAlgorithm => None,
|
Self::KP0015KeyObjectJwsEs256DerInvalid => None,
|
||||||
OperationError::KP0020KeyObjectNoActiveSigningKeys => None,
|
Self::KP0016KeyObjectJwsEs256DerInvalid => None,
|
||||||
OperationError::KP0021KeyObjectJwsEs256Signature => None,
|
Self::KP0017KeyProviderNoSuchKey => None,
|
||||||
OperationError::KP0022KeyObjectJwsNotAssociated => None,
|
Self::KP0018KeyProviderNoSuchKey => None,
|
||||||
OperationError::KP0023KeyObjectJwsKeyRevoked => None,
|
Self::KP0019KeyProviderUnsupportedAlgorithm => None,
|
||||||
OperationError::KP0024KeyObjectJwsInvalid => None,
|
Self::KP0020KeyObjectNoActiveSigningKeys => None,
|
||||||
OperationError::KP0025KeyProviderNotAvailable => None,
|
Self::KP0021KeyObjectJwsEs256Signature => None,
|
||||||
OperationError::KP0026KeyObjectNoSuchKey => None,
|
Self::KP0022KeyObjectJwsNotAssociated => None,
|
||||||
OperationError::KP0027KeyObjectPublicToDer => None,
|
Self::KP0023KeyObjectJwsKeyRevoked => None,
|
||||||
OperationError::KP0028KeyObjectImportJwsEs256DerInvalid => None,
|
Self::KP0024KeyObjectJwsInvalid => None,
|
||||||
OperationError::KP0029KeyObjectSignerToVerifier => None,
|
Self::KP0025KeyProviderNotAvailable => None,
|
||||||
OperationError::KP0030KeyObjectPublicToDer => None,
|
Self::KP0026KeyObjectNoSuchKey => None,
|
||||||
OperationError::KP0031KeyObjectNotFound => None,
|
Self::KP0027KeyObjectPublicToDer => None,
|
||||||
OperationError::KP0032KeyProviderNoSuchKey => None,
|
Self::KP0028KeyObjectImportJwsEs256DerInvalid => None,
|
||||||
OperationError::KP0033KeyProviderNoSuchKey => None,
|
Self::KP0029KeyObjectSignerToVerifier => None,
|
||||||
OperationError::KP0034KeyProviderUnsupportedAlgorithm => None,
|
Self::KP0030KeyObjectPublicToDer => None,
|
||||||
OperationError::KP0035KeyObjectJweA128GCMGeneration => None,
|
Self::KP0031KeyObjectNotFound => None,
|
||||||
OperationError::KP0036KeyObjectPrivateToBytes => None,
|
Self::KP0032KeyProviderNoSuchKey => None,
|
||||||
OperationError::KP0037KeyObjectImportJweA128GCMInvalid => None,
|
Self::KP0033KeyProviderNoSuchKey => None,
|
||||||
OperationError::KP0038KeyObjectImportJweA128GCMInvalid => None,
|
Self::KP0034KeyProviderUnsupportedAlgorithm => None,
|
||||||
OperationError::KP0039KeyObjectJweNotAssociated => None,
|
Self::KP0035KeyObjectJweA128GCMGeneration => None,
|
||||||
OperationError::KP0040KeyObjectJweInvalid => None,
|
Self::KP0036KeyObjectPrivateToBytes => None,
|
||||||
OperationError::KP0041KeyObjectJweRevoked => None,
|
Self::KP0037KeyObjectImportJweA128GCMInvalid => None,
|
||||||
OperationError::KP0042KeyObjectNoActiveEncryptionKeys => None,
|
Self::KP0038KeyObjectImportJweA128GCMInvalid => None,
|
||||||
OperationError::KP0043KeyObjectJweA128GCMEncryption => None,
|
Self::KP0039KeyObjectJweNotAssociated => None,
|
||||||
OperationError::KP0044KeyObjectJwsPublicJwk => None,
|
Self::KP0040KeyObjectJweInvalid => None,
|
||||||
OperationError::PL0001GidOverlapsSystemRange => None,
|
Self::KP0041KeyObjectJweRevoked => None,
|
||||||
|
Self::KP0042KeyObjectNoActiveEncryptionKeys => None,
|
||||||
|
Self::KP0043KeyObjectJweA128GCMEncryption => None,
|
||||||
|
Self::KP0044KeyObjectJwsPublicJwk => None,
|
||||||
|
Self::PL0001GidOverlapsSystemRange => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -465,6 +465,65 @@ impl QueryServerReadV1 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(
|
||||||
|
level = "info",
|
||||||
|
skip_all,
|
||||||
|
fields(uuid = ?eventid)
|
||||||
|
)]
|
||||||
|
pub async fn handle_search_refers(
|
||||||
|
&self,
|
||||||
|
client_auth_info: ClientAuthInfo,
|
||||||
|
filter: Filter<FilterInvalid>,
|
||||||
|
uuid_or_name: String,
|
||||||
|
attrs: Option<Vec<String>>,
|
||||||
|
eventid: Uuid,
|
||||||
|
) -> Result<Vec<ProtoEntry>, OperationError> {
|
||||||
|
let ct = duration_from_epoch_now();
|
||||||
|
let mut idms_prox_read = self.idms.proxy_read().await;
|
||||||
|
let ident = idms_prox_read
|
||||||
|
.validate_client_auth_info_to_ident(client_auth_info, ct)
|
||||||
|
.map_err(|e| {
|
||||||
|
admin_error!("Invalid identity: {:?}", e);
|
||||||
|
e
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let target_uuid = idms_prox_read
|
||||||
|
.qs_read
|
||||||
|
.name_to_uuid(uuid_or_name.as_str())
|
||||||
|
.map_err(|e| {
|
||||||
|
admin_error!("Error resolving id to target");
|
||||||
|
e
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Update the filter with the target_uuid
|
||||||
|
let filter = Filter::join_parts_and(
|
||||||
|
filter,
|
||||||
|
filter_all!(f_eq(Attribute::Refers, PartialValue::Refer(target_uuid))),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Make an event from the request
|
||||||
|
let srch = match SearchEvent::from_internal_message(
|
||||||
|
ident,
|
||||||
|
&filter,
|
||||||
|
attrs.as_deref(),
|
||||||
|
&mut idms_prox_read.qs_read,
|
||||||
|
) {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(e) => {
|
||||||
|
admin_error!("Failed to begin internal api search: {:?}", e);
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
trace!(?srch, "Begin event");
|
||||||
|
|
||||||
|
match idms_prox_read.qs_read.search_ext(&srch) {
|
||||||
|
Ok(entries) => SearchResult::new(&mut idms_prox_read.qs_read, &entries)
|
||||||
|
.map(|ok_sr| ok_sr.into_proto_array()),
|
||||||
|
Err(e) => Err(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[instrument(
|
#[instrument(
|
||||||
level = "info",
|
level = "info",
|
||||||
skip_all,
|
skip_all,
|
||||||
|
|
|
@ -97,6 +97,8 @@ impl Modify for SecurityAddon {
|
||||||
super::v1::person_id_put_attr,
|
super::v1::person_id_put_attr,
|
||||||
super::v1::person_id_post_attr,
|
super::v1::person_id_post_attr,
|
||||||
super::v1::person_id_delete_attr,
|
super::v1::person_id_delete_attr,
|
||||||
|
super::v1::person_get_id_certificate,
|
||||||
|
super::v1::person_post_id_certificate,
|
||||||
super::v1::person_get_id_credential_status,
|
super::v1::person_get_id_credential_status,
|
||||||
super::v1::person_id_credential_update_get,
|
super::v1::person_id_credential_update_get,
|
||||||
super::v1::person_id_credential_update_intent_get,
|
super::v1::person_id_credential_update_intent_get,
|
||||||
|
|
|
@ -39,10 +39,11 @@ use hyper::body::Incoming;
|
||||||
use hyper_util::rt::{TokioExecutor, TokioIo};
|
use hyper_util::rt::{TokioExecutor, TokioIo};
|
||||||
use kanidm_proto::{constants::KSESSIONID, internal::COOKIE_AUTH_SESSION_ID};
|
use kanidm_proto::{constants::KSESSIONID, internal::COOKIE_AUTH_SESSION_ID};
|
||||||
use kanidmd_lib::{idm::ClientCertInfo, status::StatusActor};
|
use kanidmd_lib::{idm::ClientCertInfo, status::StatusActor};
|
||||||
use openssl::nid;
|
|
||||||
use openssl::ssl::{Ssl, SslAcceptor, SslFiletype, SslMethod, SslSessionCacheMode, SslVerifyMode};
|
use openssl::ssl::{Ssl, SslAcceptor, SslFiletype, SslMethod, SslSessionCacheMode, SslVerifyMode};
|
||||||
use openssl::x509::X509;
|
use openssl::x509::X509;
|
||||||
|
|
||||||
|
use kanidm_lib_crypto::x509_cert::{der::Decode, x509_public_key_s256, Certificate};
|
||||||
|
|
||||||
use sketching::*;
|
use sketching::*;
|
||||||
use tokio::{
|
use tokio::{
|
||||||
net::{TcpListener, TcpStream},
|
net::{TcpListener, TcpStream},
|
||||||
|
@ -554,8 +555,8 @@ pub(crate) async fn handle_conn(
|
||||||
std::io::Error::from(ErrorKind::ConnectionAborted)
|
std::io::Error::from(ErrorKind::ConnectionAborted)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let mut tls_stream = SslStream::new(ssl, stream).map_err(|e| {
|
let mut tls_stream = SslStream::new(ssl, stream).map_err(|err| {
|
||||||
error!("Failed to create TLS stream: {:?}", e);
|
error!(?err, "Failed to create TLS stream");
|
||||||
std::io::Error::from(ErrorKind::ConnectionAborted)
|
std::io::Error::from(ErrorKind::ConnectionAborted)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
@ -565,25 +566,28 @@ pub(crate) async fn handle_conn(
|
||||||
let client_cert = if let Some(peer_cert) = tls_stream.ssl().peer_certificate() {
|
let client_cert = if let Some(peer_cert) = tls_stream.ssl().peer_certificate() {
|
||||||
// TODO: This is where we should be checking the CRL!!!
|
// TODO: This is where we should be checking the CRL!!!
|
||||||
|
|
||||||
let subject_key_id = peer_cert
|
// Extract the cert from openssl to x509-cert which is a better
|
||||||
.subject_key_id()
|
// parser to handle the various extensions.
|
||||||
.map(|ski| ski.as_slice().to_vec());
|
|
||||||
|
|
||||||
let cn = if let Some(cn) = peer_cert
|
let cert_der = peer_cert.to_der().map_err(|ossl_err| {
|
||||||
.subject_name()
|
error!(?ossl_err, "unable to process x509 certificate as DER");
|
||||||
.entries_by_nid(nid::Nid::COMMONNAME)
|
std::io::Error::from(ErrorKind::ConnectionAborted)
|
||||||
.next()
|
})?;
|
||||||
{
|
|
||||||
String::from_utf8(cn.data().as_slice().to_vec())
|
|
||||||
.map_err(|err| {
|
|
||||||
warn!(?err, "client certificate CN contains invalid utf-8 - the CN will be ignored!");
|
|
||||||
})
|
|
||||||
.ok()
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
Some(ClientCertInfo { subject_key_id, cn })
|
let certificate = Certificate::from_der(&cert_der).map_err(|ossl_err| {
|
||||||
|
error!(?ossl_err, "unable to process DER certificate to x509");
|
||||||
|
std::io::Error::from(ErrorKind::ConnectionAborted)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let public_key_s256 = x509_public_key_s256(&certificate).ok_or_else(|| {
|
||||||
|
error!("subject public key bitstring is not octet aligned");
|
||||||
|
std::io::Error::from(ErrorKind::ConnectionAborted)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Some(ClientCertInfo {
|
||||||
|
public_key_s256,
|
||||||
|
certificate,
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
|
@ -239,6 +239,8 @@ pub async fn json_rest_event_get(
|
||||||
.map_err(WebError::from)
|
.map_err(WebError::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Common event handler to search and retrieve entries with a name or id
|
||||||
|
/// and return the result as json proto entries
|
||||||
pub async fn json_rest_event_get_id(
|
pub async fn json_rest_event_get_id(
|
||||||
state: ServerState,
|
state: ServerState,
|
||||||
id: String,
|
id: String,
|
||||||
|
@ -258,6 +260,24 @@ pub async fn json_rest_event_get_id(
|
||||||
.map_err(WebError::from)
|
.map_err(WebError::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Common event handler to search and retrieve entries that reference another
|
||||||
|
/// entry by the value of name or id and return the result as json proto entries
|
||||||
|
pub async fn json_rest_event_get_refers_id(
|
||||||
|
state: ServerState,
|
||||||
|
refers_id: String,
|
||||||
|
filter: Filter<FilterInvalid>,
|
||||||
|
attrs: Option<Vec<String>>,
|
||||||
|
kopid: KOpId,
|
||||||
|
client_auth_info: ClientAuthInfo,
|
||||||
|
) -> Result<Json<Vec<ProtoEntry>>, WebError> {
|
||||||
|
state
|
||||||
|
.qe_r_ref
|
||||||
|
.handle_search_refers(client_auth_info, filter, refers_id, attrs, kopid.eventid)
|
||||||
|
.await
|
||||||
|
.map(Json::from)
|
||||||
|
.map_err(WebError::from)
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn json_rest_event_delete_id(
|
pub async fn json_rest_event_delete_id(
|
||||||
state: ServerState,
|
state: ServerState,
|
||||||
id: String,
|
id: String,
|
||||||
|
@ -656,6 +676,59 @@ pub async fn person_id_delete(
|
||||||
json_rest_event_delete_id(state, id, filter, kopid, client_auth_info).await
|
json_rest_event_delete_id(state, id, filter, kopid, client_auth_info).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// == person -> certificates
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/v1/person/{id}/_certificate",
|
||||||
|
responses(
|
||||||
|
(status=200, body=Option<ProtoEntry>, content_type="application/json"),
|
||||||
|
ApiResponseWithout200,
|
||||||
|
),
|
||||||
|
security(("token_jwt" = [])),
|
||||||
|
tag = "v1/person/certificate",
|
||||||
|
operation_id = "person_get_id_certificate",
|
||||||
|
)]
|
||||||
|
pub async fn person_get_id_certificate(
|
||||||
|
State(state): State<ServerState>,
|
||||||
|
Path(id): Path<String>,
|
||||||
|
Extension(kopid): Extension<KOpId>,
|
||||||
|
VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
|
||||||
|
) -> Result<Json<Vec<ProtoEntry>>, WebError> {
|
||||||
|
let filter = filter_all!(f_eq(Attribute::Class, EntryClass::ClientCertificate.into()));
|
||||||
|
json_rest_event_get_refers_id(state, id, filter, None, kopid, client_auth_info).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/v1/person/{id}/_certificate",
|
||||||
|
responses(
|
||||||
|
DefaultApiResponse,
|
||||||
|
),
|
||||||
|
request_body=ProtoEntry,
|
||||||
|
security(("token_jwt" = [])),
|
||||||
|
tag = "v1/person/certificate",
|
||||||
|
operation_id = "person_post_id_certificate",
|
||||||
|
)]
|
||||||
|
/// Expects the following fields in the attrs field of the req: [certificate]
|
||||||
|
///
|
||||||
|
/// The person's id will be added implicitly as a reference.
|
||||||
|
pub async fn person_post_id_certificate(
|
||||||
|
State(state): State<ServerState>,
|
||||||
|
Path(id): Path<String>,
|
||||||
|
Extension(kopid): Extension<KOpId>,
|
||||||
|
VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
|
||||||
|
Json(mut obj): Json<ProtoEntry>,
|
||||||
|
) -> Result<Json<()>, WebError> {
|
||||||
|
let classes: Vec<String> = vec![
|
||||||
|
EntryClass::ClientCertificate.into(),
|
||||||
|
EntryClass::Object.into(),
|
||||||
|
];
|
||||||
|
obj.attrs.insert(Attribute::Refers.to_string(), vec![id]);
|
||||||
|
|
||||||
|
json_rest_event_post(state, classes, obj, kopid, client_auth_info).await
|
||||||
|
}
|
||||||
|
|
||||||
// // == account ==
|
// // == account ==
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
|
@ -3085,16 +3158,14 @@ pub(crate) fn route_setup(state: ServerState) -> Router<ServerState> {
|
||||||
.post(person_id_post_attr)
|
.post(person_id_post_attr)
|
||||||
.delete(person_id_delete_attr),
|
.delete(person_id_delete_attr),
|
||||||
)
|
)
|
||||||
// .route("/v1/person/:id/_lock", get(|| async { "TODO" }))
|
.route(
|
||||||
// .route("/v1/person/:id/_credential", get(|| async { "TODO" }))
|
"/v1/person/:id/_certificate",
|
||||||
|
get(person_get_id_certificate).post(person_post_id_certificate),
|
||||||
|
)
|
||||||
.route(
|
.route(
|
||||||
"/v1/person/:id/_credential/_status",
|
"/v1/person/:id/_credential/_status",
|
||||||
get(person_get_id_credential_status),
|
get(person_get_id_credential_status),
|
||||||
)
|
)
|
||||||
// .route(
|
|
||||||
// "/v1/person/:id/_credential/:cid/_lock",
|
|
||||||
// get(|| async { "TODO" }),
|
|
||||||
// )
|
|
||||||
.route(
|
.route(
|
||||||
"/v1/person/:id/_credential/_update",
|
"/v1/person/:id/_credential/_update",
|
||||||
get(person_id_credential_update_get),
|
get(person_id_credential_update_get),
|
||||||
|
|
|
@ -620,6 +620,11 @@ pub enum DbValueKeyInternal {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
|
||||||
|
pub enum DbValueCertificate {
|
||||||
|
V1 { certificate_der: Vec<u8> },
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
pub enum DbValueV1 {
|
pub enum DbValueV1 {
|
||||||
#[serde(rename = "U8")]
|
#[serde(rename = "U8")]
|
||||||
|
@ -777,6 +782,8 @@ pub enum DbValueSetV2 {
|
||||||
KeyInternal(Vec<DbValueKeyInternal>),
|
KeyInternal(Vec<DbValueKeyInternal>),
|
||||||
#[serde(rename = "HS")]
|
#[serde(rename = "HS")]
|
||||||
HexString(Vec<String>),
|
HexString(Vec<String>),
|
||||||
|
#[serde(rename = "X509")]
|
||||||
|
Certificate(Vec<DbValueCertificate>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DbValueSetV2 {
|
impl DbValueSetV2 {
|
||||||
|
@ -828,6 +835,7 @@ impl DbValueSetV2 {
|
||||||
DbValueSetV2::CredentialType(set) => set.len(),
|
DbValueSetV2::CredentialType(set) => set.len(),
|
||||||
DbValueSetV2::WebauthnAttestationCaList { ca_list } => ca_list.len(),
|
DbValueSetV2::WebauthnAttestationCaList { ca_list } => ca_list.len(),
|
||||||
DbValueSetV2::KeyInternal(set) => set.len(),
|
DbValueSetV2::KeyInternal(set) => set.len(),
|
||||||
|
DbValueSetV2::Certificate(set) => set.len(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2209,3 +2209,38 @@ lazy_static! {
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
pub static ref IDM_ACP_HP_CLIENT_CERTIFICATE_MANAGER_DL7: BuiltinAcp = BuiltinAcp {
|
||||||
|
classes: vec![
|
||||||
|
EntryClass::Object,
|
||||||
|
EntryClass::AccessControlProfile,
|
||||||
|
EntryClass::AccessControlCreate,
|
||||||
|
EntryClass::AccessControlDelete,
|
||||||
|
EntryClass::AccessControlModify,
|
||||||
|
EntryClass::AccessControlSearch
|
||||||
|
],
|
||||||
|
name: "idm_acp_hp_client_certificate_manager",
|
||||||
|
uuid: UUID_IDM_ACP_HP_CLIENT_CERTIFICATE_MANAGER,
|
||||||
|
description: "Builtin IDM Control for allowing client certificate management.",
|
||||||
|
receiver: BuiltinAcpReceiver::Group(vec![UUID_IDM_CLIENT_CERTIFICATE_ADMINS]),
|
||||||
|
target: BuiltinAcpTarget::Filter(ProtoFilter::And(vec![
|
||||||
|
ProtoFilter::Eq(
|
||||||
|
EntryClass::Class.to_string(),
|
||||||
|
EntryClass::ClientCertificate.to_string()
|
||||||
|
),
|
||||||
|
FILTER_ANDNOT_TOMBSTONE_OR_RECYCLED.clone()
|
||||||
|
])),
|
||||||
|
search_attrs: vec![
|
||||||
|
Attribute::Class,
|
||||||
|
Attribute::Uuid,
|
||||||
|
Attribute::Certificate,
|
||||||
|
Attribute::Refers,
|
||||||
|
],
|
||||||
|
modify_removed_attrs: vec![Attribute::Certificate, Attribute::Refers,],
|
||||||
|
modify_present_attrs: vec![Attribute::Certificate, Attribute::Refers,],
|
||||||
|
create_attrs: vec![Attribute::Class, Attribute::Certificate, Attribute::Refers,],
|
||||||
|
create_classes: vec![EntryClass::Object, EntryClass::ClientCertificate,],
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -59,6 +59,7 @@ pub enum Attribute {
|
||||||
AuthSessionExpiry,
|
AuthSessionExpiry,
|
||||||
AuthPasswordMinimumLength,
|
AuthPasswordMinimumLength,
|
||||||
BadlistPassword,
|
BadlistPassword,
|
||||||
|
Certificate,
|
||||||
Claim,
|
Claim,
|
||||||
Class,
|
Class,
|
||||||
ClassName,
|
ClassName,
|
||||||
|
@ -153,6 +154,7 @@ pub enum Attribute {
|
||||||
PrivilegeExpiry,
|
PrivilegeExpiry,
|
||||||
RadiusSecret,
|
RadiusSecret,
|
||||||
RecycledDirectMemberOf,
|
RecycledDirectMemberOf,
|
||||||
|
Refers,
|
||||||
Replicated,
|
Replicated,
|
||||||
Rs256PrivateKeyDer,
|
Rs256PrivateKeyDer,
|
||||||
Scope,
|
Scope,
|
||||||
|
@ -256,6 +258,7 @@ impl TryFrom<String> for Attribute {
|
||||||
ATTR_AUTH_SESSION_EXPIRY => Attribute::AuthSessionExpiry,
|
ATTR_AUTH_SESSION_EXPIRY => Attribute::AuthSessionExpiry,
|
||||||
ATTR_AUTH_PASSWORD_MINIMUM_LENGTH => Attribute::AuthPasswordMinimumLength,
|
ATTR_AUTH_PASSWORD_MINIMUM_LENGTH => Attribute::AuthPasswordMinimumLength,
|
||||||
ATTR_BADLIST_PASSWORD => Attribute::BadlistPassword,
|
ATTR_BADLIST_PASSWORD => Attribute::BadlistPassword,
|
||||||
|
ATTR_CERTIFICATE => Attribute::Certificate,
|
||||||
ATTR_CLAIM => Attribute::Claim,
|
ATTR_CLAIM => Attribute::Claim,
|
||||||
ATTR_CLASS => Attribute::Class,
|
ATTR_CLASS => Attribute::Class,
|
||||||
ATTR_CLASSNAME => Attribute::ClassName,
|
ATTR_CLASSNAME => Attribute::ClassName,
|
||||||
|
@ -351,6 +354,7 @@ impl TryFrom<String> for Attribute {
|
||||||
ATTR_PRIVILEGE_EXPIRY => Attribute::PrivilegeExpiry,
|
ATTR_PRIVILEGE_EXPIRY => Attribute::PrivilegeExpiry,
|
||||||
ATTR_RADIUS_SECRET => Attribute::RadiusSecret,
|
ATTR_RADIUS_SECRET => Attribute::RadiusSecret,
|
||||||
ATTR_RECYCLEDDIRECTMEMBEROF => Attribute::RecycledDirectMemberOf,
|
ATTR_RECYCLEDDIRECTMEMBEROF => Attribute::RecycledDirectMemberOf,
|
||||||
|
ATTR_REFERS => Attribute::Refers,
|
||||||
ATTR_REPLICATED => Attribute::Replicated,
|
ATTR_REPLICATED => Attribute::Replicated,
|
||||||
ATTR_RS256_PRIVATE_KEY_DER => Attribute::Rs256PrivateKeyDer,
|
ATTR_RS256_PRIVATE_KEY_DER => Attribute::Rs256PrivateKeyDer,
|
||||||
ATTR_SCOPE => Attribute::Scope,
|
ATTR_SCOPE => Attribute::Scope,
|
||||||
|
@ -429,6 +433,7 @@ impl From<Attribute> for &'static str {
|
||||||
Attribute::AuthSessionExpiry => ATTR_AUTH_SESSION_EXPIRY,
|
Attribute::AuthSessionExpiry => ATTR_AUTH_SESSION_EXPIRY,
|
||||||
Attribute::AuthPasswordMinimumLength => ATTR_AUTH_PASSWORD_MINIMUM_LENGTH,
|
Attribute::AuthPasswordMinimumLength => ATTR_AUTH_PASSWORD_MINIMUM_LENGTH,
|
||||||
Attribute::BadlistPassword => ATTR_BADLIST_PASSWORD,
|
Attribute::BadlistPassword => ATTR_BADLIST_PASSWORD,
|
||||||
|
Attribute::Certificate => ATTR_CERTIFICATE,
|
||||||
Attribute::Claim => ATTR_CLAIM,
|
Attribute::Claim => ATTR_CLAIM,
|
||||||
Attribute::Class => ATTR_CLASS,
|
Attribute::Class => ATTR_CLASS,
|
||||||
Attribute::ClassName => ATTR_CLASSNAME,
|
Attribute::ClassName => ATTR_CLASSNAME,
|
||||||
|
@ -524,6 +529,7 @@ impl From<Attribute> for &'static str {
|
||||||
Attribute::PrivilegeExpiry => ATTR_PRIVILEGE_EXPIRY,
|
Attribute::PrivilegeExpiry => ATTR_PRIVILEGE_EXPIRY,
|
||||||
Attribute::RadiusSecret => ATTR_RADIUS_SECRET,
|
Attribute::RadiusSecret => ATTR_RADIUS_SECRET,
|
||||||
Attribute::RecycledDirectMemberOf => ATTR_RECYCLEDDIRECTMEMBEROF,
|
Attribute::RecycledDirectMemberOf => ATTR_RECYCLEDDIRECTMEMBEROF,
|
||||||
|
Attribute::Refers => ATTR_REFERS,
|
||||||
Attribute::Replicated => ATTR_REPLICATED,
|
Attribute::Replicated => ATTR_REPLICATED,
|
||||||
Attribute::Rs256PrivateKeyDer => ATTR_RS256_PRIVATE_KEY_DER,
|
Attribute::Rs256PrivateKeyDer => ATTR_RS256_PRIVATE_KEY_DER,
|
||||||
Attribute::Scope => ATTR_SCOPE,
|
Attribute::Scope => ATTR_SCOPE,
|
||||||
|
@ -624,6 +630,7 @@ pub enum EntryClass {
|
||||||
Builtin,
|
Builtin,
|
||||||
Class,
|
Class,
|
||||||
ClassType,
|
ClassType,
|
||||||
|
ClientCertificate,
|
||||||
Conflict,
|
Conflict,
|
||||||
DomainInfo,
|
DomainInfo,
|
||||||
DynGroup,
|
DynGroup,
|
||||||
|
@ -677,6 +684,7 @@ impl From<EntryClass> for &'static str {
|
||||||
EntryClass::Builtin => ENTRYCLASS_BUILTIN,
|
EntryClass::Builtin => ENTRYCLASS_BUILTIN,
|
||||||
EntryClass::Class => ATTR_CLASS,
|
EntryClass::Class => ATTR_CLASS,
|
||||||
EntryClass::ClassType => "classtype",
|
EntryClass::ClassType => "classtype",
|
||||||
|
EntryClass::ClientCertificate => "client_certificate",
|
||||||
EntryClass::Conflict => "conflict",
|
EntryClass::Conflict => "conflict",
|
||||||
EntryClass::DomainInfo => "domain_info",
|
EntryClass::DomainInfo => "domain_info",
|
||||||
EntryClass::DynGroup => ATTR_DYNGROUP,
|
EntryClass::DynGroup => ATTR_DYNGROUP,
|
||||||
|
|
|
@ -248,6 +248,16 @@ lazy_static! {
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Builtin IDM Group for managing client authentication certificates.
|
||||||
|
pub static ref BUILTIN_GROUP_CLIENT_CERTIFICATE_ADMINS_DL7: BuiltinGroup = BuiltinGroup {
|
||||||
|
name: "idm_client_certificate_admins",
|
||||||
|
description: "Builtin Client Certificate Administration Group.",
|
||||||
|
uuid: UUID_IDM_CLIENT_CERTIFICATE_ADMINS,
|
||||||
|
entry_managed_by: Some(UUID_IDM_ADMINS),
|
||||||
|
members: vec![UUID_IDM_ADMINS],
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
/// Builtin IDM Group for granting elevated group write and lifecycle permissions.
|
/// Builtin IDM Group for granting elevated group write and lifecycle permissions.
|
||||||
pub static ref IDM_GROUP_ADMINS_V1: BuiltinGroup = BuiltinGroup {
|
pub static ref IDM_GROUP_ADMINS_V1: BuiltinGroup = BuiltinGroup {
|
||||||
name: "idm_group_admins",
|
name: "idm_group_admins",
|
||||||
|
@ -367,6 +377,36 @@ lazy_static! {
|
||||||
],
|
],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// This must be the last group to init to include the UUID of the other high priv groups.
|
||||||
|
pub static ref IDM_HIGH_PRIVILEGE_DL7: BuiltinGroup = BuiltinGroup {
|
||||||
|
name: "idm_high_privilege",
|
||||||
|
uuid: UUID_IDM_HIGH_PRIVILEGE,
|
||||||
|
entry_managed_by: Some(UUID_IDM_ACCESS_CONTROL_ADMINS),
|
||||||
|
description: "Builtin IDM provided groups with high levels of access that should be audited and limited in modification.",
|
||||||
|
members: vec![
|
||||||
|
UUID_SYSTEM_ADMINS,
|
||||||
|
UUID_IDM_ADMINS,
|
||||||
|
UUID_DOMAIN_ADMINS,
|
||||||
|
UUID_IDM_SERVICE_DESK,
|
||||||
|
UUID_IDM_RECYCLE_BIN_ADMINS,
|
||||||
|
UUID_IDM_SCHEMA_ADMINS,
|
||||||
|
UUID_IDM_ACCESS_CONTROL_ADMINS,
|
||||||
|
UUID_IDM_OAUTH2_ADMINS,
|
||||||
|
UUID_IDM_RADIUS_ADMINS,
|
||||||
|
UUID_IDM_ACCOUNT_POLICY_ADMINS,
|
||||||
|
UUID_IDM_RADIUS_SERVERS,
|
||||||
|
UUID_IDM_GROUP_ADMINS,
|
||||||
|
UUID_IDM_UNIX_ADMINS,
|
||||||
|
UUID_IDM_PEOPLE_PII_READ,
|
||||||
|
UUID_IDM_PEOPLE_ADMINS,
|
||||||
|
UUID_IDM_PEOPLE_ON_BOARDING,
|
||||||
|
UUID_IDM_SERVICE_ACCOUNT_ADMINS,
|
||||||
|
UUID_IDM_CLIENT_CERTIFICATE_ADMINS,
|
||||||
|
UUID_IDM_HIGH_PRIVILEGE,
|
||||||
|
],
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Make a list of all the non-admin BuiltinGroup's that are created by default, doing it in a standard-ish way so we can use it around the platform
|
/// Make a list of all the non-admin BuiltinGroup's that are created by default, doing it in a standard-ish way so we can use it around the platform
|
||||||
|
|
|
@ -706,6 +706,24 @@ pub static ref SCHEMA_ATTR_DOMAIN_DEVELOPMENT_TAINT_DL7: SchemaAttribute = Schem
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub static ref SCHEMA_ATTR_REFERS_DL7: SchemaAttribute = SchemaAttribute {
|
||||||
|
uuid: UUID_SCHEMA_ATTR_REFERS,
|
||||||
|
name: Attribute::Refers.into(),
|
||||||
|
description: "A reference to linked object".to_string(),
|
||||||
|
multivalue: false,
|
||||||
|
syntax: SyntaxType::ReferenceUuid,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
pub static ref SCHEMA_ATTR_CERTIFICATE_DL7: SchemaAttribute = SchemaAttribute {
|
||||||
|
uuid: UUID_SCHEMA_ATTR_CERTIFICATE,
|
||||||
|
name: Attribute::Certificate.into(),
|
||||||
|
description: "An x509 Certificate".to_string(),
|
||||||
|
multivalue: false,
|
||||||
|
syntax: SyntaxType::Certificate,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
// === classes ===
|
// === classes ===
|
||||||
|
|
||||||
pub static ref SCHEMA_CLASS_PERSON: SchemaClass = SchemaClass {
|
pub static ref SCHEMA_CLASS_PERSON: SchemaClass = SchemaClass {
|
||||||
|
@ -1333,4 +1351,18 @@ pub static ref SCHEMA_CLASS_KEY_OBJECT_INTERNAL_DL6: SchemaClass = SchemaClass {
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// =========================================
|
||||||
|
|
||||||
|
pub static ref SCHEMA_CLASS_CLIENT_CERTIFICATE_DL7: SchemaClass = SchemaClass {
|
||||||
|
uuid: UUID_SCHEMA_CLASS_CLIENT_CERTIFICATE,
|
||||||
|
name: EntryClass::ClientCertificate.into(),
|
||||||
|
description: "A client authentication certificate".to_string(),
|
||||||
|
systemmay: vec![],
|
||||||
|
systemmust: vec![
|
||||||
|
Attribute::Certificate.into(),
|
||||||
|
Attribute::Refers.into(),
|
||||||
|
],
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
);
|
);
|
||||||
|
|
|
@ -67,6 +67,7 @@ pub const UUID_IDM_PEOPLE_ON_BOARDING: Uuid = uuid!("00000000-0000-0000-0000-000
|
||||||
pub const UUID_IDM_SERVICE_ACCOUNT_ADMINS: Uuid = uuid!("00000000-0000-0000-0000-000000000046");
|
pub const UUID_IDM_SERVICE_ACCOUNT_ADMINS: Uuid = uuid!("00000000-0000-0000-0000-000000000046");
|
||||||
pub const UUID_IDM_ACCOUNT_POLICY_ADMINS: Uuid = uuid!("00000000-0000-0000-0000-000000000047");
|
pub const UUID_IDM_ACCOUNT_POLICY_ADMINS: Uuid = uuid!("00000000-0000-0000-0000-000000000047");
|
||||||
pub const UUID_IDM_PEOPLE_SELF_NAME_WRITE: Uuid = uuid!("00000000-0000-0000-0000-000000000048");
|
pub const UUID_IDM_PEOPLE_SELF_NAME_WRITE: Uuid = uuid!("00000000-0000-0000-0000-000000000048");
|
||||||
|
pub const UUID_IDM_CLIENT_CERTIFICATE_ADMINS: Uuid = uuid!("00000000-0000-0000-0000-000000000049");
|
||||||
|
|
||||||
//
|
//
|
||||||
pub const UUID_IDM_HIGH_PRIVILEGE: Uuid = uuid!("00000000-0000-0000-0000-000000001000");
|
pub const UUID_IDM_HIGH_PRIVILEGE: Uuid = uuid!("00000000-0000-0000-0000-000000001000");
|
||||||
|
@ -304,6 +305,10 @@ pub const UUID_SCHEMA_CLASS_KEY_OBJECT_JWE_A128GCM: Uuid =
|
||||||
pub const UUID_SCHEMA_ATTR_PATCH_LEVEL: Uuid = uuid!("00000000-0000-0000-0000-ffff00000175");
|
pub const UUID_SCHEMA_ATTR_PATCH_LEVEL: Uuid = uuid!("00000000-0000-0000-0000-ffff00000175");
|
||||||
pub const UUID_SCHEMA_ATTR_DOMAIN_DEVELOPMENT_TAINT: Uuid =
|
pub const UUID_SCHEMA_ATTR_DOMAIN_DEVELOPMENT_TAINT: Uuid =
|
||||||
uuid!("00000000-0000-0000-0000-ffff00000176");
|
uuid!("00000000-0000-0000-0000-ffff00000176");
|
||||||
|
pub const UUID_SCHEMA_ATTR_REFERS: Uuid = uuid!("00000000-0000-0000-0000-ffff00000177");
|
||||||
|
pub const UUID_SCHEMA_ATTR_CERTIFICATE: Uuid = uuid!("00000000-0000-0000-0000-ffff00000178");
|
||||||
|
pub const UUID_SCHEMA_CLASS_CLIENT_CERTIFICATE: Uuid =
|
||||||
|
uuid!("00000000-0000-0000-0000-ffff00000179");
|
||||||
|
|
||||||
// System and domain infos
|
// System and domain infos
|
||||||
// I'd like to strongly criticise william of the past for making poor choices about these allocations.
|
// I'd like to strongly criticise william of the past for making poor choices about these allocations.
|
||||||
|
@ -416,8 +421,9 @@ pub const UUID_IDM_ACP_HP_GROUP_UNIX_MANAGE_V1: Uuid =
|
||||||
uuid!("00000000-0000-0000-0000-ffffff000067");
|
uuid!("00000000-0000-0000-0000-ffffff000067");
|
||||||
pub const UUID_IDM_ACP_GROUP_UNIX_MANAGE_V1: Uuid = uuid!("00000000-0000-0000-0000-ffffff000068");
|
pub const UUID_IDM_ACP_GROUP_UNIX_MANAGE_V1: Uuid = uuid!("00000000-0000-0000-0000-ffffff000068");
|
||||||
pub const UUID_IDM_ACP_ACCOUNT_UNIX_EXTEND_V1: Uuid = uuid!("00000000-0000-0000-0000-ffffff000069");
|
pub const UUID_IDM_ACP_ACCOUNT_UNIX_EXTEND_V1: Uuid = uuid!("00000000-0000-0000-0000-ffffff000069");
|
||||||
|
|
||||||
pub const UUID_KEY_PROVIDER_INTERNAL: Uuid = uuid!("00000000-0000-0000-0000-ffffff000070");
|
pub const UUID_KEY_PROVIDER_INTERNAL: Uuid = uuid!("00000000-0000-0000-0000-ffffff000070");
|
||||||
|
pub const UUID_IDM_ACP_HP_CLIENT_CERTIFICATE_MANAGER: Uuid =
|
||||||
|
uuid!("00000000-0000-0000-0000-ffffff000071");
|
||||||
|
|
||||||
// End of system ranges
|
// End of system ranges
|
||||||
pub const UUID_DOES_NOT_EXIST: Uuid = uuid!("00000000-0000-0000-0000-fffffffffffe");
|
pub const UUID_DOES_NOT_EXIST: Uuid = uuid!("00000000-0000-0000-0000-fffffffffffe");
|
||||||
|
|
|
@ -390,6 +390,45 @@ impl Account {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Given the currently bound client certificate, yield a user auth token that
|
||||||
|
/// represents the current session for the account.
|
||||||
|
pub(crate) fn client_cert_info_to_userauthtoken(
|
||||||
|
&self,
|
||||||
|
certificate_id: Uuid,
|
||||||
|
session_is_rw: bool,
|
||||||
|
ct: Duration,
|
||||||
|
account_policy: &ResolvedAccountPolicy,
|
||||||
|
) -> Option<UserAuthToken> {
|
||||||
|
let issued_at = OffsetDateTime::UNIX_EPOCH + ct;
|
||||||
|
|
||||||
|
let limit_search_max_results = account_policy.limit_search_max_results();
|
||||||
|
let limit_search_max_filter_test = account_policy.limit_search_max_filter_test();
|
||||||
|
|
||||||
|
let purpose = if session_is_rw {
|
||||||
|
UatPurpose::ReadWrite { expiry: None }
|
||||||
|
} else {
|
||||||
|
UatPurpose::ReadOnly
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(UserAuthToken {
|
||||||
|
session_id: certificate_id,
|
||||||
|
expiry: None,
|
||||||
|
issued_at,
|
||||||
|
purpose,
|
||||||
|
uuid: self.uuid,
|
||||||
|
displayname: self.displayname.clone(),
|
||||||
|
spn: self.spn.clone(),
|
||||||
|
mail_primary: self.mail_primary.clone(),
|
||||||
|
ui_hints: self.ui_hints.clone(),
|
||||||
|
// application: None,
|
||||||
|
// groups: self.groups.iter().map(|g| g.to_proto()).collect(),
|
||||||
|
limit_search_max_results,
|
||||||
|
limit_search_max_filter_test,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Determine if an entry is within it's validity period using it's `valid_from` and
|
||||||
|
/// `expire` attributes. `true` indicates the account is within the valid period.
|
||||||
pub fn check_within_valid_time(
|
pub fn check_within_valid_time(
|
||||||
ct: Duration,
|
ct: Duration,
|
||||||
valid_from: Option<&OffsetDateTime>,
|
valid_from: Option<&OffsetDateTime>,
|
||||||
|
@ -416,11 +455,14 @@ impl Account {
|
||||||
vmin && vmax
|
vmin && vmax
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Determine if this account is within it's validity period. `true` indicates the
|
||||||
|
/// account is within the valid period.
|
||||||
pub fn is_within_valid_time(&self, ct: Duration) -> bool {
|
pub fn is_within_valid_time(&self, ct: Duration) -> bool {
|
||||||
Self::check_within_valid_time(ct, self.valid_from.as_ref(), self.expire.as_ref())
|
Self::check_within_valid_time(ct, self.valid_from.as_ref(), self.expire.as_ref())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get related inputs, such as account name, email, etc.
|
/// Get related inputs, such as account name, email, etc. This is used for password
|
||||||
|
/// quality checking.
|
||||||
pub fn related_inputs(&self) -> Vec<&str> {
|
pub fn related_inputs(&self) -> Vec<&str> {
|
||||||
let mut inputs = Vec::with_capacity(4 + self.mail.len());
|
let mut inputs = Vec::with_capacity(4 + self.mail.len());
|
||||||
self.mail.iter().for_each(|m| {
|
self.mail.iter().for_each(|m| {
|
||||||
|
|
|
@ -24,6 +24,7 @@ pub(crate) mod unix;
|
||||||
|
|
||||||
use crate::server::identity::Source;
|
use crate::server::identity::Source;
|
||||||
use compact_jwt::JwsCompact;
|
use compact_jwt::JwsCompact;
|
||||||
|
use kanidm_lib_crypto::{x509_cert::Certificate, Sha256Digest};
|
||||||
use kanidm_proto::v1::{AuthAllowed, AuthIssueSession, AuthMech};
|
use kanidm_proto::v1::{AuthAllowed, AuthIssueSession, AuthMech};
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
|
@ -55,8 +56,8 @@ pub struct ClientAuthInfo {
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ClientCertInfo {
|
pub struct ClientCertInfo {
|
||||||
pub subject_key_id: Option<Vec<u8>>,
|
pub public_key_s256: Sha256Digest,
|
||||||
pub cn: Option<String>,
|
pub certificate: Certificate,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
|
@ -367,10 +367,8 @@ pub trait IdmServerTransaction<'a> {
|
||||||
} = client_auth_info;
|
} = client_auth_info;
|
||||||
|
|
||||||
match (client_cert, bearer_token) {
|
match (client_cert, bearer_token) {
|
||||||
(Some(_client_cert_info), _) => {
|
(Some(client_cert_info), _) => {
|
||||||
// TODO: Cert validation here.
|
self.client_certificate_to_identity(&client_cert_info, ct, source)
|
||||||
warn!("Unable to process client certificate identity");
|
|
||||||
Err(OperationError::NotAuthenticated)
|
|
||||||
}
|
}
|
||||||
(None, Some(token)) => match self.validate_and_parse_token_to_token(&token, ct)? {
|
(None, Some(token)) => match self.validate_and_parse_token_to_token(&token, ct)? {
|
||||||
Token::UserAuthToken(uat) => self.process_uat_to_identity(&uat, ct, source),
|
Token::UserAuthToken(uat) => self.process_uat_to_identity(&uat, ct, source),
|
||||||
|
@ -385,6 +383,9 @@ pub trait IdmServerTransaction<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// This function is not using in authentication flows - it is a reflector of the
|
||||||
|
/// current session state to allow a user-auth-token to be presented to the
|
||||||
|
/// user via the whoami call.
|
||||||
#[instrument(level = "info", skip_all)]
|
#[instrument(level = "info", skip_all)]
|
||||||
fn validate_client_auth_info_to_uat(
|
fn validate_client_auth_info_to_uat(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
@ -399,10 +400,8 @@ pub trait IdmServerTransaction<'a> {
|
||||||
} = client_auth_info;
|
} = client_auth_info;
|
||||||
|
|
||||||
match (client_cert, bearer_token) {
|
match (client_cert, bearer_token) {
|
||||||
(Some(_client_cert_info), _) => {
|
(Some(client_cert_info), _) => {
|
||||||
// TODO: Cert validation here.
|
self.client_certificate_to_user_auth_token(&client_cert_info, ct)
|
||||||
warn!("Unable to process client certificate identity");
|
|
||||||
Err(OperationError::NotAuthenticated)
|
|
||||||
}
|
}
|
||||||
(None, Some(token)) => match self.validate_and_parse_token_to_token(&token, ct)? {
|
(None, Some(token)) => match self.validate_and_parse_token_to_token(&token, ct)? {
|
||||||
Token::UserAuthToken(uat) => Ok(uat),
|
Token::UserAuthToken(uat) => Ok(uat),
|
||||||
|
@ -677,6 +676,135 @@ pub trait IdmServerTransaction<'a> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn client_cert_info_entry(
|
||||||
|
&mut self,
|
||||||
|
client_cert_info: &ClientCertInfo,
|
||||||
|
) -> Result<Arc<EntrySealedCommitted>, OperationError> {
|
||||||
|
let pks256 = hex::encode(&client_cert_info.public_key_s256);
|
||||||
|
// Using the certificate hash, find our matching cert.
|
||||||
|
let mut maybe_cert_entries = self.get_qs_txn().internal_search(filter!(f_eq(
|
||||||
|
Attribute::Certificate,
|
||||||
|
PartialValue::HexString(pks256.clone())
|
||||||
|
)))?;
|
||||||
|
|
||||||
|
let maybe_cert_entry = maybe_cert_entries.pop();
|
||||||
|
|
||||||
|
if let Some(cert_entry) = maybe_cert_entry {
|
||||||
|
if maybe_cert_entries.is_empty() {
|
||||||
|
Ok(cert_entry)
|
||||||
|
} else {
|
||||||
|
debug!(?pks256, "Multiple certificates matched, unable to proceed.");
|
||||||
|
Err(OperationError::NotAuthenticated)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
debug!(?pks256, "No certificates were able to be mapped.");
|
||||||
|
Err(OperationError::NotAuthenticated)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Given a certificate, validate it and discover the associated entry that
|
||||||
|
/// the certificate relates to. Currently, this relies on mapping the public
|
||||||
|
/// key sha256 to a stored client certificate, which then links to the owner.
|
||||||
|
///
|
||||||
|
/// In the future we *could* consider alternate mapping strategies such as
|
||||||
|
/// subjectAltName or subject DN, but these have subtle security risks and
|
||||||
|
/// configuration challenges, so binary mapping is the simplest - and safest -
|
||||||
|
/// option today.
|
||||||
|
#[instrument(level = "debug", skip_all)]
|
||||||
|
fn client_certificate_to_identity(
|
||||||
|
&mut self,
|
||||||
|
client_cert_info: &ClientCertInfo,
|
||||||
|
ct: Duration,
|
||||||
|
source: Source,
|
||||||
|
) -> Result<Identity, OperationError> {
|
||||||
|
let cert_entry = self.client_cert_info_entry(client_cert_info)?;
|
||||||
|
|
||||||
|
// This is who the certificate belongs to.
|
||||||
|
let refers_uuid = cert_entry
|
||||||
|
.get_ava_single_refer(Attribute::Refers)
|
||||||
|
.ok_or_else(|| {
|
||||||
|
warn!("Invalid certificate entry, missing refers");
|
||||||
|
OperationError::InvalidState
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Now get the related entry.
|
||||||
|
let entry = self.get_qs_txn().internal_search_uuid(refers_uuid)?;
|
||||||
|
|
||||||
|
let (account, account_policy) =
|
||||||
|
Account::try_from_entry_with_policy(entry.as_ref(), self.get_qs_txn())?;
|
||||||
|
|
||||||
|
// Is the account in it's valid window?
|
||||||
|
if !account.is_within_valid_time(ct) {
|
||||||
|
// Nope, expired
|
||||||
|
return Err(OperationError::SessionExpired);
|
||||||
|
};
|
||||||
|
|
||||||
|
// scope is related to the cert. For now, default to RO.
|
||||||
|
let scope = AccessScope::ReadOnly;
|
||||||
|
|
||||||
|
let mut limits = Limits::default();
|
||||||
|
// Apply the limits from the account policy
|
||||||
|
if let Some(lim) = account_policy
|
||||||
|
.limit_search_max_results()
|
||||||
|
.and_then(|v| v.try_into().ok())
|
||||||
|
{
|
||||||
|
limits.search_max_results = lim;
|
||||||
|
}
|
||||||
|
if let Some(lim) = account_policy
|
||||||
|
.limit_search_max_filter_test()
|
||||||
|
.and_then(|v| v.try_into().ok())
|
||||||
|
{
|
||||||
|
limits.search_max_filter_test = lim;
|
||||||
|
}
|
||||||
|
|
||||||
|
let certificate_uuid = cert_entry.get_uuid();
|
||||||
|
|
||||||
|
Ok(Identity {
|
||||||
|
origin: IdentType::User(IdentUser { entry }),
|
||||||
|
source,
|
||||||
|
// session_id is the certificate uuid.
|
||||||
|
session_id: certificate_uuid,
|
||||||
|
scope,
|
||||||
|
limits,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(level = "debug", skip_all)]
|
||||||
|
fn client_certificate_to_user_auth_token(
|
||||||
|
&mut self,
|
||||||
|
client_cert_info: &ClientCertInfo,
|
||||||
|
ct: Duration,
|
||||||
|
) -> Result<UserAuthToken, OperationError> {
|
||||||
|
let cert_entry = self.client_cert_info_entry(client_cert_info)?;
|
||||||
|
|
||||||
|
// This is who the certificate belongs to.
|
||||||
|
let refers_uuid = cert_entry
|
||||||
|
.get_ava_single_refer(Attribute::Refers)
|
||||||
|
.ok_or_else(|| {
|
||||||
|
warn!("Invalid certificate entry, missing refers");
|
||||||
|
OperationError::InvalidState
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Now get the related entry.
|
||||||
|
let entry = self.get_qs_txn().internal_search_uuid(refers_uuid)?;
|
||||||
|
|
||||||
|
let (account, account_policy) =
|
||||||
|
Account::try_from_entry_with_policy(entry.as_ref(), self.get_qs_txn())?;
|
||||||
|
|
||||||
|
// Is the account in it's valid window?
|
||||||
|
if !account.is_within_valid_time(ct) {
|
||||||
|
// Nope, expired
|
||||||
|
return Err(OperationError::SessionExpired);
|
||||||
|
};
|
||||||
|
|
||||||
|
let certificate_uuid = cert_entry.get_uuid();
|
||||||
|
let session_is_rw = false;
|
||||||
|
|
||||||
|
account
|
||||||
|
.client_cert_info_to_userauthtoken(certificate_uuid, session_is_rw, ct, &account_policy)
|
||||||
|
.ok_or(OperationError::InvalidState)
|
||||||
|
}
|
||||||
|
|
||||||
#[instrument(level = "debug", skip_all)]
|
#[instrument(level = "debug", skip_all)]
|
||||||
fn validate_ldap_session(
|
fn validate_ldap_session(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use super::cid::Cid;
|
use super::cid::Cid;
|
||||||
use super::entry::EntryChangeState;
|
use super::entry::EntryChangeState;
|
||||||
use super::entry::State;
|
use super::entry::State;
|
||||||
|
use crate::be::dbvalue::DbValueCertificate;
|
||||||
use crate::be::dbvalue::DbValueImage;
|
use crate::be::dbvalue::DbValueImage;
|
||||||
use crate::be::dbvalue::DbValueKeyInternal;
|
use crate::be::dbvalue::DbValueKeyInternal;
|
||||||
use crate::be::dbvalue::DbValueOauthClaimMapJoinV1;
|
use crate::be::dbvalue::DbValueOauthClaimMapJoinV1;
|
||||||
|
@ -459,6 +460,9 @@ pub enum ReplAttrV1 {
|
||||||
HexString {
|
HexString {
|
||||||
set: Vec<String>,
|
set: Vec<String>,
|
||||||
},
|
},
|
||||||
|
Certificate {
|
||||||
|
set: Vec<DbValueCertificate>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
|
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
|
||||||
|
|
|
@ -237,8 +237,9 @@ impl SchemaAttribute {
|
||||||
SyntaxType::Image => matches!(v, PartialValue::Utf8(_)),
|
SyntaxType::Image => matches!(v, PartialValue::Utf8(_)),
|
||||||
SyntaxType::CredentialType => matches!(v, PartialValue::CredentialType(_)),
|
SyntaxType::CredentialType => matches!(v, PartialValue::CredentialType(_)),
|
||||||
|
|
||||||
SyntaxType::HexString => matches!(v, PartialValue::HexString(_)),
|
SyntaxType::HexString | SyntaxType::Certificate | SyntaxType::KeyInternal => {
|
||||||
SyntaxType::KeyInternal => matches!(v, PartialValue::HexString(_)),
|
matches!(v, PartialValue::HexString(_))
|
||||||
|
}
|
||||||
|
|
||||||
SyntaxType::WebauthnAttestationCaList => false,
|
SyntaxType::WebauthnAttestationCaList => false,
|
||||||
};
|
};
|
||||||
|
@ -303,6 +304,7 @@ impl SchemaAttribute {
|
||||||
}
|
}
|
||||||
SyntaxType::KeyInternal => matches!(v, Value::KeyInternal { .. }),
|
SyntaxType::KeyInternal => matches!(v, Value::KeyInternal { .. }),
|
||||||
SyntaxType::HexString => matches!(v, Value::HexString(_)),
|
SyntaxType::HexString => matches!(v, Value::HexString(_)),
|
||||||
|
SyntaxType::Certificate => matches!(v, Value::Certificate(_)),
|
||||||
};
|
};
|
||||||
if r {
|
if r {
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -679,9 +679,12 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
||||||
let idm_schema_classes = [
|
let idm_schema_classes = [
|
||||||
SCHEMA_ATTR_PATCH_LEVEL_DL7.clone().into(),
|
SCHEMA_ATTR_PATCH_LEVEL_DL7.clone().into(),
|
||||||
SCHEMA_ATTR_DOMAIN_DEVELOPMENT_TAINT_DL7.clone().into(),
|
SCHEMA_ATTR_DOMAIN_DEVELOPMENT_TAINT_DL7.clone().into(),
|
||||||
|
SCHEMA_ATTR_REFERS_DL7.clone().into(),
|
||||||
|
SCHEMA_ATTR_CERTIFICATE_DL7.clone().into(),
|
||||||
SCHEMA_CLASS_DOMAIN_INFO_DL7.clone().into(),
|
SCHEMA_CLASS_DOMAIN_INFO_DL7.clone().into(),
|
||||||
SCHEMA_CLASS_SERVICE_ACCOUNT_DL7.clone().into(),
|
SCHEMA_CLASS_SERVICE_ACCOUNT_DL7.clone().into(),
|
||||||
SCHEMA_CLASS_SYNC_ACCOUNT_DL7.clone().into(),
|
SCHEMA_CLASS_SYNC_ACCOUNT_DL7.clone().into(),
|
||||||
|
SCHEMA_CLASS_CLIENT_CERTIFICATE_DL7.clone().into(),
|
||||||
];
|
];
|
||||||
|
|
||||||
idm_schema_classes
|
idm_schema_classes
|
||||||
|
@ -700,6 +703,10 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
||||||
.clone()
|
.clone()
|
||||||
.try_into()?,
|
.try_into()?,
|
||||||
IDM_PEOPLE_SELF_MAIL_WRITE_DL7.clone().try_into()?,
|
IDM_PEOPLE_SELF_MAIL_WRITE_DL7.clone().try_into()?,
|
||||||
|
BUILTIN_GROUP_CLIENT_CERTIFICATE_ADMINS_DL7
|
||||||
|
.clone()
|
||||||
|
.try_into()?,
|
||||||
|
IDM_HIGH_PRIVILEGE_DL7.clone().try_into()?,
|
||||||
];
|
];
|
||||||
|
|
||||||
idm_data
|
idm_data
|
||||||
|
@ -715,6 +722,7 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
||||||
let idm_data = [
|
let idm_data = [
|
||||||
IDM_ACP_SELF_WRITE_DL7.clone().into(),
|
IDM_ACP_SELF_WRITE_DL7.clone().into(),
|
||||||
IDM_ACP_SELF_NAME_WRITE_DL7.clone().into(),
|
IDM_ACP_SELF_NAME_WRITE_DL7.clone().into(),
|
||||||
|
IDM_ACP_HP_CLIENT_CERTIFICATE_MANAGER_DL7.clone().into(),
|
||||||
];
|
];
|
||||||
|
|
||||||
idm_data
|
idm_data
|
||||||
|
|
|
@ -658,6 +658,8 @@ pub trait QueryServerTransaction<'a> {
|
||||||
SyntaxType::KeyInternal => Err(OperationError::InvalidAttribute("Internal keys are generated and not able to be set.".to_string())),
|
SyntaxType::KeyInternal => Err(OperationError::InvalidAttribute("Internal keys are generated and not able to be set.".to_string())),
|
||||||
SyntaxType::HexString => Value::new_hex_string_s(value)
|
SyntaxType::HexString => Value::new_hex_string_s(value)
|
||||||
.ok_or_else(|| OperationError::InvalidAttribute("Invalid hex string syntax".to_string())),
|
.ok_or_else(|| OperationError::InvalidAttribute("Invalid hex string syntax".to_string())),
|
||||||
|
SyntaxType::Certificate => Value::new_certificate_s(value)
|
||||||
|
.ok_or_else(|| OperationError::InvalidAttribute("Invalid x509 certificate syntax".to_string())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
|
@ -781,10 +783,10 @@ pub trait QueryServerTransaction<'a> {
|
||||||
SyntaxType::WebauthnAttestationCaList => Err(OperationError::InvalidAttribute(
|
SyntaxType::WebauthnAttestationCaList => Err(OperationError::InvalidAttribute(
|
||||||
"Invalid - unable to query attestation CA list".to_string(),
|
"Invalid - unable to query attestation CA list".to_string(),
|
||||||
)),
|
)),
|
||||||
SyntaxType::HexString | SyntaxType::KeyInternal => {
|
SyntaxType::HexString | SyntaxType::KeyInternal | SyntaxType::Certificate => {
|
||||||
PartialValue::new_hex_string_s(value).ok_or_else(|| {
|
PartialValue::new_hex_string_s(value).ok_or_else(|| {
|
||||||
OperationError::InvalidAttribute(
|
OperationError::InvalidAttribute(
|
||||||
"Invalid key identifer syntax, expected hex string".to_string(),
|
"Invalid syntax, expected hex string".to_string(),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@ use std::time::Duration;
|
||||||
use base64::{engine::general_purpose, Engine as _};
|
use base64::{engine::general_purpose, Engine as _};
|
||||||
use compact_jwt::{crypto::JwsRs256Signer, JwsEs256Signer};
|
use compact_jwt::{crypto::JwsRs256Signer, JwsEs256Signer};
|
||||||
use hashbrown::HashSet;
|
use hashbrown::HashSet;
|
||||||
|
use kanidm_lib_crypto::x509_cert::{der::DecodePem, Certificate};
|
||||||
use kanidm_proto::internal::ImageValue;
|
use kanidm_proto::internal::ImageValue;
|
||||||
use num_enum::TryFromPrimitive;
|
use num_enum::TryFromPrimitive;
|
||||||
use openssl::ec::EcKey;
|
use openssl::ec::EcKey;
|
||||||
|
@ -275,6 +276,7 @@ pub enum SyntaxType {
|
||||||
OauthClaimMap = 37,
|
OauthClaimMap = 37,
|
||||||
KeyInternal = 38,
|
KeyInternal = 38,
|
||||||
HexString = 39,
|
HexString = 39,
|
||||||
|
Certificate = 40,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<&str> for SyntaxType {
|
impl TryFrom<&str> for SyntaxType {
|
||||||
|
@ -323,6 +325,7 @@ impl TryFrom<&str> for SyntaxType {
|
||||||
"OAUTH_CLAIM_MAP" => Ok(SyntaxType::OauthClaimMap),
|
"OAUTH_CLAIM_MAP" => Ok(SyntaxType::OauthClaimMap),
|
||||||
"KEY_INTERNAL" => Ok(SyntaxType::KeyInternal),
|
"KEY_INTERNAL" => Ok(SyntaxType::KeyInternal),
|
||||||
"HEX_STRING" => Ok(SyntaxType::HexString),
|
"HEX_STRING" => Ok(SyntaxType::HexString),
|
||||||
|
"CERTIFICATE" => Ok(SyntaxType::Certificate),
|
||||||
_ => Err(()),
|
_ => Err(()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -371,6 +374,7 @@ impl fmt::Display for SyntaxType {
|
||||||
SyntaxType::OauthClaimMap => "OAUTH_CLAIM_MAP",
|
SyntaxType::OauthClaimMap => "OAUTH_CLAIM_MAP",
|
||||||
SyntaxType::KeyInternal => "KEY_INTERNAL",
|
SyntaxType::KeyInternal => "KEY_INTERNAL",
|
||||||
SyntaxType::HexString => "HEX_STRING",
|
SyntaxType::HexString => "HEX_STRING",
|
||||||
|
SyntaxType::Certificate => "CERTIFICATE",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1181,6 +1185,8 @@ pub enum Value {
|
||||||
},
|
},
|
||||||
|
|
||||||
HexString(String),
|
HexString(String),
|
||||||
|
|
||||||
|
Certificate(Certificate),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialEq for Value {
|
impl PartialEq for Value {
|
||||||
|
@ -1471,6 +1477,10 @@ impl Value {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn new_certificate_s(cert_str: &str) -> Option<Self> {
|
||||||
|
Certificate::from_pem(cert_str).map(Value::Certificate).ok()
|
||||||
|
}
|
||||||
|
|
||||||
/// Want a `Value::Image`? use this!
|
/// Want a `Value::Image`? use this!
|
||||||
pub fn new_image(input: &str) -> Result<Self, OperationError> {
|
pub fn new_image(input: &str) -> Result<Self, OperationError> {
|
||||||
serde_json::from_str::<ImageValue>(input)
|
serde_json::from_str::<ImageValue>(input)
|
||||||
|
@ -2008,6 +2018,7 @@ impl Value {
|
||||||
|
|
||||||
Value::PhoneNumber(_, _) => true,
|
Value::PhoneNumber(_, _) => true,
|
||||||
Value::Address(_) => true,
|
Value::Address(_) => true,
|
||||||
|
Value::Certificate(_) => true,
|
||||||
|
|
||||||
Value::Uuid(_)
|
Value::Uuid(_)
|
||||||
| Value::Bool(_)
|
| Value::Bool(_)
|
||||||
|
|
248
server/lib/src/valueset/certificate.rs
Normal file
248
server/lib/src/valueset/certificate.rs
Normal file
|
@ -0,0 +1,248 @@
|
||||||
|
use crate::be::dbvalue::DbValueCertificate;
|
||||||
|
use crate::prelude::*;
|
||||||
|
use crate::repl::proto::ReplAttrV1;
|
||||||
|
use crate::schema::SchemaAttribute;
|
||||||
|
use crate::valueset::{DbValueSetV2, ValueSet};
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
|
use kanidm_lib_crypto::{
|
||||||
|
x509_cert::{
|
||||||
|
der::{Decode, Encode, EncodePem},
|
||||||
|
pem::LineEnding,
|
||||||
|
x509_public_key_s256, Certificate,
|
||||||
|
},
|
||||||
|
Sha256Digest,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ValueSetCertificate {
|
||||||
|
map: BTreeMap<Sha256Digest, Certificate>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ValueSetCertificate {
|
||||||
|
pub fn new(certificate: Certificate) -> Result<Box<Self>, OperationError> {
|
||||||
|
let mut map = BTreeMap::new();
|
||||||
|
|
||||||
|
let pk_s256 = x509_public_key_s256(&certificate).ok_or_else(|| {
|
||||||
|
error!("Unable to digest public key");
|
||||||
|
OperationError::VS0002CertificatePublicKeyDigest
|
||||||
|
})?;
|
||||||
|
map.insert(pk_s256, certificate);
|
||||||
|
|
||||||
|
Ok(Box::new(ValueSetCertificate { map }))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_dbvs2(data: Vec<DbValueCertificate>) -> Result<ValueSet, OperationError> {
|
||||||
|
Self::from_dbv_iter(data.into_iter())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_repl_v1(data: &[DbValueCertificate]) -> Result<ValueSet, OperationError> {
|
||||||
|
Self::from_dbv_iter(data.iter().cloned())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_dbv_iter(
|
||||||
|
certs: impl Iterator<Item = DbValueCertificate>,
|
||||||
|
) -> Result<ValueSet, OperationError> {
|
||||||
|
let mut map = BTreeMap::new();
|
||||||
|
|
||||||
|
for db_cert in certs {
|
||||||
|
match db_cert {
|
||||||
|
DbValueCertificate::V1 { certificate_der } => {
|
||||||
|
// Parse the DER
|
||||||
|
let certificate =
|
||||||
|
Certificate::from_der(&certificate_der).map_err(|x509_err| {
|
||||||
|
error!(?x509_err, "Unable to restore certificate from DER");
|
||||||
|
OperationError::VS0003CertificateDerDecode
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// sha256 the public key
|
||||||
|
let pk_s256 = x509_public_key_s256(&certificate).ok_or_else(|| {
|
||||||
|
error!("Unable to digest public key");
|
||||||
|
OperationError::VS0004CertificatePublicKeyDigest
|
||||||
|
})?;
|
||||||
|
|
||||||
|
map.insert(pk_s256, certificate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Box::new(ValueSetCertificate { map }))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_vec_dbvs(&self) -> Vec<DbValueCertificate> {
|
||||||
|
self.map
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(pk_s256, cert)| {
|
||||||
|
cert.to_der()
|
||||||
|
.map_err(|der_err| {
|
||||||
|
error!(
|
||||||
|
?pk_s256,
|
||||||
|
?der_err,
|
||||||
|
"Failed to serialise certificate to der. This value will be dropped!"
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.ok()
|
||||||
|
})
|
||||||
|
.map(|certificate_der| DbValueCertificate::V1 { certificate_der })
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_iter<T>(iter: T) -> Option<Box<Self>>
|
||||||
|
where
|
||||||
|
T: IntoIterator<Item = Certificate>,
|
||||||
|
{
|
||||||
|
let mut map = BTreeMap::new();
|
||||||
|
|
||||||
|
for certificate in iter {
|
||||||
|
let pk_s256 = x509_public_key_s256(&certificate)?;
|
||||||
|
map.insert(pk_s256, certificate);
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(Box::new(ValueSetCertificate { map }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ValueSetT for ValueSetCertificate {
|
||||||
|
fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError> {
|
||||||
|
match value {
|
||||||
|
Value::Certificate(certificate) => {
|
||||||
|
let pk_s256 = x509_public_key_s256(&certificate).ok_or_else(|| {
|
||||||
|
error!("Unable to digest public key");
|
||||||
|
OperationError::VS0005CertificatePublicKeyDigest
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// bool -> true if the insert did not trigger a duplicate.
|
||||||
|
Ok(self.map.insert(pk_s256, certificate).is_none())
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
debug_assert!(false);
|
||||||
|
Err(OperationError::InvalidValueState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clear(&mut self) {
|
||||||
|
self.map.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove(&mut self, pv: &PartialValue, _cid: &Cid) -> bool {
|
||||||
|
match pv {
|
||||||
|
PartialValue::HexString(hs) => {
|
||||||
|
let mut buf = Sha256Digest::default();
|
||||||
|
if hex::decode_to_slice(&hs, &mut buf).is_ok() {
|
||||||
|
self.map.remove(&buf).is_some()
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn contains(&self, pv: &PartialValue) -> bool {
|
||||||
|
match pv {
|
||||||
|
PartialValue::HexString(hs) => {
|
||||||
|
let mut buf = Sha256Digest::default();
|
||||||
|
if hex::decode_to_slice(&hs, &mut buf).is_ok() {
|
||||||
|
self.map.contains_key(&buf)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn substring(&self, _pv: &PartialValue) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn startswith(&self, _pv: &PartialValue) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn endswith(&self, _pv: &PartialValue) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lessthan(&self, _pv: &PartialValue) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn len(&self) -> usize {
|
||||||
|
self.map.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_idx_eq_keys(&self) -> Vec<String> {
|
||||||
|
self.map.keys().map(hex::encode).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn syntax(&self) -> SyntaxType {
|
||||||
|
SyntaxType::Certificate
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate(&self, _schema_attr: &SchemaAttribute) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_proto_string_clone_iter(&self) -> Box<dyn Iterator<Item = String> + '_> {
|
||||||
|
Box::new(self.map.iter().filter_map(|(pk_s256, cert)| {
|
||||||
|
cert.to_pem(LineEnding::LF)
|
||||||
|
.ok()
|
||||||
|
.map(|pem| format!("{}\n{}", hex::encode(pk_s256), pem))
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
|
||||||
|
let data = self.to_vec_dbvs();
|
||||||
|
DbValueSetV2::Certificate(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_repl_v1(&self) -> ReplAttrV1 {
|
||||||
|
let set = self.to_vec_dbvs();
|
||||||
|
ReplAttrV1::Certificate { set }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_partialvalue_iter(&self) -> Box<dyn Iterator<Item = PartialValue> + '_> {
|
||||||
|
Box::new(
|
||||||
|
self.map
|
||||||
|
.keys()
|
||||||
|
.map(hex::encode)
|
||||||
|
.map(PartialValue::HexString),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_value_iter(&self) -> Box<dyn Iterator<Item = Value> + '_> {
|
||||||
|
Box::new(self.map.values().cloned().map(Value::Certificate))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn equal(&self, other: &ValueSet) -> bool {
|
||||||
|
if let Some(other) = other.as_certificate_set() {
|
||||||
|
&self.map == other
|
||||||
|
} else {
|
||||||
|
debug_assert!(false);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn merge(&mut self, other: &ValueSet) -> Result<(), OperationError> {
|
||||||
|
if let Some(b) = other.as_certificate_set() {
|
||||||
|
mergemaps!(self.map, b)
|
||||||
|
} else {
|
||||||
|
debug_assert!(false);
|
||||||
|
Err(OperationError::InvalidValueState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_certificate_single(&self) -> Option<&Certificate> {
|
||||||
|
if self.map.len() == 1 {
|
||||||
|
self.map.values().take(1).next()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_certificate_set(&self) -> Option<&BTreeMap<Sha256Digest, Certificate>> {
|
||||||
|
Some(&self.map)
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,6 +3,7 @@ use std::collections::{BTreeMap, BTreeSet};
|
||||||
use compact_jwt::{crypto::JwsRs256Signer, JwsEs256Signer};
|
use compact_jwt::{crypto::JwsRs256Signer, JwsEs256Signer};
|
||||||
use dyn_clone::DynClone;
|
use dyn_clone::DynClone;
|
||||||
use hashbrown::HashSet;
|
use hashbrown::HashSet;
|
||||||
|
use kanidm_lib_crypto::{x509_cert::Certificate, Sha256Digest};
|
||||||
use kanidm_proto::internal::ImageValue;
|
use kanidm_proto::internal::ImageValue;
|
||||||
use openssl::ec::EcKey;
|
use openssl::ec::EcKey;
|
||||||
use openssl::pkey::Private;
|
use openssl::pkey::Private;
|
||||||
|
@ -28,6 +29,7 @@ pub use self::address::{ValueSetAddress, ValueSetEmailAddress};
|
||||||
pub use self::auditlogstring::{ValueSetAuditLogString, AUDIT_LOG_STRING_CAPACITY};
|
pub use self::auditlogstring::{ValueSetAuditLogString, AUDIT_LOG_STRING_CAPACITY};
|
||||||
pub use self::binary::{ValueSetPrivateBinary, ValueSetPublicBinary};
|
pub use self::binary::{ValueSetPrivateBinary, ValueSetPublicBinary};
|
||||||
pub use self::bool::ValueSetBool;
|
pub use self::bool::ValueSetBool;
|
||||||
|
pub use self::certificate::ValueSetCertificate;
|
||||||
pub use self::cid::ValueSetCid;
|
pub use self::cid::ValueSetCid;
|
||||||
pub use self::cred::{
|
pub use self::cred::{
|
||||||
ValueSetAttestedPasskey, ValueSetCredential, ValueSetCredentialType, ValueSetIntentToken,
|
ValueSetAttestedPasskey, ValueSetCredential, ValueSetCredentialType, ValueSetIntentToken,
|
||||||
|
@ -64,6 +66,7 @@ mod address;
|
||||||
mod auditlogstring;
|
mod auditlogstring;
|
||||||
mod binary;
|
mod binary;
|
||||||
mod bool;
|
mod bool;
|
||||||
|
mod certificate;
|
||||||
mod cid;
|
mod cid;
|
||||||
mod cred;
|
mod cred;
|
||||||
mod datetime;
|
mod datetime;
|
||||||
|
@ -612,6 +615,16 @@ pub trait ValueSetT: std::fmt::Debug + DynClone {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn to_certificate_single(&self) -> Option<&Certificate> {
|
||||||
|
debug_assert!(false);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_certificate_set(&self) -> Option<&BTreeMap<Sha256Digest, Certificate>> {
|
||||||
|
debug_assert!(false);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
fn repl_merge_valueset(
|
fn repl_merge_valueset(
|
||||||
&self,
|
&self,
|
||||||
_older: &ValueSet,
|
_older: &ValueSet,
|
||||||
|
@ -688,6 +701,7 @@ pub fn from_result_value_iter(
|
||||||
Value::EcKeyPrivate(k) => ValueSetEcKeyPrivate::new(&k),
|
Value::EcKeyPrivate(k) => ValueSetEcKeyPrivate::new(&k),
|
||||||
Value::Image(imagevalue) => image::ValueSetImage::new(imagevalue),
|
Value::Image(imagevalue) => image::ValueSetImage::new(imagevalue),
|
||||||
Value::CredentialType(c) => ValueSetCredentialType::new(c),
|
Value::CredentialType(c) => ValueSetCredentialType::new(c),
|
||||||
|
Value::Certificate(c) => ValueSetCertificate::new(c)?,
|
||||||
Value::WebauthnAttestationCaList(_)
|
Value::WebauthnAttestationCaList(_)
|
||||||
| Value::PhoneNumber(_, _)
|
| Value::PhoneNumber(_, _)
|
||||||
| Value::Passkey(_, _, _)
|
| Value::Passkey(_, _, _)
|
||||||
|
@ -778,6 +792,7 @@ pub fn from_value_iter(mut iter: impl Iterator<Item = Value>) -> Result<ValueSet
|
||||||
status_cid,
|
status_cid,
|
||||||
der,
|
der,
|
||||||
} => ValueSetKeyInternal::new(id, usage, valid_from, status, status_cid, der),
|
} => ValueSetKeyInternal::new(id, usage, valid_from, status, status_cid, der),
|
||||||
|
Value::Certificate(certificate) => ValueSetCertificate::new(certificate)?,
|
||||||
|
|
||||||
Value::PhoneNumber(_, _) => {
|
Value::PhoneNumber(_, _) => {
|
||||||
debug_assert!(false);
|
debug_assert!(false);
|
||||||
|
@ -842,6 +857,7 @@ pub fn from_db_valueset_v2(dbvs: DbValueSetV2) -> Result<ValueSet, OperationErro
|
||||||
DbValueSetV2::OauthClaimMap(set) => ValueSetOauthClaimMap::from_dbvs2(set),
|
DbValueSetV2::OauthClaimMap(set) => ValueSetOauthClaimMap::from_dbvs2(set),
|
||||||
DbValueSetV2::KeyInternal(set) => ValueSetKeyInternal::from_dbvs2(set),
|
DbValueSetV2::KeyInternal(set) => ValueSetKeyInternal::from_dbvs2(set),
|
||||||
DbValueSetV2::HexString(set) => ValueSetHexString::from_dbvs2(set),
|
DbValueSetV2::HexString(set) => ValueSetHexString::from_dbvs2(set),
|
||||||
|
DbValueSetV2::Certificate(set) => ValueSetCertificate::from_dbvs2(set),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -894,5 +910,6 @@ pub fn from_repl_v1(rv1: &ReplAttrV1) -> Result<ValueSet, OperationError> {
|
||||||
ReplAttrV1::OauthClaimMap { set } => ValueSetOauthClaimMap::from_repl_v1(set),
|
ReplAttrV1::OauthClaimMap { set } => ValueSetOauthClaimMap::from_repl_v1(set),
|
||||||
ReplAttrV1::KeyInternal { set } => ValueSetKeyInternal::from_repl_v1(set),
|
ReplAttrV1::KeyInternal { set } => ValueSetKeyInternal::from_repl_v1(set),
|
||||||
ReplAttrV1::HexString { set } => ValueSetHexString::from_repl_v1(set),
|
ReplAttrV1::HexString { set } => ValueSetHexString::from_repl_v1(set),
|
||||||
|
ReplAttrV1::Certificate { set } => ValueSetCertificate::from_repl_v1(set),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,7 +51,7 @@ shellexpand = { workspace = true }
|
||||||
time = { workspace = true, features = ["serde", "std"] }
|
time = { workspace = true, features = ["serde", "std"] }
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
tracing-subscriber = { workspace = true, features = ["env-filter", "fmt"] }
|
tracing-subscriber = { workspace = true, features = ["env-filter", "fmt"] }
|
||||||
tokio = { workspace = true, features = ["rt", "macros"] }
|
tokio = { workspace = true, features = ["rt", "macros", "fs"] }
|
||||||
url = { workspace = true, features = ["serde"] }
|
url = { workspace = true, features = ["serde"] }
|
||||||
uuid = { workspace = true }
|
uuid = { workspace = true }
|
||||||
zxcvbn = { workspace = true }
|
zxcvbn = { workspace = true }
|
||||||
|
|
|
@ -24,8 +24,8 @@ use uuid::Uuid;
|
||||||
|
|
||||||
use crate::webauthn::get_authenticator;
|
use crate::webauthn::get_authenticator;
|
||||||
use crate::{
|
use crate::{
|
||||||
handle_client_error, password_prompt, AccountCredential, AccountRadius, AccountSsh,
|
handle_client_error, password_prompt, AccountCertificate, AccountCredential, AccountRadius,
|
||||||
AccountUserAuthToken, AccountValidity, OutputMode, PersonOpt, PersonPosix,
|
AccountSsh, AccountUserAuthToken, AccountValidity, OutputMode, PersonOpt, PersonPosix,
|
||||||
};
|
};
|
||||||
|
|
||||||
impl PersonOpt {
|
impl PersonOpt {
|
||||||
|
@ -62,6 +62,10 @@ impl PersonOpt {
|
||||||
AccountValidity::ExpireAt(ano) => ano.copt.debug,
|
AccountValidity::ExpireAt(ano) => ano.copt.debug,
|
||||||
AccountValidity::BeginFrom(ano) => ano.copt.debug,
|
AccountValidity::BeginFrom(ano) => ano.copt.debug,
|
||||||
},
|
},
|
||||||
|
PersonOpt::Certificate { commands } => match commands {
|
||||||
|
AccountCertificate::Status { copt, .. }
|
||||||
|
| AccountCertificate::Create { copt, .. } => copt.debug,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -497,6 +501,60 @@ impl PersonOpt {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, // end PersonOpt::Validity
|
}, // end PersonOpt::Validity
|
||||||
|
PersonOpt::Certificate { commands } => commands.exec().await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AccountCertificate {
|
||||||
|
pub async fn exec(&self) {
|
||||||
|
match self {
|
||||||
|
AccountCertificate::Status { account_id, copt } => {
|
||||||
|
let client = copt.to_client(OpType::Read).await;
|
||||||
|
match client.idm_person_certificate_list(account_id).await {
|
||||||
|
Ok(r) => match copt.output_mode {
|
||||||
|
OutputMode::Json => {
|
||||||
|
let r_attrs: Vec<_> = r.iter().map(|entry| &entry.attrs).collect();
|
||||||
|
println!(
|
||||||
|
"{}",
|
||||||
|
serde_json::to_string(&r_attrs).expect("Failed to serialise json")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
OutputMode::Text => {
|
||||||
|
if r.is_empty() {
|
||||||
|
println!("No certificates available")
|
||||||
|
} else {
|
||||||
|
r.iter().for_each(|ent| println!("{}", ent))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(e) => handle_client_error(e, copt.output_mode),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AccountCertificate::Create {
|
||||||
|
account_id,
|
||||||
|
certificate_path,
|
||||||
|
copt,
|
||||||
|
} => {
|
||||||
|
let pem_data = match tokio::fs::read_to_string(certificate_path).await {
|
||||||
|
Ok(pd) => pd,
|
||||||
|
Err(io_err) => {
|
||||||
|
error!(?io_err, ?certificate_path, "Unable to read PEM data");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let client = copt.to_client(OpType::Write).await;
|
||||||
|
|
||||||
|
if let Err(e) = client
|
||||||
|
.idm_person_certificate_create(&account_id, &pem_data)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
handle_client_error(e, copt.output_mode);
|
||||||
|
} else {
|
||||||
|
println!("Success");
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -531,6 +531,24 @@ pub enum AccountValidity {
|
||||||
BeginFrom(AccountNamedValidDateTimeOpt),
|
BeginFrom(AccountNamedValidDateTimeOpt),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Subcommand)]
|
||||||
|
pub enum AccountCertificate {
|
||||||
|
#[clap(name = "status")]
|
||||||
|
Status {
|
||||||
|
account_id: String,
|
||||||
|
#[clap(flatten)]
|
||||||
|
copt: CommonOpt,
|
||||||
|
},
|
||||||
|
#[clap(name = "create")]
|
||||||
|
Create {
|
||||||
|
account_id: String,
|
||||||
|
certificate_path: PathBuf,
|
||||||
|
#[clap(flatten)]
|
||||||
|
copt: CommonOpt,
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Subcommand)]
|
#[derive(Debug, Subcommand)]
|
||||||
pub enum AccountUserAuthToken {
|
pub enum AccountUserAuthToken {
|
||||||
/// Show the status of logged in sessions associated to this account.
|
/// Show the status of logged in sessions associated to this account.
|
||||||
|
@ -603,6 +621,11 @@ pub enum PersonOpt {
|
||||||
#[clap(subcommand)]
|
#[clap(subcommand)]
|
||||||
commands: AccountValidity,
|
commands: AccountValidity,
|
||||||
},
|
},
|
||||||
|
#[clap(name = "certificate", hide = true)]
|
||||||
|
Certificate {
|
||||||
|
#[clap(subcommand)]
|
||||||
|
commands: AccountCertificate,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Subcommand)]
|
#[derive(Debug, Subcommand)]
|
||||||
|
|
Loading…
Reference in a new issue