Add support for storing security token key in domain config (#581)

This commit is contained in:
Firstyear 2021-09-25 11:24:00 +10:00 committed by GitHub
parent dbb57e9a7b
commit 573e346476
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 377 additions and 107 deletions

16
Cargo.lock generated
View file

@ -499,9 +499,9 @@ checksum = "cfa8873f51c92e232f9bac4065cddef41b714152812bfc5f7672ba16d6ef8cd9"
[[package]]
name = "bstr"
version = "0.2.16"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90682c8d613ad3373e66de8c6411e0ae2ab2571e879d2efbf73558cc66f21279"
checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223"
dependencies = [
"lazy_static",
"memchr",
@ -511,9 +511,9 @@ dependencies = [
[[package]]
name = "bumpalo"
version = "3.7.0"
version = "3.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c59e7af012c713f529e7a3ee57ce9b31ddd858d4b512923602f74608b009631"
checksum = "d9df67f7bf9ef8498769f994239c45613ef0c5899415fb58e9add412d2c1a538"
[[package]]
name = "bundy"
@ -3640,9 +3640,9 @@ checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6"
[[package]]
name = "tracing"
version = "0.1.27"
version = "0.1.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2ba9ab62b7d6497a8638dfda5e5c4fb3b2d5a7fca4118f2b96151c8ef1a437e"
checksum = "84f96e095c0c82419687c20ddf5cb3eadb61f4e1405923c9dc8e53a1adacbda8"
dependencies = [
"cfg-if 1.0.0",
"pin-project-lite 0.2.7",
@ -3693,9 +3693,9 @@ dependencies = [
[[package]]
name = "tracing-subscriber"
version = "0.2.23"
version = "0.2.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56c42e73a9d277d4d2b6a88389a137ccf3c58599660b17e8f5fc39305e490669"
checksum = "fdd0568dbfe3baf7048b7908d2b32bca0d81cd56bec6d2a8f894b01d74f86be3"
dependencies = [
"ansi_term",
"chrono",

View file

@ -20,6 +20,16 @@ buildx/kanidmd/simd:
$(ARGS) .
@docker buildx imagetools $(EXT_OPTS) inspect $(IMAGE_BASE)/server:$(IMAGE_VERSION)
buildx/kanidmd/x86_64_v3: ## build multiarch server images
buildx/kanidmd/x86_64_v3:
@docker buildx build $(EXT_OPTS) --pull --push --platform "linux/amd64" \
--allow security.insecure \
-f kanidmd/Dockerfile -t $(IMAGE_BASE)/server:x86_64_$(IMAGE_VERSION) \
--build-arg "KANIDM_BUILD_PROFILE=container_x86_64_v3" \
--build-arg "KANIDM_FEATURES=" \
$(ARGS) .
@docker buildx imagetools $(EXT_OPTS) inspect $(IMAGE_BASE)/server:$(IMAGE_VERSION)
buildx/kanidmd: ## build multiarch server images
buildx/kanidmd:
@docker buildx build $(EXT_OPTS) --pull --push --platform $(IMAGE_ARCH) \

View file

@ -1318,18 +1318,13 @@ impl KanidmAsyncClient {
}
// ==== domain_info (aka domain)
pub async fn idm_domain_list(&self) -> Result<Vec<Entry>, ClientError> {
self.perform_get_request("/v1/domain").await
pub async fn idm_domain_get(&self) -> Result<Entry, ClientError> {
let r: Result<Vec<Entry>, ClientError> = self.perform_get_request("/v1/domain").await;
r.and_then(|mut v| v.pop().ok_or(ClientError::EmptyResponse))
}
pub async fn idm_domain_get(&self, id: &str) -> Result<Entry, ClientError> {
self.perform_get_request(format!("/v1/domain/{}", id).as_str())
.await
}
// pub fn idm_domain_get_attr
pub async fn idm_domain_get_ssid(&self, id: &str) -> Result<String, ClientError> {
self.perform_get_request(format!("/v1/domain/{}/_attr/domain_ssid", id).as_str())
pub async fn idm_domain_get_ssid(&self) -> Result<String, ClientError> {
self.perform_get_request("/v1/domain/_attr/domain_ssid")
.await
.and_then(|mut r: Vec<String>|
// Get the first result
@ -1339,13 +1334,14 @@ impl KanidmAsyncClient {
))
}
// pub fn idm_domain_put_attr
pub async fn idm_domain_set_ssid(&self, id: &str, ssid: &str) -> Result<(), ClientError> {
self.perform_put_request(
format!("/v1/domain/{}/_attr/domain_ssid", id).as_str(),
vec![ssid.to_string()],
)
.await
pub async fn idm_domain_set_ssid(&self, ssid: &str) -> Result<(), ClientError> {
self.perform_put_request("/v1/domain/_attr/domain_ssid", vec![ssid.to_string()])
.await
}
pub async fn idm_domain_reset_token_key(&self) -> Result<(), ClientError> {
self.perform_delete_request("/v1/domain/_attr/domain_token_key")
.await
}
// ==== schema

View file

@ -846,22 +846,22 @@ impl KanidmClient {
}
// ==== domain_info (aka domain)
pub fn idm_domain_list(&self) -> Result<Vec<Entry>, ClientError> {
tokio_block_on(self.asclient.idm_domain_list())
}
pub fn idm_domain_get(&self, id: &str) -> Result<Entry, ClientError> {
tokio_block_on(self.asclient.idm_domain_get(id))
pub fn idm_domain_get(&self) -> Result<Entry, ClientError> {
tokio_block_on(self.asclient.idm_domain_get())
}
// pub fn idm_domain_get_attr
pub fn idm_domain_get_ssid(&self, id: &str) -> Result<String, ClientError> {
tokio_block_on(self.asclient.idm_domain_get_ssid(id))
pub fn idm_domain_get_ssid(&self) -> Result<String, ClientError> {
tokio_block_on(self.asclient.idm_domain_get_ssid())
}
// pub fn idm_domain_put_attr
pub fn idm_domain_set_ssid(&self, id: &str, ssid: &str) -> Result<(), ClientError> {
tokio_block_on(self.asclient.idm_domain_set_ssid(id, ssid))
pub fn idm_domain_set_ssid(&self, ssid: &str) -> Result<(), ClientError> {
tokio_block_on(self.asclient.idm_domain_set_ssid(ssid))
}
pub fn idm_domain_reset_token_key(&self) -> Result<(), ClientError> {
tokio_block_on(self.asclient.idm_domain_reset_token_key())
}
// ==== schema

View file

@ -505,19 +505,12 @@ fn test_server_rest_domain_lifecycle() {
let res = rsclient.auth_simple_password("admin", ADMIN_TEST_PASSWORD);
assert!(res.is_ok());
let mut dlist = rsclient.idm_domain_list().unwrap();
assert!(dlist.len() == 1);
let dlocal = rsclient.idm_domain_get("domain_local").unwrap();
// There should be one, and it's the domain_local
assert!(dlist.pop().unwrap().attrs == dlocal.attrs);
let _dlocal = rsclient.idm_domain_get().unwrap();
// Change the ssid
rsclient
.idm_domain_set_ssid("domain_local", "new_ssid")
.unwrap();
rsclient.idm_domain_set_ssid("new_ssid").unwrap();
// check get and get the ssid and domain info
let nssid = rsclient.idm_domain_get_ssid("domain_local").unwrap();
let nssid = rsclient.idm_domain_get_ssid().unwrap();
assert!(nssid == "new_ssid");
});
}

View file

@ -0,0 +1,28 @@
use crate::DomainOpt;
impl DomainOpt {
pub fn debug(&self) -> bool {
match self {
DomainOpt::Show(copt) | DomainOpt::ResetTokenKey(copt) => copt.debug,
}
}
pub fn exec(&self) {
match self {
DomainOpt::Show(copt) => {
let client = copt.to_client();
match client.idm_domain_get() {
Ok(e) => println!("{}", e),
Err(e) => eprintln!("Error -> {:?}", e),
}
}
DomainOpt::ResetTokenKey(copt) => {
let client = copt.to_client();
match client.idm_domain_reset_token_key() {
Ok(_) => println!("Success"),
Err(e) => eprintln!("Error -> {:?}", e),
}
}
}
}
}

View file

@ -17,6 +17,7 @@ include!("../opt/kanidm.rs");
pub mod account;
pub mod common;
pub mod domain;
pub mod group;
pub mod oauth2;
pub mod raw;
@ -76,12 +77,14 @@ impl SystemOpt {
pub fn debug(&self) -> bool {
match self {
SystemOpt::Oauth2(oopt) => oopt.debug(),
SystemOpt::Domain(dopt) => dopt.debug(),
}
}
pub fn exec(&self) {
match self {
SystemOpt::Oauth2(oopt) => oopt.exec(),
SystemOpt::Domain(dopt) => dopt.exec(),
}
}
}

View file

@ -379,11 +379,25 @@ pub enum Oauth2Opt {
Delete(Named),
}
#[derive(Debug, StructOpt)]
pub enum DomainOpt {
#[structopt(name = "show")]
/// Show information about this systems domain
Show(CommonOpt),
#[structopt(name = "reset_token_key")]
/// Reset this domain token signing key. This will cause all user sessions to be
/// invalidated (logged out).
ResetTokenKey(CommonOpt),
}
#[derive(Debug, StructOpt)]
pub enum SystemOpt {
#[structopt(name = "oauth2")]
/// Configure and display oauth2/oidc resource server configuration
Oauth2(Oauth2Opt),
#[structopt(name = "domain")]
/// Configure and display domain configuration
Domain(DomainOpt),
}
#[derive(Debug, StructOpt)]

View file

@ -2,7 +2,8 @@ ARG BASE_IMAGE=opensuse/tumbleweed:latest
FROM ${BASE_IMAGE} AS builder
LABEL mantainer william@blackhats.net.au
RUN zypper -vv ref && \
RUN zypper ar obs://devel:languages:rust devel:languages:rust && \
zypper --gpg-auto-import-keys ref --force && \
zypper dup -y && \
zypper install -y \
cargo \

View file

@ -826,10 +826,12 @@ pub const JSON_IDM_ACP_DOMAIN_ADMIN_PRIV_V1: &str = r#"{
"uuid",
"domain_name",
"domain_ssid",
"domain_uuid"
"domain_uuid",
"domain_token_key"
],
"acp_modify_removedattr": [
"domain_ssid"
"domain_ssid",
"domain_token_key"
],
"acp_modify_presentattr": [
"domain_ssid"

View file

@ -346,7 +346,7 @@ pub const JSON_SYSTEM_INFO_V1: &str = r#"{
"class": ["object", "system_info", "system"],
"uuid": ["00000000-0000-0000-0000-ffffff000001"],
"description": ["System (local) info and metadata object."],
"version": ["3"]
"version": ["4"]
}
}"#;

View file

@ -274,6 +274,34 @@ pub const JSON_SCHEMA_ATTR_DOMAIN_SSID: &str = r#"{
]
}
}"#;
pub const JSON_SCHEMA_ATTR_DOMAIN_TOKEN_KEY: &str = r#"{
"attrs": {
"class": [
"object",
"system",
"attributetype"
],
"description": [
"The domains token signing key, which is shared between IDM servers."
],
"index": [],
"unique": [
"false"
],
"multivalue": [
"false"
],
"attributename": [
"domain_token_key"
],
"syntax": [
"SECRET_UTF8STRING"
],
"uuid": [
"00000000-0000-0000-0000-ffff00000088"
]
}
}"#;
pub const JSON_SCHEMA_ATTR_GIDNUMBER: &str = r#"{
"attrs": {
@ -748,7 +776,8 @@ pub const JSON_SCHEMA_CLASS_DOMAIN_INFO: &str = r#"
"systemmust": [
"name",
"domain_uuid",
"domain_name"
"domain_name",
"domain_token_key"
],
"uuid": [
"00000000-0000-0000-0000-ffff00000052"

View file

@ -142,8 +142,8 @@ pub const _STR_UUID_SCHEMA_ATTR_OAUTH2_RS_BASIC_TOKEN_KEY: &str =
"00000000-0000-0000-0000-ffff00000084";
pub const STR_UUID_SCHEMA_CLASS_OAUTH2_RS: &str = "00000000-0000-0000-0000-ffff00000085";
pub const STR_UUID_SCHEMA_CLASS_OAUTH2_RS_BASIC: &str = "00000000-0000-0000-0000-ffff00000086";
pub const STR_UUID_SCHEMA_ATTR_CN: &str = "00000000-0000-0000-0000-ffff00000087";
pub const STR_UUID_SCHEMA_ATTR_DOMAIN_TOKEN_KEY: &str = "00000000-0000-0000-0000-ffff00000088";
// System and domain infos
// I'd like to strongly criticise william of the past for making poor choices about these allocations.
@ -321,4 +321,6 @@ lazy_static! {
Uuid::parse_str(STR_UUID_SCHEMA_CLASS_OAUTH2_RS).unwrap();
pub static ref UUID_SCHEMA_CLASS_OAUTH2_RS_BASIC: Uuid =
Uuid::parse_str(STR_UUID_SCHEMA_CLASS_OAUTH2_RS_BASIC).unwrap();
pub static ref UUID_SCHEMA_ATTR_DOMAIN_TOKEN_KEY: Uuid =
Uuid::parse_str(STR_UUID_SCHEMA_ATTR_DOMAIN_TOKEN_KEY).unwrap();
}

View file

@ -553,11 +553,11 @@ pub fn create_https_server(
let mut domain_route = appserver.at("/v1/domain");
domain_route.at("/").get(domain_get);
domain_route.at("/:id").get(domain_id_get);
domain_route
.at("/:id/_attr/:attr")
.get(domain_id_get_attr)
.put(domain_id_put_attr);
.at("/_attr/:attr")
.get(domain_get_attr)
.put(domain_put_attr)
.delete(domain_delete_attr);
let mut recycle_route = appserver.at("/v1/recycle_bin");
recycle_route.at("/").get(recycle_bin_get);

View file

@ -115,15 +115,15 @@ pub async fn json_rest_event_delete_id(
to_tide_response(res, hvalue)
}
pub async fn json_rest_event_get_id_attr(
pub async fn json_rest_event_get_attr(
req: tide::Request<AppState>,
id: &str,
filter: Filter<FilterInvalid>,
) -> tide::Result {
let id = req.get_url_param("id")?;
let attr = req.get_url_param("attr")?;
let uat = req.get_current_uat();
let filter = Filter::join_parts_and(filter, filter_all!(f_id(id)));
let filter = Filter::join_parts_and(filter, filter_all!(f_id(id.as_str())));
let (eventid, hvalue) = req.new_eventid();
let attrs = Some(vec![attr.clone()]);
@ -137,6 +137,14 @@ pub async fn json_rest_event_get_id_attr(
to_tide_response(res, hvalue)
}
pub async fn json_rest_event_get_id_attr(
req: tide::Request<AppState>,
filter: Filter<FilterInvalid>,
) -> tide::Result {
let id = req.get_url_param("id")?;
json_rest_event_get_attr(req, id.as_str(), filter).await
}
pub async fn json_rest_event_post(
mut req: tide::Request<AppState>,
classes: Vec<String>,
@ -170,12 +178,12 @@ pub async fn json_rest_event_post_id_attr(
to_tide_response(res, hvalue)
}
pub async fn json_rest_event_put_id_attr(
pub async fn json_rest_event_put_attr(
mut req: tide::Request<AppState>,
uuid_or_name: String,
filter: Filter<FilterInvalid>,
) -> tide::Result {
let uat = req.get_current_uat();
let uuid_or_name = req.get_url_param("id")?;
let attr = req.get_url_param("attr")?;
let values: Vec<String> = req.body_json().await?;
@ -188,14 +196,31 @@ pub async fn json_rest_event_put_id_attr(
to_tide_response(res, hvalue)
}
pub async fn json_rest_event_put_id_attr(
req: tide::Request<AppState>,
filter: Filter<FilterInvalid>,
) -> tide::Result {
let uuid_or_name = req.get_url_param("id")?;
json_rest_event_put_attr(req, uuid_or_name, filter).await
}
pub async fn json_rest_event_delete_id_attr(
req: tide::Request<AppState>,
filter: Filter<FilterInvalid>,
attr: String,
) -> tide::Result {
let uuid_or_name = req.get_url_param("id")?;
json_rest_event_delete_attr(req, filter, uuid_or_name, attr).await
}
pub async fn json_rest_event_delete_attr(
mut req: tide::Request<AppState>,
filter: Filter<FilterInvalid>,
uuid_or_name: String,
// Seperate for account_delete_id_radius
attr: String,
) -> tide::Result {
let uat = req.get_current_uat();
let uuid_or_name = req.get_url_param("id")?;
let (eventid, hvalue) = req.new_eventid();
// TODO #211: Attempt to get an option Vec<String> here?
@ -672,26 +697,24 @@ pub async fn group_get_id_unix_token(req: tide::Request<AppState>) -> tide::Resu
}
pub async fn domain_get(req: tide::Request<AppState>) -> tide::Result {
let filter = filter_all!(f_eq("class", PartialValue::new_class("domain_info")));
let filter = filter_all!(f_eq("uuid", PartialValue::new_uuidr(&UUID_DOMAIN_INFO)));
json_rest_event_get(req, filter, None).await
}
pub async fn domain_id_get(req: tide::Request<AppState>) -> tide::Result {
pub async fn domain_get_attr(req: tide::Request<AppState>) -> tide::Result {
let filter = filter_all!(f_eq("class", PartialValue::new_class("domain_info")));
json_rest_event_get_id(req, filter, None).await
json_rest_event_get_attr(req, STR_UUID_DOMAIN_INFO, filter).await
}
pub async fn domain_id_get_attr(
req: tide::Request<AppState>,
// (path, session, state): (Path<(String, String)>, Session, Data<AppState>),
) -> tide::Result {
pub async fn domain_put_attr(req: tide::Request<AppState>) -> tide::Result {
let filter = filter_all!(f_eq("class", PartialValue::new_class("domain_info")));
json_rest_event_get_id_attr(req, filter).await
json_rest_event_put_attr(req, STR_UUID_DOMAIN_INFO.to_string(), filter).await
}
pub async fn domain_id_put_attr(req: tide::Request<AppState>) -> tide::Result {
pub async fn domain_delete_attr(req: tide::Request<AppState>) -> tide::Result {
let filter = filter_all!(f_eq("class", PartialValue::new_class("domain_info")));
json_rest_event_put_id_attr(req, filter).await
let attr = req.get_url_param("attr")?;
json_rest_event_delete_attr(req, filter, STR_UUID_DOMAIN_INFO.to_string(), attr).await
}
pub async fn recycle_bin_get(req: tide::Request<AppState>) -> tide::Result {

View file

@ -459,11 +459,9 @@ pub fn recover_account_core(config: &Configuration, name: &str) {
// Run the password change.
let mut idms_prox_write = task::block_on(idms.proxy_write_async(duration_from_epoch_now()));
match idms_prox_write.recover_account(name, None) {
let new_pw = match idms_prox_write.recover_account(name, None) {
Ok(new_pw) => match idms_prox_write.commit() {
Ok(()) => {
eprintln!("Password reset to -> {}", new_pw);
}
Ok(_) => new_pw,
Err(e) => {
error!("A critical error during commit occured {:?}", e);
std::process::exit(1);
@ -476,6 +474,7 @@ pub fn recover_account_core(config: &Configuration, name: &str) {
std::process::exit(1);
}
};
eprintln!("Success - password reset to -> {}", new_pw);
}
pub async fn create_server_core(config: Configuration) -> Result<(), ()> {

View file

@ -428,6 +428,10 @@ impl Entry<EntryInit, EntryNew> {
}).collect();
vs.unwrap()
}
"domain_token_key" => {
let vs: Option<ValueSet> = vs.into_iter().map(|v| Value::new_secret_str(&v)).collect();
vs.unwrap()
}
ia => {
warn!("WARNING: Allowing invalid attribute {} to be interpretted as UTF8 string. YOU MAY ENCOUNTER ODD BEHAVIOUR!!!", ia);
let vs: Option<ValueSet> = vs.into_iter().map(|v| Value::new_utf8(v)).collect();

View file

@ -158,10 +158,11 @@ impl IdmServer {
let (async_tx, async_rx) = unbounded();
// Get the domain name, as the relying party id.
let (rp_id, pw_badlist_set, oauth2rs_set) = {
let (rp_id, token_key, pw_badlist_set, oauth2rs_set) = {
let qs_read = task::block_on(qs.read_async());
(
qs_read.get_domain_name()?,
qs_read.get_domain_token_key()?,
qs_read.get_password_badlist()?,
// Add a read/reload of all oauth2 configurations.
qs_read.get_oauth2rs_set()?,
@ -198,12 +199,10 @@ impl IdmServer {
});
// Setup our auth token signing key.
let bundy_handle = HS512::generate_key()
.and_then(|bundy_key| HS512::from_str(&bundy_key))
.map_err(|e| {
admin_error!("Failed to generate uat_bundy_hmac - {:?}", e);
OperationError::InvalidState
})?;
let bundy_handle = HS512::from_str(&token_key).map_err(|e| {
admin_error!("Failed to generate uat_bundy_hmac - {:?}", e);
OperationError::InvalidState
})?;
let uat_bundy_hmac = Arc::new(CowCell::new(bundy_handle));
let oauth2rs = Oauth2ResourceServers::try_from(oauth2rs_set).map_err(|e| {
@ -1943,15 +1942,29 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
.get_oauth2rs_set()
.and_then(|oauth2rs_set| self.oauth2rs.reload(oauth2rs_set))?;
}
if self.qs_write.get_changed_domain() {
// reload token_key?
self.qs_write
.get_domain_token_key()
.and_then(|token_key| {
HS512::from_str(&token_key).map_err(|e| {
admin_error!("Failed to generate uat_bundy_hmac - {:?}", e);
OperationError::InvalidState
})
})
.map(|new_handle| {
*self.uat_bundy_hmac = new_handle;
})?;
}
// Commit everything.
self.oauth2rs.commit();
self.uat_bundy_hmac.commit();
self.pw_badlist_cache.commit();
self.mfareg_sessions.commit();
self.qs_write.commit()
})
}
// TODO: tracing
fn reload_password_badlist(&mut self) -> Result<(), OperationError> {
match self.qs_write.get_password_badlist() {
Ok(badlist_entry) => {
@ -3804,4 +3817,44 @@ mod tests {
assert!(!ident.has_claim("authclass_single"));
})
}
#[test]
fn test_idm_bundy_uat_token_key_reload() {
run_idm_test!(
|qs: &QueryServer, idms: &IdmServer, _idms_delayed: &mut IdmServerDelayed| {
let ct = Duration::from_secs(TEST_CURRENT_TIME);
init_admin_w_password(qs, TEST_PASSWORD).expect("Failed to setup admin account");
let token = check_admin_password(idms, TEST_PASSWORD);
let idms_prox_read = idms.proxy_read();
// Check it's valid.
idms_prox_read
.validate_and_parse_uat(Some(token.as_str()), ct)
.expect("Failed to validate");
drop(idms_prox_read);
// Now reset the token_key - we can cheat and push this
// through the migrate 3 to 4 code.
let idms_prox_write = idms.proxy_write(ct.clone());
idms_prox_write
.qs_write
.migrate_3_to_4()
.expect("Failed to reset domain token key");
assert!(idms_prox_write.commit().is_ok());
// Check the old token is invalid, due to reload.
let new_token = check_admin_password(idms, TEST_PASSWORD);
let idms_prox_read = idms.proxy_read();
assert!(idms_prox_read
.validate_and_parse_uat(Some(token.as_str()), ct)
.is_err());
// A new token will work due to the matching key.
idms_prox_read
.validate_and_parse_uat(Some(new_token.as_str()), ct)
.expect("Failed to validate");
}
)
}
}

View file

@ -248,7 +248,7 @@ macro_rules! run_create_test {
{
let qs_write = qs.write(duration_from_epoch_now());
let r = qs_write.create(&ce);
debug!("test result: {:?}", r);
trace!("test result: {:?}", r);
assert!(r == $expect);
$check(&qs_write);
match r {
@ -261,9 +261,9 @@ macro_rules! run_create_test {
}
}
// Make sure there are no errors.
debug!("starting verification");
trace!("starting verification");
let ver = qs.verify();
debug!("verification -> {:?}", ver);
trace!("verification -> {:?}", ver);
assert!(ver.len() == 0);
});
}};
@ -303,7 +303,7 @@ macro_rules! run_modify_test {
spanned!("plugins::macros::run_modify_test -> post_test check", {
$check(&qs_write)
});
debug!("test result: {:?}", r);
trace!("test result: {:?}", r);
assert!(r == $expect);
match r {
Ok(_) => {
@ -315,9 +315,9 @@ macro_rules! run_modify_test {
}
}
// Make sure there are no errors.
debug!("starting verification");
trace!("starting verification");
let ver = qs.verify();
debug!("verification -> {:?}", ver);
trace!("verification -> {:?}", ver);
assert!(ver.len() == 0);
});
}};
@ -352,7 +352,7 @@ macro_rules! run_delete_test {
{
let qs_write = qs.write(duration_from_epoch_now());
let r = qs_write.delete(&de);
debug!("test result: {:?}", r);
trace!("test result: {:?}", r);
$check(&qs_write);
assert!(r == $expect);
match r {
@ -365,9 +365,9 @@ macro_rules! run_delete_test {
}
}
// Make sure there are no errors.
debug!("starting verification");
trace!("starting verification");
let ver = qs.verify();
debug!("verification -> {:?}", ver);
trace!("verification -> {:?}", ver);
assert!(ver.len() == 0);
});
}};

View file

@ -6,8 +6,9 @@
// relationships.
use crate::plugins::Plugin;
use crate::event::CreateEvent;
use crate::event::{CreateEvent, ModifyEvent};
use crate::prelude::*;
use bundy::hs512::HS512;
use kanidm_proto::v1::OperationError;
use tracing::trace;
@ -28,8 +29,7 @@ impl Plugin for Domain {
cand: &mut Vec<Entry<EntryInvalid, EntryNew>>,
_ce: &CreateEvent,
) -> Result<(), OperationError> {
trace!("Entering plugin_domain pre_create_transform");
cand.iter_mut().for_each(|e| {
cand.iter_mut().try_for_each(|e| {
if e.attribute_equality("class", &PVCLASS_DOMAIN_INFO)
&& e.attribute_equality("uuid", &PVUUID_DOMAIN_INFO)
{
@ -43,11 +43,49 @@ impl Plugin for Domain {
e.set_ava("domain_name", btreeset![n]);
trace!("plugin_domain: Applying domain_name transform");
}
if !e.attribute_pres("domain_token_key") {
let k = HS512::generate_key()
.map(|k| Value::new_secret_str(&k))
.map_err(|e| {
admin_error!(err = ?e, "Failed to generate domain_token_key");
OperationError::InvalidState
})?;
e.set_ava("domain_token_key", btreeset![k]);
trace!("plugin_domain: Applying domain_token_key transform");
}
trace!(?e);
Ok(())
} else {
Ok(())
}
});
trace!("Ending plugin_domain pre_create_transform");
Ok(())
})
}
fn pre_modify(
_qs: &QueryServerWriteTransaction,
cand: &mut Vec<Entry<EntryInvalid, EntryCommitted>>,
_me: &ModifyEvent,
) -> Result<(), OperationError> {
cand.iter_mut().try_for_each(|e| {
if e.attribute_equality("class", &PVCLASS_DOMAIN_INFO)
&& e.attribute_equality("uuid", &PVUUID_DOMAIN_INFO)
{
if !e.attribute_pres("domain_token_key") {
let k = HS512::generate_key()
.map(|k| Value::new_secret_str(&k))
.map_err(|e| {
admin_error!(err = ?e, "Failed to generate domain_token_key");
OperationError::InvalidState
})?;
e.set_ava("domain_token_key", btreeset![k]);
trace!("plugin_domain: Applying domain_token_key transform");
}
trace!(?e);
Ok(())
} else {
Ok(())
}
})
}
}

View file

@ -166,6 +166,7 @@ impl Plugins {
.and_then(|_| password_import::PasswordImport::pre_modify(qs, cand, me))
.and_then(|_| oauth2::Oauth2Secrets::pre_modify(qs, cand, me))
.and_then(|_| gidnumber::GidNumber::pre_modify(qs, cand, me))
.and_then(|_| domain::Domain::pre_modify(qs, cand, me))
.and_then(|_| spn::Spn::pre_modify(qs, cand, me))
// attr unique should always be last
.and_then(|_| attrunique::AttrUnique::pre_modify(qs, cand, me))

View file

@ -23,6 +23,7 @@ lazy_static! {
m.insert("may");
// Allow modification of some domain info types for local configuration.
m.insert("domain_ssid");
m.insert("domain_token_key");
m.insert("badlist_password");
m
};
@ -192,10 +193,10 @@ mod tests {
],
"acp_search_attr": ["name", "class", "uuid", "classname", "attributename"],
"acp_modify_class": ["system", "domain_info"],
"acp_modify_removedattr": ["class", "displayname", "may", "must", "domain_name", "domain_uuid", "domain_ssid"],
"acp_modify_presentattr": ["class", "displayname", "may", "must", "domain_name", "domain_uuid", "domain_ssid"],
"acp_modify_removedattr": ["class", "displayname", "may", "must", "domain_name", "domain_uuid", "domain_ssid", "domain_token_key"],
"acp_modify_presentattr": ["class", "displayname", "may", "must", "domain_name", "domain_uuid", "domain_ssid", "domain_token_key"],
"acp_create_class": ["object", "person", "system", "domain_info"],
"acp_create_attr": ["name", "class", "description", "displayname", "domain_name", "domain_uuid", "domain_ssid", "uuid"]
"acp_create_attr": ["name", "class", "description", "displayname", "domain_name", "domain_uuid", "domain_ssid", "uuid", "domain_token_key"]
}
}"#;
@ -326,9 +327,10 @@ mod tests {
"name": ["domain_example.net.au"],
"uuid": ["96fd1112-28bc-48ae-9dda-5acb4719aaba"],
"domain_uuid": ["96fd1112-28bc-48ae-9dda-5acb4719aaba"],
"description": ["Demonstration of a remote domain's info being created for uuid generaiton"],
"description": ["Demonstration of a remote domain's info being created for uuid generation"],
"domain_name": ["example.net.au"],
"domain_ssid": ["Example_Wifi"]
"domain_ssid": ["Example_Wifi"],
"domain_token_key": ["ABCD"]
}
}"#,
);
@ -363,9 +365,10 @@ mod tests {
"name": ["domain_example.net.au"],
"uuid": ["96fd1112-28bc-48ae-9dda-5acb4719aaba"],
"domain_uuid": ["96fd1112-28bc-48ae-9dda-5acb4719aaba"],
"description": ["Demonstration of a remote domain's info being created for uuid generaiton"],
"description": ["Demonstration of a remote domain's info being created for uuid generation"],
"domain_name": ["example.net.au"],
"domain_ssid": ["Example_Wifi"]
"domain_ssid": ["Example_Wifi"],
"domain_token_key": ["ABCD"]
}
}"#,
);
@ -391,9 +394,10 @@ mod tests {
"name": ["domain_example.net.au"],
"uuid": ["96fd1112-28bc-48ae-9dda-5acb4719aaba"],
"domain_uuid": ["96fd1112-28bc-48ae-9dda-5acb4719aaba"],
"description": ["Demonstration of a remote domain's info being created for uuid generaiton"],
"description": ["Demonstration of a remote domain's info being created for uuid generation"],
"domain_name": ["example.net.au"],
"domain_ssid": ["Example_Wifi"]
"domain_ssid": ["Example_Wifi"],
"domain_token_key": ["ABCD"]
}
}"#,
);

View file

@ -48,6 +48,7 @@ lazy_static! {
static ref PVCLASS_ACC: PartialValue = PartialValue::new_class("access_control_create");
static ref PVCLASS_ACP: PartialValue = PartialValue::new_class("access_control_profile");
static ref PVCLASS_OAUTH2_RS: PartialValue = PartialValue::new_class("oauth2_resource_server");
static ref PVUUID_DOMAIN_INFO: PartialValue = PartialValue::new_uuidr(&UUID_DOMAIN_INFO);
static ref PVACP_ENABLE_FALSE: PartialValue = PartialValue::new_bool(false);
}
@ -88,6 +89,7 @@ pub struct QueryServerWriteTransaction<'a> {
changed_schema: Cell<bool>,
changed_acp: Cell<bool>,
changed_oauth2: Cell<bool>,
changed_domain: Cell<bool>,
// Store the list of changed uuids for other invalidation needs?
changed_uuid: Cell<HashSet<Uuid>>,
_db_ticket: SemaphorePermit<'a>,
@ -686,6 +688,19 @@ pub trait QueryServerTransaction<'a> {
})
}
fn get_domain_token_key(&self) -> Result<String, OperationError> {
self.internal_search_uuid(&UUID_DOMAIN_INFO)
.and_then(|e| {
e.get_ava_single_secret("domain_token_key")
.map(str::to_string)
.ok_or(OperationError::InvalidEntryState)
})
.map_err(|e| {
admin_error!(?e, "Error getting domain token key");
e
})
}
// ! TRACING INTEGRATED
// This is a helper to get password badlist.
fn get_password_badlist(&self) -> Result<HashSet<String>, OperationError> {
@ -928,6 +943,7 @@ impl QueryServer {
changed_schema: Cell::new(false),
changed_acp: Cell::new(false),
changed_oauth2: Cell::new(false),
changed_domain: Cell::new(false),
changed_uuid: Cell::new(HashSet::new()),
_db_ticket: db_ticket,
_write_ticket: write_ticket,
@ -1007,6 +1023,10 @@ impl QueryServer {
migrate_txn.migrate_2_to_3()?;
}
if system_info_version < 4 {
migrate_txn.migrate_3_to_4()?;
}
migrate_txn.commit()?;
// Migrations complete. Init idm will now set the version as needed.
@ -1136,6 +1156,13 @@ impl<'a> QueryServerWriteTransaction<'a> {
.any(|e| e.attribute_equality("class", &PVCLASS_OAUTH2_RS)),
)
}
if !self.changed_domain.get() {
self.changed_domain.set(
commit_cand
.iter()
.any(|e| e.attribute_equality("uuid", &PVUUID_DOMAIN_INFO)),
)
}
let cu = self.changed_uuid.as_ptr();
unsafe {
@ -1145,6 +1172,7 @@ impl<'a> QueryServerWriteTransaction<'a> {
schema_reload = ?self.changed_schema,
acp_reload = ?self.changed_acp,
oauth2_reload = ?self.changed_oauth2,
domain_reload = ?self.changed_domain,
);
// We are complete, finalise logging and return
@ -1266,6 +1294,13 @@ impl<'a> QueryServerWriteTransaction<'a> {
.any(|e| e.attribute_equality("class", &PVCLASS_OAUTH2_RS)),
)
}
if !self.changed_domain.get() {
self.changed_domain.set(
del_cand
.iter()
.any(|e| e.attribute_equality("uuid", &PVUUID_DOMAIN_INFO)),
)
}
let cu = self.changed_uuid.as_ptr();
unsafe {
@ -1276,6 +1311,7 @@ impl<'a> QueryServerWriteTransaction<'a> {
schema_reload = ?self.changed_schema,
acp_reload = ?self.changed_acp,
oauth2_reload = ?self.changed_oauth2,
domain_reload = ?self.changed_domain,
);
// Send result
@ -1611,6 +1647,14 @@ impl<'a> QueryServerWriteTransaction<'a> {
.any(|e| e.attribute_equality("class", &PVCLASS_OAUTH2_RS)),
)
}
if !self.changed_domain.get() {
self.changed_domain.set(
norm_cand
.iter()
.chain(pre_candidates.iter().map(|e| e.as_ref()))
.any(|e| e.attribute_equality("uuid", &PVUUID_DOMAIN_INFO)),
)
}
let cu = self.changed_uuid.as_ptr();
unsafe {
@ -1626,6 +1670,7 @@ impl<'a> QueryServerWriteTransaction<'a> {
schema_reload = ?self.changed_schema,
acp_reload = ?self.changed_acp,
oauth2_reload = ?self.changed_oauth2,
domain_reload = ?self.changed_domain,
);
// return
@ -1758,6 +1803,13 @@ impl<'a> QueryServerWriteTransaction<'a> {
.any(|e| e.attribute_equality("class", &PVCLASS_OAUTH2_RS)),
)
}
if !self.changed_domain.get() {
self.changed_domain.set(
norm_cand
.iter()
.any(|e| e.attribute_equality("uuid", &PVUUID_DOMAIN_INFO)),
)
}
let cu = self.changed_uuid.as_ptr();
unsafe {
(*cu).extend(
@ -1771,6 +1823,7 @@ impl<'a> QueryServerWriteTransaction<'a> {
schema_reload = ?self.changed_schema,
acp_reload = ?self.changed_acp,
oauth2_reload = ?self.changed_oauth2,
domain_reload = ?self.changed_domain,
);
trace!("Modify operation success");
@ -1839,6 +1892,18 @@ impl<'a> QueryServerWriteTransaction<'a> {
})
}
/// Migrate 3 to 4 - this triggers a regen of the domains security token
/// as we previously did not have it in the entry.
pub fn migrate_3_to_4(&self) -> Result<(), OperationError> {
spanned!("server::migrate_3_to_4", {
admin_warn!("starting 3 to 4 migration.");
let filter = filter!(f_eq("uuid", (*PVUUID_DOMAIN_INFO).clone()));
let modlist = ModifyList::new_purge("domain_token_key");
self.internal_modify(&filter, &modlist)
// Complete
})
}
// These are where searches and other actions are actually implemented. This
// is the "internal" version, where we define the event as being internal
// only, allowing certain plugin by passes etc.
@ -2115,6 +2180,7 @@ impl<'a> QueryServerWriteTransaction<'a> {
JSON_SCHEMA_ATTR_DOMAIN_NAME,
JSON_SCHEMA_ATTR_DOMAIN_UUID,
JSON_SCHEMA_ATTR_DOMAIN_SSID,
JSON_SCHEMA_ATTR_DOMAIN_TOKEN_KEY,
JSON_SCHEMA_ATTR_GIDNUMBER,
JSON_SCHEMA_ATTR_BADLIST_PASSWORD,
JSON_SCHEMA_ATTR_LOGINSHELL,
@ -2502,6 +2568,10 @@ impl<'a> QueryServerWriteTransaction<'a> {
self.changed_oauth2.get()
}
pub fn get_changed_domain(&self) -> bool {
self.changed_domain.get()
}
pub fn commit(mut self) -> Result<(), OperationError> {
// This could be faster if we cache the set of classes changed
// in an operation so we can check if we need to do the reload or not