Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions crates/api-types/src/error_conv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ use openstack_keystone_core_types::auth::AuthenticationError;
use openstack_keystone_core_types::auth_plugin_identity::AuthPluginIdentityProviderError;
use openstack_keystone_core_types::catalog::CatalogProviderError;
use openstack_keystone_core_types::credential::CredentialProviderError;
use openstack_keystone_core_types::domain_config::DomainConfigProviderError;
use openstack_keystone_core_types::error::BuilderError;
use openstack_keystone_core_types::error::KeystoneError;
use openstack_keystone_core_types::identity::IdentityProviderError;
Expand Down Expand Up @@ -384,6 +385,39 @@ impl From<ResourceProviderError> for KeystoneApiError {
}
}

impl From<DomainConfigProviderError> for KeystoneApiError {
fn from(value: DomainConfigProviderError) -> Self {
match value {
DomainConfigProviderError::NotFound {
domain_id,
group_or_option,
} => Self::NotFound {
resource: group_or_option,
identifier: domain_id,
},
ref err @ DomainConfigProviderError::Conflict(..) => Self::Conflict(err.to_string()),
// A group or option that is not domain-configurable, or a write
// against the read-only filesystem driver, is a permissions
// statement rather than a malformed request.
err @ (DomainConfigProviderError::UnsupportedGroup(..)
| DomainConfigProviderError::UnsupportedOption { .. }
| DomainConfigProviderError::UnsupportedDriver(..)
| DomainConfigProviderError::Readonly(..)) => Self::forbidden(err),
// Everything else that is not a backend fault is a bad request:
// empty body, group/option shape mismatch, un-decodable value,
// failed validation.
err @ (DomainConfigProviderError::EmptyConfig
| DomainConfigProviderError::GroupMismatch(..)
| DomainConfigProviderError::GroupNotAMapping(..)
| DomainConfigProviderError::OptionWithoutGroup(..)
| DomainConfigProviderError::InvalidOptionValue { .. }
| DomainConfigProviderError::InvalidValue { .. }
| DomainConfigProviderError::Validation { .. }) => Self::BadRequest(err.to_string()),
other => Self::InternalError(other.to_string()),
}
}
}

impl From<RevokeProviderError> for KeystoneApiError {
fn from(value: RevokeProviderError) -> Self {
match value {
Expand Down Expand Up @@ -692,6 +726,7 @@ impl From<KeystoneError> for KeystoneApiError {
KeystoneError::Authentication { source } => source.into(),
KeystoneError::CatalogProvider { source } => source.into(),
KeystoneError::CredentialProvider { source } => source.into(),
KeystoneError::DomainConfigProvider { source } => source.into(),
KeystoneError::FederationProvider { source } => source.into(),
KeystoneError::Json { source } => source.into(),
KeystoneError::K8sAuthProvider { source } => source.into(),
Expand Down
3 changes: 3 additions & 0 deletions crates/api-types/src/v3.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ pub mod application_credential;
pub mod auth;
pub mod credential;
pub mod domain;
pub mod domain_config;
pub mod ec2tokens;
pub mod endpoint;
pub mod group;
Expand All @@ -36,6 +37,8 @@ mod auth_conv;
#[cfg(feature = "conv")]
mod credential_conv;
#[cfg(feature = "conv")]
mod domain_config_conv;
#[cfg(feature = "conv")]
mod domain_conv;
#[cfg(feature = "conv")]
mod endpoint_conv;
Expand Down
50 changes: 50 additions & 0 deletions crates/api-types/src/v3/domain_config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// SPDX-License-Identifier: Apache-2.0
//! # Domain configuration API types.
//!
//! Every domain-configuration endpoint nests its payload under a single
//! `config` key. The value is an object of groups (`identity`, `ldap`), each an
//! object of option name to value:
//!
//! ```json
//! {"config": {"identity": {"driver": "ldap"},
//! "ldap": {"url": "ldap://localhost", "user_tree_dn": "ou=Users,dc=example,dc=org"}}}
//! ```
//!
//! Group- and option-scoped requests and responses use the same envelope with
//! only the addressed group (or option) present. Sensitive options
//! (`ldap.password`) may be written but are never echoed back in a response.

use serde::{Deserialize, Serialize};
use serde_json::Value;

/// `PUT` / `PATCH` body for the domain-configuration endpoints.
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct DomainConfigRequest {
/// The configuration, an object of groups each mapping option name to
/// value. For a group- or option-scoped request only the addressed group
/// is present.
pub config: Value,
}

/// Response body for the domain-configuration endpoints, including the
/// defaults endpoints.
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct DomainConfigResponse {
/// The configuration, an object of groups each mapping option name to
/// value. Sensitive options are omitted.
pub config: Value,
}
103 changes: 103 additions & 0 deletions crates/api-types/src/v3/domain_config_conv.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// SPDX-License-Identifier: Apache-2.0
//! Domain configuration API types conversions.

use serde_json::{Map, Value};

use openstack_keystone_core_types::domain_config as core;

use crate::v3::domain_config as api_types;

/// Serialize a value that only ever holds JSON objects and scalars, falling
/// back to an empty object on the impossible serialization failure (avoids
/// `unwrap` while keeping the conversions infallible).
fn to_object(value: impl serde::Serialize) -> Value {
serde_json::to_value(value).unwrap_or_else(|_| Value::Object(Map::new()))
}

impl From<core::DomainConfig> for api_types::DomainConfigResponse {
fn from(value: core::DomainConfig) -> Self {
// `DomainConfig`'s `Serialize` already drops sensitive options.
Self {
config: to_object(&value),
}
}
}

impl From<core::DomainConfigGroup> for api_types::DomainConfigResponse {
fn from(value: core::DomainConfigGroup) -> Self {
let mut config = Map::new();
config.insert(value.name().to_string(), to_object(&value));
Self {
config: Value::Object(config),
}
}
}

impl From<core::DomainConfigOption> for api_types::DomainConfigResponse {
fn from(value: core::DomainConfigOption) -> Self {
let mut options = Map::new();
options.insert(value.option.clone(), value.value.as_value().clone());
let mut config = Map::new();
config.insert(value.group.to_string(), Value::Object(options));
Self {
config: Value::Object(config),
}
}
}

#[cfg(test)]
mod tests {
use serde_json::json;

use openstack_keystone_core_types::domain_config::{
DomainConfig, DomainConfigGroupName, DomainConfigOption,
};

use super::*;

#[test]
fn whole_config_response_drops_sensitive_options() {
let config = DomainConfig::from_value(json!({
"ldap": {"url": "ldap://example", "password": "s3cr3t"}
}))
.expect("valid config");

let response = api_types::DomainConfigResponse::from(config);

assert_eq!(response.config["ldap"]["url"], json!("ldap://example"));
assert!(response.config["ldap"].get("password").is_none());
}

#[test]
fn group_response_is_wrapped_under_the_group_name() {
let group = DomainConfig::from_value(json!({"ldap": {"url": "ldap://example"}}))
.expect("valid config")
.into_group(DomainConfigGroupName::Ldap)
.expect("ldap group");

let response = api_types::DomainConfigResponse::from(group);

assert_eq!(response.config, json!({"ldap": {"url": "ldap://example"}}));
}

#[test]
fn option_response_is_wrapped_under_group_and_option() {
let option = DomainConfigOption::new(DomainConfigGroupName::Identity, "driver", "sql");

let response = api_types::DomainConfigResponse::from(option);

assert_eq!(response.config, json!({"identity": {"driver": "sql"}}));
}
}
153 changes: 153 additions & 0 deletions crates/core/src/domain_config/api.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// SPDX-License-Identifier: Apache-2.0

//! # Domain configuration provider API
//!
//! The subset of [`DomainConfigBackend`](crate::domain_config::DomainConfigBackend)
//! that backs the `/v3/domains/{domain_id}/config` REST endpoints: every verb
//! at the three granularities (whole configuration, single group, single
//! option), plus the read-only defaults. The registration lock methods of the
//! backend are intentionally excluded here -- they only matter once the stored
//! configuration drives identity-driver selection (issue #960).

use async_trait::async_trait;

use openstack_keystone_core_types::domain_config::*;

use crate::domain_config::DomainConfigProviderError;
use crate::keystone::ServiceState;

/// Domain configuration provider API.
///
/// Signatures mirror the backend trait one-for-one so the service layer can
/// delegate straight through. See the backend trait for the sensitive-option
/// contract: `ldap.password` may be written but is never returned by the
/// group- or option-scoped getters.
#[async_trait]
pub trait DomainConfigApi: Send + Sync {
/// Replace the whole configuration of a domain
/// (`PUT /v3/domains/{domain_id}/config`).
async fn create_domain_config<'a>(
&self,
state: &ServiceState,
domain_id: &'a str,
config: DomainConfigCreate,
) -> Result<DomainConfig, DomainConfigProviderError>;

/// Get the whole configuration of a domain
/// (`GET /v3/domains/{domain_id}/config`).
async fn get_domain_config<'a>(
&self,
state: &ServiceState,
domain_id: &'a str,
) -> Result<Option<DomainConfig>, DomainConfigProviderError>;

/// Get a single configuration group of a domain
/// (`GET /v3/domains/{domain_id}/config/{group}`).
async fn get_domain_config_group<'a>(
&self,
state: &ServiceState,
domain_id: &'a str,
group: DomainConfigGroupName,
) -> Result<Option<DomainConfigGroup>, DomainConfigProviderError>;

/// Get a single configuration option of a domain
/// (`GET /v3/domains/{domain_id}/config/{group}/{option}`).
async fn get_domain_config_option<'a>(
&self,
state: &ServiceState,
domain_id: &'a str,
group: DomainConfigGroupName,
option: &'a str,
) -> Result<Option<DomainConfigOption>, DomainConfigProviderError>;

/// Merge changes into the whole configuration of a domain
/// (`PATCH /v3/domains/{domain_id}/config`).
async fn update_domain_config<'a>(
&self,
state: &ServiceState,
domain_id: &'a str,
config: DomainConfigUpdate,
) -> Result<DomainConfig, DomainConfigProviderError>;

/// Merge changes into a single configuration group of a domain
/// (`PATCH /v3/domains/{domain_id}/config/{group}`).
async fn update_domain_config_group<'a>(
&self,
state: &ServiceState,
domain_id: &'a str,
group: DomainConfigGroupName,
config: DomainConfigUpdate,
) -> Result<DomainConfigGroup, DomainConfigProviderError>;

/// Change a single configuration option of a domain
/// (`PATCH /v3/domains/{domain_id}/config/{group}/{option}`).
async fn update_domain_config_option<'a>(
&self,
state: &ServiceState,
domain_id: &'a str,
option: DomainConfigOption,
) -> Result<DomainConfigOption, DomainConfigProviderError>;

/// Delete the whole configuration of a domain
/// (`DELETE /v3/domains/{domain_id}/config`).
async fn delete_domain_config<'a>(
&self,
state: &ServiceState,
domain_id: &'a str,
) -> Result<(), DomainConfigProviderError>;

/// Delete a single configuration group of a domain
/// (`DELETE /v3/domains/{domain_id}/config/{group}`).
async fn delete_domain_config_group<'a>(
&self,
state: &ServiceState,
domain_id: &'a str,
group: DomainConfigGroupName,
) -> Result<(), DomainConfigProviderError>;

/// Delete a single configuration option of a domain
/// (`DELETE /v3/domains/{domain_id}/config/{group}/{option}`).
async fn delete_domain_config_option<'a>(
&self,
state: &ServiceState,
domain_id: &'a str,
group: DomainConfigGroupName,
option: &'a str,
) -> Result<(), DomainConfigProviderError>;

/// Get the global defaults every domain without configuration falls back
/// to (`GET /v3/domains/config/default`).
async fn get_default_config(
&self,
state: &ServiceState,
) -> Result<DomainConfig, DomainConfigProviderError>;

/// Get the global defaults of a single group
/// (`GET /v3/domains/config/{group}/default`).
async fn get_default_group(
&self,
state: &ServiceState,
group: DomainConfigGroupName,
) -> Result<DomainConfigGroup, DomainConfigProviderError>;

/// Get the global default of a single option
/// (`GET /v3/domains/config/{group}/{option}/default`).
async fn get_default_option<'a>(
&self,
state: &ServiceState,
group: DomainConfigGroupName,
option: &'a str,
) -> Result<Option<DomainConfigOption>, DomainConfigProviderError>;
}
7 changes: 7 additions & 0 deletions crates/core/src/domain_config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,17 @@
//! single group, or a single option — which is why the backend trait carries a
//! method triple for each verb.

pub mod api;
pub mod backend;
pub mod error;
pub mod resolver;
pub mod service;

pub use api::DomainConfigApi;
pub use backend::DomainConfigBackend;
pub use error::DomainConfigProviderError;
pub use resolver::DomainConfigResolver;
pub use service::DomainConfigService;

#[cfg(any(test, feature = "mock"))]
pub use crate::mocks::MockDomainConfigProvider;
Loading
Loading