diff --git a/Cargo.lock b/Cargo.lock index 1760a112..aac240d5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -101,9 +101,9 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "anstream" -version = "0.6.21" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -122,9 +122,9 @@ checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" [[package]] name = "anstyle-parse" -version = "0.2.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] @@ -768,9 +768,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.60" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" dependencies = [ "clap_builder", "clap_derive", @@ -778,9 +778,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.60" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", @@ -790,9 +790,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.55" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -924,13 +924,12 @@ dependencies = [ [[package]] name = "console" -version = "0.16.2" +version = "0.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03e45a4a8926227e4197636ba97a9fc9b00477e9f4bd711395687c5f0734bec4" +checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87" dependencies = [ "encode_unicode", "libc", - "once_cell", "unicode-width", "windows-sys 0.61.2", ] @@ -2923,18 +2922,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "mockall_double" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1ca96e5ac35256ae3e13536edd39b172b88f41615e1d7b653c8ad24524113e8" -dependencies = [ - "cfg-if", - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "multimap" version = "0.10.1" @@ -3302,46 +3289,29 @@ dependencies = [ "axum", "base64 0.22.1", "base64urlsafedata", - "bcrypt", - "byteorder", - "bytes", "chrono", "clap", "color-eyre", - "config", "criterion", "derive_builder", - "dyn-clone", "eyre", - "fernet", - "futures-util", "http-body-util", "httpmock", "hyper", "hyper-util", - "itertools 0.14.0", - "jsonwebtoken", "mockall", - "mockall_double", - "nix", "openidconnect", "openstack-keystone-api-types", + "openstack-keystone-core", "openstack-keystone-distributed-storage", - "rand 0.10.0", - "regex", "reqwest 0.13.2", - "rmp", "rstest", - "schemars 1.2.1", - "scopeguard", "sea-orm", "sea-orm-migration", "secrecy", "serde", - "serde_bytes", "serde_json", "serde_urlencoded", - "tempfile", "thiserror 2.0.18", "tokio", "tokio-util", @@ -3352,7 +3322,6 @@ dependencies = [ "tracing-subscriber", "tracing-test", "url", - "url-macro", "utoipa", "utoipa-axum", "utoipa-swagger-ui", @@ -3381,6 +3350,52 @@ dependencies = [ "webauthn-rs-proto", ] +[[package]] +name = "openstack-keystone-core" +version = "0.1.1" +dependencies = [ + "async-trait", + "axum", + "base64 0.22.1", + "base64urlsafedata", + "bcrypt", + "byteorder", + "bytes", + "chrono", + "config", + "derive_builder", + "eyre", + "fernet", + "futures-util", + "httpmock", + "itertools 0.14.0", + "jsonwebtoken", + "mockall", + "nix", + "openstack-keystone-api-types", + "rand 0.10.0", + "regex", + "reqwest 0.13.2", + "rmp", + "rstest", + "schemars 1.2.1", + "scopeguard", + "sea-orm", + "secrecy", + "serde", + "serde_json", + "serde_urlencoded", + "tempfile", + "thiserror 2.0.18", + "tokio", + "tracing", + "tracing-test", + "url", + "url-macro", + "uuid", + "validator", +] + [[package]] name = "openstack-keystone-distributed-storage" version = "0.1.0" @@ -5678,9 +5693,9 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tempfile" -version = "3.26.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", "getrandom 0.4.2", @@ -5752,6 +5767,7 @@ dependencies = [ "eyre", "itertools 0.14.0", "openstack-keystone", + "openstack-keystone-core", "sea-orm", "secrecy", "serde", diff --git a/Cargo.toml b/Cargo.toml index fe66cba5..803ee57d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] members = [ "crates/api-types", + "crates/core", "crates/keystone", "crates/storage", "tests/api", @@ -36,7 +37,7 @@ bcrypt = { version = "0.19" } byteorder = { version = "1.5" } bytes = { version = "1.11" } chrono = { version = "0.4", default-features = false } -clap = { version = "4.5" } +clap = { version = "4.6" } color-eyre = { version = "0.6" } config = { version = "0.15" } criterion = { version = "0.8" } @@ -71,7 +72,7 @@ serde = { version = "1.0" } serde_bytes = { version = "0.11" } serde_json = { version = "1.0" } serde_urlencoded = { version = "0.7" } -tempfile = { version = "3.26" } +tempfile = { version = "3.27" } thiserror = { version = "2.0" } tokio = { version = "1.50" } tokio-util = { version = "0.7" } diff --git a/Dockerfile b/Dockerfile index 3faf9000..d30a588c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,8 +15,9 @@ RUN USER=root cargo new keystone # We want dependencies cached, so copy those first. COPY Cargo.toml Cargo.lock /usr/src/keystone/ -COPY crates/keystone/Cargo.toml /usr/src/keystone/crates/keystone/ COPY crates/api-types/Cargo.toml /usr/src/keystone/crates/api-types/ +COPY crates/core/Cargo.toml /usr/src/keystone/crates/core/ +COPY crates/keystone/Cargo.toml /usr/src/keystone/crates/keystone/ COPY crates/storage/Cargo.toml /usr/src/keystone/crates/storage/ COPY tests/federation/Cargo.toml /usr/src/keystone/tests/federation/ COPY tests/integration/Cargo.toml /usr/src/keystone/tests/integration/ @@ -25,12 +26,11 @@ COPY tests/loadtest/Cargo.toml /usr/src/keystone/tests/loadtest/ RUN mkdir -p keystone/crates/keystone/src/bin && touch keystone/crates/keystone/src/lib.rs &&\ cp keystone/src/main.rs keystone/crates/keystone/src/bin/keystone.rs &&\ cp keystone/src/main.rs keystone/crates/keystone/src/bin/keystone_db.rs &&\ - mkdir keystone/tests/loadtest/src &&\ + mkdir -p keystone/tests/loadtest/src &&\ cp keystone/src/main.rs keystone/tests/loadtest/src/main.rs &&\ - mkdir keystone/crates/api-types/src &&\ - touch keystone/crates/api-types/src/lib.rs &&\ - mkdir keystone/crates/storage/src &&\ - touch keystone/crates/storage/src/lib.rs + mkdir -p keystone/crates/api-types/src && touch keystone/crates/api-types/src/lib.rs &&\ + mkdir -p keystone/crates/core/src && touch keystone/crates/core/src/lib.rs &&\ + mkdir -p keystone/crates/storage/src && touch keystone/crates/storage/src/lib.rs # Set the working directory WORKDIR /usr/src/keystone @@ -41,6 +41,7 @@ RUN cargo build -p openstack-keystone --release # Now copy in the rest of the sources COPY crates/keystone/ /usr/src/keystone/crates/keystone +COPY crates/core/ /usr/src/keystone/crates/core COPY crates/api-types/ /usr/src/keystone/crates/api-types COPY crates/storage/ /usr/src/keystone/crates/storage diff --git a/crates/api-types/Cargo.toml b/crates/api-types/Cargo.toml index f2392fe5..cff4635e 100644 --- a/crates/api-types/Cargo.toml +++ b/crates/api-types/Cargo.toml @@ -24,5 +24,3 @@ thiserror.workspace = true utoipa = { workspace = true, features = ["chrono"] } validator = { workspace = true, features = ["derive"] } webauthn-rs-proto.workspace = true - -[dev-dependencies] diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml new file mode 100644 index 00000000..4f3f62fa --- /dev/null +++ b/crates/core/Cargo.toml @@ -0,0 +1,62 @@ +[package] +name = "openstack-keystone-core" +description = "OpenStack Keystone service" +version = "0.1.1" +rust-version.workspace = true +edition.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true + +[dependencies] +async-trait.workspace = true +axum = { workspace = true } +base64.workspace = true +bcrypt = { workspace = true, features = ["alloc"] } +byteorder.workspace = true +bytes.workspace = true +chrono.workspace = true +config = { workspace = true, features = ["async", "ini"] } +derive_builder.workspace = true +eyre.workspace = true +fernet = { workspace = true, features = ["rustcrypto"] } +futures-util.workspace = true +itertools.workspace = true +jsonwebtoken = { version = "10.3", features = ["rust_crypto"] } +openstack-keystone-api-types = { version = "0.1", path = "../api-types/"} +mockall = { workspace = true, optional = true } +nix = { workspace = true, features = ["fs", "user"] } +rand.workspace = true +regex.workspace = true +reqwest = { workspace = true, features = ["json", "http2", "gzip", "deflate"] } +rmp.workspace = true +schemars.workspace = true +scopeguard.workspace = true +sea-orm.workspace = true +secrecy = { workspace = true, features = ["serde"] } +serde.workspace = true +serde_json.workspace = true +serde_urlencoded.workspace = true +tempfile.workspace = true +thiserror.workspace = true +tokio = { workspace = true , features = ["fs"]} +tracing.workspace = true +url = { workspace = true, features = ["serde"] } +url-macro.workspace = true +uuid = { workspace = true, features = ["v4"] } +validator = { workspace = true, features = ["derive"] } + +[dev-dependencies] +base64urlsafedata.workspace = true +httpmock = { version = "0.8", features = ["http2"] } +mockall.workspace = true +rstest.workspace = true +sea-orm = { workspace = true, features = ["mock" ]} +tempfile.workspace = true +tracing-test = { workspace = true, features = ["no-env-filter"] } +url.workspace = true + +[features] +default = [] +bench_internals = [] +mock = ["dep:mockall"] diff --git a/crates/core/README.md b/crates/core/README.md new file mode 100644 index 00000000..527cc844 --- /dev/null +++ b/crates/core/README.md @@ -0,0 +1,112 @@ +# OpenStack Keystone in Rust + +The legacy Keystone identity service (written in Python and maintained upstream +by OpenStack Foundation) has served the OpenStack ecosystem reliably for years. +It handles authentication, authorization, token issuance, service catalog, +project/tenant management, and federation services across thousands of +deployments. However, as we embarked on adding next-generation identity +features—such as native WebAuthn (“passkeys”), modern federation flows, direct +OIDC support, JWT login, workload authorization, restricted tokens and +service-accounts—it became clear that certain design and performance +limitations of the Python codebase would hamper efficient implementation of +these new features. + +Consequently, we initiated a project termed “Keystone-NG”: a Rust-based +component that augments rather than fully replaces the existing Keystone +service. The original plan was to implement only the new feature-set in Rust +and route those new API paths to the Rust component, while keeping the core +Python Keystone service in place for existing users and workflows. + +As development progressed, however, the breadth of new functionality (and the +opportunity to revisit some of the existing limitations) led to a partial +re-implementation of certain core identity flows in Rust. This allows us to +benefit from Rust's memory safety, concurrency model, performance, and modern +tooling, while still preserving the upstream Keystone Python service as the +canonical “master” identity service, routing only the new endpoints and +capabilities through the Rust component. + +In practice, this architecture means: + +- The upstream Python Keystone remains the main identity interface, preserving + backward compatibility, integration with other OpenStack services, existing + user workflows, catalogs, policies and plugins. + +- The Rust “Keystone-NG” component handles new functionality, specifically: + + - Native WebAuthN (passkeys) support for passwordless / phishing-resistant MFA + + - A reworked federation service, enabling modern identity brokering and + advanced federation semantics OIDC (OpenID Connect) Direct in Keystone, + enabling Keystone to act as an OIDC Provider or integrate with external + OIDC identity providers natively JWT login flows, enabling stateless, + compact tokens suitable for new micro-services, CLI, SDK, and + workload-to-workload scenarios + + - Workload Authorization, designed for service-to-service authorization in + cloud native contexts (not just human users) + + - Restricted Tokens and Service Accounts, which allow fine-grained, + limited‐scope credentials for automation, agents, and service accounts, + with explicit constraints and expiry + +By routing only the new flows through the Rust component we preserve the +stability and ecosystem compatibility of Keystone, while enabling a +forward-looking identity architecture. Over time, additional identity flows +may be migrated or refactored into the Rust component as needed, but our +current objective is to retain the existing Keystone Python implementation as +the trusted, mature baseline and incrementally build the “Keystone-NG” Rust +service as the complement. + +We believe this approach allows the best of both worlds: the trusted maturity +of Keystone's Python code-base, combined with the modern, high-safety, +high-performance capabilities of Rust where they matter most. + +## Documentation + +Project documentation can be found [here](https://openstack-experimental.github.io/keystone). +It is a work in progress. Target is to provide a comprehensive documentation of +the new functionality and provide missing insides to the python Keystone +functionality with Architecture Decision Records, Specs, Thread analysis and +many more. + +## Config + +It is supposed, that the configuration for the python Keystone can be used +without changes also for the rust implementation. + +## Api + OpenAPI + +OpenAPI are being built directly from the code to guarantee the documentation +matches the implementation. + +## Database + +Sea-ORM is being used to access database. PostgreSQL and MySQL are supported. +Functional tests [would] test the compatibility. + +## Load test + +A very brief load test is implemented in `loadtest` using `Goose` framework. +It generates test load by first incrementally increasing requests up to the +configured amount (defaults to count of the cpu cores), keeps the load for the +configured amount of time while measuring the response latency and the +throughput (RPS). + +For every PR load test suite is being executed. It is absolutely clear that the +Rust implementation currently misses certain things original Keystone doe, but +the gap is being closed over the time. However test shows difference of factor +**10-100** which is already remarkable. New tests will appear to have a more +thorough coverage of the exposed API. + +## Trying + +Trying Keystone (assuming you have the Rust build environment or you are in the +possession of the binary is as easy as `keystone -c etc/keystone.conf -vv` + +Alternatively you can try it with `docker compose -f docker-compose.yaml up`. + +## Talks + +Detailed introduction of the project was given as +* [ALASCA tech talk](https://www.youtube.com/watch?v=0Hx4Q22ZNFU) +* [OpenStack Summit 2025](https://www.youtube.com/watch?v=XOHYqE2HRw4&list=PLKqaoAnDyfgr91wN_12nwY321504Ctw1s&index=30) diff --git a/crates/core/src/api.rs b/crates/core/src/api.rs new file mode 100644 index 00000000..3f832e90 --- /dev/null +++ b/crates/core/src/api.rs @@ -0,0 +1,20 @@ +// 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 + +pub mod error; +pub mod types; +pub mod v3; +pub mod v4; + +pub use error::KeystoneApiError; diff --git a/crates/core/src/api/error.rs b/crates/core/src/api/error.rs new file mode 100644 index 00000000..bb825b9e --- /dev/null +++ b/crates/core/src/api/error.rs @@ -0,0 +1,374 @@ +// 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 +//! # Keystone API error. +use axum::{ + Json, + extract::rejection::JsonRejection, + http::StatusCode, + response::{IntoResponse, Response}, +}; +use serde_json::json; +use thiserror::Error; +use tracing::error; + +use crate::assignment::error::AssignmentProviderError; +use crate::auth::AuthenticationError; +use crate::catalog::error::CatalogProviderError; +use crate::error::BuilderError; +use crate::identity::error::IdentityProviderError; +use crate::policy::PolicyError; +use crate::resource::error::ResourceProviderError; +use crate::revoke::error::RevokeProviderError; +use crate::role::error::RoleProviderError; +use crate::token::error::TokenProviderError; + +/// Keystone API operation errors. +#[derive(Debug, Error)] +pub enum KeystoneApiError { + /// Selected authentication is forbidden. + #[error("changing current authentication scope is forbidden")] + AuthenticationRescopeForbidden, + + #[error("Attempted to authenticate with an unsupported method.")] + AuthMethodNotSupported, + + #[error("{0}.")] + BadRequest(String), + + /// Base64 decoding error. + #[error(transparent)] + Base64Decode(#[from] base64::DecodeError), + + #[error("conflict, resource already existing")] + Conflict(String), + + #[error("domain id or name must be present")] + DomainIdOrName, + + #[error("You are not authorized to perform the requested action.")] + Forbidden { + /// The source of the error. + #[source] + source: Box, + }, + + #[error("invalid header header")] + InvalidHeader, + + #[error("invalid token")] + InvalidToken, + + #[error(transparent)] + JsonExtractorRejection(#[from] JsonRejection), + + #[error("internal server error: {0}")] + InternalError(String), + + #[error("could not find {resource}: {identifier}")] + NotFound { + resource: String, + identifier: String, + }, + + /// Others. + #[error(transparent)] + Other(#[from] eyre::Report), + + #[error(transparent)] + Policy { + #[from] + source: PolicyError, + }, + + #[error("project id or name must be present")] + ProjectIdOrName, + + #[error("project domain must be present")] + ProjectDomain, + + /// Selected authentication is forbidden. + #[error("selected authentication is forbidden")] + SelectedAuthenticationForbidden, + + /// (de)serialization error. + #[error(transparent)] + Serde { + #[from] + source: serde_json::Error, + }, + + #[error("missing x-subject-token header")] + SubjectTokenMissing, + + #[error("The request you have made requires authentication.")] + UnauthorizedNoContext, + + #[error("{}", .context.clone().unwrap_or("The request you have made requires authentication.".to_string()))] + Unauthorized { + context: Option, + /// The source of the error. + #[source] + source: Box, + }, + + /// Request validation error. + #[error("request validation failed: {source}")] + Validator { + /// The source of the error. + #[from] + source: validator::ValidationErrors, + }, +} + +impl KeystoneApiError { + pub fn forbidden(error: E) -> Self + where + E: std::error::Error + Send + Sync + 'static, + { + Self::Forbidden { + source: Box::new(error), + } + } + + pub fn internal(error: E) -> Self + where + E: std::error::Error + Send + Sync + 'static, + { + Self::InternalError(error.to_string()) + //{ + // source: Box::new(error), + //} + } + + pub fn unauthorized(error: E, context: Option) -> Self + where + E: std::error::Error + Send + Sync + 'static, + C: Into, + { + Self::Unauthorized { + context: context.map(Into::into), + source: Box::new(error), + } + } +} + +impl IntoResponse for KeystoneApiError { + fn into_response(self) -> Response { + error!("Error happened during request processing: {:#?}", self); + + let status_code = match self { + KeystoneApiError::Conflict(_) => StatusCode::CONFLICT, + KeystoneApiError::NotFound { .. } => StatusCode::NOT_FOUND, + KeystoneApiError::BadRequest(..) => StatusCode::BAD_REQUEST, + KeystoneApiError::Unauthorized { .. } => StatusCode::UNAUTHORIZED, + KeystoneApiError::UnauthorizedNoContext => StatusCode::UNAUTHORIZED, + KeystoneApiError::Forbidden { .. } => StatusCode::FORBIDDEN, + KeystoneApiError::Policy { .. } => StatusCode::FORBIDDEN, + KeystoneApiError::SelectedAuthenticationForbidden + | KeystoneApiError::AuthenticationRescopeForbidden => StatusCode::BAD_REQUEST, + KeystoneApiError::InternalError(_) | KeystoneApiError::Other(..) => { + StatusCode::INTERNAL_SERVER_ERROR + } + _ => StatusCode::BAD_REQUEST, + }; + + ( + status_code, + Json(json!({"error": {"code": status_code.as_u16(), "message": self.to_string()}})), + ) + .into_response() + } +} + +impl From for KeystoneApiError { + fn from(value: AuthenticationError) -> Self { + match value { + AuthenticationError::DomainDisabled(..) => { + KeystoneApiError::unauthorized(value, None::) + } + AuthenticationError::ProjectDisabled(..) => { + KeystoneApiError::unauthorized(value, None::) + } + AuthenticationError::StructBuilder { source } => { + KeystoneApiError::InternalError(source.to_string()) + } + AuthenticationError::UserDisabled(ref user_id) => { + let uid = user_id.clone(); + KeystoneApiError::unauthorized( + value, + Some(format!("The account is disabled for the user: {uid}")), + ) + } + AuthenticationError::UserLocked(ref user_id) => { + let uid = user_id.clone(); + KeystoneApiError::unauthorized( + value, + Some(format!("The account is locked for the user: {uid}")), + ) + } + AuthenticationError::UserPasswordExpired(ref user_id) => { + let uid = user_id.clone(); + KeystoneApiError::unauthorized( + value, + Some(format!( + "The password is expired and need to be changed for user: {uid}" + )), + ) + } + AuthenticationError::UserNameOrPasswordWrong => KeystoneApiError::unauthorized( + value, + Some("Invalid username or password".to_string()), + ), + AuthenticationError::TokenRenewalForbidden => { + KeystoneApiError::SelectedAuthenticationForbidden + } + AuthenticationError::Unauthorized => { + KeystoneApiError::unauthorized(value, None::) + } + } + } +} + +impl From for KeystoneApiError { + fn from(source: AssignmentProviderError) -> Self { + match source { + AssignmentProviderError::AssignmentNotFound(x) => Self::NotFound { + resource: "assignment".into(), + identifier: x, + }, + AssignmentProviderError::RoleNotFound(x) => Self::NotFound { + resource: "role".into(), + identifier: x, + }, + ref err @ AssignmentProviderError::Conflict(..) => Self::Conflict(err.to_string()), + ref err @ AssignmentProviderError::Validation { .. } => { + Self::BadRequest(err.to_string()) + } + other => Self::InternalError(other.to_string()), + } + } +} + +impl From for KeystoneApiError { + fn from(value: crate::error::BuilderError) -> Self { + Self::InternalError(value.to_string()) + } +} + +impl From for KeystoneApiError { + fn from(value: openstack_keystone_api_types::error::BuilderError) -> Self { + Self::InternalError(value.to_string()) + } +} + +impl From for KeystoneApiError { + fn from(source: RoleProviderError) -> Self { + match source { + RoleProviderError::RoleNotFound(x) => Self::NotFound { + resource: "role".into(), + identifier: x, + }, + ref err @ RoleProviderError::Conflict(..) => Self::Conflict(err.to_string()), + ref err @ RoleProviderError::Validation { .. } => Self::BadRequest(err.to_string()), + other => Self::InternalError(other.to_string()), + } + } +} + +impl From for KeystoneApiError { + fn from(value: serde_urlencoded::ser::Error) -> Self { + Self::InternalError(value.to_string()) + } +} + +impl From for KeystoneApiError { + fn from(value: url::ParseError) -> Self { + Self::InternalError(value.to_string()) + } +} + +impl From for KeystoneApiError { + fn from(value: CatalogProviderError) -> Self { + match value { + ref err @ CatalogProviderError::Conflict(..) => Self::Conflict(err.to_string()), + other => Self::InternalError(other.to_string()), + } + } +} + +impl From for KeystoneApiError { + fn from(value: IdentityProviderError) -> Self { + match value { + IdentityProviderError::Authentication { source } => source.into(), + IdentityProviderError::UserNotFound(x) => Self::NotFound { + resource: "user".into(), + identifier: x, + }, + IdentityProviderError::GroupNotFound(x) => Self::NotFound { + resource: "group".into(), + identifier: x, + }, + other => Self::InternalError(other.to_string()), + } + } +} + +impl From for KeystoneApiError { + fn from(value: ResourceProviderError) -> Self { + match value { + ref err @ ResourceProviderError::Conflict(..) => Self::BadRequest(err.to_string()), + ResourceProviderError::DomainNotFound(x) => Self::NotFound { + resource: "domain".into(), + identifier: x, + }, + other => Self::InternalError(other.to_string()), + } + } +} + +impl From for KeystoneApiError { + fn from(value: RevokeProviderError) -> Self { + match value { + ref err @ RevokeProviderError::Conflict(..) => Self::BadRequest(err.to_string()), + other => Self::InternalError(other.to_string()), + } + } +} + +impl From for KeystoneApiError { + fn from(value: TokenProviderError) -> Self { + match value { + TokenProviderError::Authentication(source) => source.into(), + TokenProviderError::DomainDisabled(x) => Self::NotFound { + resource: "domain".into(), + identifier: x, + }, + TokenProviderError::TokenRestrictionNotFound(x) => Self::NotFound { + resource: "token restriction".into(), + identifier: x, + }, + TokenProviderError::ProjectDisabled(x) => Self::NotFound { + resource: "project".into(), + identifier: x, + }, + other => Self::InternalError(other.to_string()), + } + } +} + +impl From for KeystoneApiError { + fn from(value: uuid::Error) -> Self { + Self::InternalError(value.to_string()) + } +} diff --git a/crates/core/src/api/types.rs b/crates/core/src/api/types.rs new file mode 100644 index 00000000..8850fe0d --- /dev/null +++ b/crates/core/src/api/types.rs @@ -0,0 +1,134 @@ +// 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 +//! Keystone API types. +pub use openstack_keystone_api_types::Link; +pub use openstack_keystone_api_types::catalog::*; +pub use openstack_keystone_api_types::scope::*; +pub use openstack_keystone_api_types::version::*; + +use crate::catalog::types::Endpoint as ProviderEndpoint; +use crate::common::types as provider_types; +use crate::resource::types as resource_provider_types; + +//impl From<(Service, Vec)> for CatalogService { +// fn from(value: (Service, Vec)) -> Self { +// Self { +// id: value.0.id.clone(), +// name: value.0.name.clone(), +// r#type: value.0.r#type, +// endpoints: value.1.into_iter().map(Into::into).collect(), +// } +// } +//} + +impl From for Endpoint { + fn from(value: ProviderEndpoint) -> Self { + Self { + id: value.id.clone(), + interface: value.interface.clone(), + url: value.url.clone(), + region: value.region_id.clone(), + region_id: value.region_id.clone(), + } + } +} + +//impl From)>> for Catalog { +// fn from(value: Vec<(Service, Vec)>) -> Self { +// Self( +// value +// .into_iter() +// .map(|(srv, eps)| (srv, eps).into()) +// .collect(), +// ) +// } +//} + +impl From for Domain { + fn from(value: resource_provider_types::Domain) -> Self { + Self { + id: Some(value.id), + name: Some(value.name), + } + } +} + +impl From<&resource_provider_types::Domain> for Domain { + fn from(value: &resource_provider_types::Domain) -> Self { + Self { + id: Some(value.id.clone()), + name: Some(value.name.clone()), + } + } +} + +impl From for provider_types::Domain { + fn from(value: Domain) -> Self { + Self { + id: value.id, + name: value.name, + } + } +} + +impl From for Domain { + fn from(value: provider_types::Domain) -> Self { + Self { + id: value.id, + name: value.name, + } + } +} + +impl From for provider_types::Project { + fn from(value: ScopeProject) -> Self { + Self { + id: value.id, + name: value.name, + domain: value.domain.map(Into::into), + } + } +} + +impl From for ScopeProject { + fn from(value: provider_types::Project) -> Self { + Self { + id: value.id, + name: value.name, + domain: value.domain.map(Into::into), + } + } +} + +impl From<&provider_types::Project> for ScopeProject { + fn from(value: &provider_types::Project) -> Self { + Self::from(value.clone()) + } +} + +impl From for provider_types::System { + fn from(value: System) -> Self { + Self { all: value.all } + } +} + +impl From for provider_types::Scope { + fn from(value: Scope) -> Self { + match value { + Scope::Project(scope) => Self::Project(scope.into()), + Scope::Domain(scope) => Self::Domain(scope.into()), + Scope::System(scope) => Self::System(scope.into()), + } + } +} diff --git a/crates/core/src/api/v3.rs b/crates/core/src/api/v3.rs new file mode 100644 index 00000000..fd263e7b --- /dev/null +++ b/crates/core/src/api/v3.rs @@ -0,0 +1,20 @@ +// 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 + +pub mod auth; +pub mod group; +pub mod project; +pub mod role; +pub mod role_assignment; +pub mod user; diff --git a/crates/core/src/api/v3/auth.rs b/crates/core/src/api/v3/auth.rs new file mode 100644 index 00000000..82570910 --- /dev/null +++ b/crates/core/src/api/v3/auth.rs @@ -0,0 +1,58 @@ +// 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 + +pub use openstack_keystone_api_types::v3::auth::token::*; + +use crate::error::BuilderError; +use crate::identity::types as identity_types; +use crate::token::Token as BackendToken; + +impl TryFrom for identity_types::UserPasswordAuthRequest { + type Error = BuilderError; + + fn try_from(value: UserPassword) -> Result { + let mut upa = identity_types::UserPasswordAuthRequestBuilder::default(); + if let Some(id) = &value.id { + upa.id(id); + } + if let Some(name) = &value.name { + upa.name(name); + } + if let Some(domain) = &value.domain { + let mut domain_builder = identity_types::DomainBuilder::default(); + if let Some(id) = &domain.id { + domain_builder.id(id); + } + if let Some(name) = &domain.name { + domain_builder.name(name); + } + upa.domain(domain_builder.build()?); + } + upa.password(value.password.clone()); + upa.build() + } +} + +impl TryFrom<&BackendToken> for Token { + type Error = openstack_keystone_api_types::error::BuilderError; + + fn try_from(value: &BackendToken) -> Result { + let mut token = TokenBuilder::default(); + token.user(UserBuilder::default().id(value.user_id()).build()?); + token.methods(value.methods().clone()); + token.audit_ids(value.audit_ids().clone()); + token.expires_at(*value.expires_at()); + token.build() + } +} diff --git a/crates/core/src/api/v3/group.rs b/crates/core/src/api/v3/group.rs new file mode 100644 index 00000000..cfc059da --- /dev/null +++ b/crates/core/src/api/v3/group.rs @@ -0,0 +1,69 @@ +// 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 + +use axum::{ + Json, + http::StatusCode, + response::{IntoResponse, Response}, +}; + +pub use openstack_keystone_api_types::v3::group::*; + +use crate::identity::types; + +impl From for Group { + fn from(value: types::Group) -> Self { + Self { + id: value.id, + domain_id: value.domain_id, + name: value.name, + description: value.description, + extra: value.extra, + } + } +} + +impl From for types::GroupCreate { + fn from(value: GroupCreateRequest) -> Self { + let group = value.group; + Self { + id: None, + name: group.name, + domain_id: group.domain_id, + extra: group.extra, + description: group.description, + } + } +} + +impl IntoResponse for types::Group { + fn into_response(self) -> Response { + ( + StatusCode::OK, + Json(GroupResponse { + group: Group::from(self), + }), + ) + .into_response() + } +} + +impl From for types::GroupListParameters { + fn from(value: GroupListParameters) -> Self { + Self { + domain_id: value.domain_id, + name: value.name, + } + } +} diff --git a/crates/core/src/api/v3/project.rs b/crates/core/src/api/v3/project.rs new file mode 100644 index 00000000..7072587f --- /dev/null +++ b/crates/core/src/api/v3/project.rs @@ -0,0 +1,70 @@ +// 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 +//! Project API types. + +pub use openstack_keystone_api_types::v3::project::*; + +use crate::resource::types as provider_types; + +impl From for ProjectShort { + fn from(value: provider_types::Project) -> Self { + Self { + domain_id: value.domain_id, + enabled: value.enabled, + id: value.id, + name: value.name, + } + } +} + +impl From<&provider_types::Project> for ProjectShort { + fn from(value: &provider_types::Project) -> Self { + Self { + domain_id: value.domain_id.clone(), + enabled: value.enabled, + id: value.id.clone(), + name: value.name.clone(), + } + } +} + +impl From for Project { + fn from(value: provider_types::Project) -> Self { + Self { + description: value.description, + domain_id: value.domain_id, + enabled: value.enabled, + extra: value.extra, + id: value.id, + is_domain: value.is_domain, + name: value.name, + parent_id: value.parent_id, + } + } +} + +impl From for provider_types::ProjectCreate { + fn from(value: ProjectCreate) -> Self { + Self { + description: value.description, + domain_id: value.domain_id, + enabled: value.enabled, + extra: value.extra, + id: None, + is_domain: value.is_domain, + name: value.name, + parent_id: value.parent_id, + } + } +} diff --git a/crates/core/src/api/v3/role.rs b/crates/core/src/api/v3/role.rs new file mode 100644 index 00000000..9eefd2ba --- /dev/null +++ b/crates/core/src/api/v3/role.rs @@ -0,0 +1,78 @@ +// 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 + +use axum::{ + Json, + http::StatusCode, + response::{IntoResponse, Response}, +}; + +pub use openstack_keystone_api_types::v3::role::*; + +use crate::role::types; + +impl From for Role { + fn from(value: types::Role) -> Self { + Self { + id: value.id, + domain_id: value.domain_id, + name: value.name, + description: value.description, + extra: value.extra, + } + } +} + +impl From for RoleRef { + fn from(value: types::RoleRef) -> Self { + Self { + id: value.id, + domain_id: value.domain_id, + name: value.name.unwrap_or_default(), + } + } +} + +impl IntoResponse for types::Role { + fn into_response(self) -> Response { + ( + StatusCode::OK, + Json(RoleResponse { + role: Role::from(self), + }), + ) + .into_response() + } +} + +impl From for types::RoleListParameters { + fn from(value: RoleListParameters) -> Self { + Self { + domain_id: Some(value.domain_id), + name: value.name, + } + } +} + +impl From for types::RoleCreate { + fn from(value: RoleCreate) -> Self { + Self { + description: value.description, + domain_id: value.domain_id, + extra: value.extra, + id: None, + name: value.name, + } + } +} diff --git a/crates/core/src/api/v3/role_assignment.rs b/crates/core/src/api/v3/role_assignment.rs new file mode 100644 index 00000000..05677997 --- /dev/null +++ b/crates/core/src/api/v3/role_assignment.rs @@ -0,0 +1,205 @@ +// 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 + +pub use openstack_keystone_api_types::v3::role_assignment::*; + +use crate::api::error::KeystoneApiError; +use crate::assignment::types; + +impl TryFrom for Assignment { + type Error = KeystoneApiError; + + fn try_from(value: types::Assignment) -> Result { + let mut builder = AssignmentBuilder::default(); + builder.role(Role { + id: value.role_id, + name: value.role_name, + }); + match value.r#type { + types::AssignmentType::GroupDomain => { + builder.group(Group { id: value.actor_id }); + builder.scope(Scope::Domain(Domain { + id: value.target_id, + })); + } + types::AssignmentType::GroupProject => { + builder.group(Group { id: value.actor_id }); + builder.scope(Scope::Project(Project { + id: value.target_id, + })); + } + types::AssignmentType::UserDomain => { + builder.user(User { id: value.actor_id }); + builder.scope(Scope::Domain(Domain { + id: value.target_id, + })); + } + types::AssignmentType::UserProject => { + builder.user(User { id: value.actor_id }); + builder.scope(Scope::Project(Project { + id: value.target_id, + })); + } + types::AssignmentType::UserSystem => { + builder.user(User { id: value.actor_id }); + builder.scope(Scope::System(System { + id: value.target_id, + })); + } + types::AssignmentType::GroupSystem => { + builder.group(Group { id: value.actor_id }); + builder.scope(Scope::System(System { + id: value.target_id, + })); + } + } + Ok(builder.build()?) + } +} + +impl TryFrom for types::RoleAssignmentListParameters { + type Error = KeystoneApiError; + + fn try_from(value: RoleAssignmentListParameters) -> Result { + let mut builder = types::RoleAssignmentListParametersBuilder::default(); + // Filter by role + if let Some(val) = &value.role_id { + builder.role_id(val); + } + + // Filter by actor + if let Some(val) = &value.user_id { + builder.user_id(val); + } else if let Some(val) = &value.group_id { + builder.group_id(val); + } + + // Filter by target + if let Some(val) = &value.project_id { + builder.project_id(val); + } else if let Some(val) = &value.domain_id { + builder.domain_id(val); + } + + if let Some(val) = value.effective { + builder.effective(val); + } + if let Some(val) = value.include_names { + builder.include_names(val); + } + Ok(builder.build()?) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::assignment::types; + + #[test] + fn test_assignment_conversion() { + assert_eq!( + Assignment { + role: Role { + id: "role".into(), + name: Some("role_name".into()) + }, + user: Some(User { id: "actor".into() }), + scope: Scope::Project(Project { + id: "target".into() + }), + group: None, + }, + Assignment::try_from(types::Assignment { + role_id: "role".into(), + role_name: Some("role_name".into()), + actor_id: "actor".into(), + target_id: "target".into(), + r#type: types::AssignmentType::UserProject, + inherited: false, + implied_via: None, + }) + .unwrap() + ); + assert_eq!( + Assignment { + role: Role { + id: "role".into(), + name: None + }, + user: Some(User { id: "actor".into() }), + scope: Scope::Domain(Domain { + id: "target".into() + }), + group: None, + }, + Assignment::try_from(types::Assignment { + role_id: "role".into(), + role_name: None, + actor_id: "actor".into(), + target_id: "target".into(), + r#type: types::AssignmentType::UserDomain, + inherited: false, + implied_via: None, + }) + .unwrap() + ); + assert_eq!( + Assignment { + role: Role { + id: "role".into(), + name: None + }, + group: Some(Group { id: "actor".into() }), + scope: Scope::Project(Project { + id: "target".into() + }), + user: None, + }, + Assignment::try_from(types::Assignment { + role_id: "role".into(), + role_name: None, + actor_id: "actor".into(), + target_id: "target".into(), + r#type: types::AssignmentType::GroupProject, + inherited: false, + implied_via: None, + }) + .unwrap() + ); + assert_eq!( + Assignment { + role: Role { + id: "role".into(), + name: None + }, + group: Some(Group { id: "actor".into() }), + scope: Scope::Domain(Domain { + id: "target".into() + }), + user: None, + }, + Assignment::try_from(types::Assignment { + role_id: "role".into(), + role_name: None, + actor_id: "actor".into(), + target_id: "target".into(), + r#type: types::AssignmentType::GroupDomain, + inherited: false, + implied_via: None, + }) + .unwrap() + ); + } +} diff --git a/crates/core/src/api/v3/user.rs b/crates/core/src/api/v3/user.rs new file mode 100644 index 00000000..44b6919b --- /dev/null +++ b/crates/core/src/api/v3/user.rs @@ -0,0 +1,141 @@ +// 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 + +use axum::{ + Json, + http::StatusCode, + response::{IntoResponse, Response}, +}; + +pub use openstack_keystone_api_types::v3::user::*; + +use crate::identity::types as identity_types; + +impl From for UserOptions { + fn from(value: identity_types::UserOptions) -> Self { + Self { + ignore_change_password_upon_first_use: value.ignore_change_password_upon_first_use, + ignore_password_expiry: value.ignore_password_expiry, + ignore_lockout_failure_attempts: value.ignore_lockout_failure_attempts, + lock_password: value.lock_password, + ignore_user_inactivity: value.ignore_user_inactivity, + multi_factor_auth_rules: value.multi_factor_auth_rules, + multi_factor_auth_enabled: value.multi_factor_auth_enabled, + } + } +} + +impl From for identity_types::UserOptions { + fn from(value: UserOptions) -> Self { + Self { + ignore_change_password_upon_first_use: value.ignore_change_password_upon_first_use, + ignore_password_expiry: value.ignore_password_expiry, + ignore_lockout_failure_attempts: value.ignore_lockout_failure_attempts, + lock_password: value.lock_password, + ignore_user_inactivity: value.ignore_user_inactivity, + multi_factor_auth_rules: value.multi_factor_auth_rules, + multi_factor_auth_enabled: value.multi_factor_auth_enabled, + is_service_account: None, + } + } +} + +impl From for User { + fn from(value: identity_types::UserResponse) -> Self { + let opts: UserOptions = value.options.clone().into(); + // We only want to see user options if there is at least 1 option set + let opts = if opts.ignore_change_password_upon_first_use.is_some() + || opts.ignore_password_expiry.is_some() + || opts.ignore_lockout_failure_attempts.is_some() + || opts.lock_password.is_some() + || opts.ignore_user_inactivity.is_some() + || opts.multi_factor_auth_rules.is_some() + || opts.multi_factor_auth_enabled.is_some() + { + Some(opts) + } else { + None + }; + Self { + default_project_id: value.default_project_id, + domain_id: value.domain_id, + enabled: value.enabled, + extra: value.extra, + federated: value + .federated + .map(|val| val.into_iter().map(Into::into).collect()), + id: value.id, + name: value.name, + options: opts, + password_expires_at: value.password_expires_at, + } + } +} + +impl From for identity_types::UserCreate { + fn from(value: UserCreateRequest) -> Self { + let user = value.user; + Self { + default_project_id: user.default_project_id, + domain_id: user.domain_id, + enabled: Some(user.enabled), + extra: user.extra, + id: None, + federated: None, + name: user.name, + options: user.options.map(Into::into), + password: user.password, + } + } +} + +impl From for Federation { + fn from(value: identity_types::Federation) -> Self { + Self { + idp_id: value.idp_id, + protocols: value.protocols.into_iter().map(Into::into).collect(), + } + } +} +impl From for FederationProtocol { + fn from(value: identity_types::FederationProtocol) -> Self { + Self { + protocol_id: value.protocol_id, + unique_id: value.unique_id, + } + } +} + +impl IntoResponse for identity_types::UserResponse { + fn into_response(self) -> Response { + ( + StatusCode::OK, + Json(UserResponse { + user: User::from(self), + }), + ) + .into_response() + } +} + +impl From for identity_types::UserListParameters { + fn from(value: UserListParameters) -> Self { + Self { + domain_id: value.domain_id, + name: value.name, + unique_id: value.unique_id, + ..Default::default() // limit: value.limit, + } + } +} diff --git a/crates/core/src/api/v4.rs b/crates/core/src/api/v4.rs new file mode 100644 index 00000000..f05b8525 --- /dev/null +++ b/crates/core/src/api/v4.rs @@ -0,0 +1,16 @@ +// 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 + +pub mod auth; +pub mod token_restriction; diff --git a/crates/core/src/api/v4/auth.rs b/crates/core/src/api/v4/auth.rs new file mode 100644 index 00000000..3bb01d4e --- /dev/null +++ b/crates/core/src/api/v4/auth.rs @@ -0,0 +1,58 @@ +// 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 + +pub use openstack_keystone_api_types::v4::auth::token::*; + +use crate::error::BuilderError; +use crate::identity::types as identity_types; +use crate::token::Token as BackendToken; + +impl TryFrom for identity_types::UserPasswordAuthRequest { + type Error = BuilderError; + + fn try_from(value: UserPassword) -> Result { + let mut upa = identity_types::UserPasswordAuthRequestBuilder::default(); + if let Some(id) = &value.id { + upa.id(id); + } + if let Some(name) = &value.name { + upa.name(name); + } + if let Some(domain) = &value.domain { + let mut domain_builder = identity_types::DomainBuilder::default(); + if let Some(id) = &domain.id { + domain_builder.id(id); + } + if let Some(name) = &domain.name { + domain_builder.name(name); + } + upa.domain(domain_builder.build()?); + } + upa.password(value.password.clone()); + upa.build() + } +} + +impl TryFrom<&BackendToken> for Token { + type Error = openstack_keystone_api_types::error::BuilderError; + + fn try_from(value: &BackendToken) -> Result { + let mut token = TokenBuilder::default(); + token.user(UserBuilder::default().id(value.user_id()).build()?); + token.methods(value.methods().clone()); + token.audit_ids(value.audit_ids().clone()); + token.expires_at(*value.expires_at()); + token.build() + } +} diff --git a/crates/core/src/api/v4/token_restriction.rs b/crates/core/src/api/v4/token_restriction.rs new file mode 100644 index 00000000..54ba4cde --- /dev/null +++ b/crates/core/src/api/v4/token_restriction.rs @@ -0,0 +1,110 @@ +// 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 +//! Token restriction types. +use axum::{ + Json, + http::StatusCode, + response::{IntoResponse, Response}, +}; + +pub use openstack_keystone_api_types::v4::token_restriction::*; + +use crate::token::types::{ + self as types, TokenRestriction as ProviderTokenRestriction, + TokenRestrictionCreate as ProviderTokenRestrictionCreate, + TokenRestrictionUpdate as ProviderTokenRestrictionUpdate, +}; + +impl From for types::TokenRestrictionListParameters { + fn from(value: TokenRestrictionListParameters) -> Self { + Self { + domain_id: value.domain_id, + user_id: value.user_id, + project_id: value.project_id, + } + } +} + +impl From for TokenRestriction { + fn from(value: ProviderTokenRestriction) -> Self { + Self { + allow_rescope: value.allow_rescope, + allow_renew: value.allow_renew, + id: value.id, + domain_id: value.domain_id, + project_id: value.project_id, + user_id: value.user_id, + roles: value + .roles + .map(|roles| roles.into_iter().map(Into::into).collect()) + .unwrap_or_default(), + } + } +} + +impl From for ProviderTokenRestrictionCreate { + fn from(value: TokenRestrictionCreateRequest) -> Self { + Self { + allow_rescope: value.restriction.allow_rescope, + allow_renew: value.restriction.allow_renew, + id: String::new(), + domain_id: value.restriction.domain_id, + project_id: value.restriction.project_id, + user_id: value.restriction.user_id, + role_ids: value + .restriction + .roles + .into_iter() + .map(|role| role.id) + .collect(), + } + } +} + +impl From for ProviderTokenRestrictionUpdate { + fn from(value: TokenRestrictionUpdateRequest) -> Self { + Self { + allow_rescope: value.restriction.allow_rescope, + allow_renew: value.restriction.allow_renew, + project_id: value.restriction.project_id, + user_id: value.restriction.user_id, + role_ids: value + .restriction + .roles + .map(|roles| roles.into_iter().map(|role| role.id).collect()), + } + } +} + +//impl From for RoleRef { +// fn from(value: crate::role::types::RoleRef) -> Self { +// Self { +// id: value.id, +// name: value.name.unwrap_or_default(), +// domain_id: value.domain_id, +// } +// } +//} + +impl IntoResponse for ProviderTokenRestriction { + fn into_response(self) -> Response { + ( + StatusCode::OK, + Json(TokenRestrictionResponse { + restriction: TokenRestriction::from(self), + }), + ) + .into_response() + } +} diff --git a/crates/core/src/application_credential/backend.rs b/crates/core/src/application_credential/backend.rs new file mode 100644 index 00000000..a8a06d7c --- /dev/null +++ b/crates/core/src/application_credential/backend.rs @@ -0,0 +1,46 @@ +// 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 +//! # Application credential provider backend + +use async_trait::async_trait; + +use crate::application_credential::ApplicationCredentialProviderError; +use crate::application_credential::types::*; +use crate::keystone::ServiceState; + +/// Application Credential backend driver interface. +#[cfg_attr(test, mockall::automock)] +#[async_trait] +pub trait ApplicationCredentialBackend: Send + Sync { + /// Create a new application credential. + async fn create_application_credential( + &self, + state: &ServiceState, + rec: ApplicationCredentialCreate, + ) -> Result; + + /// Get a single application credential by ID. + async fn get_application_credential<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result, ApplicationCredentialProviderError>; + + /// List application credentials. + async fn list_application_credentials( + &self, + state: &ServiceState, + params: &ApplicationCredentialListParameters, + ) -> Result, ApplicationCredentialProviderError>; +} diff --git a/crates/keystone/src/application_credential/error.rs b/crates/core/src/application_credential/error.rs similarity index 96% rename from crates/keystone/src/application_credential/error.rs rename to crates/core/src/application_credential/error.rs index 31a29207..caf5c313 100644 --- a/crates/keystone/src/application_credential/error.rs +++ b/crates/core/src/application_credential/error.rs @@ -72,7 +72,7 @@ pub enum ApplicationCredentialProviderError { source: BuilderError, }, /// Unsupported driver. - #[error("unsupported driver {0}")] + #[error("unsupported driver `{0}` for the application credential provider")] UnsupportedDriver(String), /// Request validation error. diff --git a/crates/keystone/src/application_credential/mock.rs b/crates/core/src/application_credential/mock.rs similarity index 87% rename from crates/keystone/src/application_credential/mock.rs rename to crates/core/src/application_credential/mock.rs index 942d5ab4..a8763731 100644 --- a/crates/keystone/src/application_credential/mock.rs +++ b/crates/core/src/application_credential/mock.rs @@ -17,14 +17,10 @@ use mockall::mock; use crate::application_credential::ApplicationCredentialApi; use crate::application_credential::ApplicationCredentialProviderError; use crate::application_credential::types::*; -use crate::config::Config; use crate::keystone::ServiceState; -use crate::plugin_manager::PluginManager; mock! { - pub ApplicationCredentialProvider { - pub fn new(cfg: &Config, plugin_manager: &PluginManager) -> Result; - } + pub ApplicationCredentialProvider {} #[async_trait] impl ApplicationCredentialApi for ApplicationCredentialProvider { diff --git a/crates/core/src/application_credential/mod.rs b/crates/core/src/application_credential/mod.rs new file mode 100644 index 00000000..88a0946f --- /dev/null +++ b/crates/core/src/application_credential/mod.rs @@ -0,0 +1,195 @@ +// 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 + +//! # Application credentials provider +//! +//! Application credentials provide a way to delegate a user's authorization to +//! an application without sharing the user's password authentication. This is a +//! useful security measure, especially for situations where the user's +//! identification is provided by an external source, such as LDAP or a +//! single-sign-on service. Instead of storing user passwords in config files, a +//! user creates an application credential for a specific project, with all or a +//! subset of the role assignments they have on that project, and then stores +//! the application credential identifier and secret in the config file. +//! +//! Multiple application credentials may be active at once, so you can easily +//! rotate application credentials by creating a second one, converting your +//! applications to use it one by one, and finally deleting the first one. +//! +//! Application credentials are limited by the lifespan of the user that created +//! them. If the user is deleted, disabled, or loses a role assignment on a +//! project, the application credential is deleted. +//! +//! Application credentials can have their privileges limited in two ways. +//! First, the owner may specify a subset of their own roles that the +//! application credential may assume when getting a token for a project. For +//! example, if a user has the member role on a project, they also have the +//! implied role reader and can grant the application credential only the reader +//! role for the project: +//! +//! ```yaml +//! "roles": [ +//! {"name": "reader"} +//! ] +//! ``` +//! +//! Users also have the option of delegating more fine-grained access control to +//! their application credentials by using access rules. For example, to create +//! an application credential that is constricted to creating servers in nova, +//! the user can add the following access rules: +//! +//! ```yaml +//! "access_rules": [ +//! { +//! "path": "/v2.1/servers", +//! "method": "POST", +//! "service": "compute" +//! } +//! ] +//! ``` +//! +//! The "path" attribute of application credential access rules uses a wildcard +//! syntax to make it more flexible. For example, to create an application +//! credential that is constricted to listing server IP addresses, you could use +//! either of the following access rules: +//! +//! ```yaml +//! "access_rules": [ +//! { +//! "path": "/v2.1/servers/*/ips", +//! "method": "GET", +//! "service": "compute" +//! } +//! ] +//! ``` +//! +//! or equivalently: +//! +//! ```yaml +//! "access_rules": [ +//! { +//! "path": "/v2.1/servers/{server_id}/ips", +//! "method": "GET", +//! "service": "compute" +//! } +//! ] +//! ``` +//! +//! In both cases, a request path containing any server ID will match the access +//! rule. For even more flexibility, the recursive wildcard ** indicates that +//! request paths containing any number of / will be matched. For example: +//! +//! ```yaml +//! "access_rules": [ +//! { +//! "path": "/v2.1/**", +//! "method": "GET", +//! "service": "compute" +//! } +//! ] +//! ``` +//! +//! will match any nova API for version 2.1. +//! +//! An access rule created for one application credential can be re-used by +//! providing its ID to another application credential, for example: +//! +//! ```yaml +//! "access_rules": [ +//! { +//! "id": "abcdef" +//! } +//! ] +//! ``` +use async_trait::async_trait; + +pub mod backend; +pub mod error; +#[cfg(any(test, feature = "mock"))] +mod mock; +pub mod service; +pub mod types; + +use crate::config::Config; +use crate::keystone::ServiceState; +use crate::plugin_manager::PluginManagerApi; +use service::ApplicationCredentialService; +use types::*; + +pub use error::ApplicationCredentialProviderError; +#[cfg(any(test, feature = "mock"))] +pub use mock::MockApplicationCredentialProvider; +pub use types::ApplicationCredentialApi; + +/// Application Credential Provider. +pub enum ApplicationCredentialProvider { + Service(ApplicationCredentialService), + #[cfg(any(test, feature = "mock"))] + Mock(MockApplicationCredentialProvider), +} + +impl ApplicationCredentialProvider { + pub fn new( + config: &Config, + plugin_manager: &P, + ) -> Result { + Ok(Self::Service(ApplicationCredentialService::new( + config, + plugin_manager, + )?)) + } +} + +#[async_trait] +impl ApplicationCredentialApi for ApplicationCredentialProvider { + /// Create a new application credential. + async fn create_application_credential( + &self, + state: &ServiceState, + rec: ApplicationCredentialCreate, + ) -> Result { + match self { + Self::Service(provider) => provider.create_application_credential(state, rec).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.create_application_credential(state, rec).await, + } + } + + /// Get a single application credential by ID. + #[tracing::instrument(level = "info", skip(self, state))] + async fn get_application_credential<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result, ApplicationCredentialProviderError> { + match self { + Self::Service(provider) => provider.get_application_credential(state, id).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.get_application_credential(state, id).await, + } + } + + /// List application credentials. + #[tracing::instrument(level = "info", skip(self, state))] + async fn list_application_credentials( + &self, + state: &ServiceState, + params: &ApplicationCredentialListParameters, + ) -> Result, ApplicationCredentialProviderError> { + match self { + Self::Service(provider) => provider.list_application_credentials(state, params).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.list_application_credentials(state, params).await, + } + } +} diff --git a/crates/core/src/application_credential/service.rs b/crates/core/src/application_credential/service.rs new file mode 100644 index 00000000..bc69760f --- /dev/null +++ b/crates/core/src/application_credential/service.rs @@ -0,0 +1,166 @@ +// 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 + +//! # Application credentials provider +use std::collections::BTreeMap; +use std::sync::Arc; + +use async_trait::async_trait; +use base64::{Engine as _, engine::general_purpose}; +use rand::{RngExt, rng}; +use secrecy::SecretString; +use uuid::Uuid; +use validator::Validate; + +use crate::application_credential::{ + ApplicationCredentialProviderError, backend::ApplicationCredentialBackend, types::*, +}; +use crate::config::Config; +use crate::keystone::ServiceState; +use crate::plugin_manager::PluginManagerApi; +use crate::role::{ + RoleApi, + types::{Role, RoleListParameters}, +}; + +/// Application Credential Provider. +pub struct ApplicationCredentialService { + backend_driver: Arc, +} + +impl ApplicationCredentialService { + pub fn new( + config: &Config, + plugin_manager: &P, + ) -> Result { + let backend_driver = plugin_manager + .get_application_credential_backend(config.application_credential.driver.clone())? + .clone(); + Ok(Self { backend_driver }) + } +} + +#[async_trait] +impl ApplicationCredentialApi for ApplicationCredentialService { + /// Create a new application credential. + async fn create_application_credential( + &self, + state: &ServiceState, + rec: ApplicationCredentialCreate, + ) -> Result { + rec.validate()?; + // TODO: Check app creds count + let mut new_rec = rec; + if new_rec.id.is_none() { + new_rec.id = Some(Uuid::new_v4().simple().to_string()); + } + if let Some(ref mut rules) = new_rec.access_rules { + for rule in rules { + if rule.id.is_none() { + rule.id = Some(Uuid::new_v4().simple().to_string()); + } + } + } + if new_rec.secret.is_none() { + new_rec.secret = Some(generate_secret()); + } + self.backend_driver + .create_application_credential(state, new_rec) + .await + } + + /// Get a single application credential by ID. + #[tracing::instrument(level = "info", skip(self, state))] + async fn get_application_credential<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result, ApplicationCredentialProviderError> { + if let Some(mut app_cred) = self + .backend_driver + .get_application_credential(state, id) + .await? + { + let roles: BTreeMap = state + .provider + .get_role_provider() + .list_roles(state, &RoleListParameters::default()) + .await? + .into_iter() + .map(|x| (x.id.clone(), x)) + .collect(); + for cred_role in app_cred.roles.iter_mut() { + if let Some(role) = roles.get(&cred_role.id) { + cred_role.name = Some(role.name.clone()); + cred_role.domain_id = role.domain_id.clone(); + } + } + Ok(Some(app_cred)) + } else { + Ok(None) + } + } + + /// List application credentials. + #[tracing::instrument(level = "info", skip(self, state))] + async fn list_application_credentials( + &self, + state: &ServiceState, + params: &ApplicationCredentialListParameters, + ) -> Result, ApplicationCredentialProviderError> { + params.validate()?; + let mut creds = self + .backend_driver + .list_application_credentials(state, params) + .await?; + + let roles: BTreeMap = state + .provider + .get_role_provider() + .list_roles(state, &RoleListParameters::default()) + .await? + .into_iter() + .map(|x| (x.id.clone(), x)) + .collect(); + for cred in creds.iter_mut() { + for cred_role in cred.roles.iter_mut() { + if let Some(role) = roles.get(&cred_role.id) { + cred_role.name = Some(role.name.clone()); + cred_role.domain_id = role.domain_id.clone(); + } + } + } + Ok(creds) + } +} + +/// Generate application credential secret. +/// +/// Use the same algorithm as the python Keystone uses: +/// +/// - use random 64 bytes +/// - apply base64 encoding with no padding +pub fn generate_secret() -> SecretString { + const LENGTH: usize = 64; + + // 1. Generate 64 cryptographically secure random bytes (Analogous to + // `secrets.token_bytes(length)`) + let mut secret_bytes = [0u8; LENGTH]; + rng().fill(&mut secret_bytes[..]); + + // 2. Base64 URL-safe encoding (Analogous to `base64.urlsafe_b64encode(secret)`) + // with stripping padding handled automatically by `URL_SAFE_NO_PAD` engine. + let encoded_secret = general_purpose::URL_SAFE_NO_PAD.encode(secret_bytes); + + SecretString::new(encoded_secret.into()) +} diff --git a/crates/keystone/src/application_credential/types.rs b/crates/core/src/application_credential/types.rs similarity index 100% rename from crates/keystone/src/application_credential/types.rs rename to crates/core/src/application_credential/types.rs diff --git a/crates/keystone/src/application_credential/types/access_rule.rs b/crates/core/src/application_credential/types/access_rule.rs similarity index 100% rename from crates/keystone/src/application_credential/types/access_rule.rs rename to crates/core/src/application_credential/types/access_rule.rs diff --git a/crates/keystone/src/application_credential/types/application_credential.rs b/crates/core/src/application_credential/types/application_credential.rs similarity index 100% rename from crates/keystone/src/application_credential/types/application_credential.rs rename to crates/core/src/application_credential/types/application_credential.rs diff --git a/crates/keystone/src/application_credential/types/provider_api.rs b/crates/core/src/application_credential/types/provider_api.rs similarity index 93% rename from crates/keystone/src/application_credential/types/provider_api.rs rename to crates/core/src/application_credential/types/provider_api.rs index ccdf0dec..912f855b 100644 --- a/crates/keystone/src/application_credential/types/provider_api.rs +++ b/crates/core/src/application_credential/types/provider_api.rs @@ -40,5 +40,5 @@ pub trait ApplicationCredentialApi: Send + Sync { &self, state: &ServiceState, params: &ApplicationCredentialListParameters, - ) -> Result, ApplicationCredentialProviderError>; + ) -> Result, ApplicationCredentialProviderError>; } diff --git a/crates/core/src/assignment/backend.rs b/crates/core/src/assignment/backend.rs new file mode 100644 index 00000000..8e5a8892 --- /dev/null +++ b/crates/core/src/assignment/backend.rs @@ -0,0 +1,63 @@ +// 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 + +use async_trait::async_trait; + +use crate::assignment::AssignmentProviderError; +use crate::assignment::types::assignment::*; +use crate::keystone::ServiceState; + +#[cfg_attr(test, mockall::automock)] +#[async_trait] +pub trait AssignmentBackend: Send + Sync { + /// Check assignment grant. + async fn check_grant( + &self, + state: &ServiceState, + params: &Assignment, + ) -> Result; + + /// Create assignment grant. + async fn create_grant( + &self, + state: &ServiceState, + params: AssignmentCreate, + ) -> Result; + + /// List Role assignments + async fn list_assignments( + &self, + state: &ServiceState, + params: &RoleAssignmentListParameters, + ) -> Result, AssignmentProviderError>; + + /// List all role assignments for multiple actors on multiple targets + /// + /// It is a naive interpretation of the effective role assignments where we + /// check all roles assigned to the user (including groups) on a + /// concrete target (including all higher targets the role can be + /// inherited from) + async fn list_assignments_for_multiple_actors_and_targets( + &self, + state: &ServiceState, + params: &RoleAssignmentListForMultipleActorTargetParameters, + ) -> Result, AssignmentProviderError>; + + /// Revoke assignment grant. + async fn revoke_grant( + &self, + state: &ServiceState, + params: &Assignment, + ) -> Result<(), AssignmentProviderError>; +} diff --git a/crates/keystone/src/assignment/error.rs b/crates/core/src/assignment/error.rs similarity index 97% rename from crates/keystone/src/assignment/error.rs rename to crates/core/src/assignment/error.rs index 7bade69e..d6fd9f99 100644 --- a/crates/keystone/src/assignment/error.rs +++ b/crates/core/src/assignment/error.rs @@ -85,7 +85,7 @@ pub enum AssignmentProviderError { }, /// Unsupported driver. - #[error("unsupported driver {0}")] + #[error("unsupported driver `{0}` for the assignment provider")] UnsupportedDriver(String), /// Validation error. diff --git a/crates/keystone/src/assignment/mock.rs b/crates/core/src/assignment/mock.rs similarity index 87% rename from crates/keystone/src/assignment/mock.rs rename to crates/core/src/assignment/mock.rs index 9f7cc26f..05313ec4 100644 --- a/crates/keystone/src/assignment/mock.rs +++ b/crates/core/src/assignment/mock.rs @@ -17,14 +17,10 @@ use mockall::mock; use crate::assignment::AssignmentApi; use crate::assignment::AssignmentProviderError; use crate::assignment::types::*; -use crate::config::Config; use crate::keystone::ServiceState; -use crate::plugin_manager::PluginManager; mock! { - pub AssignmentProvider { - pub fn new(cfg: &Config, plugin_manager: &PluginManager) -> Result; - } + pub AssignmentProvider {} #[async_trait] impl AssignmentApi for AssignmentProvider { diff --git a/crates/core/src/assignment/mod.rs b/crates/core/src/assignment/mod.rs new file mode 100644 index 00000000..53703d7a --- /dev/null +++ b/crates/core/src/assignment/mod.rs @@ -0,0 +1,119 @@ +// 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 +//! # Assignments provider +//! +//! Assignments provider implements RBAC concept of granting an actor set of +//! roles on the target. An actor could be a user or a group of users, in which +//! case such roles are granted implicitly to the all users which are the member +//! of the group. The target is the domain, project or the system. +//! +//! Keystone implements few additional features for the role assignments: +//! +//! ## Role inference +//! +//! Roles in Keystone may imply other roles building an inference chain. For +//! example a role `manager` can imply the `member` role, which in turn implies +//! the `reader` role. As such with a single assignment of the `manager` role +//! the user will automatically get `manager`, `member` and `reader` roles. This +//! helps limiting number of necessary direct assignments. +//! +//! ## Target assignment inheritance +//! +//! Keystone adds `inherited` parameter to the assignment of the role on the +//! target. In such case an assignment actor gets this role assignment +//! (including role inference) on the whole subtree targets excluding the target +//! itself. This way for an assignment on the domain level the actor +//! will get the role on the every project of the domain, but not the domain +//! itself. +use async_trait::async_trait; + +pub mod backend; +pub mod error; +#[cfg(any(test, feature = "mock"))] +mod mock; +pub mod service; +pub mod types; + +use crate::assignment::service::AssignmentService; +use crate::config::Config; +use crate::keystone::ServiceState; +use crate::plugin_manager::PluginManagerApi; +use types::*; + +pub use error::AssignmentProviderError; +#[cfg(any(test, feature = "mock"))] +pub use mock::MockAssignmentProvider; +pub use types::AssignmentApi; + +pub enum AssignmentProvider { + Service(AssignmentService), + #[cfg(any(test, feature = "mock"))] + Mock(MockAssignmentProvider), +} + +impl AssignmentProvider { + pub fn new( + config: &Config, + plugin_manager: &P, + ) -> Result { + Ok(Self::Service(AssignmentService::new( + config, + plugin_manager, + )?)) + } +} + +#[async_trait] +impl AssignmentApi for AssignmentProvider { + /// Create assignment grant. + #[tracing::instrument(level = "info", skip(self, state))] + async fn create_grant( + &self, + state: &ServiceState, + grant: AssignmentCreate, + ) -> Result { + match self { + Self::Service(provider) => provider.create_grant(state, grant).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.create_grant(state, grant).await, + } + } + + /// List role assignments + #[tracing::instrument(level = "info", skip(self, state))] + async fn list_role_assignments( + &self, + state: &ServiceState, + params: &RoleAssignmentListParameters, + ) -> Result, AssignmentProviderError> { + match self { + Self::Service(provider) => provider.list_role_assignments(state, params).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.list_role_assignments(state, params).await, + } + } + + /// Revoke grant + #[tracing::instrument(level = "info", skip(self, state))] + async fn revoke_grant( + &self, + state: &ServiceState, + grant: Assignment, + ) -> Result<(), AssignmentProviderError> { + match self { + Self::Service(provider) => provider.revoke_grant(state, grant).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.revoke_grant(state, grant).await, + } + } +} diff --git a/crates/core/src/assignment/service.rs b/crates/core/src/assignment/service.rs new file mode 100644 index 00000000..3f7b52ec --- /dev/null +++ b/crates/core/src/assignment/service.rs @@ -0,0 +1,176 @@ +// 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 +//! # Assignments provider +use async_trait::async_trait; +use std::sync::Arc; +use validator::Validate; + +use crate::assignment::{AssignmentProviderError, backend::AssignmentBackend, types::*}; +use crate::config::Config; +use crate::identity::IdentityApi; +use crate::keystone::ServiceState; +use crate::plugin_manager::PluginManagerApi; +use crate::resource::ResourceApi; +use crate::revoke::{RevokeApi, types::RevocationEventCreate}; + +pub struct AssignmentService { + backend_driver: Arc, +} + +impl AssignmentService { + pub fn new( + config: &Config, + plugin_manager: &P, + ) -> Result { + let backend_driver = plugin_manager + .get_assignment_backend(config.assignment.driver.clone())? + .clone(); + Ok(Self { backend_driver }) + } +} + +#[async_trait] +impl AssignmentApi for AssignmentService { + /// Create assignment grant. + #[tracing::instrument(level = "info", skip(self, state))] + async fn create_grant( + &self, + state: &ServiceState, + grant: AssignmentCreate, + ) -> Result { + self.backend_driver.create_grant(state, grant).await + } + + /// List role assignments + #[tracing::instrument(level = "info", skip(self, state))] + async fn list_role_assignments( + &self, + state: &ServiceState, + params: &RoleAssignmentListParameters, + ) -> Result, AssignmentProviderError> { + params.validate()?; + let mut request = RoleAssignmentListForMultipleActorTargetParametersBuilder::default(); + let mut actors: Vec = Vec::new(); + let mut targets: Vec = Vec::new(); + if let Some(role_id) = ¶ms.role_id { + request.role_id(role_id); + } + if let Some(uid) = ¶ms.user_id { + actors.push(uid.into()); + } + if let Some(true) = ¶ms.effective + && let Some(uid) = ¶ms.user_id + { + let users = state + .provider + .get_identity_provider() + .list_groups_of_user(state, uid) + .await?; + actors.extend(users.into_iter().map(|x| x.id)); + }; + if let Some(val) = ¶ms.project_id { + targets.push(RoleAssignmentTarget { + id: val.clone(), + r#type: RoleAssignmentTargetType::Project, + inherited: Some(false), + }); + if let Some(parents) = state + .provider + .get_resource_provider() + .get_project_parents(state, val) + .await? + { + parents.iter().for_each(|parent_project| { + targets.push(RoleAssignmentTarget { + id: parent_project.id.clone(), + r#type: RoleAssignmentTargetType::Project, + inherited: Some(true), + }); + }); + } + } else if let Some(val) = ¶ms.domain_id { + targets.push(RoleAssignmentTarget { + id: val.clone(), + r#type: RoleAssignmentTargetType::Domain, + inherited: Some(false), + }); + } else if let Some(val) = ¶ms.system_id { + targets.push(RoleAssignmentTarget { + id: val.clone(), + r#type: RoleAssignmentTargetType::System, + inherited: Some(false), + }) + } + request.targets(targets); + request.actors(actors); + self.backend_driver + .list_assignments_for_multiple_actors_and_targets(state, &request.build()?) + .await + } + + /// Revoke grant + #[tracing::instrument(level = "info", skip(self, state))] + async fn revoke_grant( + &self, + state: &ServiceState, + grant: Assignment, + ) -> Result<(), AssignmentProviderError> { + // Call backend with reference (no move) + self.backend_driver.revoke_grant(state, &grant).await?; + + // Determine user_id or group_id + let user_id = match &grant.r#type { + AssignmentType::UserDomain + | AssignmentType::UserProject + | AssignmentType::UserSystem => Some(grant.actor_id.clone()), + + AssignmentType::GroupDomain + | AssignmentType::GroupProject + | AssignmentType::GroupSystem => None, + }; + + // Determine project_id or domain_id + let (project_id, domain_id) = match &grant.r#type { + AssignmentType::UserProject | AssignmentType::GroupProject => { + (Some(grant.target_id.clone()), None) + } + AssignmentType::UserDomain | AssignmentType::GroupDomain => { + (None, Some(grant.target_id.clone())) + } + AssignmentType::UserSystem | AssignmentType::GroupSystem => (None, None), + }; + + let revocation_event = RevocationEventCreate { + domain_id, + project_id, + user_id, + role_id: Some(grant.role_id.clone()), + trust_id: None, + consumer_id: None, + access_token_id: None, + issued_before: chrono::Utc::now(), + expires_at: None, + audit_id: None, + audit_chain_id: None, + revoked_at: chrono::Utc::now(), + }; + + state + .provider + .get_revoke_provider() + .create_revocation_event(state, revocation_event) + .await?; + + Ok(()) + } +} diff --git a/crates/keystone/src/assignment/types.rs b/crates/core/src/assignment/types.rs similarity index 100% rename from crates/keystone/src/assignment/types.rs rename to crates/core/src/assignment/types.rs diff --git a/crates/keystone/src/assignment/types/assignment.rs b/crates/core/src/assignment/types/assignment.rs similarity index 93% rename from crates/keystone/src/assignment/types/assignment.rs rename to crates/core/src/assignment/types/assignment.rs index f06b94fa..db7b265c 100644 --- a/crates/keystone/src/assignment/types/assignment.rs +++ b/crates/core/src/assignment/types/assignment.rs @@ -18,6 +18,8 @@ use serde::{Deserialize, Serialize}; use std::fmt; use validator::Validate; +use crate::assignment::AssignmentProviderError; + /// The assignment object. #[derive(Builder, Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize, Validate)] #[builder(build_fn(error = "BuilderError"))] @@ -200,6 +202,21 @@ impl fmt::Display for AssignmentType { } } +impl TryFrom<&str> for AssignmentType { + type Error = AssignmentProviderError; + fn try_from(value: &str) -> Result { + match value { + "GroupDomain" => Ok(Self::GroupDomain), + "GroupProject" => Ok(Self::GroupProject), + "GroupSystem" => Ok(Self::GroupSystem), + "UserDomain" => Ok(Self::UserDomain), + "UserProject" => Ok(Self::UserProject), + "UserSystem" => Ok(Self::UserSystem), + _ => Err(AssignmentProviderError::InvalidAssignmentType(value.into())), + } + } +} + /// Parameters for listing role assignments for role/target/actor. #[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize, Validate)] #[builder(build_fn(error = "BuilderError"))] diff --git a/crates/keystone/src/assignment/types/provider_api.rs b/crates/core/src/assignment/types/provider_api.rs similarity index 100% rename from crates/keystone/src/assignment/types/provider_api.rs rename to crates/core/src/assignment/types/provider_api.rs diff --git a/crates/core/src/auth/mod.rs b/crates/core/src/auth/mod.rs new file mode 100644 index 00000000..c9cf9022 --- /dev/null +++ b/crates/core/src/auth/mod.rs @@ -0,0 +1,339 @@ +// 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 + +//! # Authorization and authentication information. +//! +//! Authentication and authorization types with corresponding validation. +//! Authentication specific validation may stay in the corresponding provider +//! (i.e. user password is expired), but general validation rules must be +//! present here to be shared across different authentication methods. The +//! same is valid for the authorization validation (project/domain must exist +//! and be enabled). + +use chrono::{DateTime, Utc}; +use derive_builder::Builder; +use serde::{Deserialize, Serialize}; +use thiserror::Error; +use tracing::warn; + +use crate::application_credential::types::ApplicationCredential; +use crate::error::BuilderError; +use crate::identity::types::{Group, UserResponse}; +use crate::resource::types::{Domain, Project}; +use crate::trust::types::Trust; + +#[derive(Error, Debug)] +pub enum AuthenticationError { + /// Domain is disabled. + #[error("The domain is disabled.")] + DomainDisabled(String), + + /// Project is disabled. + #[error("The project is disabled.")] + ProjectDisabled(String), + + /// Structures builder error. + #[error(transparent)] + StructBuilder { + /// The source of the error. + #[from] + source: BuilderError, + }, + + /// Token renewal is forbidden. + #[error("Token renewal (getting token from token) is prohibited.")] + TokenRenewalForbidden, + + /// Unauthorized. + #[error("The request you have made requires authentication.")] + Unauthorized, + + /// User is disabled. + #[error("The account is disabled for user: {0}")] + UserDisabled(String), + + /// User is locked due to the multiple failed attempts. + #[error("The account is temporarily disabled for user: {0}")] + UserLocked(String), + + /// User name password combination is wrong. + #[error("wrong username or password")] + UserNameOrPasswordWrong, + + /// User password is expired. + #[error("The password is expired for user: {0}")] + UserPasswordExpired(String), +} + +/// Information about successful authentication. +#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +#[builder(build_fn(error = "BuilderError"))] +#[builder(setter(into, strip_option))] +pub struct AuthenticatedInfo { + /// Application credential. + #[builder(default)] + pub application_credential: Option, + + /// Audit IDs. + #[builder(default)] + pub audit_ids: Vec, + + /// Authentication expiration. + #[builder(default)] + pub expires_at: Option>, + + /// Federated IDP id. + #[builder(default)] + pub idp_id: Option, + + /// Authentication methods. + #[builder(default)] + pub methods: Vec, + + /// Federated protocol id. + #[builder(default)] + pub protocol_id: Option, + + /// Token restriction. + #[builder(default)] + pub token_restriction_id: Option, + + /// Resolved user object. + #[builder(default)] + pub user: Option, + + /// Resolved user domain information. + #[builder(default)] + pub user_domain: Option, + + /// Resolved user object. + #[builder(default)] + pub user_groups: Vec, + + /// User id. + pub user_id: String, +} + +impl AuthenticatedInfo { + pub fn builder() -> AuthenticatedInfoBuilder { + AuthenticatedInfoBuilder::default() + } + + /// Validate the authentication information: + /// + /// - User attribute must be set + /// - User must be enabled + /// - User object id must match user_id + pub fn validate(&self) -> Result<(), AuthenticationError> { + // TODO: all validations (disabled user, locked, etc) should be placed here + // since every authentication method goes different way and we risk + // missing validations + if let Some(user) = &self.user { + if user.id != self.user_id { + warn!( + "User data does not match the user_id attribute: {} vs {}", + self.user_id, user.id + ); + return Err(AuthenticationError::Unauthorized); + } + if !user.enabled { + return Err(AuthenticationError::UserDisabled(self.user_id.clone())); + } + } else { + warn!( + "User data must be resolved in the AuthenticatedInfo before validating: {:?}", + self + ); + return Err(AuthenticationError::Unauthorized); + } + + Ok(()) + } +} + +/// Authorization information. +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub enum AuthzInfo { + /// Domain scope. + Domain(Domain), + /// Project scope. + Project(Project), + /// System scope. + System, + /// Trust scope. + Trust(Trust), + /// Unscoped. + Unscoped, +} + +impl AuthzInfo { + /// Validate the authorization information: + /// + /// - Unscoped: always valid + /// - Project: check if the project is enabled + /// - Domain: check if the domain is enabled + pub fn validate(&self) -> Result<(), AuthenticationError> { + match self { + AuthzInfo::Domain(domain) => { + if !domain.enabled { + return Err(AuthenticationError::DomainDisabled(domain.id.clone())); + } + } + AuthzInfo::Project(project) => { + if !project.enabled { + return Err(AuthenticationError::ProjectDisabled(project.id.clone())); + } + } + AuthzInfo::System => {} + AuthzInfo::Trust(_) => {} + AuthzInfo::Unscoped => {} + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use tracing_test::traced_test; + + use crate::identity::types::{UserOptions, UserResponse}; + + #[test] + fn test_authn_validate_no_user() { + let authn = AuthenticatedInfo::builder().user_id("uid").build().unwrap(); + if let Err(AuthenticationError::Unauthorized) = authn.validate() { + } else { + panic!("should be unauthorized"); + } + } + + #[test] + #[traced_test] + fn test_authn_validate_user_disabled() { + let authn = AuthenticatedInfo::builder() + .user_id("uid") + .user(UserResponse { + id: "uid".to_string(), + enabled: false, + default_project_id: None, + domain_id: "did".into(), + extra: None, + name: "foo".into(), + options: UserOptions::default(), + federated: None, + password_expires_at: None, + }) + .build() + .unwrap(); + if let Err(AuthenticationError::UserDisabled(uid)) = authn.validate() { + assert_eq!("uid", uid); + } else { + panic!("should fail for disabled user"); + } + } + + #[test] + #[traced_test] + fn test_authn_validate_user_mismatch() { + let authn = AuthenticatedInfo::builder() + .user_id("uid1") + .user(UserResponse { + id: "uid2".to_string(), + enabled: false, + default_project_id: None, + domain_id: "did".into(), + extra: None, + name: "foo".into(), + options: UserOptions::default(), + federated: None, + password_expires_at: None, + }) + .build() + .unwrap(); + if let Err(AuthenticationError::Unauthorized) = authn.validate() { + } else { + panic!("should fail when user_id != user.id"); + } + } + + #[test] + #[traced_test] + fn test_authz_validate_project() { + let authz = AuthzInfo::Project(Project { + id: "pid".into(), + domain_id: "pdid".into(), + enabled: true, + ..Default::default() + }); + assert!(authz.validate().is_ok()); + } + + #[test] + #[traced_test] + fn test_authz_validate_project_disabled() { + let authz = AuthzInfo::Project(Project { + id: "pid".into(), + domain_id: "pdid".into(), + enabled: false, + ..Default::default() + }); + if let Err(AuthenticationError::ProjectDisabled(..)) = authz.validate() { + } else { + panic!("should fail when project is not enabled"); + } + } + + #[test] + #[traced_test] + fn test_authz_validate_domain() { + let authz = AuthzInfo::Domain(Domain { + id: "id".into(), + name: "name".into(), + enabled: true, + ..Default::default() + }); + assert!(authz.validate().is_ok()); + } + + #[test] + #[traced_test] + fn test_authz_validate_domain_disabled() { + let authz = AuthzInfo::Domain(Domain { + id: "id".into(), + name: "name".into(), + enabled: false, + ..Default::default() + }); + if let Err(AuthenticationError::DomainDisabled(..)) = authz.validate() { + } else { + panic!("should fail when domain is not enabled"); + } + } + + #[test] + #[traced_test] + fn test_authz_validate_system() { + let authz = AuthzInfo::System; + assert!(authz.validate().is_ok()); + } + + #[test] + #[traced_test] + fn test_authz_validate_unscoped() { + let authz = AuthzInfo::Unscoped; + assert!(authz.validate().is_ok()); + } +} diff --git a/crates/core/src/catalog/backend.rs b/crates/core/src/catalog/backend.rs new file mode 100644 index 00000000..1212eec0 --- /dev/null +++ b/crates/core/src/catalog/backend.rs @@ -0,0 +1,58 @@ +// 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 + +use async_trait::async_trait; + +use crate::catalog::error::CatalogProviderError; +use crate::catalog::types::{Endpoint, EndpointListParameters, Service, ServiceListParameters}; +use crate::keystone::ServiceState; + +#[cfg_attr(test, mockall::automock)] +#[async_trait] +pub trait CatalogBackend: Send + Sync { + /// List services + async fn list_services( + &self, + state: &ServiceState, + params: &ServiceListParameters, + ) -> Result, CatalogProviderError>; + + /// Get single service by ID + async fn get_service<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result, CatalogProviderError>; + + /// List Endpoints + async fn list_endpoints( + &self, + state: &ServiceState, + params: &EndpointListParameters, + ) -> Result, CatalogProviderError>; + + /// Get single endpoint by ID + async fn get_endpoint<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result, CatalogProviderError>; + + /// Get Catalog (Services with Endpoints) + async fn get_catalog( + &self, + state: &ServiceState, + enabled: bool, + ) -> Result)>, CatalogProviderError>; +} diff --git a/crates/keystone/src/catalog/error.rs b/crates/core/src/catalog/error.rs similarity index 95% rename from crates/keystone/src/catalog/error.rs rename to crates/core/src/catalog/error.rs index d92cdc80..c73e5e53 100644 --- a/crates/keystone/src/catalog/error.rs +++ b/crates/core/src/catalog/error.rs @@ -44,6 +44,6 @@ pub enum CatalogProviderError { }, /// Unsupported driver. - #[error("unsupported driver {0}")] + #[error("unsupported driver `{0}` for the catalog provider.")] UnsupportedDriver(String), } diff --git a/crates/keystone/src/catalog/mock.rs b/crates/core/src/catalog/mock.rs similarity index 88% rename from crates/keystone/src/catalog/mock.rs rename to crates/core/src/catalog/mock.rs index 49b7f062..80b0bfe1 100644 --- a/crates/keystone/src/catalog/mock.rs +++ b/crates/core/src/catalog/mock.rs @@ -13,21 +13,15 @@ // SPDX-License-Identifier: Apache-2.0 use async_trait::async_trait; -#[cfg(test)] use mockall::mock; use crate::catalog::CatalogApi; use crate::catalog::error::CatalogProviderError; use crate::catalog::types::{Endpoint, EndpointListParameters, Service, ServiceListParameters}; -use crate::config::Config; use crate::keystone::ServiceState; -use crate::plugin_manager::PluginManager; -#[cfg(test)] mock! { - pub CatalogProvider { - pub fn new(cfg: &Config, plugin_manager: &PluginManager) -> Result; - } + pub CatalogProvider {} #[async_trait] impl CatalogApi for CatalogProvider { diff --git a/crates/core/src/catalog/mod.rs b/crates/core/src/catalog/mod.rs new file mode 100644 index 00000000..784a5aca --- /dev/null +++ b/crates/core/src/catalog/mod.rs @@ -0,0 +1,140 @@ +// 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 +//! # Catalog provider +//! +//! Catalog provider takes care of returning the list of the service endpoints +//! that the API user is able to use according to the valid authentication. +//! +//! Following Keystone concepts are covered: +//! +//! ## Endpoint +//! +//! A network-accessible address, usually a URL, through which you can access a +//! service. If you are using an extension for templates, you can create an +//! endpoint template that represents the templates of all consumable services +//! that are available across the regions. +//! +//! ## Service +//! +//! An OpenStack service, such as Compute (nova), Object Storage (swift), or +//! Image service (glance), that provides one or more endpoints through which +//! users can access resources and perform operations. +use async_trait::async_trait; + +pub mod backend; +pub mod error; +#[cfg(any(test, feature = "mock"))] +mod mock; +pub mod service; +pub mod types; + +use crate::config::Config; +use crate::keystone::ServiceState; +use crate::plugin_manager::PluginManagerApi; +use service::CatalogService; + +pub use crate::catalog::error::CatalogProviderError; +#[cfg(any(test, feature = "mock"))] +pub use mock::MockCatalogProvider; +pub use types::CatalogApi; + +use types::*; + +pub enum CatalogProvider { + Service(CatalogService), + #[cfg(any(test, feature = "mock"))] + Mock(MockCatalogProvider), +} + +impl CatalogProvider { + pub fn new( + config: &Config, + plugin_manager: &P, + ) -> Result { + Ok(Self::Service(CatalogService::new(config, plugin_manager)?)) + } +} + +#[async_trait] +impl CatalogApi for CatalogProvider { + /// List services + #[tracing::instrument(level = "info", skip(self, state))] + async fn list_services( + &self, + state: &ServiceState, + params: &ServiceListParameters, + ) -> Result, CatalogProviderError> { + match self { + Self::Service(provider) => provider.list_services(state, params).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.list_services(state, params).await, + } + } + + /// Get single service by ID + #[tracing::instrument(level = "info", skip(self, state))] + async fn get_service<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result, CatalogProviderError> { + match self { + Self::Service(provider) => provider.get_service(state, id).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.get_service(state, id).await, + } + } + + /// List Endpoints + #[tracing::instrument(level = "info", skip(self, state))] + async fn list_endpoints( + &self, + state: &ServiceState, + params: &EndpointListParameters, + ) -> Result, CatalogProviderError> { + match self { + Self::Service(provider) => provider.list_endpoints(state, params).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.list_endpoints(state, params).await, + } + } + + /// Get single endpoint by ID + #[tracing::instrument(level = "info", skip(self, state))] + async fn get_endpoint<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result, CatalogProviderError> { + match self { + Self::Service(provider) => provider.get_endpoint(state, id).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.get_endpoint(state, id).await, + } + } + + /// Get catalog + #[tracing::instrument(level = "info", skip(self, state))] + async fn get_catalog( + &self, + state: &ServiceState, + enabled: bool, + ) -> Result)>, CatalogProviderError> { + match self { + Self::Service(provider) => provider.get_catalog(state, enabled).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.get_catalog(state, enabled).await, + } + } +} diff --git a/crates/core/src/catalog/service.rs b/crates/core/src/catalog/service.rs new file mode 100644 index 00000000..02dd20a4 --- /dev/null +++ b/crates/core/src/catalog/service.rs @@ -0,0 +1,90 @@ +// 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 +//! # Catalog provider +use async_trait::async_trait; +use std::sync::Arc; + +use crate::catalog::{CatalogProviderError, backend::CatalogBackend, types::*}; +use crate::config::Config; +use crate::keystone::ServiceState; +use crate::plugin_manager::PluginManagerApi; + +pub struct CatalogService { + backend_driver: Arc, +} + +impl CatalogService { + pub fn new( + config: &Config, + plugin_manager: &P, + ) -> Result { + let backend_driver = plugin_manager + .get_catalog_backend(config.catalog.driver.clone())? + .clone(); + Ok(Self { backend_driver }) + } +} + +#[async_trait] +impl CatalogApi for CatalogService { + /// List services + #[tracing::instrument(level = "info", skip(self, state))] + async fn list_services( + &self, + state: &ServiceState, + params: &ServiceListParameters, + ) -> Result, CatalogProviderError> { + self.backend_driver.list_services(state, params).await + } + + /// Get single service by ID + #[tracing::instrument(level = "info", skip(self, state))] + async fn get_service<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result, CatalogProviderError> { + self.backend_driver.get_service(state, id).await + } + + /// List Endpoints + #[tracing::instrument(level = "info", skip(self, state))] + async fn list_endpoints( + &self, + state: &ServiceState, + params: &EndpointListParameters, + ) -> Result, CatalogProviderError> { + self.backend_driver.list_endpoints(state, params).await + } + + /// Get single endpoint by ID + #[tracing::instrument(level = "info", skip(self, state))] + async fn get_endpoint<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result, CatalogProviderError> { + self.backend_driver.get_endpoint(state, id).await + } + + /// Get catalog + #[tracing::instrument(level = "info", skip(self, state))] + async fn get_catalog( + &self, + state: &ServiceState, + enabled: bool, + ) -> Result)>, CatalogProviderError> { + self.backend_driver.get_catalog(state, enabled).await + } +} diff --git a/crates/keystone/src/catalog/types.rs b/crates/core/src/catalog/types.rs similarity index 100% rename from crates/keystone/src/catalog/types.rs rename to crates/core/src/catalog/types.rs diff --git a/crates/keystone/src/catalog/types/endpoint.rs b/crates/core/src/catalog/types/endpoint.rs similarity index 100% rename from crates/keystone/src/catalog/types/endpoint.rs rename to crates/core/src/catalog/types/endpoint.rs diff --git a/crates/keystone/src/catalog/types/provider_api.rs b/crates/core/src/catalog/types/provider_api.rs similarity index 100% rename from crates/keystone/src/catalog/types/provider_api.rs rename to crates/core/src/catalog/types/provider_api.rs diff --git a/crates/keystone/src/catalog/types/service.rs b/crates/core/src/catalog/types/service.rs similarity index 100% rename from crates/keystone/src/catalog/types/service.rs rename to crates/core/src/catalog/types/service.rs diff --git a/crates/core/src/common.rs b/crates/core/src/common.rs new file mode 100644 index 00000000..c58376de --- /dev/null +++ b/crates/core/src/common.rs @@ -0,0 +1,16 @@ +// 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 +//! # Common functionality +pub mod password_hashing; +pub mod types; diff --git a/crates/keystone/src/common/password_hashing.rs b/crates/core/src/common/password_hashing.rs similarity index 99% rename from crates/keystone/src/common/password_hashing.rs rename to crates/core/src/common/password_hashing.rs index ac805255..5e0f2854 100644 --- a/crates/keystone/src/common/password_hashing.rs +++ b/crates/core/src/common/password_hashing.rs @@ -73,7 +73,7 @@ pub async fn hash_password>( task::spawn_blocking(move || bcrypt::hash(password_bytes, rounds as u32)).await??; Ok(hash) } - #[cfg(test)] + //#[cfg(test)] PasswordHashingAlgo::None => Ok(str::from_utf8(password.as_ref())?.to_string()), } } @@ -108,7 +108,7 @@ pub async fn verify_password, H: AsRef>( } } } - #[cfg(test)] + //#[cfg(test)] PasswordHashingAlgo::None => Ok(str::from_utf8(password.as_ref())?.eq(hash.as_ref())), } } diff --git a/crates/keystone/src/common/types.rs b/crates/core/src/common/types.rs similarity index 100% rename from crates/keystone/src/common/types.rs rename to crates/core/src/common/types.rs diff --git a/crates/core/src/config.rs b/crates/core/src/config.rs new file mode 100644 index 00000000..a6837912 --- /dev/null +++ b/crates/core/src/config.rs @@ -0,0 +1,184 @@ +// 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 +//! # Keystone configuration +//! +//! Parsing of the Keystone configuration file implementation. +use config::{File, FileFormat}; +use eyre::{Report, WrapErr}; +use serde::Deserialize; +use std::path::PathBuf; + +mod application_credentials; +mod assignment; +mod auth; +mod catalog; +mod common; +mod database; +mod default; +mod distributed_storage; +mod federation; +mod fernet_token; +mod identity; +mod identity_mapping; +mod k8s_auth; +mod policy; +mod resource; +mod revoke; +mod role; +mod security_compliance; +mod token; +mod token_restriction; +mod trust; +mod webauthn; + +use application_credentials::ApplicationCredentialProvider; +use assignment::AssignmentProvider; +use auth::AuthProvider; +use catalog::CatalogProvider; +use database::DatabaseSection; +pub use default::DefaultSection; +use distributed_storage::DistributedStorageConfiguration; +use federation::FederationProvider; +pub use fernet_token::FernetTokenProvider; +pub use identity::*; +use identity_mapping::IdentityMappingProvider; +use k8s_auth::K8sAuthProvider; +use policy::PolicyProvider; +use resource::ResourceProvider; +use revoke::RevokeProvider; +use role::RoleProvider; +use security_compliance::SecurityComplianceProvider; +use token::TokenProvider; +pub use token::TokenProviderDriver; +use token_restriction::TokenRestrictionProvider; +use trust::TrustProvider; +use webauthn::WebauthnSection; + +/// Keystone configuration. +#[derive(Debug, Default, Deserialize, Clone)] +pub struct Config { + /// Application credentials provider configuration. + #[serde(default)] + pub application_credential: ApplicationCredentialProvider, + + /// API policy enforcement. + #[serde(default)] + pub api_policy: PolicyProvider, + + /// Assignments (roles) provider configuration. + #[serde(default)] + pub assignment: AssignmentProvider, + + /// Authentication configuration. + pub auth: AuthProvider, + + /// Catalog provider configuration. + #[serde(default)] + pub catalog: CatalogProvider, + + /// Database configuration. + //#[serde(default)] + pub database: DatabaseSection, + + /// Global configuration options. + #[serde(rename = "DEFAULT", default)] + pub default: DefaultSection, + + /// Distributed storage configuration. + #[serde(default)] + pub distributed_storage: Option, + + /// Federation provider configuration. + #[serde(default)] + pub federation: FederationProvider, + + /// Fernet tokens provider configuration. + #[serde(default)] + pub fernet_tokens: FernetTokenProvider, + + /// Identity provider configuration. + #[serde(default)] + pub identity: IdentityProvider, + + /// Identity mapping provider configuration. + #[serde(default)] + pub identity_mapping: IdentityMappingProvider, + + /// K8s Auth provider configuration. + #[serde(default)] + pub k8s_auth: K8sAuthProvider, + + /// Resource provider configuration. + #[serde(default)] + pub resource: ResourceProvider, + + /// Revoke provider configuration. + #[serde(default)] + pub revoke: RevokeProvider, + + /// Role provider configuration. + #[serde(default)] + pub role: RoleProvider, + + /// Security compliance configuration. + #[serde(default)] + pub security_compliance: SecurityComplianceProvider, + + /// Token provider configuration. + #[serde(default)] + pub token: TokenProvider, + + /// Token restriction provider configuration. + #[serde(default)] + pub token_restriction: TokenRestrictionProvider, + + /// Trust provider configuration. + #[serde(default)] + pub trust: TrustProvider, + + /// Webauthn configuration. + #[serde(default)] + pub webauthn: WebauthnSection, +} + +impl Config { + pub fn new(path: PathBuf) -> Result { + let mut builder = config::Config::builder(); + + if std::path::Path::new(&path).is_file() { + builder = builder + .add_source(File::from(path).format(FileFormat::Ini)) + .add_source( + config::Environment::with_prefix("OS") + .prefix_separator("_") + .separator("__"), + ); + } + + builder.try_into() + } +} + +impl TryFrom> for Config { + type Error = Report; + fn try_from( + builder: config::ConfigBuilder, + ) -> Result { + builder + .build() + .wrap_err("Failed to read configuration file")? + .try_deserialize() + .wrap_err("Failed to parse configuration file") + } +} diff --git a/crates/keystone/src/config/application_credentials.rs b/crates/core/src/config/application_credentials.rs similarity index 100% rename from crates/keystone/src/config/application_credentials.rs rename to crates/core/src/config/application_credentials.rs diff --git a/crates/keystone/src/config/assignment.rs b/crates/core/src/config/assignment.rs similarity index 100% rename from crates/keystone/src/config/assignment.rs rename to crates/core/src/config/assignment.rs diff --git a/crates/keystone/src/config/auth.rs b/crates/core/src/config/auth.rs similarity index 100% rename from crates/keystone/src/config/auth.rs rename to crates/core/src/config/auth.rs diff --git a/crates/keystone/src/config/catalog.rs b/crates/core/src/config/catalog.rs similarity index 100% rename from crates/keystone/src/config/catalog.rs rename to crates/core/src/config/catalog.rs diff --git a/crates/keystone/src/config/common.rs b/crates/core/src/config/common.rs similarity index 100% rename from crates/keystone/src/config/common.rs rename to crates/core/src/config/common.rs diff --git a/crates/keystone/src/config/database.rs b/crates/core/src/config/database.rs similarity index 100% rename from crates/keystone/src/config/database.rs rename to crates/core/src/config/database.rs diff --git a/crates/keystone/src/config/default.rs b/crates/core/src/config/default.rs similarity index 100% rename from crates/keystone/src/config/default.rs rename to crates/core/src/config/default.rs diff --git a/crates/keystone/src/config/distributed_storage.rs b/crates/core/src/config/distributed_storage.rs similarity index 100% rename from crates/keystone/src/config/distributed_storage.rs rename to crates/core/src/config/distributed_storage.rs diff --git a/crates/keystone/src/config/federation.rs b/crates/core/src/config/federation.rs similarity index 94% rename from crates/keystone/src/config/federation.rs rename to crates/core/src/config/federation.rs index 7428ce37..cfccb671 100644 --- a/crates/keystone/src/config/federation.rs +++ b/crates/core/src/config/federation.rs @@ -43,7 +43,7 @@ impl FederationProvider { /// /// Calculate the oldest time for the expiring user group membership to not /// be considered as valid. - pub(crate) fn get_expiring_user_group_membership_cutof_datetime(&self) -> DateTime { + pub fn get_expiring_user_group_membership_cutof_datetime(&self) -> DateTime { Utc::now() .checked_sub_signed(TimeDelta::seconds(self.default_authorization_ttl.into())) .unwrap_or(Utc::now()) diff --git a/crates/keystone/src/config/fernet_token.rs b/crates/core/src/config/fernet_token.rs similarity index 100% rename from crates/keystone/src/config/fernet_token.rs rename to crates/core/src/config/fernet_token.rs diff --git a/crates/keystone/src/config/identity.rs b/crates/core/src/config/identity.rs similarity index 99% rename from crates/keystone/src/config/identity.rs rename to crates/core/src/config/identity.rs index 28edcd4a..f012c763 100644 --- a/crates/keystone/src/config/identity.rs +++ b/crates/core/src/config/identity.rs @@ -62,7 +62,7 @@ pub enum PasswordHashingAlgo { /// Bcrypt. #[default] Bcrypt, - #[cfg(test)] + // #[cfg(test)] /// None. Should not be used outside of testing where expected value is /// necessary. None, diff --git a/crates/keystone/src/config/identity_mapping.rs b/crates/core/src/config/identity_mapping.rs similarity index 100% rename from crates/keystone/src/config/identity_mapping.rs rename to crates/core/src/config/identity_mapping.rs diff --git a/crates/keystone/src/config/k8s_auth.rs b/crates/core/src/config/k8s_auth.rs similarity index 100% rename from crates/keystone/src/config/k8s_auth.rs rename to crates/core/src/config/k8s_auth.rs diff --git a/crates/keystone/src/config/policy.rs b/crates/core/src/config/policy.rs similarity index 100% rename from crates/keystone/src/config/policy.rs rename to crates/core/src/config/policy.rs diff --git a/crates/keystone/src/config/resource.rs b/crates/core/src/config/resource.rs similarity index 100% rename from crates/keystone/src/config/resource.rs rename to crates/core/src/config/resource.rs diff --git a/crates/keystone/src/config/revoke.rs b/crates/core/src/config/revoke.rs similarity index 100% rename from crates/keystone/src/config/revoke.rs rename to crates/core/src/config/revoke.rs diff --git a/crates/keystone/src/config/role.rs b/crates/core/src/config/role.rs similarity index 100% rename from crates/keystone/src/config/role.rs rename to crates/core/src/config/role.rs diff --git a/crates/keystone/src/config/security_compliance.rs b/crates/core/src/config/security_compliance.rs similarity index 99% rename from crates/keystone/src/config/security_compliance.rs rename to crates/core/src/config/security_compliance.rs index 5c7d8f13..c74b36ca 100644 --- a/crates/keystone/src/config/security_compliance.rs +++ b/crates/core/src/config/security_compliance.rs @@ -165,7 +165,7 @@ impl SecurityComplianceProvider { /// SecurityComplianceProvider::disable_user_account_days_inactive) /// is set return the corresponding oldest user activity date for it to be /// considered as disabled. When the option is not set returns `None`. - pub(crate) fn get_user_last_activity_cutof_date(&self) -> Option { + pub fn get_user_last_activity_cutof_date(&self) -> Option { self.disable_user_account_days_inactive .and_then(|inactive_after_days| { Utc::now() diff --git a/crates/keystone/src/config/token.rs b/crates/core/src/config/token.rs similarity index 100% rename from crates/keystone/src/config/token.rs rename to crates/core/src/config/token.rs diff --git a/crates/keystone/src/config/token_restriction.rs b/crates/core/src/config/token_restriction.rs similarity index 80% rename from crates/keystone/src/config/token_restriction.rs rename to crates/core/src/config/token_restriction.rs index 22f79feb..f560ddf2 100644 --- a/crates/keystone/src/config/token_restriction.rs +++ b/crates/core/src/config/token_restriction.rs @@ -19,9 +19,17 @@ use serde::Deserialize; use crate::config::common::default_sql_driver; /// Token restriction provider. -#[derive(Debug, Default, Deserialize, Clone)] +#[derive(Debug, Deserialize, Clone)] pub struct TokenRestrictionProvider { - /// Token restriction provider driver. + /// Token restriction driver. #[serde(default = "default_sql_driver")] pub driver: String, } + +impl Default for TokenRestrictionProvider { + fn default() -> Self { + Self { + driver: default_sql_driver(), + } + } +} diff --git a/crates/keystone/src/config/trust.rs b/crates/core/src/config/trust.rs similarity index 100% rename from crates/keystone/src/config/trust.rs rename to crates/core/src/config/trust.rs diff --git a/crates/keystone/src/config/webauthn.rs b/crates/core/src/config/webauthn.rs similarity index 100% rename from crates/keystone/src/config/webauthn.rs rename to crates/core/src/config/webauthn.rs diff --git a/crates/core/src/error.rs b/crates/core/src/error.rs new file mode 100644 index 00000000..b9d67ab3 --- /dev/null +++ b/crates/core/src/error.rs @@ -0,0 +1,279 @@ +// 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 +//! # Error +//! +//! Diverse errors that can occur during the Keystone processing (not the API). +use thiserror::Error; + +use crate::application_credential::error::ApplicationCredentialProviderError; +use crate::assignment::error::AssignmentProviderError; +use crate::catalog::error::CatalogProviderError; +use crate::federation::error::FederationProviderError; +use crate::identity::error::IdentityProviderError; +use crate::identity_mapping::error::IdentityMappingProviderError; +use crate::k8s_auth::error::K8sAuthProviderError; +use crate::policy::PolicyError; +use crate::resource::error::ResourceProviderError; +use crate::revoke::error::RevokeProviderError; +use crate::role::error::RoleProviderError; +use crate::token::TokenProviderError; +use crate::trust::TrustProviderError; +//use crate::webauthn::WebauthnError; + +/// Keystone error. +#[derive(Debug, Error)] +pub enum KeystoneError { + /// Application credential provider. + #[error(transparent)] + ApplicationCredential { + /// The source of the error. + #[from] + source: ApplicationCredentialProviderError, + }, + + /// Assignment provider. + #[error(transparent)] + AssignmentProvider { + /// The source of the error. + #[from] + source: AssignmentProviderError, + }, + + /// Catalog provider. + #[error(transparent)] + CatalogProvider { + /// The source of the error. + #[from] + source: CatalogProviderError, + }, + + /// Federation provider. + #[error(transparent)] + FederationProvider { + /// The source of the error. + #[from] + source: FederationProviderError, + }, + + /// Identity provider. + #[error(transparent)] + IdentityProvider { + /// The source of the error. + #[from] + source: IdentityProviderError, + }, + + /// Identity mapping provider. + #[error(transparent)] + IdentityMapping { + /// The source of the error. + #[from] + source: IdentityMappingProviderError, + }, + + /// IO error. + #[error(transparent)] + IO { + /// The source of the error. + #[from] + source: std::io::Error, + }, + + /// Json serialization error. + #[error("json serde error: {}", source)] + Json { + /// The source of the error. + #[from] + source: serde_json::Error, + }, + + /// K8s auth provider. + #[error(transparent)] + K8sAuthProvider { + /// The source of the error. + #[from] + source: K8sAuthProviderError, + }, + + /// Policy engine. + #[error(transparent)] + Policy { + /// The source of the error. + #[from] + source: PolicyError, + }, + + /// Policy engine is not available. + #[error("policy enforcement is requested, but not available with the enabled features")] + PolicyEnforcementNotAvailable, + + /// Resource provider. + #[error(transparent)] + ResourceProvider { + /// The source of the error. + #[from] + source: ResourceProviderError, + }, + + /// Revoke provider error. + #[error(transparent)] + RevokeProvider { + /// The source of the error. + #[from] + source: RevokeProviderError, + }, + + /// Role provider. + #[error(transparent)] + RoleProvider { + /// The source of the error. + #[from] + source: RoleProviderError, + }, + + /// Token provider. + #[error(transparent)] + TokenProvider { + /// The source of the error. + #[from] + source: TokenProviderError, + }, + + /// Trust provider. + #[error(transparent)] + TrustProvider { + /// The source of the error. + #[from] + source: TrustProviderError, + }, + + /// Url parsing error. + #[error(transparent)] + UrlParse { + #[from] + source: url::ParseError, + }, + + #[error("provider error: {}", source)] + Provider { + #[source] + source: Box, + }, + // /// WebauthN error. + // #[error(transparent)] + // Webauthn { + // /// The source of the error. + // #[from] + // source: WebauthnError, + // }, +} + +/// Builder error. +/// +/// A wrapper error that is used instead of the error generated by the +/// `derive_builder`. +#[derive(Debug, Error)] +#[non_exhaustive] +pub enum BuilderError { + /// Uninitialized field. + #[error("{0}")] + UninitializedField(String), + /// Custom validation error. + #[error("{0}")] + Validation(String), +} + +impl From for BuilderError { + fn from(s: String) -> Self { + Self::Validation(s) + } +} + +impl From for BuilderError { + fn from(value: openstack_keystone_api_types::error::BuilderError) -> Self { + match value { + openstack_keystone_api_types::error::BuilderError::UninitializedField(e) => { + Self::UninitializedField(e) + } + openstack_keystone_api_types::error::BuilderError::Validation(e) => Self::Validation(e), + } + } +} + +impl From for BuilderError { + fn from(ufe: derive_builder::UninitializedFieldError) -> Self { + Self::UninitializedField(ufe.to_string()) + } +} + +/// Context aware database error. +#[derive(Debug, Error)] +pub enum DatabaseError { + /// Conflict. + #[error("{message} while {context}")] + Conflict { + /// The error message. + message: String, + /// The error context. + context: String, + }, + + /// Database error. + #[error("Database error {source} while {context}")] + Database { + /// The source of the error. + source: sea_orm::DbErr, + /// The error context. + context: String, + }, + + /// SqlError. + #[error("{message} while {context}")] + Sql { + /// The error message. + message: String, + /// The error context. + context: String, + }, +} + +/// The trait wrapping the SQL error with the context information. +pub trait DbContextExt { + fn context(self, msg: impl Into) -> Result; +} + +impl DbContextExt for Result { + fn context(self, context: impl Into) -> Result { + self.map_err(|err| match err.sql_err() { + Some(sea_orm::SqlErr::UniqueConstraintViolation(descr)) => DatabaseError::Conflict { + message: descr.to_string(), + context: context.into(), + }, + Some(sea_orm::SqlErr::ForeignKeyConstraintViolation(descr)) => { + DatabaseError::Conflict { + message: descr.to_string(), + context: context.into(), + } + } + Some(other) => DatabaseError::Sql { + message: other.to_string(), + context: context.into(), + }, + None => DatabaseError::Database { + source: err, + context: context.into(), + }, + }) + } +} diff --git a/crates/core/src/federation/api.rs b/crates/core/src/federation/api.rs new file mode 100644 index 00000000..15f6f16c --- /dev/null +++ b/crates/core/src/federation/api.rs @@ -0,0 +1,101 @@ +// 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 +//! # Federation API +//! +//! - IDP +//! - Mapping +//! - Auth initialization +//! - Auth callback +// use utoipa::{ +// Modify, OpenApi, +// openapi::security::{ +// AuthorizationCode, Flow, HttpAuthScheme, HttpBuilder, OAuth2, Scopes, SecurityScheme, +// }, +// }; +// use utoipa_axum::router::OpenApiRouter; +// +// use crate::keystone::ServiceState; +// +// pub mod auth; +// mod common; +pub mod error; +// pub mod identity_provider; +// pub mod jwt; +// pub mod mapping; +// pub mod oidc; +pub mod types; +// +// /// OpenApi specification for the federation. +// #[derive(OpenApi)] +// #[openapi( +// modifiers(&SecurityFederationAddon), +// tags( +// (name="identity_providers", description=r#"Identity providers API. +// +// Identity provider resource allows to federate users from an external Identity Provider (i.e. +// Keycloak, Azure AD, etc.). +// +// Using the Identity provider requires creation of the mapping, which describes how to map attributes +// of the remote Idp to local users. +// +// Identity provider with an empty domain_id are considered globals and every domain may use it with +// appropriate mapping."#), +// (name="mappings", description=r#"Federation mappings API. +// +// Mappings define how the user attributes on the remote IDP are mapped to the local user. +// +// Mappings with an empty domain_id are considered globals and every domain may use it. Such mappings +// require the `domain_id_claim` attribute to be set to identify the domain_id for the respective +// user."#), +// ) +// )] +// pub struct ApiDoc; +// +// pub fn openapi_router() -> OpenApiRouter { +// OpenApiRouter::new() +// .nest("/identity_providers", identity_provider::openapi_router()) +// .nest("/mappings", mapping::openapi_router()) +// .merge(auth::openapi_router()) +// .merge(jwt::openapi_router()) +// .merge(oidc::openapi_router()) +// } +// +// struct SecurityFederationAddon; +// impl Modify for SecurityFederationAddon { +// fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) { +// if let Some(components) = openapi.components.as_mut() { +// components.add_security_scheme( +// "jwt", +// SecurityScheme::Http( +// HttpBuilder::new() +// .scheme(HttpAuthScheme::Bearer) +// .bearer_format("JWT") +// .description(Some("JWT (ID) Token issued by the federated IDP")) +// .build(), +// ), +// ); +// // TODO: This must be dynamic +// components.add_security_scheme( +// "oauth2", +// SecurityScheme::OAuth2(OAuth2::new([Flow::AuthorizationCode( +// AuthorizationCode::new( +// "https://localhost/authorization/token", +// "https://localhost/token/url", +// Scopes::from_iter([("openid", "default scope")]), +// ), +// )])), +// ); +// } +// } +// } diff --git a/crates/core/src/federation/api/auth.rs b/crates/core/src/federation/api/auth.rs new file mode 100644 index 00000000..ce5716eb --- /dev/null +++ b/crates/core/src/federation/api/auth.rs @@ -0,0 +1,221 @@ +// 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 + +use axum::{ + Json, debug_handler, + extract::{Path, State}, + response::IntoResponse, +}; +use chrono::{Local, TimeDelta}; +use std::collections::HashSet; +use tracing::debug; +use utoipa_axum::{router::OpenApiRouter, routes}; +use validator::Validate; + +use openidconnect::core::{CoreAuthenticationFlow, CoreClient, CoreProviderMetadata}; +use openidconnect::reqwest; +use openidconnect::{ + ClientId, ClientSecret, CsrfToken, IssuerUrl, Nonce, PkceCodeChallenge, RedirectUrl, Scope, +}; + +use crate::api::error::KeystoneApiError; +use crate::federation::types::{AuthState, MappingListParameters as ProviderMappingListParameters}; +use crate::federation::{FederationApi, api::error::OidcError, api::types::*}; +use crate::keystone::ServiceState; + +pub(super) fn openapi_router() -> OpenApiRouter { + OpenApiRouter::new().routes(routes!(post)) +} + +/// Authenticate using identity provider. +/// +/// Initiate the authentication for the given identity provider. Mapping can be +/// passed, otherwise the one which is set as a default on the identity provider +/// level is used. +/// +/// The API returns the link to the identity provider which must be open in the +/// web browser. Once user authenticates in the identity provider UI a redirect +/// to the url passed as a callback in the request is being done as a typical +/// oauth2 authorization code callback. The client is responsible for serving +/// this callback server and use received authorization code and state to +/// exchange it for the Keystone token passing it to the +/// `/v4/federation/oidc/callback`. +/// +/// Desired scope (OpenStack) can be also passed to get immediately scoped token +/// after the authentication completes instead of the unscoped token. +/// +/// This is an unauthenticated API call. User, mapping, scope validation will +/// happen when the callback is invoked. +#[utoipa::path( + post, + path = "/identity_providers/{idp_id}/auth", + operation_id = "federation/identity_provider/auth:post", + request_body = IdentityProviderAuthRequest, + responses( + (status = CREATED, description = "Authentication data", body = IdentityProviderAuthResponse), + ), + tag="identity_providers" +)] +#[tracing::instrument( + name = "api::identity_provider_auth", + level = "debug", + skip(state), + err(Debug) +)] +#[debug_handler] +pub async fn post( + State(state): State, + Path(idp_id): Path, + Json(req): Json, +) -> Result { + req.validate()?; + state + .config + .auth + .methods + .iter() + .find(|m| *m == "openid") + .ok_or(KeystoneApiError::AuthMethodNotSupported)?; + + let idp = state + .provider + .get_federation_provider() + .get_identity_provider(&state, &idp_id) + .await + .map(|x| { + x.ok_or_else(|| KeystoneApiError::NotFound { + resource: "identity provider".into(), + identifier: idp_id, + }) + })??; + + let mapping = if let Some(mapping_id) = req.mapping_id { + state + .provider + .get_federation_provider() + .get_mapping(&state, &mapping_id) + .await + .map(|x| { + x.ok_or_else(|| KeystoneApiError::NotFound { + resource: "mapping".into(), + identifier: mapping_id.clone(), + }) + })?? + } else if let Some(mapping_name) = req.mapping_name.or(idp.default_mapping_name) { + state + .provider + .get_federation_provider() + .list_mappings( + &state, + &ProviderMappingListParameters { + idp_id: Some(idp.id.clone()), + name: Some(mapping_name.clone()), + ..Default::default() + }, + ) + .await? + .first() + .ok_or(KeystoneApiError::NotFound { + resource: "mapping".into(), + identifier: mapping_name.clone(), + })? + .to_owned() + } else { + return Err(OidcError::MappingRequired)?; + }; + + // Check for IdP and mapping `enabled` state + if !idp.enabled { + return Err(OidcError::IdentityProviderDisabled)?; + } + if !mapping.enabled { + return Err(OidcError::MappingDisabled)?; + } + + let client = if let Some(discovery_url) = &idp.oidc_discovery_url { + let http_client = reqwest::ClientBuilder::new() + // Following redirects opens the client up to SSRF vulnerabilities. + .redirect(reqwest::redirect::Policy::none()) + .build() + .map_err(OidcError::from)?; + + let provider_metadata = CoreProviderMetadata::discover_async( + IssuerUrl::new(discovery_url.to_string()).map_err(OidcError::from)?, + &http_client, + ) + .await + .map_err(|err| OidcError::discovery(discovery_url, &err))?; + CoreClient::from_provider_metadata( + provider_metadata, + ClientId::new(idp.oidc_client_id.ok_or(OidcError::ClientIdRequired)?), + idp.oidc_client_secret.map(ClientSecret::new), + ) + // Set the URL the user will be redirected to after the authorization process. + // TODO: Check the redirect uri against mapping.allowed_redirect_uris + .set_redirect_uri(RedirectUrl::new(req.redirect_uri.clone()).map_err(OidcError::from)?) + } else { + return Err(OidcError::ClientWithoutDiscoveryNotSupported)?; + }; + + let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256(); + + // `oidc` scope is the default in the openidconnect crate and do not need to be + // added explicitly. + let oidc_scopes: HashSet = mapping + .oidc_scopes + .map(|scopes| HashSet::from_iter(scopes.into_iter().map(Scope::new))) + .unwrap_or_default(); + + // Generate the full authorization URL. + let (auth_url, csrf_token, nonce) = client + .authorize_url( + CoreAuthenticationFlow::AuthorizationCode, + CsrfToken::new_random, + Nonce::new_random, + ) + .add_scopes(oidc_scopes) + // Set the PKCE code challenge. + .set_pkce_challenge(pkce_challenge) + .url(); + + state + .provider + .get_federation_provider() + .create_auth_state( + &state, + AuthState { + state: csrf_token.secret().clone(), + nonce: nonce.secret().clone(), + idp_id: idp.id.clone(), + mapping_id: mapping.id.clone(), + redirect_uri: req.redirect_uri.clone(), + pkce_verifier: pkce_verifier.into_secret(), + expires_at: (Local::now() + TimeDelta::seconds(180)).into(), + // TODO: Make this configurable + scope: req.scope.map(Into::into), + }, + ) + .await?; + + debug!( + "url: {:?}, csrf: {:?}, nonce: {:?}", + auth_url, + csrf_token.secret(), + nonce.secret() + ); + Ok(IdentityProviderAuthResponse { + auth_url: auth_url.to_string(), + } + .into_response()) +} diff --git a/crates/core/src/federation/api/common.rs b/crates/core/src/federation/api/common.rs new file mode 100644 index 00000000..b743482f --- /dev/null +++ b/crates/core/src/federation/api/common.rs @@ -0,0 +1,176 @@ +// 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 + +use serde_json::Value; + +use openidconnect::IdTokenClaims; +use openidconnect::core::CoreGenderClaim; + +use crate::federation::api::{ + error::OidcError, + types::{AllOtherClaims, MappedUserData, MappedUserDataBuilder}, +}; +use crate::federation::types::{ + identity_provider::IdentityProvider as ProviderIdentityProvider, + mapping::Mapping as ProviderMapping, +}; +use crate::keystone::ServiceState; + +/// Validate bound claims in the token. +/// +/// # Arguments +/// +/// * `mapping` - The mapping to validate against +/// * `claims` - The claims to validate +/// * `claims_as_json` - The claims as json to validate +/// +/// # Returns +/// +/// * `Result<(), OidcError>` +pub(super) fn validate_bound_claims( + mapping: &ProviderMapping, + claims: &IdTokenClaims, + claims_as_json: &Value, +) -> Result<(), OidcError> { + if let Some(bound_subject) = &mapping.bound_subject + && bound_subject != claims.subject().as_str() + { + return Err(OidcError::BoundSubjectMismatch { + expected: bound_subject.to_string(), + found: claims.subject().as_str().into(), + }); + } + if let Some(bound_audiences) = &mapping.bound_audiences { + let mut bound_audiences_match: bool = false; + for claim_audience in claims.audiences() { + if bound_audiences.iter().any(|x| x == claim_audience.as_str()) { + bound_audiences_match = true; + } + } + if !bound_audiences_match { + return Err(OidcError::BoundAudiencesMismatch { + expected: bound_audiences.join(","), + found: claims + .audiences() + .iter() + .map(|x| x.as_str()) + .collect::>() + .join(","), + }); + } + } + if let Some(bound_claims) = &mapping.bound_claims + && let Some(required_claims) = bound_claims.as_object() + { + for (claim, value) in required_claims.iter() { + if !claims_as_json + .get(claim) + .map(|x| x == value) + .is_some_and(|val| val) + { + return Err(OidcError::BoundClaimsMismatch { + claim: claim.to_string(), + expected: value.to_string(), + found: claims_as_json + .get(claim) + .map(|x| x.to_string()) + .unwrap_or_default(), + }); + } + } + } + Ok(()) +} + +/// Map the user data using the referred mapping. +/// +/// # Arguments +/// * `idp` - The identity provider +/// * `mapping` - The mapping to use +/// * `claims_as_json` - The claims as json +/// +/// # Returns +/// The mapped user data. +pub(super) async fn map_user_data( + _state: &ServiceState, + idp: &ProviderIdentityProvider, + mapping: &ProviderMapping, + claims_as_json: &Value, +) -> Result { + let mut builder = MappedUserDataBuilder::default(); + //if let Some(token_user_id) = &mapping.token_user_id { + // // TODO: How to check that the user belongs to the right domain) + // if let Ok(Some(user)) = state + // .provider + // .get_identity_provider() + // .get_user(&state.db, token_user_id) + // .await + // { + // builder.unique_id(token_user_id.clone()); + // builder.user_name(user.name.clone()); + // } else { + // return Err(OidcError::UserNotFound(token_user_id.clone()))?; + // } + //} else { + builder.unique_id( + claims_as_json + .get(&mapping.user_id_claim) + .and_then(|x| x.as_str()) + .ok_or_else(|| OidcError::UserIdClaimRequired(mapping.user_id_claim.clone()))? + .to_string(), + ); + + builder.user_name( + claims_as_json + .get(&mapping.user_name_claim) + .and_then(|x| x.as_str()) + .ok_or_else(|| OidcError::UserNameClaimRequired(mapping.user_name_claim.clone()))?, + ); + //} + + builder.domain_id( + mapping + .domain_id + .as_ref() + .or(idp.domain_id.as_ref()) + .or(mapping + .domain_id_claim + .as_ref() + .and_then(|claim| { + claims_as_json + .get(claim) + .and_then(|x| x.as_str().map(|v| v.to_string())) + }) + .as_ref()) + .ok_or(OidcError::UserDomainUnbound)?, + ); + + if let Some(groups_claim) = &mapping.groups_claim + && let Some(group_names_data) = &claims_as_json.get(groups_claim) + { + builder.group_names( + group_names_data + .as_array() + .map(|names| { + names + .iter() + .map(|group| group.as_str().map(|v| v.to_string())) + .collect::>>() + }) + .ok_or(OidcError::GroupsClaimNotArrayOfStrings)?, + ); + } + + Ok(builder.build()?) +} diff --git a/crates/core/src/federation/api/error.rs b/crates/core/src/federation/api/error.rs new file mode 100644 index 00000000..2a952be7 --- /dev/null +++ b/crates/core/src/federation/api/error.rs @@ -0,0 +1,265 @@ +// 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 + +//use thiserror::Error; +//use tracing::{Level, error, instrument}; + +use crate::api::error::KeystoneApiError; +//use crate::federation::api::types::*; +use crate::federation::error::FederationProviderError; + +//#[derive(Error, Debug)] +//pub enum OidcError { +// /// OIDC Discovery error. +// #[error("discovery error for {url}: {msg}")] +// Discovery { +// /// IdP URL. +// url: String, +// /// Error message. +// msg: String, +// }, +// +// #[error("client without discovery is not supported")] +// ClientWithoutDiscoveryNotSupported, +// +// #[error( +// "federated authentication requires mapping being specified in the payload or default set on the identity provider" +// )] +// MappingRequired, +// +// #[error("JWT login requires `openstack-mapping` header to be present.")] +// MappingRequiredJwt, +// +// #[error("`bearer` authorization token is missing.")] +// BearerJwtTokenMissing, +// +// #[error("mapping id or mapping name with idp id must be specified")] +// MappingIdOrNameWithIdp, +// +// #[error("groups claim must be an array of strings")] +// GroupsClaimNotArrayOfStrings, +// +// /// IdP is disabled. +// #[error("identity provider is disabled")] +// IdentityProviderDisabled, +// +// /// Mapping is disabled. +// #[error("mapping is disabled")] +// MappingDisabled, +// +// #[error("request token error")] +// RequestToken { msg: String }, +// +// #[error("claim verification error")] +// ClaimVerification { +// #[from] +// source: openidconnect::ClaimsVerificationError, +// }, +// +// #[error(transparent)] +// OpenIdConnectReqwest { +// #[from] +// source: openidconnect::reqwest::Error, +// }, +// +// #[error(transparent)] +// OpenIdConnectConfiguration { +// #[from] +// source: openidconnect::ConfigurationError, +// }, +// +// #[error(transparent)] +// UrlParse { +// #[from] +// source: url::ParseError, +// }, +// +// #[error("server did not returned an ID token")] +// NoToken, +// +// #[error("identity Provider client_id is missing")] +// ClientIdRequired, +// +// #[error("ID token does not contain user id claim {0}")] +// UserIdClaimRequired(String), +// +// #[error("ID token does not contain user id claim {0}")] +// UserNameClaimRequired(String), +// +// /// Domain_id for the user cannot be identified. +// #[error("can not identify resulting domain_id for the user")] +// UserDomainUnbound, +// +// /// Bound subject mismatch. +// #[error("bound subject mismatches {expected} != {found}")] +// BoundSubjectMismatch { expected: String, found: String }, +// +// /// Bound audiences mismatch. +// #[error("bound audiences mismatch {expected} != {found}")] +// BoundAudiencesMismatch { expected: String, found: String }, +// +// /// Bound claims mismatch. +// #[error("bound claims mismatch")] +// BoundClaimsMismatch { +// claim: String, +// expected: String, +// found: String, +// }, +// +// /// Error building user data. +// #[error(transparent)] +// MappedUserDataBuilder { +// #[from] +// #[allow(private_interfaces)] +// source: MappedUserDataBuilderError, +// }, +// +// /// Authentication expired. +// #[error("authentication expired")] +// AuthStateExpired, +// +// /// Cannot use OIDC attribute mapping for JWT login. +// #[error("non jwt mapping requested for jwt login")] +// NonJwtMapping, +// +// /// No JWT issuer can be identified for the mapping. +// #[error("no jwt issuer can be determined")] +// NoJwtIssuer, +// +// /// User not found. +// #[error("token user not found")] +// UserNotFound(String), +//} +// +//impl OidcError { +// pub fn discovery, T: std::error::Error>(url: U, fail: &T) -> Self { +// Self::Discovery { +// url: url.as_ref().to_string(), +// msg: fail.to_string(), +// } +// } +// pub fn request_token(fail: &T) -> Self { +// Self::RequestToken { +// msg: fail.to_string(), +// } +// } +//} +// +///// Convert OIDC error into the [HTTP](KeystoneApiError) with the expected +///// message. +//impl From for KeystoneApiError { +// #[instrument(level = Level::ERROR)] +// fn from(value: OidcError) -> Self { +// error!("Federation error: {:#?}", value); +// match value { +// e @ OidcError::Discovery { .. } => { +// KeystoneApiError::InternalError(e.to_string()) +// } +// e @ OidcError::ClientWithoutDiscoveryNotSupported => { +// KeystoneApiError::InternalError(e.to_string()) +// } +// OidcError::IdentityProviderDisabled => { +// KeystoneApiError::BadRequest("Federated Identity Provider is disabled.".to_string()) +// } +// OidcError::MappingDisabled => { +// KeystoneApiError::BadRequest("Federated Identity Provider mapping is disabled.".to_string()) +// } +// OidcError::MappingRequired => { +// KeystoneApiError::BadRequest("Federated authentication requires mapping being specified in the payload or default set on the identity provider.".to_string()) +// } +// OidcError::MappingRequiredJwt => { +// KeystoneApiError::BadRequest("JWT authentication requires `openstack-mapping` header to be provided.".to_string()) +// } +// OidcError::BearerJwtTokenMissing => { +// KeystoneApiError::BadRequest("`bearer` token is missing in the `Authorization` header.".to_string()) +// } +// OidcError::MappingIdOrNameWithIdp => { +// KeystoneApiError::BadRequest("Federated authentication requires mapping being specified in the payload either with ID or name with identity provider id.".to_string()) +// } +// OidcError::GroupsClaimNotArrayOfStrings => { +// KeystoneApiError::BadRequest("Groups claim must be an array of strings representing group names.".to_string()) +// } +// OidcError::RequestToken { msg } => { +// KeystoneApiError::BadRequest(format!("Error exchanging authorization code for the authorization token: {msg}")) +// } +// OidcError::ClaimVerification { source } => { +// KeystoneApiError::BadRequest(format!("Error in claims verification: {source}")) +// } +// OidcError::OpenIdConnectReqwest { source } => { +// KeystoneApiError::InternalError(format!("Error in OpenIDConnect logic: {source}")) +// } +// OidcError::OpenIdConnectConfiguration { source } => { +// KeystoneApiError::InternalError(format!("Error in OpenIDConnect logic: {source}")) +// } +// OidcError::UrlParse { source } => { +// KeystoneApiError::BadRequest(format!("Error in OpenIDConnect logic: {source}")) +// } +// e @ OidcError::NoToken => { +// KeystoneApiError::InternalError(format!("Error in OpenIDConnect logic: {e}")) +// } +// OidcError::ClientIdRequired => { +// KeystoneApiError::BadRequest("Identity Provider mut set `client_id`.".to_string()) +// } +// OidcError::UserIdClaimRequired(source) => { +// KeystoneApiError::BadRequest(format!("OIDC ID token does not contain user id claim: {source}")) +// } +// OidcError::UserNameClaimRequired(source) => { +// KeystoneApiError::BadRequest(format!("OIDC ID token does not contain user name claim: {source}")) +// } +// OidcError::UserDomainUnbound => { +// KeystoneApiError::BadRequest("Cannot identify domain_id of the user.".to_string()) +// } +// OidcError::BoundSubjectMismatch{ expected, found } => { +// KeystoneApiError::BadRequest(format!("OIDC Bound subject mismatches: {expected} != {found}")) +// } +// OidcError::BoundAudiencesMismatch{ expected, found } => { +// KeystoneApiError::BadRequest(format!("OIDC Bound audiences mismatches: {expected} != {found}")) +// } +// OidcError::BoundClaimsMismatch{ claim, expected, found } => { +// KeystoneApiError::BadRequest(format!("OIDC Bound claim {claim} mismatch: {expected} != {found}")) +// } +// e @ OidcError::MappedUserDataBuilder { .. } => { +// KeystoneApiError::InternalError(e.to_string()) +// } +// OidcError::AuthStateExpired => { +// KeystoneApiError::BadRequest("Authentication has expired. Please start again.".to_string()) +// } +// OidcError::NonJwtMapping | OidcError::NoJwtIssuer => { +// // Not exposing info about mapping and idp existence. +// KeystoneApiError::unauthorized(value, Some("mapping error")) +// } +// OidcError::UserNotFound(_) => { +// // Not exposing info about mapping and idp existence. +// KeystoneApiError::unauthorized(value, Some("User not found")) +// } +// } +// } +//} + +impl From for KeystoneApiError { + fn from(source: FederationProviderError) -> Self { + match source { + FederationProviderError::IdentityProviderNotFound(x) => Self::NotFound { + resource: "identity provider".into(), + identifier: x, + }, + FederationProviderError::MappingNotFound(x) => Self::NotFound { + resource: "mapping provider".into(), + identifier: x, + }, + FederationProviderError::Conflict(x) => Self::Conflict(x), + other => Self::InternalError(other.to_string()), + } + } +} diff --git a/crates/core/src/federation/api/identity_provider.rs b/crates/core/src/federation/api/identity_provider.rs new file mode 100644 index 00000000..f4d53920 --- /dev/null +++ b/crates/core/src/federation/api/identity_provider.rs @@ -0,0 +1,107 @@ +// 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 + +//! # Identity providers API +use utoipa_axum::{router::OpenApiRouter, routes}; + +use crate::keystone::ServiceState; + +mod create; +mod delete; +mod list; +mod show; +mod update; + +pub(super) fn openapi_router() -> OpenApiRouter { + OpenApiRouter::new() + .routes(routes!(list::list, create::create)) + .routes(routes!(show::show, update::update, delete::remove)) +} + +#[cfg(test)] +mod tests { + use sea_orm::DatabaseConnection; + use std::sync::Arc; + + use crate::config::Config; + use crate::federation::MockFederationProvider; + use crate::identity::types::UserResponseBuilder; + use crate::keystone::{Service, ServiceState}; + use crate::policy::{MockPolicy, MockPolicyFactory, PolicyError, PolicyEvaluationResult}; + use crate::provider::Provider; + use crate::token::{MockTokenProvider, Token, UnscopedPayload}; + + pub(crate) fn get_mocked_state( + federation_mock: MockFederationProvider, + policy_allowed: bool, + policy_allowed_see_other_domains: Option, + ) -> ServiceState { + let mut token_mock = MockTokenProvider::default(); + token_mock.expect_validate_token().returning(|_, _, _, _| { + Ok(Token::Unscoped(UnscopedPayload { + user_id: "bar".into(), + user: Some( + UserResponseBuilder::default() + .id("bar") + .domain_id("udid") + .enabled(true) + .name("name") + .build() + .unwrap(), + ), + ..Default::default() + })) + }); + + let provider = Provider::mocked_builder() + .federation(federation_mock) + .token(token_mock) + .build() + .unwrap(); + + let mut policy_factory_mock = MockPolicyFactory::default(); + if policy_allowed { + policy_factory_mock.expect_instantiate().returning(move || { + let mut policy_mock = MockPolicy::default(); + if policy_allowed_see_other_domains.is_some_and(|x| x) { + policy_mock + .expect_enforce() + .returning(|_, _, _, _| Ok(PolicyEvaluationResult::allowed_admin())); + } else { + policy_mock + .expect_enforce() + .returning(|_, _, _, _| Ok(PolicyEvaluationResult::allowed())); + } + Ok(policy_mock) + }); + } else { + policy_factory_mock.expect_instantiate().returning(|| { + let mut policy_mock = MockPolicy::default(); + policy_mock.expect_enforce().returning(|_, _, _, _| { + Err(PolicyError::Forbidden(PolicyEvaluationResult::forbidden())) + }); + Ok(policy_mock) + }); + } + Arc::new( + Service::new( + Config::default(), + DatabaseConnection::Disconnected, + provider, + policy_factory_mock, + ) + .unwrap(), + ) + } +} diff --git a/crates/core/src/federation/api/identity_provider/create.rs b/crates/core/src/federation/api/identity_provider/create.rs new file mode 100644 index 00000000..42e82f8f --- /dev/null +++ b/crates/core/src/federation/api/identity_provider/create.rs @@ -0,0 +1,147 @@ +// 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 + +//! Identity providers: create IDP. +use axum::{Json, debug_handler, extract::State, http::StatusCode, response::IntoResponse}; +use mockall_double::double; +use validator::Validate; + +use crate::api::auth::Auth; +use crate::api::error::KeystoneApiError; +use crate::federation::{FederationApi, api::types::*}; +use crate::keystone::ServiceState; +#[double] +use crate::policy::Policy; + +/// Create the identity provider. +/// +/// Create the identity provider with the specified properties. +/// +/// It is expected that only admin user is able to create global identity +/// providers. +#[utoipa::path( + post, + path = "/", + operation_id = "/federation/identity_provider:create", + responses( + (status = CREATED, description = "identity provider object", body = IdentityProviderResponse), + ), + security(("x-auth" = [])), + tag="identity_providers" +)] +#[tracing::instrument( + name = "api::identity_provider_create", + level = "debug", + skip(state, user_auth, policy), + err(Debug) +)] +#[debug_handler] +pub(super) async fn create( + Auth(user_auth): Auth, + mut policy: Policy, + State(state): State, + Json(req): Json, +) -> Result { + req.validate()?; + policy + .enforce( + "identity/identity_provider_create", + &user_auth, + serde_json::to_value(&req.identity_provider)?, + None, + ) + .await?; + + let res = state + .provider + .get_federation_provider() + .create_identity_provider(&state, req.into()) + .await?; + Ok((StatusCode::CREATED, res).into_response()) +} + +#[cfg(test)] +mod tests { + use axum::{ + body::Body, + http::{Request, StatusCode, header}, + }; + use http_body_util::BodyExt; // for `collect` + + use tower::ServiceExt; // for `call`, `oneshot`, and `ready` + use tower_http::trace::TraceLayer; + use tracing_test::traced_test; + + use super::{ + super::{openapi_router, tests::get_mocked_state}, + *, + }; + + use crate::federation::{MockFederationProvider, types as provider_types}; + + #[tokio::test] + #[traced_test] + async fn test_create() { + let mut federation_mock = MockFederationProvider::default(); + federation_mock + .expect_create_identity_provider() + .withf(|_, req: &provider_types::IdentityProviderCreate| req.name == "name") + .returning(|_, _| { + Ok(provider_types::IdentityProvider { + id: "bar".into(), + name: "name".into(), + domain_id: Some("did".into()), + ..Default::default() + }) + }); + + let state = get_mocked_state(federation_mock, true, None); + + let mut api = openapi_router() + .layer(TraceLayer::new_for_http()) + .with_state(state.clone()); + + let req = IdentityProviderCreateRequest { + identity_provider: IdentityProviderCreate { + name: "name".into(), + domain_id: Some("did".into()), + ..Default::default() + }, + }; + + let response = api + .as_service() + .oneshot( + Request::builder() + .method("POST") + .header(header::CONTENT_TYPE, "application/json") + .uri("/") + .header("x-auth-token", "foo") + .body(Body::from(serde_json::to_string(&req).unwrap())) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::CREATED); + + let body = response.into_body().collect().await.unwrap().to_bytes(); + let res: IdentityProviderResponse = serde_json::from_slice(&body).unwrap(); + assert_eq!(res.identity_provider.name, req.identity_provider.name); + assert_eq!( + res.identity_provider.domain_id, + req.identity_provider.domain_id + ); + } +} diff --git a/crates/core/src/federation/api/identity_provider/delete.rs b/crates/core/src/federation/api/identity_provider/delete.rs new file mode 100644 index 00000000..ca007a59 --- /dev/null +++ b/crates/core/src/federation/api/identity_provider/delete.rs @@ -0,0 +1,182 @@ +// 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 + +//! Identity providers: delete IDP. +use axum::{ + extract::{Path, State}, + http::StatusCode, + response::IntoResponse, +}; +use mockall_double::double; + +use crate::api::auth::Auth; +use crate::api::error::KeystoneApiError; +use crate::federation::FederationApi; +use crate::keystone::ServiceState; +#[double] +use crate::policy::Policy; + +/// Delete Identity provider. +/// +/// Deletes the existing identity provider. +/// +/// It is expected that only admin user is allowed to delete the global identity +/// provider +#[utoipa::path( + delete, + path = "/{idp_id}", + operation_id = "/federation/identity_provider:delete", + params( + ("idp_id" = String, Path, description = "The ID of the identity provider") + ), + responses( + (status = 204, description = "Deleted"), + (status = 404, description = "identity provider not found", example = json!(KeystoneApiError::NotFound(String::from("id = 1")))) + ), + security(("x-auth" = [])), + tag="identity_providers" +)] +#[tracing::instrument( + name = "api::identity_provider_delete", + level = "debug", + skip(state, user_auth, policy), + err(Debug) +)] +pub(super) async fn remove( + Auth(user_auth): Auth, + mut policy: Policy, + Path(id): Path, + State(state): State, +) -> Result { + let current = state + .provider + .get_federation_provider() + .get_identity_provider(&state, &id) + .await?; + + policy + .enforce( + "identity/identity_provider_delete", + &user_auth, + serde_json::to_value(¤t)?, + None, + ) + .await?; + + // TODO: decide what to do with the users provisioned using this IDP, mappings, + // ... + + if current.is_some() { + state + .provider + .get_federation_provider() + .delete_identity_provider(&state, &id) + .await?; + } else { + return Err(KeystoneApiError::NotFound { + resource: "identity_provider".to_string(), + identifier: id.clone(), + }); + } + Ok((StatusCode::NO_CONTENT).into_response()) +} + +#[cfg(test)] +mod tests { + use axum::{ + body::Body, + http::{Request, StatusCode}, + }; + // for `collect` + + use tower::ServiceExt; // for `call`, `oneshot`, and `ready` + use tower_http::trace::TraceLayer; + use tracing_test::traced_test; + + use super::super::{openapi_router, tests::get_mocked_state}; + + use crate::federation::{ + MockFederationProvider, error::FederationProviderError, types as provider_types, + }; + + #[tokio::test] + #[traced_test] + async fn test_delete() { + let mut federation_mock = MockFederationProvider::default(); + federation_mock + .expect_get_identity_provider() + .withf(|_, id: &'_ str| id == "foo") + .returning(|_, _| Ok(None)); + federation_mock + .expect_get_identity_provider() + .withf(|_, id: &'_ str| id == "bar") + .returning(|_, _| { + Ok(Some(provider_types::IdentityProvider { + id: "bar".into(), + name: "name".into(), + domain_id: Some("did".into()), + ..Default::default() + })) + }); + federation_mock + .expect_delete_identity_provider() + .withf(|_, id: &'_ str| id == "foo") + .returning(|_, _| { + Err(FederationProviderError::IdentityProviderNotFound( + "foo".into(), + )) + }); + + federation_mock + .expect_delete_identity_provider() + .withf(|_, id: &'_ str| id == "bar") + .returning(|_, _| Ok(())); + + let state = get_mocked_state(federation_mock, true, None); + + let mut api = openapi_router() + .layer(TraceLayer::new_for_http()) + .with_state(state.clone()); + + let response = api + .as_service() + .oneshot( + Request::builder() + .method("DELETE") + .uri("/foo") + .header("x-auth-token", "foo") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::NOT_FOUND); + + let response = api + .as_service() + .oneshot( + Request::builder() + .method("DELETE") + .uri("/bar") + .header("x-auth-token", "foo") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::NO_CONTENT); + } +} diff --git a/crates/core/src/federation/api/identity_provider/list.rs b/crates/core/src/federation/api/identity_provider/list.rs new file mode 100644 index 00000000..f2c081b1 --- /dev/null +++ b/crates/core/src/federation/api/identity_provider/list.rs @@ -0,0 +1,409 @@ +// 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 + +//! Identity providers: list IDP. +use axum::{ + extract::{OriginalUri, Query, State}, + response::IntoResponse, +}; +use mockall_double::double; +use serde_json::to_value; +use std::collections::HashSet; +use validator::Validate; + +use crate::api::{KeystoneApiError, auth::Auth, common::build_pagination_links}; +use crate::federation::{ + FederationApi, api::types::*, + types::IdentityProviderListParameters as ProviderIdentityProviderListParameters, +}; +use crate::keystone::ServiceState; +#[double] +use crate::policy::Policy; + +/// List identity providers. +/// +/// List identity providers. Without any filters only global identity providers +/// are returned. With the `domain_id` identity providers owned by the specified +/// identity provider are returned. +/// +/// It is expected that only global or owned identity providers can be returned, +/// while an admin user is able to list all providers. +#[utoipa::path( + get, + path = "/", + operation_id = "/federation/identity_provider:list", + params(IdentityProviderListParameters), + responses( + (status = OK, description = "List of identity providers", body = IdentityProviderList), + (status = 500, description = "Internal error", example = json!(KeystoneApiError::InternalError(String::from("id = 1")))) + ), + security(("x-auth" = [])), + tag="identity_providers" +)] +#[tracing::instrument( + name = "api::identity_provider_list", + level = "debug", + skip(state, user_auth, policy), + err(Debug) +)] +pub(super) async fn list( + Auth(user_auth): Auth, + mut policy: Policy, + OriginalUri(original_url): OriginalUri, + Query(query): Query, + State(state): State, +) -> Result { + query.validate()?; + let res = policy + .enforce( + "identity/identity_provider_list", + &user_auth, + to_value(&query)?, + None, + ) + .await?; + + let domain_ids = if query.domain_id.as_ref().is_none() { + if !res.can_see_other_domain_resources.is_some_and(|x| x) { + let domain_ids: HashSet> = HashSet::from([ + None, + // TODO: perhaps we should first look at the domain_scope and than user domain. + user_auth.user().as_ref().map(|val| val.domain_id.clone()), + ]); + Some(domain_ids) + } else { + // User can see other domain's resources and query is empty - leave it empty + None + } + } else { + Some(HashSet::from([query.domain_id.clone()])) + }; + let mut provider_list_params = ProviderIdentityProviderListParameters::from(query.clone()); + provider_list_params.domain_ids = domain_ids; + + let identity_providers: Vec = state + .provider + .get_federation_provider() + .list_identity_providers(&state, &provider_list_params) + .await? + .into_iter() + .map(Into::into) + .collect(); + + let links = build_pagination_links( + &state.config, + identity_providers.as_slice(), + &query, + original_url.path(), + )?; + Ok(IdentityProviderList { + identity_providers, + links, + }) +} + +#[cfg(test)] +mod tests { + use axum::{ + body::Body, + http::{Request, StatusCode}, + }; + use http_body_util::BodyExt; // for `collect` + + use std::collections::HashSet; + + use tower::ServiceExt; // for `call`, `oneshot`, and `ready` + use tower_http::trace::TraceLayer; + use tracing_test::traced_test; + + use super::{ + super::{openapi_router, tests::get_mocked_state}, + *, + }; + use crate::federation::{MockFederationProvider, types as provider_types}; + + #[tokio::test] + #[traced_test] + async fn test_list() { + let mut federation_mock = MockFederationProvider::default(); + federation_mock + .expect_list_identity_providers() + .withf(|_, _: &provider_types::IdentityProviderListParameters| true) + .returning(|_, _| { + Ok(vec![provider_types::IdentityProvider { + id: "id".into(), + name: "name".into(), + domain_id: Some("did".into()), + enabled: true, + default_mapping_name: Some("dummy".into()), + ..Default::default() + }]) + }); + let state = get_mocked_state(federation_mock, true, None); + + let mut api = openapi_router() + .layer(TraceLayer::new_for_http()) + .with_state(state); + + let response = api + .as_service() + .oneshot( + Request::builder() + .uri("/") + .header("x-auth-token", "foo") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + + let body = response.into_body().collect().await.unwrap().to_bytes(); + let res: IdentityProviderList = serde_json::from_slice(&body).unwrap(); + assert_eq!( + vec![IdentityProvider { + id: "id".into(), + name: "name".into(), + domain_id: Some("did".into()), + enabled: true, + oidc_discovery_url: None, + oidc_client_id: None, + oidc_response_mode: None, + oidc_response_types: None, + jwks_url: None, + jwt_validation_pubkeys: None, + bound_issuer: None, + default_mapping_name: Some("dummy".into()), + provider_config: None + }], + res.identity_providers + ); + } + + #[tokio::test] + #[traced_test] + async fn test_list_qp() { + let mut federation_mock = MockFederationProvider::default(); + federation_mock + .expect_list_identity_providers() + .withf(|_, qp: &provider_types::IdentityProviderListParameters| { + provider_types::IdentityProviderListParameters { + name: Some("name".into()), + domain_ids: Some(HashSet::from([Some("did".into())])), + limit: Some(1), + marker: Some("marker".into()), + } == *qp + }) + .returning(|_, _| { + Ok(vec![provider_types::IdentityProvider { + id: "id".into(), + name: "name".into(), + domain_id: Some("did".into()), + ..Default::default() + }]) + }); + + let state = get_mocked_state(federation_mock, true, None); + + let mut api = openapi_router() + .layer(TraceLayer::new_for_http()) + .with_state(state); + + let response = api + .as_service() + .oneshot( + Request::builder() + .uri("/?name=name&domain_id=did&limit=1&marker=marker") + .header("x-auth-token", "foo") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + + let body = response.into_body().collect().await.unwrap().to_bytes(); + let _res: IdentityProviderList = serde_json::from_slice(&body).unwrap(); + } + + #[tokio::test] + #[traced_test] + async fn test_list_forbidden() { + let federation_mock = MockFederationProvider::default(); + let state = get_mocked_state(federation_mock, false, None); + + let mut api = openapi_router() + .layer(TraceLayer::new_for_http()) + .with_state(state); + + let response = api + .as_service() + .oneshot( + Request::builder() + .uri("/") + .header("x-auth-token", "foo") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::FORBIDDEN); + } + + #[tokio::test] + #[traced_test] + async fn test_list_shared_and_own() { + let mut federation_mock = MockFederationProvider::default(); + federation_mock + .expect_list_identity_providers() + .withf(|_, qp: &provider_types::IdentityProviderListParameters| { + provider_types::IdentityProviderListParameters { + name: Some("name".into()), + domain_ids: Some(HashSet::from([None, Some("udid".into())])), + limit: Some(20), + marker: None, + } == *qp + }) + .returning(|_, _| { + Ok(vec![provider_types::IdentityProvider { + id: "id".into(), + name: "name".into(), + domain_id: Some("did".into()), + ..Default::default() + }]) + }); + + let state = get_mocked_state(federation_mock, true, Some(false)); + + let mut api = openapi_router() + .layer(TraceLayer::new_for_http()) + .with_state(state); + + let response = api + .as_service() + .oneshot( + Request::builder() + .uri("/?name=name") + .header("x-auth-token", "foo") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + + let body = response.into_body().collect().await.unwrap().to_bytes(); + let _res: IdentityProviderList = serde_json::from_slice(&body).unwrap(); + } + + #[tokio::test] + #[traced_test] + async fn test_list_all() { + // Test listing ALL idps when the user does not specify the domain_id and is + // allowed to see IDP of other domains (admin) + let mut federation_mock = MockFederationProvider::default(); + federation_mock + .expect_list_identity_providers() + .withf(|_, qp: &provider_types::IdentityProviderListParameters| { + provider_types::IdentityProviderListParameters { + name: Some("name".into()), + domain_ids: None, + limit: Some(20), + marker: None, + } == *qp + }) + .returning(|_, _| { + Ok(vec![provider_types::IdentityProvider { + id: "id".into(), + name: "name".into(), + domain_id: Some("did".into()), + ..Default::default() + }]) + }); + + let state = get_mocked_state(federation_mock, true, Some(true)); + + let mut api = openapi_router() + .layer(TraceLayer::new_for_http()) + .with_state(state); + + let response = api + .as_service() + .oneshot( + Request::builder() + .uri("/?name=name") + .header("x-auth-token", "foo") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + + let body = response.into_body().collect().await.unwrap().to_bytes(); + let _res: IdentityProviderList = serde_json::from_slice(&body).unwrap(); + } + + #[tokio::test] + #[traced_test] + async fn test_list_pagination_link() { + let mut federation_mock = MockFederationProvider::default(); + federation_mock + .expect_list_identity_providers() + .withf(|_, qp: &provider_types::IdentityProviderListParameters| { + provider_types::IdentityProviderListParameters { + limit: Some(1), + domain_ids: Some(HashSet::from([None, Some("udid".into())])), + ..Default::default() + } == *qp + }) + .returning(|_, _| { + Ok(vec![provider_types::IdentityProvider { + id: "id".into(), + name: "name".into(), + domain_id: Some("did".into()), + ..Default::default() + }]) + }); + + let state = get_mocked_state(federation_mock, true, Some(false)); + + let mut api = openapi_router() + .layer(TraceLayer::new_for_http()) + .with_state(state); + + let response = api + .as_service() + .oneshot( + Request::builder() + .uri("/?limit=1") + .header("x-auth-token", "foo") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + + let body = response.into_body().collect().await.unwrap().to_bytes(); + let res: IdentityProviderList = serde_json::from_slice(&body).unwrap(); + assert!(res.links.is_some()); + } +} diff --git a/crates/core/src/federation/api/identity_provider/show.rs b/crates/core/src/federation/api/identity_provider/show.rs new file mode 100644 index 00000000..ca66068e --- /dev/null +++ b/crates/core/src/federation/api/identity_provider/show.rs @@ -0,0 +1,216 @@ +// 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 + +//! Identity providers: show IDP. +use axum::{ + extract::{Path, State}, + response::IntoResponse, +}; +use mockall_double::double; + +use crate::api::auth::Auth; +use crate::api::error::KeystoneApiError; +use crate::federation::{FederationApi, api::types::*}; +use crate::keystone::ServiceState; +#[double] +use crate::policy::Policy; + +/// Get single identity provider. +/// +/// Shows details of the existing identity provider. +#[utoipa::path( + get, + path = "/{idp_id}", + operation_id = "/federation/identity_provider:show", + params( + ("idp_id" = String, Path, description = "The ID of the identity provider") + ), + responses( + (status = OK, description = "Identity provider object", body = IdentityProviderResponse), + (status = 404, description = "Resource not found", example = json!(KeystoneApiError::NotFound(String::from("id = 1")))) + ), + security(("x-auth" = [])), + tag="identity_providers" +)] +#[tracing::instrument( + name = "api::identity_provider_get", + level = "debug", + skip(state, user_auth, policy), + err(Debug) +)] +pub(super) async fn show( + Auth(user_auth): Auth, + mut policy: Policy, + Path(idp_id): Path, + State(state): State, +) -> Result { + let current = state + .provider + .get_federation_provider() + .get_identity_provider(&state, &idp_id) + .await + .map(|x| { + x.ok_or_else(|| KeystoneApiError::NotFound { + resource: "identity provider".into(), + identifier: idp_id, + }) + })??; + + policy + .enforce( + "identity/identity_provider_show", + &user_auth, + serde_json::to_value(¤t)?, + None, + ) + .await?; + Ok(current) +} + +#[cfg(test)] +mod tests { + use axum::{ + body::Body, + http::{Request, StatusCode}, + }; + use http_body_util::BodyExt; // for `collect` + + use tower::ServiceExt; // for `call`, `oneshot`, and `ready` + use tower_http::trace::TraceLayer; + use tracing_test::traced_test; + + use super::{ + super::{openapi_router, tests::get_mocked_state}, + *, + }; + + use crate::federation::{MockFederationProvider, types as provider_types}; + + #[tokio::test] + #[traced_test] + async fn test_get() { + let mut federation_mock = MockFederationProvider::default(); + federation_mock + .expect_get_identity_provider() + .withf(|_, id: &'_ str| id == "foo") + .returning(|_, _| Ok(None)); + + federation_mock + .expect_get_identity_provider() + .withf(|_, id: &'_ str| id == "bar") + .returning(|_, _| { + Ok(Some(provider_types::IdentityProvider { + id: "bar".into(), + name: "name".into(), + domain_id: Some("did".into()), + enabled: true, + default_mapping_name: Some("dummy".into()), + ..Default::default() + })) + }); + + let state = get_mocked_state(federation_mock, true, None); + + let mut api = openapi_router() + .layer(TraceLayer::new_for_http()) + .with_state(state.clone()); + + let response = api + .as_service() + .oneshot( + Request::builder() + .uri("/foo") + .header("x-auth-token", "foo") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::NOT_FOUND); + + let response = api + .as_service() + .oneshot( + Request::builder() + .uri("/bar") + .header("x-auth-token", "foo") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + + let body = response.into_body().collect().await.unwrap().to_bytes(); + let res: IdentityProviderResponse = serde_json::from_slice(&body).unwrap(); + assert_eq!( + IdentityProvider { + id: "bar".into(), + name: "name".into(), + domain_id: Some("did".into()), + enabled: true, + oidc_discovery_url: None, + oidc_client_id: None, + oidc_response_mode: None, + oidc_response_types: None, + jwks_url: None, + jwt_validation_pubkeys: None, + bound_issuer: None, + default_mapping_name: Some("dummy".into()), + provider_config: None + }, + res.identity_provider, + ); + } + + #[tokio::test] + #[traced_test] + async fn test_get_forbidden() { + let mut federation_mock = MockFederationProvider::default(); + federation_mock + .expect_get_identity_provider() + .withf(|_, id: &'_ str| id == "bar") + .returning(|_, _| { + Ok(Some(provider_types::IdentityProvider { + id: "bar".into(), + name: "name".into(), + domain_id: Some("did".into()), + default_mapping_name: Some("dummy".into()), + ..Default::default() + })) + }); + + let state = get_mocked_state(federation_mock, false, None); + + let mut api = openapi_router() + .layer(TraceLayer::new_for_http()) + .with_state(state.clone()); + + let response = api + .as_service() + .oneshot( + Request::builder() + .uri("/bar") + .header("x-auth-token", "foo") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::FORBIDDEN); + } +} diff --git a/crates/core/src/federation/api/identity_provider/update.rs b/crates/core/src/federation/api/identity_provider/update.rs new file mode 100644 index 00000000..a7fb7fbc --- /dev/null +++ b/crates/core/src/federation/api/identity_provider/update.rs @@ -0,0 +1,169 @@ +// 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 + +//! Identity providers: update existing IDP. +use axum::{ + Json, + extract::{Path, State}, + response::IntoResponse, +}; +use mockall_double::double; +use validator::Validate; + +use crate::api::auth::Auth; +use crate::api::error::KeystoneApiError; +use crate::federation::{FederationApi, api::types::*}; +use crate::keystone::ServiceState; +#[double] +use crate::policy::Policy; + +/// Update single identity provider. +/// +/// Updates the existing identity provider. +#[utoipa::path( + put, + path = "/{idp_id}", + operation_id = "/federation/identity_provider:update", + params( + ("idp_id" = String, Path, description = "The ID of the identity provider") + ), + responses( + (status = OK, description = "IDP object", body = IdentityProviderResponse), + (status = 404, description = "IDP not found", example = json!(KeystoneApiError::NotFound(String::from("id = 1")))) + ), + security(("x-auth" = [])), + tag="identity_providers" +)] +#[tracing::instrument( + name = "api::identity_provider_update", + level = "debug", + skip(state, user_auth, policy), + err(Debug) +)] +pub(super) async fn update( + Auth(user_auth): Auth, + mut policy: Policy, + Path(idp_id): Path, + State(state): State, + Json(req): Json, +) -> Result { + req.validate()?; + // Fetch the current resource to pass current object into the policy evaluation + let current = state + .provider + .get_federation_provider() + .get_identity_provider(&state, &idp_id) + .await?; + + policy + .enforce( + "identity/identity_provider_update", + &user_auth, + serde_json::to_value(¤t)?, + Some(serde_json::to_value(&req.identity_provider)?), + ) + .await?; + + let res = state + .provider + .get_federation_provider() + .update_identity_provider(&state, &idp_id, req.into()) + .await?; + Ok(res.into_response()) +} + +#[cfg(test)] +mod tests { + use axum::{ + body::Body, + http::{Request, StatusCode, header}, + }; + use http_body_util::BodyExt; // for `collect` + + use tower::ServiceExt; // for `call`, `oneshot`, and `ready` + use tower_http::trace::TraceLayer; + use tracing_test::traced_test; + + use super::{ + super::{openapi_router, tests::get_mocked_state}, + *, + }; + + use crate::federation::{MockFederationProvider, types as provider_types}; + + #[tokio::test] + #[traced_test] + async fn test_update() { + let mut federation_mock = MockFederationProvider::default(); + federation_mock + .expect_get_identity_provider() + .withf(|_, id: &'_ str| id == "1") + .returning(|_, _| { + Ok(Some(provider_types::IdentityProvider { + id: "bar".into(), + name: "name".into(), + domain_id: Some("did".into()), + ..Default::default() + })) + }); + federation_mock + .expect_update_identity_provider() + .withf( + |_, id: &'_ str, req: &provider_types::IdentityProviderUpdate| { + id == "1" && req.name == Some("name".to_string()) + }, + ) + .returning(|_, _, _| { + Ok(provider_types::IdentityProvider { + id: "bar".into(), + name: "name".into(), + domain_id: Some("did".into()), + ..Default::default() + }) + }); + + let state = get_mocked_state(federation_mock, true, None); + + let mut api = openapi_router() + .layer(TraceLayer::new_for_http()) + .with_state(state.clone()); + + let req = IdentityProviderUpdateRequest { + identity_provider: IdentityProviderUpdate { + name: Some("name".into()), + oidc_client_id: Some(None), + ..Default::default() + }, + }; + + let response = api + .as_service() + .oneshot( + Request::builder() + .method("PUT") + .header(header::CONTENT_TYPE, "application/json") + .uri("/1") + .header("x-auth-token", "foo") + .body(Body::from(serde_json::to_string(&req).unwrap())) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + + let body = response.into_body().collect().await.unwrap().to_bytes(); + let _res: IdentityProviderResponse = serde_json::from_slice(&body).unwrap(); + } +} diff --git a/crates/core/src/federation/api/jwt.rs b/crates/core/src/federation/api/jwt.rs new file mode 100644 index 00000000..42c37c81 --- /dev/null +++ b/crates/core/src/federation/api/jwt.rs @@ -0,0 +1,370 @@ +// 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 + +//! JWT based authentication API. + +use axum::{ + Json, debug_handler, + extract::{Path, State}, + http::HeaderMap, + http::StatusCode, + http::header::AUTHORIZATION, + response::IntoResponse, +}; +use std::str::FromStr; +use tracing::warn; +use utoipa_axum::{router::OpenApiRouter, routes}; + +use openidconnect::core::{ + CoreClient, CoreGenderClaim, CoreJsonWebKey, CoreJweContentEncryptionAlgorithm, + CoreJwsSigningAlgorithm, CoreProviderMetadata, +}; +use openidconnect::reqwest; +use openidconnect::{Client, ClientId, IdToken, IssuerUrl, JsonWebKeySet, JsonWebKeySetUrl, Nonce}; + +use super::error::OidcError; +use crate::api::v4::auth::token::types::TokenResponse as KeystoneTokenResponse; +use crate::api::{ + KeystoneApiError, + common::get_authz_info, + types::{Catalog, CatalogService}, +}; +use crate::auth::{AuthenticatedInfo, AuthenticationError}; +use crate::catalog::CatalogApi; +use crate::common::types as provider_types; +use crate::federation::{ + FederationApi, + api::types::*, + types::{ + MappingListParameters as ProviderMappingListParameters, + MappingType as ProviderMappingType, + //Project as ProviderProject, Scope as ProviderScope, + }, +}; +use crate::identity::{ + IdentityApi, + error::IdentityProviderError, + types::{FederationBuilder, FederationProtocol, UserCreateBuilder}, +}; +use crate::keystone::ServiceState; +use crate::token::TokenApi; + +use super::common::{map_user_data, validate_bound_claims}; + +pub(super) fn openapi_router() -> OpenApiRouter { + OpenApiRouter::new().routes(routes!(login)) +} + +type FullIdToken = IdToken< + AllOtherClaims, + CoreGenderClaim, + CoreJweContentEncryptionAlgorithm, + CoreJwsSigningAlgorithm, +>; + +/// Authentication using the JWT. +/// +/// This operation allows user to exchange the JWT issued by the trusted +/// identity provider for the regular Keystone session token. Request specifies +/// the necessary authentication mapping, which is also used to validate +/// expected claims. +#[utoipa::path( + post, + //path = "/jwt/login", + path = "/identity_providers/{idp_id}/jwt", + operation_id = "/federation/identity_provider/jwt:login", + params( + ("openstack-mapping" = String, Header, description = "Federated attribute mapping"), + ), + responses( + ( + status = OK, + description = "Authentication Token object", + body = KeystoneTokenResponse, + headers( + ("x-subject-token" = String, description = "Keystone token"), + ), + ), + ), + security(("jwt" = [])), + tag="identity_providers" +)] +#[tracing::instrument( + name = "api::identity_provider_jwt_login", + level = "debug", + skip(state), + err(Debug) +)] +#[debug_handler] +pub async fn login( + State(state): State, + headers: HeaderMap, + Path(idp_id): Path, +) -> Result { + state + .config + .auth + .methods + .iter() + // TODO: is it how it should be hardcoded? + // TODO: should be better to use jwt, but it is not available in py-keystone + .find(|m| *m == "openid") + .ok_or(KeystoneApiError::AuthMethodNotSupported)?; + + let jwt: String = match headers + .get(AUTHORIZATION) + .ok_or(KeystoneApiError::SubjectTokenMissing)? + .to_str() + .map_err(|_| KeystoneApiError::InvalidHeader)? + .split_once(' ') + { + Some(("bearer", token)) => token.to_string(), + _ => return Err(OidcError::BearerJwtTokenMissing.into()), + }; + + let mapping: String = headers + .get("openstack-mapping") + .ok_or(OidcError::MappingRequiredJwt)? + .to_str() + .map_err(|_| KeystoneApiError::InvalidHeader)? + .to_string(); + + let idp = state + .provider + .get_federation_provider() + .get_identity_provider(&state, &idp_id) + .await + .map(|x| { + x.ok_or_else(|| KeystoneApiError::NotFound { + resource: "identity provider".into(), + identifier: idp_id.clone(), + }) + })??; + + let mapping = state + .provider + .get_federation_provider() + .list_mappings( + &state, + &ProviderMappingListParameters { + idp_id: Some(idp_id.clone()), + name: Some(mapping.clone()), + r#type: Some(ProviderMappingType::Jwt), + ..Default::default() + }, + ) + .await? + .first() + .ok_or(KeystoneApiError::NotFound { + resource: "mapping".into(), + identifier: mapping.clone(), + })? + .to_owned(); + + // Check for IdP and mapping `enabled` state + if !idp.enabled { + return Err(OidcError::IdentityProviderDisabled)?; + } + if !mapping.enabled { + return Err(OidcError::MappingDisabled)?; + } + + tracing::debug!("Mapping is {:?}", mapping); + let token_restriction = if let Some(tr_id) = &mapping.token_restriction_id { + state + .provider + .get_token_provider() + .get_token_restriction(&state, tr_id, true) + .await? + } else { + None + }; + + //if !matches!(mapping.r#type, ProviderMappingType::Jwt) { + // // need to log helping message, since the error is wrapped + // // to prevent existence exposure. + // warn!("Not JWT mapping used for the JWT login"); + // return Err(OidcError::NonJwtMapping)?; + //} + + let http_client = reqwest::ClientBuilder::new() + // Following redirects opens the client up to SSRF vulnerabilities. + .redirect(reqwest::redirect::Policy::none()) + .build() + .map_err(OidcError::from)?; + + // Discover metadata when issuer or jwks_url is not known + let provider_metadata: Option = if let Some(discovery_url) = + &idp.oidc_discovery_url + && (idp.bound_issuer.is_none() || idp.jwks_url.is_none()) + { + Some( + CoreProviderMetadata::discover_async( + IssuerUrl::new(discovery_url.to_string()).map_err(OidcError::from)?, + &http_client, + ) + .await + .map_err(|err| OidcError::discovery(discovery_url, &err))?, + ) + } else { + None + }; + + let issuer_url = if let Some(bound_issuer) = &idp.bound_issuer { + IssuerUrl::new(bound_issuer.clone()).map_err(OidcError::from)? + } else if let Some(metadata) = &provider_metadata { + metadata.issuer().clone() + } else { + warn!("No issuer_url can be determined for {:?}", idp); + return Err(OidcError::NoJwtIssuer)?; + }; + + let jwks_url = if let Some(jwks_url) = &idp.jwks_url { + JsonWebKeySetUrl::new(jwks_url.clone()).map_err(OidcError::from)? + } else if let Some(metadata) = &provider_metadata { + metadata.jwks_uri().clone() + } else { + warn!("No jwks_url can be determined for {:?}", idp); + return Err(OidcError::NoJwtIssuer)?; + }; + + let jwks: JsonWebKeySet = JsonWebKeySet::fetch_async(&jwks_url, &http_client) + .await + .map_err(|err| OidcError::discovery(jwks_url.as_str(), &err))?; + + // TODO: client_id should match the audience. How to get that? + let audience = "keystone"; + let client: CoreClient = Client::new(ClientId::new(audience.to_string()), issuer_url, jwks); + + let id_token = FullIdToken::from_str(&jwt)?; + + let id_token_verifier = client.id_token_verifier().require_audience_match(false); + // The nonce is not used in the JWT flow, so we can ignore it. + let nonce_verifier = |_nonce: Option<&Nonce>| Ok(()); + let claims = id_token + .into_claims(&id_token_verifier, &nonce_verifier) + .map_err(OidcError::from)?; + + let claims_as_json = serde_json::to_value(&claims)?; + + validate_bound_claims(&mapping, &claims, &claims_as_json)?; + let mapped_user_data = map_user_data(&state, &idp, &mapping, &claims_as_json).await?; + + let user = if let Some(existing_user) = state + .provider + .get_identity_provider() + .find_federated_user(&state, &idp.id, &mapped_user_data.unique_id) + .await? + { + // The user exists already + existing_user + + // TODO: update user? + } else { + // New user + let mut federated_user: FederationBuilder = FederationBuilder::default(); + federated_user.idp_id(idp.id.clone()); + federated_user.unique_id(mapped_user_data.unique_id.clone()); + federated_user.protocols(vec![FederationProtocol { + protocol_id: "oidc".into(), + unique_id: mapped_user_data.unique_id.clone(), + }]); + let mut user_builder: UserCreateBuilder = UserCreateBuilder::default(); + user_builder.domain_id(mapped_user_data.domain_id); + user_builder.enabled(true); + user_builder.name(mapped_user_data.user_name); + user_builder.federated(Vec::from([federated_user + .build() + .map_err(IdentityProviderError::from)?])); + + state + .provider + .get_identity_provider() + .create_user( + &state, + user_builder.build().map_err(IdentityProviderError::from)?, + ) + .await? + }; + let authed_info = AuthenticatedInfo::builder() + .user_id(user.id.clone()) + .user(user.clone()) + .methods(vec!["openid".into()]) + .idp_id(idp.id.clone()) + .protocol_id("jwt".to_string()) + .build() + .map_err(AuthenticationError::from)?; + authed_info.validate()?; + + // TODO: detect scope from the mapping or claims + let authz_info = get_authz_info( + &state, + mapping + .token_project_id + .as_ref() + .map(|token_project_id| { + provider_types::Scope::Project(provider_types::Project { + id: Some(token_project_id.to_string()), + ..Default::default() + }) + }) + .as_ref(), + ) + .await?; + + let mut token = state.provider.get_token_provider().issue_token( + authed_info, + authz_info, + token_restriction.as_ref(), + )?; + + // TODO: roles should be granted for the jwt login already + + token = state + .provider + .get_token_provider() + .expand_token_information(&state, &token) + .await + .map_err(KeystoneApiError::forbidden)?; + + let mut api_token = KeystoneTokenResponse { + token: token.build_api_token_v4(&state).await?, + }; + let catalog: Catalog = Catalog( + state + .provider + .get_catalog_provider() + .get_catalog(&state, true) + .await? + .into_iter() + .map(|(s, es)| CatalogService { + id: s.id.clone(), + name: s.name.clone(), + r#type: s.r#type, + endpoints: es.into_iter().map(Into::into).collect(), + }) + .collect::>(), + ); + api_token.token.catalog = Some(catalog); + + Ok(( + StatusCode::OK, + [( + "X-Subject-Token", + state.provider.get_token_provider().encode_token(&token)?, + )], + Json(api_token), + ) + .into_response()) +} diff --git a/crates/core/src/federation/api/mapping.rs b/crates/core/src/federation/api/mapping.rs new file mode 100644 index 00000000..62ee5236 --- /dev/null +++ b/crates/core/src/federation/api/mapping.rs @@ -0,0 +1,99 @@ +// 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 + +//! # Federation attribute mappings API +use utoipa_axum::{router::OpenApiRouter, routes}; + +use crate::keystone::ServiceState; + +mod create; +mod delete; +mod list; +mod show; +mod update; + +pub(super) fn openapi_router() -> OpenApiRouter { + OpenApiRouter::new() + .routes(routes!(list::list, create::create)) + .routes(routes!(show::show, update::update, delete::remove)) +} + +#[cfg(test)] +mod tests { + + use sea_orm::DatabaseConnection; + use std::sync::Arc; + + use crate::config::Config; + use crate::federation::MockFederationProvider; + use crate::keystone::{Service, ServiceState}; + use crate::policy::{MockPolicy, MockPolicyFactory, PolicyError, PolicyEvaluationResult}; + use crate::provider::Provider; + use crate::token::{MockTokenProvider, Token, UnscopedPayload}; + + pub(super) fn get_mocked_state( + federation_mock: MockFederationProvider, + policy_allowed: bool, + ) -> ServiceState { + let mut token_mock = MockTokenProvider::default(); + token_mock.expect_validate_token().returning(|_, _, _, _| { + Ok(Token::Unscoped(UnscopedPayload { + user_id: "bar".into(), + ..Default::default() + })) + }); + token_mock + .expect_expand_token_information() + .returning(|_, _| { + Ok(Token::Unscoped(UnscopedPayload { + user_id: "bar".into(), + ..Default::default() + })) + }); + + let provider = Provider::mocked_builder() + .federation(federation_mock) + .token(token_mock) + .build() + .unwrap(); + + let mut policy_factory_mock = MockPolicyFactory::default(); + if policy_allowed { + policy_factory_mock.expect_instantiate().returning(|| { + let mut policy_mock = MockPolicy::default(); + policy_mock + .expect_enforce() + .returning(|_, _, _, _| Ok(PolicyEvaluationResult::allowed())); + Ok(policy_mock) + }); + } else { + policy_factory_mock.expect_instantiate().returning(|| { + let mut policy_mock = MockPolicy::default(); + policy_mock.expect_enforce().returning(|_, _, _, _| { + Err(PolicyError::Forbidden(PolicyEvaluationResult::forbidden())) + }); + Ok(policy_mock) + }); + } + Arc::new( + Service::new( + Config::default(), + DatabaseConnection::Disconnected, + provider, + policy_factory_mock, + ) + .unwrap(), + ) + } +} diff --git a/crates/core/src/federation/api/mapping/create.rs b/crates/core/src/federation/api/mapping/create.rs new file mode 100644 index 00000000..81d777d3 --- /dev/null +++ b/crates/core/src/federation/api/mapping/create.rs @@ -0,0 +1,138 @@ +// 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 + +//! Federation attribute mapping: create. +use axum::{Json, debug_handler, extract::State, http::StatusCode, response::IntoResponse}; +use mockall_double::double; +use validator::Validate; + +use crate::api::auth::Auth; +use crate::api::error::KeystoneApiError; +use crate::federation::{FederationApi, api::types::*}; +use crate::keystone::ServiceState; +#[double] +use crate::policy::Policy; + +/// Create attribute mapping. +#[utoipa::path( + post, + path = "/", + operation_id = "/federation/mapping:create", + responses( + (status = CREATED, description = "mapping object", body = MappingResponse), + ), + security(("x-auth" = [])), + tag="mappings" +)] +#[tracing::instrument( + name = "api::mapping_create", + level = "debug", + skip(state, user_auth, policy) +)] +#[debug_handler] +pub(super) async fn create( + Auth(user_auth): Auth, + mut policy: Policy, + State(state): State, + Json(req): Json, +) -> Result { + req.validate()?; + policy + .enforce( + "identity/mapping_create", + &user_auth, + serde_json::to_value(&req.mapping)?, + None, + ) + .await?; + + let res = state + .provider + .get_federation_provider() + .create_mapping(&state, req.into()) + .await?; + Ok((StatusCode::CREATED, res).into_response()) +} + +#[cfg(test)] +mod tests { + use axum::{ + body::Body, + http::{Request, StatusCode, header}, + }; + use http_body_util::BodyExt; // for `collect` + + use tower::ServiceExt; // for `call`, `oneshot`, and `ready` + use tower_http::trace::TraceLayer; + use tracing_test::traced_test; + + use super::{ + super::{openapi_router, tests::get_mocked_state}, + *, + }; + + use crate::federation::{MockFederationProvider, types as provider_types}; + + #[tokio::test] + #[traced_test] + async fn test_create() { + let mut federation_mock = MockFederationProvider::default(); + federation_mock + .expect_create_mapping() + .withf(|_, req: &provider_types::Mapping| req.name == "name") + .returning(|_, _| { + Ok(provider_types::Mapping { + id: "bar".into(), + name: "name".into(), + domain_id: Some("did".into()), + ..Default::default() + }) + }); + + let state = get_mocked_state(federation_mock, true); + + let mut api = openapi_router() + .layer(TraceLayer::new_for_http()) + .with_state(state.clone()); + + let req = MappingCreateRequest { + mapping: MappingCreate { + name: "name".into(), + domain_id: Some("did".into()), + ..Default::default() + }, + }; + + let response = api + .as_service() + .oneshot( + Request::builder() + .method("POST") + .header(header::CONTENT_TYPE, "application/json") + .uri("/") + .header("x-auth-token", "foo") + .body(Body::from(serde_json::to_string(&req).unwrap())) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::CREATED); + + let body = response.into_body().collect().await.unwrap().to_bytes(); + let res: MappingResponse = serde_json::from_slice(&body).unwrap(); + assert_eq!(res.mapping.name, req.mapping.name); + assert_eq!(res.mapping.domain_id, req.mapping.domain_id); + } +} diff --git a/crates/core/src/federation/api/mapping/delete.rs b/crates/core/src/federation/api/mapping/delete.rs new file mode 100644 index 00000000..bc72ac09 --- /dev/null +++ b/crates/core/src/federation/api/mapping/delete.rs @@ -0,0 +1,166 @@ +// 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 + +//! Federation attribute mapping: delete. +use axum::{ + extract::{Path, State}, + http::StatusCode, + response::IntoResponse, +}; +use mockall_double::double; + +use crate::api::auth::Auth; +use crate::api::error::KeystoneApiError; +use crate::federation::FederationApi; +use crate::keystone::ServiceState; +#[double] +use crate::policy::Policy; + +/// Delete attribute mapping. +#[utoipa::path( + delete, + path = "/{id}", + operation_id = "/federation/mapping:delete", + params( + ("id" = String, Path, description = "The ID of the attribute mapping") + ), + responses( + (status = 204, description = "Deleted"), + (status = 404, description = "Mapping not found", example = json!(KeystoneApiError::NotFound(String::from("id = 1")))) + ), + security(("x-auth" = [])), + tag="mappings" +)] +#[tracing::instrument( + name = "api::mapping_delete", + level = "debug", + skip(state, user_auth, policy), + err(Debug) +)] +pub(super) async fn remove( + Auth(user_auth): Auth, + mut policy: Policy, + Path(id): Path, + State(state): State, +) -> Result { + let current = state + .provider + .get_federation_provider() + .get_mapping(&state, &id) + .await?; + + policy + .enforce( + "identity/mapping_delete", + &user_auth, + serde_json::to_value(¤t)?, + None, + ) + .await?; + if current.is_some() { + state + .provider + .get_federation_provider() + .delete_mapping(&state, &id) + .await?; + } else { + return Err(KeystoneApiError::NotFound { + resource: "mapping".to_string(), + identifier: id.clone(), + }); + } + Ok((StatusCode::NO_CONTENT).into_response()) +} + +#[cfg(test)] +mod tests { + use axum::{ + body::Body, + http::{Request, StatusCode}, + }; + use http_body_util::BodyExt; // for `collect` + + use tower::ServiceExt; // for `call`, `oneshot`, and `ready` + use tower_http::trace::TraceLayer; + use tracing_test::traced_test; + + use super::super::{openapi_router, tests::get_mocked_state}; + use crate::federation::{MockFederationProvider, types as provider_types}; + + #[tokio::test] + #[traced_test] + async fn test_delete() { + let mut federation_mock = MockFederationProvider::default(); + federation_mock + .expect_get_mapping() + .withf(|_, id: &'_ str| id == "foo") + .returning(|_, _| Ok(None)); + federation_mock + .expect_get_mapping() + .withf(|_, id: &'_ str| id == "bar") + .returning(|_, _| { + Ok(Some(provider_types::Mapping { + id: "bar".into(), + name: "name".into(), + domain_id: Some("did".into()), + ..Default::default() + })) + }); + federation_mock + .expect_delete_mapping() + .withf(|_, id: &'_ str| id == "bar") + .returning(|_, _| Ok(())); + + let state = get_mocked_state(federation_mock, true); + + let mut api = openapi_router() + .layer(TraceLayer::new_for_http()) + .with_state(state.clone()); + + let response = api + .as_service() + .oneshot( + Request::builder() + .method("DELETE") + .uri("/foo") + .header("x-auth-token", "foo") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!( + response.status(), + StatusCode::NOT_FOUND, + "{:?}", + response.into_body().collect().await.unwrap() + ); + + let response = api + .as_service() + .oneshot( + Request::builder() + .method("DELETE") + .uri("/bar") + .header("x-auth-token", "foo") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::NO_CONTENT); + } +} diff --git a/crates/core/src/federation/api/mapping/list.rs b/crates/core/src/federation/api/mapping/list.rs new file mode 100644 index 00000000..071d3898 --- /dev/null +++ b/crates/core/src/federation/api/mapping/list.rs @@ -0,0 +1,272 @@ +// 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 + +//! Federation attribute mapping: list. +use axum::{ + extract::{OriginalUri, Query, State}, + response::IntoResponse, +}; +use mockall_double::double; +use serde_json::to_value; + +use crate::api::{KeystoneApiError, auth::Auth, common::build_pagination_links}; +use crate::federation::{FederationApi, api::types::*}; +use crate::keystone::ServiceState; +#[double] +use crate::policy::Policy; + +/// List federation mappings. +/// +/// List available federation mappings. +/// +/// Without `domain_id` specified global mappings are returned. +/// +/// It is expected that listing mappings belonging to the other domain is only +/// allowed to the admin user. +#[utoipa::path( + get, + path = "/", + operation_id = "/federation/mapping:list", + params(MappingListParameters), + responses( + (status = OK, description = "List of mappings", body = MappingList), + (status = 500, description = "Internal error", example = json!(KeystoneApiError::InternalError(String::from("id = 1")))) + ), + security(("x-auth" = [])), + tag="mappings" +)] +#[tracing::instrument( + name = "api::mapping_list", + level = "debug", + skip(state, user_auth, policy), + err(Debug) +)] +pub(super) async fn list( + Auth(user_auth): Auth, + mut policy: Policy, + OriginalUri(original_url): OriginalUri, + Query(query): Query, + State(state): State, +) -> Result { + policy + .enforce("identity/mapping_list", &user_auth, to_value(&query)?, None) + .await?; + + let mappings: Vec = state + .provider + .get_federation_provider() + .list_mappings(&state, &query.clone().try_into()?) + .await? + .into_iter() + .map(Into::into) + .collect(); + + let links = build_pagination_links( + &state.config, + mappings.as_slice(), + &query, + original_url.path(), + )?; + Ok(MappingList { mappings, links }) +} + +#[cfg(test)] +mod tests { + use axum::{ + body::Body, + http::{Request, StatusCode}, + }; + use http_body_util::BodyExt; // for `collect` + + use tower::ServiceExt; // for `call`, `oneshot`, and `ready` + use tower_http::trace::TraceLayer; + use tracing_test::traced_test; + + use super::{ + super::{openapi_router, tests::get_mocked_state}, + *, + }; + use crate::federation::{MockFederationProvider, types as provider_types}; + + #[tokio::test] + #[traced_test] + async fn test_list() { + let mut federation_mock = MockFederationProvider::default(); + federation_mock + .expect_list_mappings() + .withf(|_, _: &provider_types::MappingListParameters| true) + .returning(|_, _| { + Ok(vec![provider_types::Mapping { + id: "id".into(), + name: "name".into(), + domain_id: Some("did".into()), + idp_id: "idp_id".into(), + enabled: true, + user_id_claim: "sub".into(), + user_name_claim: "preferred_username".into(), + domain_id_claim: Some("domain_id".into()), + ..Default::default() + }]) + }); + + let state = get_mocked_state(federation_mock, true); + + let mut api = openapi_router() + .layer(TraceLayer::new_for_http()) + .with_state(state); + + let response = api + .as_service() + .oneshot( + Request::builder() + .uri("/") + .header("x-auth-token", "foo") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + + let body = response.into_body().collect().await.unwrap().to_bytes(); + let res: MappingList = serde_json::from_slice(&body).unwrap(); + assert_eq!( + vec![Mapping { + id: "id".into(), + name: "name".into(), + domain_id: Some("did".into()), + idp_id: "idp_id".into(), + r#type: MappingType::default(), + enabled: true, + allowed_redirect_uris: None, + user_id_claim: "sub".into(), + user_name_claim: "preferred_username".into(), + domain_id_claim: Some("domain_id".into()), + groups_claim: None, + bound_audiences: None, + bound_subject: None, + bound_claims: None, + oidc_scopes: None, + token_project_id: None, + token_restriction_id: None + }], + res.mappings + ); + } + + #[tokio::test] + #[traced_test] + async fn test_list_qp() { + let mut federation_mock = MockFederationProvider::default(); + federation_mock + .expect_list_mappings() + .withf(|_, qp: &provider_types::MappingListParameters| { + provider_types::MappingListParameters { + name: Some("name".into()), + domain_id: Some("did".into()), + idp_id: Some("idp".into()), + ..Default::default() + } == *qp + }) + .returning(|_, _| { + Ok(vec![provider_types::Mapping { + id: "id".into(), + name: "name".into(), + domain_id: Some("did".into()), + idp_id: "idp".into(), + r#type: MappingType::default().into(), + enabled: true, + allowed_redirect_uris: None, + user_id_claim: "sub".into(), + user_name_claim: "preferred_username".into(), + domain_id_claim: Some("domain_id".into()), + groups_claim: None, + bound_audiences: None, + bound_subject: None, + bound_claims: None, + oidc_scopes: None, + token_project_id: None, + token_restriction_id: None, + }]) + }); + + let state = get_mocked_state(federation_mock, true); + + let mut api = openapi_router() + .layer(TraceLayer::new_for_http()) + .with_state(state); + + let response = api + .as_service() + .oneshot( + Request::builder() + .uri("/?name=name&domain_id=did&idp_id=idp") + .header("x-auth-token", "foo") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + + let body = response.into_body().collect().await.unwrap().to_bytes(); + let _res: MappingList = serde_json::from_slice(&body).unwrap(); + } + + #[tokio::test] + #[traced_test] + async fn test_list_pagination_link() { + let mut federation_mock = MockFederationProvider::default(); + federation_mock + .expect_list_mappings() + .withf(|_, qp: &provider_types::MappingListParameters| { + provider_types::MappingListParameters { + limit: Some(1), + ..Default::default() + } == *qp + }) + .returning(|_, _| { + Ok(vec![provider_types::Mapping { + id: "id1".into(), + ..Default::default() + }]) + }); + + let state = get_mocked_state(federation_mock, true); + + let mut api = openapi_router() + .layer(TraceLayer::new_for_http()) + .with_state(state); + + let response = api + .as_service() + .oneshot( + Request::builder() + .uri("/?limit=1") + .header("x-auth-token", "foo") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + + let body = response.into_body().collect().await.unwrap().to_bytes(); + let res: MappingList = serde_json::from_slice(&body).unwrap(); + assert!(res.links.is_some()); + } +} diff --git a/crates/core/src/federation/api/mapping/show.rs b/crates/core/src/federation/api/mapping/show.rs new file mode 100644 index 00000000..194a5084 --- /dev/null +++ b/crates/core/src/federation/api/mapping/show.rs @@ -0,0 +1,185 @@ +// 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 + +//! Federation attribute mapping: show. +use axum::{ + extract::{Path, State}, + response::IntoResponse, +}; +use mockall_double::double; + +use crate::api::auth::Auth; +use crate::api::error::KeystoneApiError; +use crate::federation::{FederationApi, api::types::*}; +use crate::keystone::ServiceState; +#[double] +use crate::policy::Policy; + +/// Get single mapping. +/// +/// Show the attribute mapping attribute by the ID. +#[utoipa::path( + get, + path = "/{id}", + operation_id = "/federation/mapping:show", + params( + ("id" = String, Path, description = "The ID of the attribute mapping.") + ), + responses( + (status = OK, description = "mapping object", body = MappingResponse), + (status = 404, description = "mapping not found", example = json!(KeystoneApiError::NotFound(String::from("id = 1")))) + ), + security(("x-auth" = [])), + tag="mappings" +)] +#[tracing::instrument( + name = "api::mapping_get", + level = "debug", + skip(state, user_auth, policy), + err(Debug), + err(Debug) +)] +pub(super) async fn show( + Auth(user_auth): Auth, + mut policy: Policy, + Path(id): Path, + State(state): State, +) -> Result { + let current = state + .provider + .get_federation_provider() + .get_mapping(&state, &id) + .await + .map(|x| { + x.ok_or_else(|| KeystoneApiError::NotFound { + resource: "mapping".into(), + identifier: id, + }) + })??; + + policy + .enforce( + "identity/mapping_show", + &user_auth, + serde_json::to_value(¤t)?, + None, + ) + .await?; + Ok(current) +} + +#[cfg(test)] +mod tests { + use axum::{ + body::Body, + http::{Request, StatusCode}, + }; + use http_body_util::BodyExt; // for `collect` + + use tower::ServiceExt; // for `call`, `oneshot`, and `ready` + use tower_http::trace::TraceLayer; + use tracing_test::traced_test; + + use super::{ + super::{openapi_router, tests::get_mocked_state}, + *, + }; + use crate::federation::{MockFederationProvider, types as provider_types}; + + #[tokio::test] + #[traced_test] + async fn test_get() { + let mut federation_mock = MockFederationProvider::default(); + federation_mock + .expect_get_mapping() + .withf(|_, id: &'_ str| id == "foo") + .returning(|_, _| Ok(None)); + + federation_mock + .expect_get_mapping() + .withf(|_, id: &'_ str| id == "bar") + .returning(|_, _| { + Ok(Some(provider_types::Mapping { + id: "bar".into(), + name: "name".into(), + domain_id: Some("did".into()), + idp_id: "idp_id".into(), + enabled: true, + user_id_claim: "sub".into(), + user_name_claim: "preferred_username".into(), + domain_id_claim: Some("domain_id".into()), + ..Default::default() + })) + }); + + let state = get_mocked_state(federation_mock, true); + + let mut api = openapi_router() + .layer(TraceLayer::new_for_http()) + .with_state(state.clone()); + + let response = api + .as_service() + .oneshot( + Request::builder() + .uri("/foo") + .header("x-auth-token", "foo") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::NOT_FOUND); + + let response = api + .as_service() + .oneshot( + Request::builder() + .uri("/bar") + .header("x-auth-token", "foo") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + + let body = response.into_body().collect().await.unwrap().to_bytes(); + let res: MappingResponse = serde_json::from_slice(&body).unwrap(); + assert_eq!( + Mapping { + id: "bar".into(), + name: "name".into(), + domain_id: Some("did".into()), + idp_id: "idp_id".into(), + r#type: MappingType::default(), + enabled: true, + allowed_redirect_uris: None, + user_id_claim: "sub".into(), + user_name_claim: "preferred_username".into(), + domain_id_claim: Some("domain_id".into()), + groups_claim: None, + bound_audiences: None, + bound_subject: None, + bound_claims: None, + oidc_scopes: None, + token_project_id: None, + token_restriction_id: None, + }, + res.mapping, + ); + } +} diff --git a/crates/core/src/federation/api/mapping/update.rs b/crates/core/src/federation/api/mapping/update.rs new file mode 100644 index 00000000..1bea2154 --- /dev/null +++ b/crates/core/src/federation/api/mapping/update.rs @@ -0,0 +1,165 @@ +// 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 + +//! Federation attribute mapping: update. +use axum::{ + Json, + extract::{Path, State}, + response::IntoResponse, +}; +use mockall_double::double; +use validator::Validate; + +use crate::api::auth::Auth; +use crate::api::error::KeystoneApiError; +use crate::federation::{FederationApi, api::types::*}; +use crate::keystone::ServiceState; +#[double] +use crate::policy::Policy; + +/// Update attribute mapping. +/// +/// TODO: describe domain_id update rules +#[utoipa::path( + put, + path = "/{id}", + operation_id = "/federation/mapping:update", + params( + ("id" = String, Path, description = "The ID of the attribute mapping.") + ), + responses( + (status = OK, description = "mapping object", body = MappingResponse), + (status = 404, description = "mapping not found", example = json!(KeystoneApiError::NotFound(String::from("id = 1")))) + ), + security(("x-auth" = [])), + tag="mappings" +)] +#[tracing::instrument( + name = "api::mapping_update", + level = "debug", + skip(state, user_auth, policy), + err(Debug) +)] +pub(super) async fn update( + Auth(user_auth): Auth, + mut policy: Policy, + Path(id): Path, + State(state): State, + Json(req): Json, +) -> Result { + req.validate()?; + let current = state + .provider + .get_federation_provider() + .get_mapping(&state, &id) + .await?; + + policy + .enforce( + "identity/mapping_update", + &user_auth, + serde_json::to_value(¤t)?, + Some(serde_json::to_value(&req.mapping)?), + ) + .await?; + + let res = state + .provider + .get_federation_provider() + .update_mapping(&state, &id, req.into()) + .await?; + Ok(res.into_response()) +} + +#[cfg(test)] +mod tests { + use axum::{ + body::Body, + http::{Request, StatusCode, header}, + }; + use http_body_util::BodyExt; // for `collect` + + use tower::ServiceExt; // for `call`, `oneshot`, and `ready` + use tower_http::trace::TraceLayer; + use tracing_test::traced_test; + + use super::{ + super::{openapi_router, tests::get_mocked_state}, + *, + }; + use crate::federation::{MockFederationProvider, types as provider_types}; + + #[tokio::test] + #[traced_test] + async fn test_update() { + let mut federation_mock = MockFederationProvider::default(); + federation_mock + .expect_get_mapping() + .withf(|_, id: &'_ str| id == "1") + .returning(|_, _| { + Ok(Some(provider_types::Mapping { + id: "bar".into(), + name: "name".into(), + domain_id: Some("did".into()), + ..Default::default() + })) + }); + + federation_mock + .expect_update_mapping() + .withf(|_, id: &'_ str, req: &provider_types::MappingUpdate| { + id == "1" && req.name == Some("name".to_string()) + }) + .returning(|_, _, _| { + Ok(provider_types::Mapping { + id: "bar".into(), + name: "name".into(), + domain_id: Some("did".into()), + ..Default::default() + }) + }); + + let state = get_mocked_state(federation_mock, true); + + let mut api = openapi_router() + .layer(TraceLayer::new_for_http()) + .with_state(state.clone()); + + let req = MappingUpdateRequest { + mapping: MappingUpdate { + name: Some("name".into()), + ..Default::default() + }, + }; + + let response = api + .as_service() + .oneshot( + Request::builder() + .method("PUT") + .header(header::CONTENT_TYPE, "application/json") + .uri("/1") + .header("x-auth-token", "foo") + .body(Body::from(serde_json::to_string(&req).unwrap())) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + + let body = response.into_body().collect().await.unwrap().to_bytes(); + let _res: MappingResponse = serde_json::from_slice(&body).unwrap(); + } +} diff --git a/crates/core/src/federation/api/oidc.rs b/crates/core/src/federation/api/oidc.rs new file mode 100644 index 00000000..b11c91da --- /dev/null +++ b/crates/core/src/federation/api/oidc.rs @@ -0,0 +1,361 @@ +// 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 +//! Finish OIDC login. + +use axum::{Json, debug_handler, extract::State, http::StatusCode, response::IntoResponse}; +use chrono::Utc; +use eyre::WrapErr; +use std::collections::{HashMap, HashSet}; +use tracing::{debug, trace}; +use url::Url; +use utoipa_axum::{router::OpenApiRouter, routes}; + +use openidconnect::core::CoreProviderMetadata; +use openidconnect::reqwest; +use openidconnect::{ + AuthorizationCode, ClientId, ClientSecret, IssuerUrl, Nonce, PkceCodeVerifier, RedirectUrl, + TokenResponse, +}; + +use crate::api::v4::auth::token::types::TokenResponse as KeystoneTokenResponse; +use crate::api::{ + KeystoneApiError, + common::get_authz_info, + types::{Catalog, CatalogService}, +}; +use crate::auth::{AuthenticatedInfo, AuthenticationError}; +use crate::catalog::CatalogApi; +use crate::federation::{ + FederationApi, + api::{error::OidcError, types::*}, +}; +use crate::identity::IdentityApi; +use crate::identity::error::IdentityProviderError; +use crate::identity::types::{FederationBuilder, FederationProtocol, UserCreateBuilder}; +use crate::identity::types::{Group, GroupCreate, GroupListParameters}; +use crate::keystone::ServiceState; +use crate::token::TokenApi; + +use super::common::{map_user_data, validate_bound_claims}; + +pub(super) fn openapi_router() -> OpenApiRouter { + OpenApiRouter::new().routes(routes!(callback)) +} + +/// Authentication callback. +/// +/// This operation allows user to exchange the authorization code retrieved from +/// the identity provider after calling the +/// `/v4/federation/identity_providers/{idp_id}/auth` for the Keystone +/// token. When desired scope was passed in that auth initialization call the +/// scoped token is returned (assuming the user is having roles assigned on that +/// scope). +#[utoipa::path( + post, + path = "/oidc/callback", + operation_id = "/federation/oidc:callback", + responses( + ( + status = OK, + description = "Authentication Token object", + body = KeystoneTokenResponse, + headers( + ("x-subject-token" = String, description = "Keystone token"), + ), + ), + ), + security(("oauth2" = ["openid"])), + tag="identity_providers" +)] +#[tracing::instrument( + name = "api::identity_provider_auth_callback", + level = "debug", + skip(state), + err(Debug) +)] +#[debug_handler] +pub async fn callback( + State(state): State, + Json(query): Json, +) -> Result { + let auth_state = state + .provider + .get_federation_provider() + .get_auth_state(&state, &query.state) + .await? + .ok_or_else(|| KeystoneApiError::NotFound { + resource: "auth state".into(), + identifier: query.state.clone(), + })?; + + if auth_state.expires_at < Utc::now() { + return Err(OidcError::AuthStateExpired)?; + } + + let idp = state + .provider + .get_federation_provider() + .get_identity_provider(&state, &auth_state.idp_id) + .await + .map(|x| { + x.ok_or_else(|| KeystoneApiError::NotFound { + resource: "identity provider".into(), + identifier: auth_state.idp_id.clone(), + }) + })??; + + let mapping = state + .provider + .get_federation_provider() + .get_mapping(&state, &auth_state.mapping_id) + .await + .map(|x| { + x.ok_or_else(|| KeystoneApiError::NotFound { + resource: "mapping".into(), + identifier: auth_state.mapping_id.clone(), + }) + })??; + + // Check for IdP and mapping `enabled` state + if !idp.enabled { + return Err(OidcError::IdentityProviderDisabled)?; + } + if !mapping.enabled { + return Err(OidcError::MappingDisabled)?; + } + + let token_restrictions = if let Some(tr_id) = &mapping.token_restriction_id { + state + .provider + .get_token_provider() + .get_token_restriction(&state, tr_id, true) + .await? + } else { + None + }; + + let http_client = reqwest::ClientBuilder::new() + // Following redirects opens the client up to SSRF vulnerabilities. + .redirect(reqwest::redirect::Policy::none()) + .build() + .map_err(OidcError::from)?; + + let client = if let Some(discovery_url) = &idp.oidc_discovery_url { + let provider_metadata = CoreProviderMetadata::discover_async( + IssuerUrl::new(discovery_url.to_string()).map_err(OidcError::from)?, + &http_client, + ) + .await + .map_err(|err| OidcError::discovery(discovery_url, &err))?; + OidcClient::from_provider_metadata( + provider_metadata, + ClientId::new( + idp.oidc_client_id + .clone() + .ok_or(OidcError::ClientIdRequired)?, + ), + idp.oidc_client_secret.clone().map(ClientSecret::new), + ) + .set_redirect_uri(RedirectUrl::new(auth_state.redirect_uri).map_err(OidcError::from)?) + } else { + return Err(OidcError::ClientWithoutDiscoveryNotSupported)?; + }; + + // Finish authorization request by exchanging the authorization code for the + // token. + let token_response = client + .exchange_code(AuthorizationCode::new(query.code)) + .map_err(OidcError::from)? + // Set the PKCE code verifier. + .set_pkce_verifier(PkceCodeVerifier::new(auth_state.pkce_verifier)) + .request_async(&http_client) + .await + .map_err(|err| OidcError::request_token(&err))?; + + //// Extract the ID token claims after verifying its authenticity and nonce. + let id_token = token_response.id_token().ok_or(OidcError::NoToken)?; + let claims = id_token + .claims(&client.id_token_verifier(), &Nonce::new(auth_state.nonce)) + .map_err(OidcError::from)?; + if let Some(bound_issuer) = &idp.bound_issuer + && Url::parse(bound_issuer) + .map_err(OidcError::from) + .wrap_err_with(|| { + format!("while parsing the mapping bound_issuer url: {bound_issuer}") + })? + == *claims.issuer().url() + {} + + let claims_as_json = serde_json::to_value(claims)?; + debug!("Claims data {claims_as_json}"); + + validate_bound_claims(&mapping, claims, &claims_as_json)?; + let mapped_user_data = map_user_data(&state, &idp, &mapping, &claims_as_json).await?; + debug!("Mapped user is {mapped_user_data:?}"); + + let user = if let Some(existing_user) = state + .provider + .get_identity_provider() + .find_federated_user(&state, &idp.id, &mapped_user_data.unique_id) + .await? + { + // The user exists already + existing_user + + // TODO: update user? + } else { + // New user + let mut federated_user: FederationBuilder = FederationBuilder::default(); + federated_user.idp_id(idp.id.clone()); + federated_user.unique_id(mapped_user_data.unique_id.clone()); + federated_user.protocols(vec![FederationProtocol { + protocol_id: "oidc".into(), + unique_id: mapped_user_data.unique_id.clone(), + }]); + let mut user_builder: UserCreateBuilder = UserCreateBuilder::default(); + user_builder.domain_id(mapped_user_data.domain_id); + user_builder.enabled(true); + user_builder.name(mapped_user_data.user_name); + user_builder.federated(Vec::from([federated_user + .build() + .map_err(IdentityProviderError::from)?])); + + state + .provider + .get_identity_provider() + .create_user( + &state, + user_builder.build().map_err(IdentityProviderError::from)?, + ) + .await? + }; + + if let Some(necessary_group_names) = mapped_user_data.group_names { + let current_domain_groups: HashMap = HashMap::from_iter( + state + .provider + .get_identity_provider() + .list_groups( + &state, + &GroupListParameters { + domain_id: Some(user.domain_id.clone()), + ..Default::default() + }, + ) + .await? + .into_iter() + .map(|group| (group.name, group.id)), + ); + let mut group_ids: HashSet = HashSet::new(); + for group_name in necessary_group_names { + group_ids.insert( + if let Some(grp_id) = current_domain_groups.get(&group_name) { + grp_id.clone() + } else { + state + .provider + .get_identity_provider() + .create_group( + &state, + GroupCreate { + domain_id: user.domain_id.clone(), + name: group_name.clone(), + ..Default::default() + }, + ) + .await? + .id + }, + ); + } + if !group_ids.is_empty() { + state + .provider + .get_identity_provider() + .set_user_groups_expiring( + &state, + &user.id, + HashSet::from_iter(group_ids.iter().map(|i| i.as_str())), + idp.id.as_ref(), + Some(&Utc::now()), + ) + .await?; + } + } + let user_groups: Vec = Vec::from_iter( + state + .provider + .get_identity_provider() + .list_groups_of_user(&state, &user.id) + .await?, + ); + + let authed_info = AuthenticatedInfo::builder() + .user_id(user.id.clone()) + .user(user.clone()) + .methods(vec!["openid".into()]) + .idp_id(idp.id.clone()) + .protocol_id("oidc".to_string()) + .user_groups(user_groups) + .build() + .map_err(AuthenticationError::from)?; + authed_info.validate()?; + + let authz_info = get_authz_info(&state, auth_state.scope.as_ref()).await?; + trace!("Granting the scope: {:?}", authz_info); + + let mut token = state.provider.get_token_provider().issue_token( + authed_info, + authz_info, + token_restrictions.as_ref(), + )?; + + token = state + .provider + .get_token_provider() + .expand_token_information(&state, &token) + .await + .map_err(KeystoneApiError::forbidden)?; + + let mut api_token = KeystoneTokenResponse { + token: token.build_api_token_v4(&state).await?, + }; + let catalog: Catalog = Catalog( + state + .provider + .get_catalog_provider() + .get_catalog(&state, true) + .await? + .into_iter() + .map(|(s, es)| CatalogService { + id: s.id.clone(), + name: s.name.clone(), + r#type: s.r#type, + endpoints: es.into_iter().map(Into::into).collect(), + }) + .collect::>(), + ); + api_token.token.catalog = Some(catalog); + + trace!("Token response is {:?}", api_token); + Ok(( + StatusCode::OK, + [( + "X-Subject-Token", + state.provider.get_token_provider().encode_token(&token)?, + )], + Json(api_token), + ) + .into_response()) +} diff --git a/crates/core/src/federation/api/types.rs b/crates/core/src/federation/api/types.rs new file mode 100644 index 00000000..ed019287 --- /dev/null +++ b/crates/core/src/federation/api/types.rs @@ -0,0 +1,89 @@ +// 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 +//use derive_builder::Builder; +//use serde::{Deserialize, Serialize}; +//use std::collections::HashMap; + +//use openidconnect::core::{ +// CoreAuthDisplay, CoreAuthPrompt, CoreErrorResponseType, CoreGenderClaim, CoreJsonWebKey, +// CoreJweContentEncryptionAlgorithm, CoreJwsSigningAlgorithm, CoreRevocableToken, +// CoreRevocationErrorResponse, CoreTokenIntrospectionResponse, CoreTokenType, +//}; +//use openidconnect::{ +// AdditionalClaims, EndpointMaybeSet, EndpointNotSet, EndpointSet, ExtraTokenFields, +// IdTokenFields, StandardErrorResponse, StandardTokenResponse, +//}; + +//pub mod auth; +pub mod identity_provider; +pub mod mapping; + +//pub use auth::*; +//pub use identity_provider::*; +//pub use mapping::*; + +//pub(super) type OidcIdTokenFields = IdTokenFields< +// AllOtherClaims, +// ExtraFields, +// CoreGenderClaim, +// CoreJweContentEncryptionAlgorithm, +// CoreJwsSigningAlgorithm, +//>; +// +//pub(super) type OidcTokenResponse = StandardTokenResponse; +// +//pub(super) type OidcClient< +// HasAuthUrl = EndpointSet, +// HasDeviceAuthUrl = EndpointNotSet, +// HasIntrospectionUrl = EndpointNotSet, +// HasRevocationUrl = EndpointNotSet, +// HasTokenUrl = EndpointMaybeSet, +// HasUserInfoUrl = EndpointMaybeSet, +//> = openidconnect::Client< +// AllOtherClaims, +// CoreAuthDisplay, +// CoreGenderClaim, +// CoreJweContentEncryptionAlgorithm, +// CoreJsonWebKey, +// CoreAuthPrompt, +// StandardErrorResponse, +// OidcTokenResponse, +// CoreTokenIntrospectionResponse, +// CoreRevocableToken, +// CoreRevocationErrorResponse, +// HasAuthUrl, +// HasDeviceAuthUrl, +// HasIntrospectionUrl, +// HasRevocationUrl, +// HasTokenUrl, +// HasUserInfoUrl, +//>; +// +//#[derive(Debug, Deserialize, Serialize)] +//pub(super) struct AllOtherClaims(HashMap); +//impl AdditionalClaims for AllOtherClaims {} +// +//#[derive(Debug, Deserialize, Serialize)] +//pub(super) struct ExtraFields(HashMap); +//impl ExtraTokenFields for ExtraFields {} +// +//#[derive(Builder, Debug, Clone)] +//#[builder(setter(into))] +//pub(super) struct MappedUserData { +// pub(super) unique_id: String, +// pub(super) user_name: String, +// pub(super) domain_id: String, +// #[builder(default)] +// pub(super) group_names: Option>, +//} diff --git a/crates/core/src/federation/api/types/auth.rs b/crates/core/src/federation/api/types/auth.rs new file mode 100644 index 00000000..b4b407f5 --- /dev/null +++ b/crates/core/src/federation/api/types/auth.rs @@ -0,0 +1,18 @@ +// 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 +//! Federated auth OIDC auth types. + +pub use openstack_keystone_api_types::federation::auth::AuthCallbackParameters; +pub use openstack_keystone_api_types::federation::auth::IdentityProviderAuthRequest; +pub use openstack_keystone_api_types::federation::auth::IdentityProviderAuthResponse; diff --git a/crates/core/src/federation/api/types/identity_provider.rs b/crates/core/src/federation/api/types/identity_provider.rs new file mode 100644 index 00000000..0a2a4669 --- /dev/null +++ b/crates/core/src/federation/api/types/identity_provider.rs @@ -0,0 +1,136 @@ +// 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 +//! Federated identity provider types. +use axum::{ + Json, + http::StatusCode, + response::{IntoResponse, Response}, +}; + +use openstack_keystone_api_types::federation::identity_provider as api_identity_provider; + +//pub use identity_provider::IdentityProvider; +//pub use identity_provider::IdentityProviderCreate; +//pub use identity_provider::IdentityProviderCreateRequest; +//pub use identity_provider::IdentityProviderList; +//pub use identity_provider::IdentityProviderListParameters; +//pub use identity_provider::IdentityProviderResponse; +//pub use identity_provider::IdentityProviderUpdate; +//pub use identity_provider::IdentityProviderUpdateRequest; + +use crate::federation::types; + +//use crate::api::common::{QueryParameterPagination, ResourceIdentifier}; + +impl From for api_identity_provider::IdentityProvider { + fn from(value: types::IdentityProvider) -> Self { + Self { + id: value.id, + name: value.name, + domain_id: value.domain_id, + enabled: value.enabled, + oidc_discovery_url: value.oidc_discovery_url, + oidc_client_id: value.oidc_client_id, + oidc_response_mode: value.oidc_response_mode, + oidc_response_types: value.oidc_response_types, + jwks_url: value.jwks_url, + jwt_validation_pubkeys: value.jwt_validation_pubkeys, + bound_issuer: value.bound_issuer, + default_mapping_name: value.default_mapping_name, + provider_config: value.provider_config, + } + } +} + +impl From for types::IdentityProviderCreate { + fn from(value: api_identity_provider::IdentityProviderCreateRequest) -> Self { + Self { + id: None, + name: value.identity_provider.name, + domain_id: value.identity_provider.domain_id, + enabled: value.identity_provider.enabled, + oidc_discovery_url: value.identity_provider.oidc_discovery_url, + oidc_client_id: value.identity_provider.oidc_client_id, + oidc_client_secret: value.identity_provider.oidc_client_secret, + oidc_response_mode: value.identity_provider.oidc_response_mode, + oidc_response_types: value.identity_provider.oidc_response_types, + jwks_url: value.identity_provider.jwks_url, + jwt_validation_pubkeys: value.identity_provider.jwt_validation_pubkeys, + bound_issuer: value.identity_provider.bound_issuer, + default_mapping_name: value.identity_provider.default_mapping_name, + provider_config: value.identity_provider.provider_config, + } + } +} + +impl From for types::IdentityProviderUpdate { + fn from(value: api_identity_provider::IdentityProviderUpdateRequest) -> Self { + Self { + name: value.identity_provider.name, + enabled: value.identity_provider.enabled, + oidc_discovery_url: value.identity_provider.oidc_discovery_url, + oidc_client_id: value.identity_provider.oidc_client_id, + oidc_client_secret: value.identity_provider.oidc_client_secret, + oidc_response_mode: value.identity_provider.oidc_response_mode, + oidc_response_types: value.identity_provider.oidc_response_types, + jwks_url: value.identity_provider.jwks_url, + jwt_validation_pubkeys: value.identity_provider.jwt_validation_pubkeys, + bound_issuer: value.identity_provider.bound_issuer, + default_mapping_name: value.identity_provider.default_mapping_name, + provider_config: value.identity_provider.provider_config, + } + } +} + +impl IntoResponse for types::IdentityProvider { + fn into_response(self) -> Response { + ( + StatusCode::OK, + Json(api_identity_provider::IdentityProviderResponse { + identity_provider: api_identity_provider::IdentityProvider::from(self), + }), + ) + .into_response() + } +} + +impl From + for types::IdentityProviderListParameters +{ + fn from(value: api_identity_provider::IdentityProviderListParameters) -> Self { + Self { + name: value.name, + domain_ids: None, //value.domain_id, + limit: value.limit, + marker: value.marker, + } + } +} + +// impl ResourceIdentifier for IdentityProvider { +// fn get_id(&self) -> String { +// self.id.clone() +// } +// } +// +// impl QueryParameterPagination for IdentityProviderListParameters { +// fn get_limit(&self) -> Option { +// self.limit +// } +// +// fn set_marker(&mut self, marker: String) -> &mut Self { +// self.marker = Some(marker); +// self +// } +// } diff --git a/crates/core/src/federation/api/types/mapping.rs b/crates/core/src/federation/api/types/mapping.rs new file mode 100644 index 00000000..f20cd85c --- /dev/null +++ b/crates/core/src/federation/api/types/mapping.rs @@ -0,0 +1,170 @@ +// 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 +//! Federated attribute mapping types. +use axum::{ + Json, + http::StatusCode, + response::{IntoResponse, Response}, +}; +use uuid::Uuid; + +use openstack_keystone_api_types::federation::mapping as api_mapping; + +use crate::api::{ + KeystoneApiError, + // common::{QueryParameterPagination, ResourceIdentifier}, +}; +use crate::federation::types; + +//pub use mapping::Mapping; +//pub use mapping::MappingCreate; +//pub use mapping::MappingCreateRequest; +//pub use mapping::MappingList; +//pub use mapping::MappingListParameters; +//pub use mapping::MappingResponse; +//pub use mapping::MappingType; +//pub use mapping::MappingUpdate; +//pub use mapping::MappingUpdateRequest; + +impl From for api_mapping::Mapping { + fn from(value: types::Mapping) -> Self { + Self { + id: value.id, + name: value.name, + domain_id: value.domain_id, + idp_id: value.idp_id, + r#type: value.r#type.into(), + enabled: value.enabled, + allowed_redirect_uris: value.allowed_redirect_uris, + user_id_claim: value.user_id_claim, + user_name_claim: value.user_name_claim, + domain_id_claim: value.domain_id_claim, + groups_claim: value.groups_claim, + bound_audiences: value.bound_audiences, + bound_subject: value.bound_subject, + bound_claims: value.bound_claims, + oidc_scopes: value.oidc_scopes, + token_project_id: value.token_project_id, + token_restriction_id: value.token_restriction_id, + } + } +} + +impl From for types::Mapping { + fn from(value: api_mapping::MappingCreateRequest) -> Self { + Self { + id: value.mapping.id.unwrap_or_else(|| Uuid::new_v4().into()), + name: value.mapping.name, + domain_id: value.mapping.domain_id, + idp_id: value.mapping.idp_id, + r#type: value.mapping.r#type.unwrap_or_default().into(), + enabled: value.mapping.enabled, + allowed_redirect_uris: value.mapping.allowed_redirect_uris, + user_id_claim: value.mapping.user_id_claim, + user_name_claim: value.mapping.user_name_claim, + domain_id_claim: value.mapping.domain_id_claim, + groups_claim: value.mapping.groups_claim, + bound_audiences: value.mapping.bound_audiences, + bound_subject: value.mapping.bound_subject, + bound_claims: value.mapping.bound_claims, + oidc_scopes: value.mapping.oidc_scopes, + token_project_id: value.mapping.token_project_id, + token_restriction_id: value.mapping.token_restriction_id, + } + } +} + +impl From for types::MappingUpdate { + fn from(value: api_mapping::MappingUpdateRequest) -> Self { + Self { + name: value.mapping.name, + idp_id: value.mapping.idp_id, + r#type: value.mapping.r#type.map(Into::into), + enabled: value.mapping.enabled, + allowed_redirect_uris: value.mapping.allowed_redirect_uris, + user_id_claim: value.mapping.user_id_claim, + user_name_claim: value.mapping.user_name_claim, + domain_id_claim: value.mapping.domain_id_claim, + groups_claim: value.mapping.groups_claim, + bound_audiences: value.mapping.bound_audiences, + bound_subject: value.mapping.bound_subject, + bound_claims: value.mapping.bound_claims, + oidc_scopes: value.mapping.oidc_scopes, + token_project_id: value.mapping.token_project_id, + token_restriction_id: value.mapping.token_restriction_id, + } + } +} + +impl IntoResponse for types::Mapping { + fn into_response(self) -> Response { + ( + StatusCode::OK, + Json(api_mapping::MappingResponse { + mapping: api_mapping::Mapping::from(self), + }), + ) + .into_response() + } +} + +impl From for api_mapping::MappingType { + fn from(value: types::MappingType) -> Self { + match value { + types::MappingType::Oidc => Self::Oidc, + types::MappingType::Jwt => Self::Jwt, + } + } +} + +impl From for types::MappingType { + fn from(value: api_mapping::MappingType) -> Self { + match value { + api_mapping::MappingType::Oidc => Self::Oidc, + api_mapping::MappingType::Jwt => Self::Jwt, + } + } +} + +impl TryFrom for types::MappingListParameters { + type Error = KeystoneApiError; + + fn try_from(value: api_mapping::MappingListParameters) -> Result { + Ok(Self { + domain_id: value.domain_id, + idp_id: value.idp_id, + limit: value.limit, + marker: value.marker, + name: value.name, + r#type: value.r#type.map(Into::into), + }) + } +} + +//impl ResourceIdentifier for Mapping { +// fn get_id(&self) -> String { +// self.id.clone() +// } +//} +// +//impl QueryParameterPagination for MappingListParameters { +// fn get_limit(&self) -> Option { +// self.limit +// } +// +// fn set_marker(&mut self, marker: String) -> &mut Self { +// self.marker = Some(marker); +// self +// } +//} diff --git a/crates/core/src/federation/backend.rs b/crates/core/src/federation/backend.rs new file mode 100644 index 00000000..8da4d4a4 --- /dev/null +++ b/crates/core/src/federation/backend.rs @@ -0,0 +1,120 @@ +// 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 + +use async_trait::async_trait; + +use crate::federation::FederationProviderError; +use crate::federation::types::*; +use crate::keystone::ServiceState; + +/// Backend driver interface for the Federation Provider. +#[cfg_attr(test, mockall::automock)] +#[async_trait] +pub trait FederationBackend: Send + Sync { + /// Cleanup expired resources. + async fn cleanup(&self, state: &ServiceState) -> Result<(), FederationProviderError>; + + /// Create new authentication state. + async fn create_auth_state( + &self, + state: &ServiceState, + auth_state: AuthState, + ) -> Result; + + /// Create Identity provider. + async fn create_identity_provider( + &self, + state: &ServiceState, + idp: IdentityProviderCreate, + ) -> Result; + + /// Create mapping. + async fn create_mapping( + &self, + state: &ServiceState, + idp: Mapping, + ) -> Result; + + /// Delete authentication state. + async fn delete_auth_state<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result<(), FederationProviderError>; + + /// Delete identity provider. + async fn delete_identity_provider<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result<(), FederationProviderError>; + + /// Delete mapping. + async fn delete_mapping<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result<(), FederationProviderError>; + + /// Get authentication state. + async fn get_auth_state<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result, FederationProviderError>; + + /// Get single identity provider by ID. + async fn get_identity_provider<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result, FederationProviderError>; + + /// Get single mapping by ID. + async fn get_mapping<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result, FederationProviderError>; + + /// List Identity Providers. + async fn list_identity_providers( + &self, + state: &ServiceState, + params: &IdentityProviderListParameters, + ) -> Result, FederationProviderError>; + + /// List Identity Providers. + async fn list_mappings( + &self, + state: &ServiceState, + params: &MappingListParameters, + ) -> Result, FederationProviderError>; + + /// Update Identity provider. + async fn update_identity_provider<'a>( + &self, + state: &ServiceState, + id: &'a str, + idp: IdentityProviderUpdate, + ) -> Result; + + /// Update mapping. + async fn update_mapping<'a>( + &self, + state: &ServiceState, + id: &'a str, + idp: MappingUpdate, + ) -> Result; +} diff --git a/crates/keystone/src/federation/error.rs b/crates/core/src/federation/error.rs similarity index 96% rename from crates/keystone/src/federation/error.rs rename to crates/core/src/federation/error.rs index fa17e692..e0a8cb8a 100644 --- a/crates/keystone/src/federation/error.rs +++ b/crates/core/src/federation/error.rs @@ -62,6 +62,6 @@ pub enum FederationProviderError { }, /// Unsupported driver. - #[error("unsupported driver {0}")] + #[error("unsupported driver `{0}` for the federation provider")] UnsupportedDriver(String), } diff --git a/crates/keystone/src/federation/mock.rs b/crates/core/src/federation/mock.rs similarity index 94% rename from crates/keystone/src/federation/mock.rs rename to crates/core/src/federation/mock.rs index 4850514d..aab8dc3e 100644 --- a/crates/keystone/src/federation/mock.rs +++ b/crates/core/src/federation/mock.rs @@ -15,16 +15,12 @@ use async_trait::async_trait; use mockall::mock; -use crate::config::Config; use crate::federation::error::FederationProviderError; use crate::federation::types::*; use crate::keystone::ServiceState; -use crate::plugin_manager::PluginManager; mock! { - pub FederationProvider { - pub fn new(cfg: &Config, plugin_manager: &PluginManager) -> Result; - } + pub FederationProvider {} #[async_trait] impl FederationApi for FederationProvider { diff --git a/crates/core/src/federation/mod.rs b/crates/core/src/federation/mod.rs new file mode 100644 index 00000000..9eb13d0f --- /dev/null +++ b/crates/core/src/federation/mod.rs @@ -0,0 +1,252 @@ +// 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 +//! # Federation provider +//! +//! Federation provider implements the functionality necessary for the user +//! federation. +use async_trait::async_trait; + +pub mod api; +pub mod backend; +pub mod error; +#[cfg(any(test, feature = "mock"))] +pub mod mock; +pub mod service; +pub mod types; + +use crate::config::Config; +use crate::keystone::ServiceState; +use crate::plugin_manager::PluginManagerApi; +use service::FederationService; +use types::*; + +pub use crate::federation::error::FederationProviderError; +#[cfg(any(test, feature = "mock"))] +pub use mock::MockFederationProvider; +pub use types::FederationApi; + +pub enum FederationProvider { + Service(FederationService), + #[cfg(any(test, feature = "mock"))] + Mock(MockFederationProvider), +} + +impl FederationProvider { + pub fn new( + config: &Config, + plugin_manager: &P, + ) -> Result { + Ok(Self::Service(FederationService::new( + config, + plugin_manager, + )?)) + } +} + +#[async_trait] +impl FederationApi for FederationProvider { + /// Cleanup expired resources. + #[tracing::instrument(level = "info", skip(self, state))] + async fn cleanup(&self, state: &ServiceState) -> Result<(), FederationProviderError> { + match self { + Self::Service(provider) => provider.cleanup(state).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.cleanup(state).await, + } + } + + /// Create new auth state. + #[tracing::instrument(level = "debug", skip(self, state))] + async fn create_auth_state( + &self, + state: &ServiceState, + auth_state: AuthState, + ) -> Result { + match self { + Self::Service(provider) => provider.create_auth_state(state, auth_state).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.create_auth_state(state, auth_state).await, + } + } + + /// Create Identity provider. + #[tracing::instrument(level = "debug", skip(self, state))] + async fn create_identity_provider( + &self, + state: &ServiceState, + idp: IdentityProviderCreate, + ) -> Result { + match self { + Self::Service(provider) => provider.create_identity_provider(state, idp).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.create_identity_provider(state, idp).await, + } + } + + /// Create mapping. + #[tracing::instrument(level = "debug", skip(self, state))] + async fn create_mapping( + &self, + state: &ServiceState, + mapping: Mapping, + ) -> Result { + match self { + Self::Service(provider) => provider.create_mapping(state, mapping).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.create_mapping(state, mapping).await, + } + } + + /// Delete auth state. + #[tracing::instrument(level = "debug", skip(self, state))] + async fn delete_auth_state<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result<(), FederationProviderError> { + match self { + Self::Service(provider) => provider.delete_identity_provider(state, id).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.delete_auth_state(state, id).await, + } + } + + /// Delete identity provider. + #[tracing::instrument(level = "debug", skip(self, state))] + async fn delete_identity_provider<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result<(), FederationProviderError> { + match self { + Self::Service(provider) => provider.delete_identity_provider(state, id).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.delete_identity_provider(state, id).await, + } + } + + /// Delete identity provider. + #[tracing::instrument(level = "debug", skip(self, state))] + async fn delete_mapping<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result<(), FederationProviderError> { + match self { + Self::Service(provider) => provider.delete_mapping(state, id).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.delete_mapping(state, id).await, + } + } + + /// Get auth state by ID. + #[tracing::instrument(level = "debug", skip(self, state))] + async fn get_auth_state<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result, FederationProviderError> { + match self { + Self::Service(provider) => provider.get_auth_state(state, id).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.get_auth_state(state, id).await, + } + } + + /// Get single IDP by ID. + #[tracing::instrument(level = "info", skip(self, state))] + async fn get_identity_provider<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result, FederationProviderError> { + match self { + Self::Service(provider) => provider.get_identity_provider(state, id).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.get_identity_provider(state, id).await, + } + } + + /// Get single mapping by ID. + #[tracing::instrument(level = "info", skip(self, state))] + async fn get_mapping<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result, FederationProviderError> { + match self { + Self::Service(provider) => provider.get_mapping(state, id).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.get_mapping(state, id).await, + } + } + + /// List IDP. + #[tracing::instrument(level = "info", skip(self, state))] + async fn list_identity_providers( + &self, + state: &ServiceState, + params: &IdentityProviderListParameters, + ) -> Result, FederationProviderError> { + match self { + Self::Service(provider) => provider.list_identity_providers(state, params).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.list_identity_providers(state, params).await, + } + } + + /// List mappings. + #[tracing::instrument(level = "info", skip(self, state))] + async fn list_mappings( + &self, + state: &ServiceState, + params: &MappingListParameters, + ) -> Result, FederationProviderError> { + match self { + Self::Service(provider) => provider.list_mappings(state, params).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.list_mappings(state, params).await, + } + } + + /// Update Identity provider. + #[tracing::instrument(level = "debug", skip(self, state))] + async fn update_identity_provider<'a>( + &self, + state: &ServiceState, + id: &'a str, + idp: IdentityProviderUpdate, + ) -> Result { + match self { + Self::Service(provider) => provider.update_identity_provider(state, id, idp).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.update_identity_provider(state, id, idp).await, + } + } + + /// Update mapping + #[tracing::instrument(level = "debug", skip(self, state))] + async fn update_mapping<'a>( + &self, + state: &ServiceState, + id: &'a str, + mapping: MappingUpdate, + ) -> Result { + match self { + Self::Service(provider) => provider.update_mapping(state, id, mapping).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.update_mapping(state, id, mapping).await, + } + } +} diff --git a/crates/core/src/federation/service.rs b/crates/core/src/federation/service.rs new file mode 100644 index 00000000..c354c402 --- /dev/null +++ b/crates/core/src/federation/service.rs @@ -0,0 +1,229 @@ +// 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 +//! # Federation provider +//! +//! Federation provider implements the functionality necessary for the user +//! federation. +use async_trait::async_trait; +use std::sync::Arc; +use uuid::Uuid; + +use crate::config::Config; +use crate::federation::{FederationProviderError, backend::FederationBackend, types::*}; +use crate::keystone::ServiceState; +use crate::plugin_manager::PluginManagerApi; + +pub struct FederationService { + backend_driver: Arc, +} + +impl FederationService { + pub fn new( + config: &Config, + plugin_manager: &P, + ) -> Result { + let backend_driver = plugin_manager + .get_federation_backend(config.federation.driver.clone())? + .clone(); + Ok(Self { backend_driver }) + } +} + +#[async_trait] +impl FederationApi for FederationService { + /// Cleanup expired resources. + #[tracing::instrument(level = "info", skip(self, state))] + async fn cleanup(&self, state: &ServiceState) -> Result<(), FederationProviderError> { + self.backend_driver.cleanup(state).await + } + + /// Create new auth state. + #[tracing::instrument(level = "debug", skip(self, state))] + async fn create_auth_state( + &self, + state: &ServiceState, + auth_state: AuthState, + ) -> Result { + self.backend_driver + .create_auth_state(state, auth_state) + .await + } + + /// Create Identity provider. + #[tracing::instrument(level = "debug", skip(self, state))] + async fn create_identity_provider( + &self, + state: &ServiceState, + idp: IdentityProviderCreate, + ) -> Result { + let mut mod_idp = idp; + if mod_idp.id.is_none() { + mod_idp.id = Some(Uuid::new_v4().simple().to_string()); + } + + self.backend_driver + .create_identity_provider(state, mod_idp) + .await + } + + /// Create mapping. + #[tracing::instrument(level = "debug", skip(self, state))] + async fn create_mapping( + &self, + state: &ServiceState, + mapping: Mapping, + ) -> Result { + let mut mod_mapping = mapping; + mod_mapping.id = Uuid::new_v4().into(); + if let Some(_pid) = &mod_mapping.token_project_id { + // ensure domain_id is set and matches the one of the project_id. + if let Some(_did) = &mod_mapping.domain_id { + // TODO: Get the project_id and compare the domain_id + } else { + return Err(FederationProviderError::MappingTokenProjectDomainUnset); + } + // TODO: ensure current user has access to the project + } + + self.backend_driver.create_mapping(state, mod_mapping).await + } + + /// Delete auth state. + #[tracing::instrument(level = "debug", skip(self, state))] + async fn delete_auth_state<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result<(), FederationProviderError> { + self.backend_driver.delete_auth_state(state, id).await + } + + /// Delete identity provider. + #[tracing::instrument(level = "debug", skip(self, state))] + async fn delete_identity_provider<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result<(), FederationProviderError> { + self.backend_driver + .delete_identity_provider(state, id) + .await + } + + /// Delete identity provider. + #[tracing::instrument(level = "debug", skip(self, state))] + async fn delete_mapping<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result<(), FederationProviderError> { + self.backend_driver.delete_mapping(state, id).await + } + + /// Get auth state by ID. + #[tracing::instrument(level = "debug", skip(self, state))] + async fn get_auth_state<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result, FederationProviderError> { + self.backend_driver.get_auth_state(state, id).await + } + + /// Get single IDP by ID. + #[tracing::instrument(level = "info", skip(self, state))] + async fn get_identity_provider<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result, FederationProviderError> { + self.backend_driver.get_identity_provider(state, id).await + } + + /// Get single mapping by ID. + #[tracing::instrument(level = "info", skip(self, state))] + async fn get_mapping<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result, FederationProviderError> { + self.backend_driver.get_mapping(state, id).await + } + + /// List IDP. + #[tracing::instrument(level = "info", skip(self, state))] + async fn list_identity_providers( + &self, + state: &ServiceState, + params: &IdentityProviderListParameters, + ) -> Result, FederationProviderError> { + self.backend_driver + .list_identity_providers(state, params) + .await + } + + /// List mappings. + #[tracing::instrument(level = "info", skip(self, state))] + async fn list_mappings( + &self, + state: &ServiceState, + params: &MappingListParameters, + ) -> Result, FederationProviderError> { + self.backend_driver.list_mappings(state, params).await + } + + /// Update Identity provider. + #[tracing::instrument(level = "debug", skip(self, state))] + async fn update_identity_provider<'a>( + &self, + state: &ServiceState, + id: &'a str, + idp: IdentityProviderUpdate, + ) -> Result { + self.backend_driver + .update_identity_provider(state, id, idp) + .await + } + + /// Update mapping + #[tracing::instrument(level = "debug", skip(self, state))] + async fn update_mapping<'a>( + &self, + state: &ServiceState, + id: &'a str, + mapping: MappingUpdate, + ) -> Result { + let current = self + .backend_driver + .get_mapping(state, id) + .await? + .ok_or_else(|| FederationProviderError::MappingNotFound(id.to_string()))?; + + if let Some(_new_idp_id) = &mapping.idp_id { + // TODO: Check the new idp_id domain escaping + } + + if let Some(_pid) = &mapping.token_project_id { + // ensure domain_id is set and matches the one of the project_id. + if let Some(_did) = ¤t.domain_id { + // TODO: Get the project_id and compare the domain_id + } else { + return Err(FederationProviderError::MappingTokenProjectDomainUnset); + } + // TODO: ensure current user has access to the project + } + // TODO: Pass current to the backend to skip re-fetching + self.backend_driver.update_mapping(state, id, mapping).await + } +} diff --git a/crates/core/src/federation/types.rs b/crates/core/src/federation/types.rs new file mode 100644 index 00000000..38f1242c --- /dev/null +++ b/crates/core/src/federation/types.rs @@ -0,0 +1,23 @@ +// 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 +//! # Federation provider types +pub mod auth_state; +pub mod identity_provider; +pub mod mapping; +pub mod provider; + +pub use auth_state::*; +pub use identity_provider::*; +pub use mapping::*; +pub use provider::FederationApi; diff --git a/crates/core/src/federation/types/auth_state.rs b/crates/core/src/federation/types/auth_state.rs new file mode 100644 index 00000000..1929c5db --- /dev/null +++ b/crates/core/src/federation/types/auth_state.rs @@ -0,0 +1,51 @@ +// 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 + +use chrono::{DateTime, Utc}; +use derive_builder::Builder; +use serde::{Deserialize, Serialize}; + +use crate::common::types::Scope; +use crate::error::BuilderError; + +#[derive(Builder, Clone, Debug, Default, Deserialize, Serialize, PartialEq)] +#[builder(build_fn(error = "BuilderError"))] +#[builder(setter(strip_option, into))] +pub struct AuthState { + /// Timestamp when the auth will expire. + #[builder(default)] + pub expires_at: DateTime, + + /// IDP ID. + pub idp_id: String, + + /// Mapping ID. + pub mapping_id: String, + + /// Nonce. + pub nonce: String, + + /// PKCE verifier value. + pub pkce_verifier: String, + + /// Requested redirect uri. + pub redirect_uri: String, + + /// Requested scope. + #[builder(default)] + pub scope: Option, + + /// Auth state (Primary key, CSRF). + pub state: String, +} diff --git a/crates/core/src/federation/types/identity_provider.rs b/crates/core/src/federation/types/identity_provider.rs new file mode 100644 index 00000000..319569df --- /dev/null +++ b/crates/core/src/federation/types/identity_provider.rs @@ -0,0 +1,186 @@ +// 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 +//! # Federated identity provider types + +use derive_builder::Builder; +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +use crate::error::BuilderError; + +/// Identity provider resource. +#[derive(Builder, Clone, Debug, Default, Deserialize, Serialize, PartialEq)] +#[builder(build_fn(error = "BuilderError"))] +#[builder(setter(strip_option, into))] +pub struct IdentityProvider { + /// Federation provider ID. + pub id: String, + + /// Provider name. + pub name: String, + + /// Domain ID. + #[builder(default)] + pub domain_id: Option, + + /// Whether the identity provider is enabled. + #[builder(default)] + pub enabled: bool, + + /// OIDC discovery url. + #[builder(default)] + pub oidc_discovery_url: Option, + + #[builder(default)] + pub oidc_client_id: Option, + + #[builder(default)] + pub oidc_client_secret: Option, + + #[builder(default)] + pub oidc_response_mode: Option, + + #[builder(default)] + pub oidc_response_types: Option>, + + #[builder(default)] + pub jwks_url: Option, + + #[builder(default)] + pub jwt_validation_pubkeys: Option>, + + #[builder(default)] + pub bound_issuer: Option, + + #[builder(default)] + pub default_mapping_name: Option, + + #[builder(default)] + pub provider_config: Option, +} + +/// New Identity provider data. +#[derive(Builder, Clone, Debug, Default, Deserialize, Serialize, PartialEq)] +#[builder(build_fn(error = "BuilderError"))] +#[builder(setter(strip_option, into))] +pub struct IdentityProviderCreate { + /// Federation provider ID. + pub id: Option, + + /// Provider name. + pub name: String, + + /// Domain ID. + #[builder(default)] + pub domain_id: Option, + + /// Whether the identity provider is enabled. + #[builder(default)] + pub enabled: bool, + + /// OIDC discovery url. + #[builder(default)] + pub oidc_discovery_url: Option, + + #[builder(default)] + pub oidc_client_id: Option, + + #[builder(default)] + pub oidc_client_secret: Option, + + #[builder(default)] + pub oidc_response_mode: Option, + + #[builder(default)] + pub oidc_response_types: Option>, + + #[builder(default)] + pub jwks_url: Option, + + #[builder(default)] + pub jwt_validation_pubkeys: Option>, + + #[builder(default)] + pub bound_issuer: Option, + + #[builder(default)] + pub default_mapping_name: Option, + + #[builder(default)] + pub provider_config: Option, +} + +/// Identity provider update data. +#[derive(Builder, Clone, Debug, Default, Deserialize, Serialize, PartialEq)] +#[builder(build_fn(error = "BuilderError"))] +#[builder(setter(into))] +pub struct IdentityProviderUpdate { + /// Provider name. + pub name: Option, + + /// Enabled flag. + pub enabled: Option, + + #[builder(default)] + pub oidc_discovery_url: Option>, + + #[builder(default)] + pub oidc_client_id: Option>, + + #[builder(default)] + pub oidc_client_secret: Option>, + + #[builder(default)] + pub oidc_response_mode: Option>, + + #[builder(default)] + pub oidc_response_types: Option>>, + + #[builder(default)] + pub jwks_url: Option>, + + #[builder(default)] + pub jwt_validation_pubkeys: Option>>, + + #[builder(default)] + pub bound_issuer: Option>, + + #[builder(default)] + pub default_mapping_name: Option>, + + #[builder(default)] + pub provider_config: Option>, +} + +/// Identity provider list request. +#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +#[builder(build_fn(error = "BuilderError"))] +#[builder(setter(strip_option, into))] +pub struct IdentityProviderListParameters { + /// Filters the response by a domain_id ID. It is an optional list of + /// optional strings to represent fetching of null and non-null values + /// in a single request. + pub domain_ids: Option>>, + + /// Limit number of entries on the single response page. + #[builder(default)] + pub limit: Option, + + /// Page marker (id of the last entry on the previous page. + #[builder(default)] + pub marker: Option, + /// + /// Filters the response by IDP name. + pub name: Option, +} diff --git a/crates/core/src/federation/types/mapping.rs b/crates/core/src/federation/types/mapping.rs new file mode 100644 index 00000000..0e31915f --- /dev/null +++ b/crates/core/src/federation/types/mapping.rs @@ -0,0 +1,193 @@ +// 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 + +use derive_builder::Builder; +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +use crate::error::BuilderError; + +/// Attribute mapping data. +#[derive(Builder, Clone, Debug, Default, Deserialize, Serialize, PartialEq)] +#[builder(build_fn(error = "BuilderError"))] +#[builder(setter(strip_option, into))] +pub struct Mapping { + /// Federation IDP attribute mapping ID. + pub id: String, + + /// Attribute mapping name. + pub name: String, + + /// ID of the domain for the attribute mapping. + #[builder(default)] + pub domain_id: Option, + + /// Identity provider for the attribute mapping. + pub idp_id: String, + + /// Mapping type. + pub r#type: MappingType, + + /// Enabled attribute. + pub enabled: bool, + + /// List of allowed redirect_uri for the oidc mapping. + #[builder(default)] + pub allowed_redirect_uris: Option>, + + /// Claim attribute name to extract `user_id`. + #[builder(default)] + pub user_id_claim: String, + + /// Claim attribute name to extract `user_name`. + #[builder(default)] + pub user_name_claim: String, + + /// Claim attribute name to extract `domain_id`. + #[builder(default)] + pub domain_id_claim: Option, + + /// Claim attribute name to extract list of groups. + #[builder(default)] + pub groups_claim: Option, + + /// Fixed (JWT) audiences that the assertion must be issued for. + #[builder(default)] + pub bound_audiences: Option>, + + /// Fixed subject that the assertion (jwt) must be issued for. + #[builder(default)] + pub bound_subject: Option, + + /// Additional claims to further restrict the attribute mapping. + #[builder(default)] + pub bound_claims: Option, + + /// List of the oidc scopes to request in the oidc flow. + #[builder(default)] + pub oidc_scopes: Option>, + + //#[builder(default)] + //pub claim_mappings: Option, + /// Fixed `project_id` scope of the token to issue for successful + /// authentication. + #[builder(default)] + pub token_project_id: Option, + + /// ID of the token restrictions. + #[builder(default)] + pub token_restriction_id: Option, +} + +/// Update attribute mapping data. +#[derive(Builder, Clone, Debug, Default, Deserialize, Serialize, PartialEq)] +#[builder(build_fn(error = "BuilderError"))] +#[builder(setter(into))] +pub struct MappingUpdate { + /// Attribute mapping name. + pub name: Option, + + /// Identity provider for the attribute mapping. + #[builder(default)] + pub idp_id: Option, + + /// Mapping type. + #[builder(default)] + pub r#type: Option, + + /// Enabled attribute. + #[builder(default)] + pub enabled: Option, + + /// List of allowed redirect_uri for the oidc mapping. + #[builder(default)] + pub allowed_redirect_uris: Option>>, + + /// Claim attribute name to extract `user_id`. + #[builder(default)] + pub user_id_claim: Option, + + /// Claim attribute name to extract `user_name`. + #[builder(default)] + pub user_name_claim: Option, + + /// Claim attribute name to extract `domain_id`. + #[builder(default)] + pub domain_id_claim: Option, + + /// claim attribute name to extract list of groups. + #[builder(default)] + pub groups_claim: Option>, + + /// Fixed (JWT) audiences that the assertion must be issued for. + #[builder(default)] + pub bound_audiences: Option>>, + + /// Fixed subject that the assertion (jwt) must be issued for. + #[builder(default)] + pub bound_subject: Option>, + + /// Additional claims to further restrict the attribute mapping. + #[builder(default)] + pub bound_claims: Option, + + /// List of the oidc scopes to request in the oidc flow. + #[builder(default)] + pub oidc_scopes: Option>>, + + /// Fixed `project_id` scope of the token to issue for successful + /// authentication. + #[builder(default)] + pub token_project_id: Option>, + + /// ID of the token restrictions. + #[builder(default)] + pub token_restriction_id: Option, +} + +/// Attribute mapping type. +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +pub enum MappingType { + #[default] + /// OIDC. + Oidc, + /// JWT. + Jwt, +} + +/// List attribute mappings request. +#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +#[builder(build_fn(error = "BuilderError"))] +#[builder(setter(strip_option, into))] +pub struct MappingListParameters { + /// Filters the response by a domain_id ID. + pub domain_id: Option, + + /// Filters the response by IDP ID. + pub idp_id: Option, + + /// Limit number of entries on the single response page. + #[builder(default)] + pub limit: Option, + + /// Page marker (id of the last entry on the previous page. + #[builder(default)] + pub marker: Option, + + /// Filters the response by Mapping name. + pub name: Option, + + /// Filters mappings by the type. + pub r#type: Option, +} diff --git a/crates/core/src/federation/types/provider.rs b/crates/core/src/federation/types/provider.rs new file mode 100644 index 00000000..6c21201a --- /dev/null +++ b/crates/core/src/federation/types/provider.rs @@ -0,0 +1,109 @@ +// 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 +//! # Federation provider +//! +//! Federation provider implements the functionality necessary for the user +//! federation. +use async_trait::async_trait; + +use crate::federation::error::FederationProviderError; +use crate::federation::types::*; +use crate::keystone::ServiceState; + +/// Federation provider interface. +#[async_trait] +pub trait FederationApi: Send + Sync { + /// Cleanup expired resources + async fn cleanup(&self, state: &ServiceState) -> Result<(), FederationProviderError>; + + async fn create_identity_provider( + &self, + state: &ServiceState, + idp: IdentityProviderCreate, + ) -> Result; + + async fn create_auth_state( + &self, + state: &ServiceState, + state: AuthState, + ) -> Result; + + async fn create_mapping( + &self, + state: &ServiceState, + mapping: Mapping, + ) -> Result; + + async fn delete_auth_state<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result<(), FederationProviderError>; + + async fn delete_identity_provider<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result<(), FederationProviderError>; + + async fn delete_mapping<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result<(), FederationProviderError>; + + async fn get_auth_state<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result, FederationProviderError>; + + async fn get_identity_provider<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result, FederationProviderError>; + + async fn get_mapping<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result, FederationProviderError>; + + async fn list_identity_providers( + &self, + state: &ServiceState, + params: &IdentityProviderListParameters, + ) -> Result, FederationProviderError>; + + async fn list_mappings( + &self, + state: &ServiceState, + params: &MappingListParameters, + ) -> Result, FederationProviderError>; + + async fn update_identity_provider<'a>( + &self, + state: &ServiceState, + id: &'a str, + idp: IdentityProviderUpdate, + ) -> Result; + + async fn update_mapping<'a>( + &self, + state: &ServiceState, + id: &'a str, + mapping: MappingUpdate, + ) -> Result; +} diff --git a/crates/core/src/identity/backend.rs b/crates/core/src/identity/backend.rs new file mode 100644 index 00000000..49b2b02c --- /dev/null +++ b/crates/core/src/identity/backend.rs @@ -0,0 +1,209 @@ +// 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 + +use async_trait::async_trait; +use chrono::{DateTime, Utc}; +use std::collections::HashSet; + +use crate::auth::AuthenticatedInfo; +use crate::identity::IdentityProviderError; +use crate::identity::types::*; +use crate::keystone::ServiceState; + +#[cfg_attr(test, mockall::automock)] +#[async_trait] +pub trait IdentityBackend: Send + Sync { + /// Add the user to the group. + async fn add_user_to_group<'a>( + &self, + state: &ServiceState, + user_id: &'a str, + group_id: &'a str, + ) -> Result<(), IdentityProviderError>; + + /// Add the user to the group with expiration. + async fn add_user_to_group_expiring<'a>( + &self, + state: &ServiceState, + user_id: &'a str, + group_id: &'a str, + idp_id: &'a str, + ) -> Result<(), IdentityProviderError>; + + /// Add user group membership relations. + async fn add_users_to_groups<'a>( + &self, + state: &ServiceState, + memberships: Vec<(&'a str, &'a str)>, + ) -> Result<(), IdentityProviderError>; + + /// Add expiring user group membership relations. + async fn add_users_to_groups_expiring<'a>( + &self, + state: &ServiceState, + memberships: Vec<(&'a str, &'a str)>, + idp_id: &'a str, + ) -> Result<(), IdentityProviderError>; + + /// Authenticate a user by a password. + async fn authenticate_by_password( + &self, + state: &ServiceState, + auth: &UserPasswordAuthRequest, + ) -> Result; + + /// Create group. + async fn create_group( + &self, + state: &ServiceState, + group: GroupCreate, + ) -> Result; + + /// Create service account. + async fn create_service_account( + &self, + state: &ServiceState, + sa: ServiceAccountCreate, + ) -> Result; + + /// Create user. + async fn create_user( + &self, + state: &ServiceState, + user: UserCreate, + ) -> Result; + + /// Delete group by ID. + async fn delete_group<'a>( + &self, + state: &ServiceState, + group_id: &'a str, + ) -> Result<(), IdentityProviderError>; + + /// Delete user. + async fn delete_user<'a>( + &self, + state: &ServiceState, + user_id: &'a str, + ) -> Result<(), IdentityProviderError>; + + /// Get single group by ID. + async fn get_group<'a>( + &self, + state: &ServiceState, + group_id: &'a str, + ) -> Result, IdentityProviderError>; + + /// Get single service account by ID. + async fn get_service_account<'a>( + &self, + state: &ServiceState, + user_id: &'a str, + ) -> Result, IdentityProviderError>; + + /// Get single user by ID. + async fn get_user<'a>( + &self, + state: &ServiceState, + user_id: &'a str, + ) -> Result, IdentityProviderError>; + + /// Get single user by ID. + async fn get_user_domain_id<'a>( + &self, + state: &ServiceState, + user_id: &'a str, + ) -> Result; + + /// Find federated user by IDP and Unique ID. + async fn find_federated_user<'a>( + &self, + state: &ServiceState, + idp_id: &'a str, + unique_id: &'a str, + ) -> Result, IdentityProviderError>; + + /// List groups. + async fn list_groups( + &self, + state: &ServiceState, + params: &GroupListParameters, + ) -> Result, IdentityProviderError>; + + /// List Users. + async fn list_users( + &self, + state: &ServiceState, + params: &UserListParameters, + ) -> Result, IdentityProviderError>; + + /// List groups a user is member of. + async fn list_groups_of_user<'a>( + &self, + state: &ServiceState, + user_id: &'a str, + ) -> Result, IdentityProviderError>; + + /// Remove the user from the group. + async fn remove_user_from_group<'a>( + &self, + state: &ServiceState, + user_id: &'a str, + group_id: &'a str, + ) -> Result<(), IdentityProviderError>; + + /// Remove the user from the group with expiration. + async fn remove_user_from_group_expiring<'a>( + &self, + state: &ServiceState, + user_id: &'a str, + group_id: &'a str, + idp_id: &'a str, + ) -> Result<(), IdentityProviderError>; + + /// Remove the user from multiple groups. + async fn remove_user_from_groups<'a>( + &self, + state: &ServiceState, + user_id: &'a str, + group_ids: HashSet<&'a str>, + ) -> Result<(), IdentityProviderError>; + + /// Remove the user from multiple expiring groups. + async fn remove_user_from_groups_expiring<'a>( + &self, + state: &ServiceState, + user_id: &'a str, + group_ids: HashSet<&'a str>, + idp_id: &'a str, + ) -> Result<(), IdentityProviderError>; + + /// Set group memberships for the user. + async fn set_user_groups<'a>( + &self, + state: &ServiceState, + user_id: &'a str, + group_ids: HashSet<&'a str>, + ) -> Result<(), IdentityProviderError>; + + /// Set expiring group memberships for the user. + async fn set_user_groups_expiring<'a>( + &self, + state: &ServiceState, + user_id: &'a str, + group_ids: HashSet<&'a str>, + idp_id: &'a str, + last_verified: Option<&'a DateTime>, + ) -> Result<(), IdentityProviderError>; +} diff --git a/crates/keystone/src/identity/error.rs b/crates/core/src/identity/error.rs similarity index 97% rename from crates/keystone/src/identity/error.rs rename to crates/core/src/identity/error.rs index 3975dc4e..513dd8d2 100644 --- a/crates/keystone/src/identity/error.rs +++ b/crates/core/src/identity/error.rs @@ -94,7 +94,7 @@ pub enum IdentityProviderError { }, /// Unsupported driver. - #[error("unsupported driver {0}")] + #[error("unsupported driver `{0}` for the identity provider")] UnsupportedDriver(String), #[error("user id must be given")] diff --git a/crates/keystone/src/identity/mock.rs b/crates/core/src/identity/mock.rs similarity index 96% rename from crates/keystone/src/identity/mock.rs rename to crates/core/src/identity/mock.rs index 01cec71c..8fed61fc 100644 --- a/crates/keystone/src/identity/mock.rs +++ b/crates/core/src/identity/mock.rs @@ -18,15 +18,11 @@ use mockall::mock; use std::collections::HashSet; use crate::auth::AuthenticatedInfo; -use crate::config::Config; use crate::identity::{IdentityApi, error::IdentityProviderError, types::*}; use crate::keystone::ServiceState; -use crate::plugin_manager::PluginManager; mock! { - pub IdentityProvider { - pub fn new(cfg: &Config, plugin_manager: &PluginManager) -> Result; - } + pub IdentityProvider {} #[async_trait] impl IdentityApi for IdentityProvider { diff --git a/crates/core/src/identity/mod.rs b/crates/core/src/identity/mod.rs new file mode 100644 index 00000000..6683c2ea --- /dev/null +++ b/crates/core/src/identity/mod.rs @@ -0,0 +1,478 @@ +// 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 + +//! # Identity provider +//! +//! Following identity concepts are covered by the identity provider: +//! +//! ## Group +//! +//! An Identity service API v3 entity. Groups are a collection of users +//! owned by a domain. A group role, granted to a domain or project, applies to +//! all users in the group. Adding or removing users to or from a group grants +//! or revokes their role and authentication to the associated domain or +//! project. +//! +//! ## User +//! +//! A digital representation of a person, system, or service that uses +//! OpenStack cloud services. The Identity service validates that incoming +//! requests are made by the user who claims to be making the call. Users have a +//! login and can access resources by using assigned tokens. Users can be +//! directly assigned to a particular project and behave as if they are +//! contained in that project. + +use async_trait::async_trait; +use chrono::{DateTime, Utc}; +use std::collections::HashSet; + +pub mod backend; +pub mod error; +#[cfg(any(test, feature = "mock"))] +pub mod mock; +pub mod types; +#[cfg(any(test, feature = "mock"))] +pub use mock::MockIdentityProvider; +pub mod service; + +use crate::auth::AuthenticatedInfo; +use crate::config::Config; +use crate::identity::types::*; +use crate::keystone::ServiceState; +use crate::plugin_manager::PluginManagerApi; +use service::IdentityService; + +pub use error::IdentityProviderError; +pub use types::IdentityApi; + +/// Identity provider. +pub enum IdentityProvider { + Service(IdentityService), + #[cfg(any(test, feature = "mock"))] + Mock(MockIdentityProvider), +} + +impl IdentityProvider { + pub fn new( + config: &Config, + plugin_manager: &P, + ) -> Result { + Ok(Self::Service(IdentityService::new(config, plugin_manager)?)) + } +} + +#[async_trait] +impl IdentityApi for IdentityProvider { + #[tracing::instrument(skip(self, state))] + async fn add_user_to_group<'a>( + &self, + state: &ServiceState, + user_id: &'a str, + group_id: &'a str, + ) -> Result<(), IdentityProviderError> { + match self { + Self::Service(provider) => provider.add_user_to_group(state, user_id, group_id).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.add_user_to_group(state, user_id, group_id).await, + } + } + + #[tracing::instrument(skip(self, state))] + async fn add_user_to_group_expiring<'a>( + &self, + state: &ServiceState, + user_id: &'a str, + group_id: &'a str, + idp_id: &'a str, + ) -> Result<(), IdentityProviderError> { + match self { + Self::Service(provider) => { + provider + .add_user_to_group_expiring(state, user_id, group_id, idp_id) + .await + } + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => { + provider + .add_user_to_group_expiring(state, user_id, group_id, idp_id) + .await + } + } + } + + #[tracing::instrument(skip(self, state))] + async fn add_users_to_groups<'a>( + &self, + state: &ServiceState, + memberships: Vec<(&'a str, &'a str)>, + ) -> Result<(), IdentityProviderError> { + match self { + Self::Service(provider) => provider.add_users_to_groups(state, memberships).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.add_users_to_groups(state, memberships).await, + } + } + + #[tracing::instrument(skip(self, state))] + async fn add_users_to_groups_expiring<'a>( + &self, + state: &ServiceState, + memberships: Vec<(&'a str, &'a str)>, + idp_id: &'a str, + ) -> Result<(), IdentityProviderError> { + match self { + Self::Service(provider) => { + provider + .add_users_to_groups_expiring(state, memberships, idp_id) + .await + } + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => { + provider + .add_users_to_groups_expiring(state, memberships, idp_id) + .await + } + } + } + + /// Authenticate user with the password auth method. + #[tracing::instrument(skip(self, state, auth))] + async fn authenticate_by_password( + &self, + state: &ServiceState, + auth: &UserPasswordAuthRequest, + ) -> Result { + match self { + Self::Service(provider) => provider.authenticate_by_password(state, auth).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.authenticate_by_password(state, auth).await, + } + } + + /// Create group. + #[tracing::instrument(skip(self, state))] + async fn create_group( + &self, + state: &ServiceState, + group: GroupCreate, + ) -> Result { + match self { + Self::Service(provider) => provider.create_group(state, group).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.create_group(state, group).await, + } + } + + /// Create service account. + #[tracing::instrument(skip(self, state))] + async fn create_service_account( + &self, + state: &ServiceState, + sa: ServiceAccountCreate, + ) -> Result { + match self { + Self::Service(provider) => provider.create_service_account(state, sa).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.create_service_account(state, sa).await, + } + } + + /// Create user. + #[tracing::instrument(skip(self, state))] + async fn create_user( + &self, + state: &ServiceState, + user: UserCreate, + ) -> Result { + match self { + Self::Service(provider) => provider.create_user(state, user).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.create_user(state, user).await, + } + } + + /// Delete group. + #[tracing::instrument(skip(self, state))] + async fn delete_group<'a>( + &self, + state: &ServiceState, + group_id: &'a str, + ) -> Result<(), IdentityProviderError> { + match self { + Self::Service(provider) => provider.delete_group(state, group_id).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.delete_group(state, group_id).await, + } + } + + /// Delete user. + #[tracing::instrument(skip(self, state))] + async fn delete_user<'a>( + &self, + state: &ServiceState, + user_id: &'a str, + ) -> Result<(), IdentityProviderError> { + match self { + Self::Service(provider) => provider.delete_user(state, user_id).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.delete_user(state, user_id).await, + } + } + + /// Get a service account by ID. + #[tracing::instrument(skip(self, state))] + async fn get_service_account<'a>( + &self, + state: &ServiceState, + user_id: &'a str, + ) -> Result, IdentityProviderError> { + match self { + Self::Service(provider) => provider.get_service_account(state, user_id).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.get_service_account(state, user_id).await, + } + } + + /// Get single user. + #[tracing::instrument(skip(self, state))] + async fn get_user<'a>( + &self, + state: &ServiceState, + user_id: &'a str, + ) -> Result, IdentityProviderError> { + match self { + Self::Service(provider) => provider.get_user(state, user_id).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.get_user(state, user_id).await, + } + } + + /// Get `domain_id` of a user. + /// + /// When the caching is enabled check for the cached value there. When no + /// data is present for the key - invoke the backend driver and place + /// the new value into the cache. Other operations (`get_user`, + /// `delete_user`) update the cache with `delete_user` purging the value + /// from the cache. + async fn get_user_domain_id<'a>( + &self, + state: &ServiceState, + user_id: &'a str, + ) -> Result { + match self { + Self::Service(provider) => provider.get_user_domain_id(state, user_id).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.get_user_domain_id(state, user_id).await, + } + } + + /// Find federated user by `idp_id` and `unique_id`. + #[tracing::instrument(skip(self, state))] + async fn find_federated_user<'a>( + &self, + state: &ServiceState, + idp_id: &'a str, + unique_id: &'a str, + ) -> Result, IdentityProviderError> { + match self { + Self::Service(provider) => provider.find_federated_user(state, idp_id, unique_id).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.find_federated_user(state, idp_id, unique_id).await, + } + } + + /// List users. + #[tracing::instrument(skip(self, state))] + async fn list_users( + &self, + state: &ServiceState, + params: &UserListParameters, + ) -> Result, IdentityProviderError> { + match self { + Self::Service(provider) => provider.list_users(state, params).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.list_users(state, params).await, + } + } + + /// List groups. + #[tracing::instrument(skip(self, state))] + async fn list_groups( + &self, + state: &ServiceState, + params: &GroupListParameters, + ) -> Result, IdentityProviderError> { + match self { + Self::Service(provider) => provider.list_groups(state, params).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.list_groups(state, params).await, + } + } + + /// Get single group. + #[tracing::instrument(skip(self, state))] + async fn get_group<'a>( + &self, + state: &ServiceState, + group_id: &'a str, + ) -> Result, IdentityProviderError> { + match self { + Self::Service(provider) => provider.get_group(state, group_id).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.get_group(state, group_id).await, + } + } + + /// List groups a user is a member of. + #[tracing::instrument(skip(self, state))] + async fn list_groups_of_user<'a>( + &self, + state: &ServiceState, + user_id: &'a str, + ) -> Result, IdentityProviderError> { + match self { + Self::Service(provider) => provider.list_groups_of_user(state, user_id).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.list_groups_of_user(state, user_id).await, + } + } + + #[tracing::instrument(skip(self, state))] + async fn remove_user_from_group<'a>( + &self, + state: &ServiceState, + user_id: &'a str, + group_id: &'a str, + ) -> Result<(), IdentityProviderError> { + match self { + Self::Service(provider) => { + provider + .remove_user_from_group(state, user_id, group_id) + .await + } + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => { + provider + .remove_user_from_group(state, user_id, group_id) + .await + } + } + } + + #[tracing::instrument(skip(self, state))] + async fn remove_user_from_group_expiring<'a>( + &self, + state: &ServiceState, + user_id: &'a str, + group_id: &'a str, + idp_id: &'a str, + ) -> Result<(), IdentityProviderError> { + match self { + Self::Service(provider) => { + provider + .remove_user_from_group_expiring(state, user_id, group_id, idp_id) + .await + } + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => { + provider + .remove_user_from_group_expiring(state, user_id, group_id, idp_id) + .await + } + } + } + + #[tracing::instrument(skip(self, state))] + async fn remove_user_from_groups<'a>( + &self, + state: &ServiceState, + user_id: &'a str, + group_ids: HashSet<&'a str>, + ) -> Result<(), IdentityProviderError> { + match self { + Self::Service(provider) => { + provider + .remove_user_from_groups(state, user_id, group_ids) + .await + } + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => { + provider + .remove_user_from_groups(state, user_id, group_ids) + .await + } + } + } + + #[tracing::instrument(skip(self, state))] + async fn remove_user_from_groups_expiring<'a>( + &self, + state: &ServiceState, + user_id: &'a str, + group_ids: HashSet<&'a str>, + idp_id: &'a str, + ) -> Result<(), IdentityProviderError> { + match self { + Self::Service(provider) => { + provider + .remove_user_from_groups_expiring(state, user_id, group_ids, idp_id) + .await + } + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => { + provider + .remove_user_from_groups_expiring(state, user_id, group_ids, idp_id) + .await + } + } + } + + #[tracing::instrument(skip(self, state))] + async fn set_user_groups<'a>( + &self, + state: &ServiceState, + user_id: &'a str, + group_ids: HashSet<&'a str>, + ) -> Result<(), IdentityProviderError> { + match self { + Self::Service(provider) => provider.set_user_groups(state, user_id, group_ids).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.set_user_groups(state, user_id, group_ids).await, + } + } + + #[tracing::instrument(skip(self, state))] + async fn set_user_groups_expiring<'a>( + &self, + state: &ServiceState, + user_id: &'a str, + group_ids: HashSet<&'a str>, + idp_id: &'a str, + last_verified: Option<&'a DateTime>, + ) -> Result<(), IdentityProviderError> { + match self { + Self::Service(provider) => { + provider + .set_user_groups_expiring(state, user_id, group_ids, idp_id, last_verified) + .await + } + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => { + provider + .set_user_groups_expiring(state, user_id, group_ids, idp_id, last_verified) + .await + } + } + } +} diff --git a/crates/core/src/identity/service.rs b/crates/core/src/identity/service.rs new file mode 100644 index 00000000..a5f408c2 --- /dev/null +++ b/crates/core/src/identity/service.rs @@ -0,0 +1,557 @@ +// 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 + +//! # Identity provider + +use async_trait::async_trait; +use chrono::{DateTime, Utc}; +use std::collections::{HashMap, HashSet}; +use std::sync::Arc; +use tokio::sync::RwLock; +use uuid::Uuid; +use validator::Validate; + +use crate::auth::AuthenticatedInfo; +use crate::config::Config; +use crate::identity::{IdentityProviderError, backend::IdentityBackend, types::*}; +use crate::keystone::ServiceState; +use crate::plugin_manager::PluginManagerApi; +use crate::resource::{ResourceApi, error::ResourceProviderError}; + +/// Identity provider. +pub struct IdentityService { + backend_driver: Arc, + /// Caching flag. When enabled certain data can be cached (i.e. `domain_id` + /// by `user_id`). + caching: bool, + /// Internal cache of `user_id` to `domain_id` mappings. This information if + /// fully static and can never change (well, except with a direct SQL + /// update). + user_id_domain_id_cache: RwLock>, +} + +impl IdentityService { + pub fn new( + config: &Config, + plugin_manager: &P, + ) -> Result { + let backend_driver = plugin_manager + .get_identity_backend(config.identity.driver.clone())? + .clone(); + Ok(Self { + backend_driver, + caching: config.identity.caching, + user_id_domain_id_cache: HashMap::new().into(), + }) + } + + pub fn from_driver(driver: I) -> Self { + Self { + backend_driver: Arc::new(driver), + caching: false, + user_id_domain_id_cache: HashMap::new().into(), + } + } +} + +#[async_trait] +impl IdentityApi for IdentityService { + #[tracing::instrument(skip(self, state))] + async fn add_user_to_group<'a>( + &self, + state: &ServiceState, + user_id: &'a str, + group_id: &'a str, + ) -> Result<(), IdentityProviderError> { + self.backend_driver + .add_user_to_group(state, user_id, group_id) + .await + } + + #[tracing::instrument(skip(self, state))] + async fn add_user_to_group_expiring<'a>( + &self, + state: &ServiceState, + user_id: &'a str, + group_id: &'a str, + idp_id: &'a str, + ) -> Result<(), IdentityProviderError> { + self.backend_driver + .add_user_to_group_expiring(state, user_id, group_id, idp_id) + .await + } + + #[tracing::instrument(skip(self, state))] + async fn add_users_to_groups<'a>( + &self, + state: &ServiceState, + memberships: Vec<(&'a str, &'a str)>, + ) -> Result<(), IdentityProviderError> { + self.backend_driver + .add_users_to_groups(state, memberships) + .await + } + + #[tracing::instrument(skip(self, state))] + async fn add_users_to_groups_expiring<'a>( + &self, + state: &ServiceState, + memberships: Vec<(&'a str, &'a str)>, + idp_id: &'a str, + ) -> Result<(), IdentityProviderError> { + self.backend_driver + .add_users_to_groups_expiring(state, memberships, idp_id) + .await + } + + /// Authenticate user with the password auth method. + #[tracing::instrument(skip(self, state, auth))] + async fn authenticate_by_password( + &self, + state: &ServiceState, + auth: &UserPasswordAuthRequest, + ) -> Result { + let mut auth = auth.clone(); + if auth.id.is_none() { + if auth.name.is_none() { + return Err(IdentityProviderError::UserIdOrNameWithDomain); + } + + if let Some(ref mut domain) = auth.domain { + if let Some(dname) = &domain.name { + let d = state + .provider + .get_resource_provider() + .find_domain_by_name(state, dname) + .await? + .ok_or(ResourceProviderError::DomainNotFound(dname.clone()))?; + domain.id = Some(d.id); + } else if domain.id.is_none() { + return Err(IdentityProviderError::UserIdOrNameWithDomain); + } + } else { + return Err(IdentityProviderError::UserIdOrNameWithDomain); + } + } + + self.backend_driver + .authenticate_by_password(state, &auth) + .await + } + + /// Create group. + #[tracing::instrument(skip(self, state))] + async fn create_group( + &self, + state: &ServiceState, + group: GroupCreate, + ) -> Result { + let mut res = group; + if res.id.is_none() { + res.id = Some(Uuid::new_v4().simple().to_string()); + } + self.backend_driver.create_group(state, res).await + } + + /// Create service account. + #[tracing::instrument(skip(self, state))] + async fn create_service_account( + &self, + state: &ServiceState, + sa: ServiceAccountCreate, + ) -> Result { + let mut mod_sa = sa; + if mod_sa.id.is_none() { + mod_sa.id = Some(Uuid::new_v4().simple().to_string()); + } + if mod_sa.enabled.is_none() { + mod_sa.enabled = Some(true); + } + mod_sa.validate()?; + self.backend_driver + .create_service_account(state, mod_sa) + .await + } + + /// Create user. + #[tracing::instrument(skip(self, state))] + async fn create_user( + &self, + state: &ServiceState, + user: UserCreate, + ) -> Result { + let mut mod_user = user; + if mod_user.id.is_none() { + mod_user.id = Some(Uuid::new_v4().simple().to_string()); + } + if mod_user.enabled.is_none() { + mod_user.enabled = Some(true); + } + mod_user.validate()?; + self.backend_driver.create_user(state, mod_user).await + } + + /// Delete group. + #[tracing::instrument(skip(self, state))] + async fn delete_group<'a>( + &self, + state: &ServiceState, + group_id: &'a str, + ) -> Result<(), IdentityProviderError> { + self.backend_driver.delete_group(state, group_id).await + } + + /// Delete user. + #[tracing::instrument(skip(self, state))] + async fn delete_user<'a>( + &self, + state: &ServiceState, + user_id: &'a str, + ) -> Result<(), IdentityProviderError> { + self.backend_driver.delete_user(state, user_id).await?; + if self.caching { + self.user_id_domain_id_cache.write().await.remove(user_id); + } + Ok(()) + } + + /// Get a service account by ID. + #[tracing::instrument(skip(self, state))] + async fn get_service_account<'a>( + &self, + state: &ServiceState, + user_id: &'a str, + ) -> Result, IdentityProviderError> { + self.backend_driver + .get_service_account(state, user_id) + .await + } + + /// Get single user. + #[tracing::instrument(skip(self, state))] + async fn get_user<'a>( + &self, + state: &ServiceState, + user_id: &'a str, + ) -> Result, IdentityProviderError> { + let user = self.backend_driver.get_user(state, user_id).await?; + if self.caching + && let Some(user) = &user + { + self.user_id_domain_id_cache + .write() + .await + .insert(user_id.to_string(), user.domain_id.clone()); + } + Ok(user) + } + + /// Get `domain_id` of a user. + /// + /// When the caching is enabled check for the cached value there. When no + /// data is present for the key - invoke the backend driver and place + /// the new value into the cache. Other operations (`get_user`, + /// `delete_user`) update the cache with `delete_user` purging the value + /// from the cache. + async fn get_user_domain_id<'a>( + &self, + state: &ServiceState, + user_id: &'a str, + ) -> Result { + if self.caching { + if let Some(domain_id) = self.user_id_domain_id_cache.read().await.get(user_id) { + return Ok(domain_id.clone()); + } else { + let domain_id = self + .backend_driver + .get_user_domain_id(state, user_id) + .await?; + self.user_id_domain_id_cache + .write() + .await + .insert(user_id.to_string(), domain_id.clone()); + return Ok(domain_id); + } + } else { + Ok(self + .backend_driver + .get_user_domain_id(state, user_id) + .await?) + } + } + + /// Find federated user by `idp_id` and `unique_id`. + #[tracing::instrument(skip(self, state))] + async fn find_federated_user<'a>( + &self, + state: &ServiceState, + idp_id: &'a str, + unique_id: &'a str, + ) -> Result, IdentityProviderError> { + self.backend_driver + .find_federated_user(state, idp_id, unique_id) + .await + } + + /// List users. + #[tracing::instrument(skip(self, state))] + async fn list_users( + &self, + state: &ServiceState, + params: &UserListParameters, + ) -> Result, IdentityProviderError> { + self.backend_driver.list_users(state, params).await + } + + /// List groups. + #[tracing::instrument(skip(self, state))] + async fn list_groups( + &self, + state: &ServiceState, + params: &GroupListParameters, + ) -> Result, IdentityProviderError> { + self.backend_driver.list_groups(state, params).await + } + + /// Get single group. + #[tracing::instrument(skip(self, state))] + async fn get_group<'a>( + &self, + state: &ServiceState, + group_id: &'a str, + ) -> Result, IdentityProviderError> { + self.backend_driver.get_group(state, group_id).await + } + + /// List groups a user is a member of. + #[tracing::instrument(skip(self, state))] + async fn list_groups_of_user<'a>( + &self, + state: &ServiceState, + user_id: &'a str, + ) -> Result, IdentityProviderError> { + self.backend_driver + .list_groups_of_user(state, user_id) + .await + } + + #[tracing::instrument(skip(self, state))] + async fn remove_user_from_group<'a>( + &self, + state: &ServiceState, + user_id: &'a str, + group_id: &'a str, + ) -> Result<(), IdentityProviderError> { + self.backend_driver + .remove_user_from_group(state, user_id, group_id) + .await + } + + #[tracing::instrument(skip(self, state))] + async fn remove_user_from_group_expiring<'a>( + &self, + state: &ServiceState, + user_id: &'a str, + group_id: &'a str, + idp_id: &'a str, + ) -> Result<(), IdentityProviderError> { + self.backend_driver + .remove_user_from_group_expiring(state, user_id, group_id, idp_id) + .await + } + + #[tracing::instrument(skip(self, state))] + async fn remove_user_from_groups<'a>( + &self, + state: &ServiceState, + user_id: &'a str, + group_ids: HashSet<&'a str>, + ) -> Result<(), IdentityProviderError> { + self.backend_driver + .remove_user_from_groups(state, user_id, group_ids) + .await + } + + #[tracing::instrument(skip(self, state))] + async fn remove_user_from_groups_expiring<'a>( + &self, + state: &ServiceState, + user_id: &'a str, + group_ids: HashSet<&'a str>, + idp_id: &'a str, + ) -> Result<(), IdentityProviderError> { + self.backend_driver + .remove_user_from_groups_expiring(state, user_id, group_ids, idp_id) + .await + } + + #[tracing::instrument(skip(self, state))] + async fn set_user_groups<'a>( + &self, + state: &ServiceState, + user_id: &'a str, + group_ids: HashSet<&'a str>, + ) -> Result<(), IdentityProviderError> { + self.backend_driver + .set_user_groups(state, user_id, group_ids) + .await + } + + #[tracing::instrument(skip(self, state))] + async fn set_user_groups_expiring<'a>( + &self, + state: &ServiceState, + user_id: &'a str, + group_ids: HashSet<&'a str>, + idp_id: &'a str, + last_verified: Option<&'a DateTime>, + ) -> Result<(), IdentityProviderError> { + self.backend_driver + .set_user_groups_expiring(state, user_id, group_ids, idp_id, last_verified) + .await + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::identity::backend::MockIdentityBackend; + use crate::identity::types::user::{UserCreateBuilder, UserResponseBuilder}; + use crate::tests::get_mocked_state; + + #[tokio::test] + async fn test_create_user() { + let state = get_mocked_state(None, None); + let mut backend = MockIdentityBackend::default(); + backend.expect_create_user().returning(|_, _| { + Ok(UserResponseBuilder::default() + .id("id") + .domain_id("domain_id") + .enabled(true) + .name("name") + .build() + .unwrap()) + }); + let provider = IdentityService::from_driver(backend); + + assert_eq!( + provider + .create_user( + &state, + UserCreateBuilder::default() + .name("uname") + .domain_id("did") + .build() + .unwrap() + ) + .await + .unwrap(), + UserResponseBuilder::default() + .domain_id("domain_id") + .enabled(true) + .id("id") + .name("name") + .build() + .unwrap() + ); + } + + #[tokio::test] + async fn test_get_user() { + let state = get_mocked_state(None, None); + let mut backend = MockIdentityBackend::default(); + backend + .expect_get_user() + .withf(|_, uid: &'_ str| uid == "uid") + .returning(|_, _| { + Ok(Some( + UserResponseBuilder::default() + .id("id") + .domain_id("domain_id") + .enabled(true) + .name("name") + .build() + .unwrap(), + )) + }); + let provider = IdentityService::from_driver(backend); + + assert_eq!( + provider + .get_user(&state, "uid") + .await + .unwrap() + .expect("user should be there"), + UserResponseBuilder::default() + .domain_id("domain_id") + .enabled(true) + .id("id") + .name("name") + .build() + .unwrap(), + ); + } + + #[tokio::test] + async fn test_get_user_domain_id() { + let state = get_mocked_state(None, None); + let mut backend = MockIdentityBackend::default(); + backend + .expect_get_user_domain_id() + .withf(|_, uid: &'_ str| uid == "uid") + .times(2) // only 2 times + .returning(|_, _| Ok("did".into())); + backend + .expect_get_user_domain_id() + .withf(|_, uid: &'_ str| uid == "missing") + .returning(|_, _| Err(IdentityProviderError::UserNotFound("missing".into()))); + let mut provider = IdentityService::from_driver(backend); + provider.caching = true; + + assert_eq!( + provider.get_user_domain_id(&state, "uid").await.unwrap(), + "did" + ); + assert_eq!( + provider.get_user_domain_id(&state, "uid").await.unwrap(), + "did", + "second time data extracted from cache" + ); + assert!( + provider + .get_user_domain_id(&state, "missing") + .await + .is_err() + ); + provider.caching = false; + assert_eq!( + provider.get_user_domain_id(&state, "uid").await.unwrap(), + "did", + "third time backend is again triggered causing total of 2 invocations" + ); + } + + #[tokio::test] + async fn test_delete_user() { + let state = get_mocked_state(None, None); + let mut backend = MockIdentityBackend::default(); + backend + .expect_delete_user() + .withf(|_, uid: &'_ str| uid == "uid") + .returning(|_, _| Ok(())); + let provider = IdentityService::from_driver(backend); + + assert!(provider.delete_user(&state, "uid").await.is_ok()); + } +} diff --git a/crates/keystone/src/identity/types.rs b/crates/core/src/identity/types.rs similarity index 100% rename from crates/keystone/src/identity/types.rs rename to crates/core/src/identity/types.rs diff --git a/crates/keystone/src/identity/types/group.rs b/crates/core/src/identity/types/group.rs similarity index 100% rename from crates/keystone/src/identity/types/group.rs rename to crates/core/src/identity/types/group.rs diff --git a/crates/keystone/src/identity/types/provider_api.rs b/crates/core/src/identity/types/provider_api.rs similarity index 96% rename from crates/keystone/src/identity/types/provider_api.rs rename to crates/core/src/identity/types/provider_api.rs index a6d84be2..6eabaed6 100644 --- a/crates/keystone/src/identity/types/provider_api.rs +++ b/crates/core/src/identity/types/provider_api.rs @@ -122,13 +122,13 @@ pub trait IdentityApi: Send + Sync { &self, state: &ServiceState, params: &GroupListParameters, - ) -> Result, IdentityProviderError>; + ) -> Result, IdentityProviderError>; async fn list_users( &self, state: &ServiceState, params: &UserListParameters, - ) -> Result, IdentityProviderError>; + ) -> Result, IdentityProviderError>; async fn delete_group<'a>( &self, @@ -141,7 +141,7 @@ pub trait IdentityApi: Send + Sync { &self, state: &ServiceState, user_id: &'a str, - ) -> Result, IdentityProviderError>; + ) -> Result, IdentityProviderError>; /// Remove the user from the single group. async fn remove_user_from_group<'a>( diff --git a/crates/keystone/src/identity/types/service_account.rs b/crates/core/src/identity/types/service_account.rs similarity index 100% rename from crates/keystone/src/identity/types/service_account.rs rename to crates/core/src/identity/types/service_account.rs diff --git a/crates/keystone/src/identity/types/user.rs b/crates/core/src/identity/types/user.rs similarity index 100% rename from crates/keystone/src/identity/types/user.rs rename to crates/core/src/identity/types/user.rs diff --git a/crates/core/src/identity_mapping/backend.rs b/crates/core/src/identity_mapping/backend.rs new file mode 100644 index 00000000..e34c944c --- /dev/null +++ b/crates/core/src/identity_mapping/backend.rs @@ -0,0 +1,38 @@ +// 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 + +use async_trait::async_trait; + +use crate::identity_mapping::{IdentityMappingProviderError, types::*}; +use crate::keystone::ServiceState; + +#[cfg_attr(test, mockall::automock)] +#[async_trait] +pub trait IdentityMappingBackend: Send + Sync { + /// Get the `IdMapping` by the local data. + async fn get_by_local_id<'a>( + &self, + state: &ServiceState, + local_id: &'a str, + domain_id: &'a str, + entity_type: IdMappingEntityType, + ) -> Result, IdentityMappingProviderError>; + + /// Get the IdMapping by the public_id. + async fn get_by_public_id<'a>( + &self, + state: &ServiceState, + public_id: &'a str, + ) -> Result, IdentityMappingProviderError>; +} diff --git a/crates/keystone/src/identity_mapping/error.rs b/crates/core/src/identity_mapping/error.rs similarity index 77% rename from crates/keystone/src/identity_mapping/error.rs rename to crates/core/src/identity_mapping/error.rs index 8d6a26f3..805fbc56 100644 --- a/crates/keystone/src/identity_mapping/error.rs +++ b/crates/core/src/identity_mapping/error.rs @@ -14,7 +14,7 @@ use thiserror::Error; -use crate::api::error::KeystoneApiError; +//use crate::api::error::KeystoneApiError; use crate::error::BuilderError; /// Identity mapping provider error. @@ -44,7 +44,7 @@ pub enum IdentityMappingProviderError { }, /// Unsupported driver. - #[error("unsupported driver {0}")] + #[error("unsupported driver `{0}` for the identity mapping provider")] UnsupportedDriver(String), /// Request validation error. @@ -56,11 +56,11 @@ pub enum IdentityMappingProviderError { }, } -impl From for KeystoneApiError { - fn from(source: IdentityMappingProviderError) -> Self { - match source { - IdentityMappingProviderError::Conflict(x) => Self::Conflict(x), - other => Self::InternalError(other.to_string()), - } - } -} +//impl From for KeystoneApiError { +// fn from(source: IdentityMappingProviderError) -> Self { +// match source { +// IdentityMappingProviderError::Conflict(x) => Self::Conflict(x), +// other => Self::InternalError(other.to_string()), +// } +// } +//} diff --git a/crates/keystone/src/identity_mapping/mock.rs b/crates/core/src/identity_mapping/mock.rs similarity index 85% rename from crates/keystone/src/identity_mapping/mock.rs rename to crates/core/src/identity_mapping/mock.rs index ebe587c3..73c11390 100644 --- a/crates/keystone/src/identity_mapping/mock.rs +++ b/crates/core/src/identity_mapping/mock.rs @@ -15,15 +15,11 @@ use async_trait::async_trait; use mockall::mock; -use crate::config::Config; use crate::identity_mapping::{IdentityMappingApi, IdentityMappingProviderError, types::*}; use crate::keystone::ServiceState; -use crate::plugin_manager::PluginManager; mock! { - pub IdentityMappingProvider { - pub fn new(cfg: &Config, plugin_manager: &PluginManager) -> Result; - } + pub IdentityMappingProvider {} #[async_trait] impl IdentityMappingApi for IdentityMappingProvider { diff --git a/crates/core/src/identity_mapping/mod.rs b/crates/core/src/identity_mapping/mod.rs new file mode 100644 index 00000000..b9ab7d7f --- /dev/null +++ b/crates/core/src/identity_mapping/mod.rs @@ -0,0 +1,96 @@ +// 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 + +//! # Identity mapping provider +//! +//! Identity mapping provider provides a mapping of the entity ID between +//! Keystone and the remote system (i.e. LDAP, IdP, OpenFGA, SCIM, etc). + +use async_trait::async_trait; + +pub mod backend; +pub mod error; +#[cfg(any(test, feature = "mock"))] +pub mod mock; +pub mod service; +pub mod types; + +use crate::config::Config; +use crate::identity_mapping::service::IdentityMappingService; +use crate::keystone::ServiceState; +use crate::plugin_manager::PluginManagerApi; +use types::*; + +pub use error::IdentityMappingProviderError; +#[cfg(any(test, feature = "mock"))] +pub use mock::MockIdentityMappingProvider; +pub use types::IdentityMappingApi; + +pub enum IdentityMappingProvider { + Service(IdentityMappingService), + #[cfg(any(test, feature = "mock"))] + Mock(MockIdentityMappingProvider), +} + +impl IdentityMappingProvider { + pub fn new( + config: &Config, + plugin_manager: &P, + ) -> Result { + Ok(Self::Service(IdentityMappingService::new( + config, + plugin_manager, + )?)) + } +} + +#[async_trait] +impl IdentityMappingApi for IdentityMappingProvider { + /// Get the `IdMapping` by the local data. + async fn get_by_local_id<'a>( + &self, + state: &ServiceState, + local_id: &'a str, + domain_id: &'a str, + entity_type: IdMappingEntityType, + ) -> Result, IdentityMappingProviderError> { + match self { + Self::Service(provider) => { + provider + .get_by_local_id(state, local_id, domain_id, entity_type) + .await + } + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => { + provider + .get_by_local_id(state, local_id, domain_id, entity_type) + .await + } + } + } + + /// Get the IdMapping by the public_id. + #[tracing::instrument(level = "info", skip(self, state))] + async fn get_by_public_id<'a>( + &self, + state: &ServiceState, + public_id: &'a str, + ) -> Result, IdentityMappingProviderError> { + match self { + Self::Service(provider) => provider.get_by_public_id(state, public_id).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.get_by_public_id(state, public_id).await, + } + } +} diff --git a/crates/core/src/identity_mapping/service.rs b/crates/core/src/identity_mapping/service.rs new file mode 100644 index 00000000..35c888ec --- /dev/null +++ b/crates/core/src/identity_mapping/service.rs @@ -0,0 +1,133 @@ +// 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 + +//! # Identity mapping provider + +use async_trait::async_trait; +use std::sync::Arc; + +use crate::config::Config; +use crate::identity_mapping::{ + IdentityMappingProviderError, backend::IdentityMappingBackend, types::*, +}; +use crate::keystone::ServiceState; +use crate::plugin_manager::PluginManagerApi; + +pub struct IdentityMappingService { + /// Backend driver. + backend_driver: Arc, +} + +impl IdentityMappingService { + pub fn new( + config: &Config, + plugin_manager: &P, + ) -> Result { + let backend_driver = plugin_manager + .get_identity_mapping_backend(config.identity_mapping.driver.clone())? + .clone(); + Ok(Self { backend_driver }) + } +} + +#[async_trait] +impl IdentityMappingApi for IdentityMappingService { + /// Get the `IdMapping` by the local data. + async fn get_by_local_id<'a>( + &self, + state: &ServiceState, + local_id: &'a str, + domain_id: &'a str, + entity_type: IdMappingEntityType, + ) -> Result, IdentityMappingProviderError> { + self.backend_driver + .get_by_local_id(state, local_id, domain_id, entity_type) + .await + } + + /// Get the IdMapping by the public_id. + #[tracing::instrument(level = "info", skip(self, state))] + async fn get_by_public_id<'a>( + &self, + state: &ServiceState, + public_id: &'a str, + ) -> Result, IdentityMappingProviderError> { + self.backend_driver.get_by_public_id(state, public_id).await + } +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use super::*; + use crate::identity_mapping::backend::MockIdentityMappingBackend; + use crate::tests::get_mocked_state; + + #[tokio::test] + async fn test_get_by_local_id() { + let state = get_mocked_state(None, None); + let sot = IdMapping { + public_id: "pid".into(), + local_id: "lid".into(), + domain_id: "did".into(), + entity_type: IdMappingEntityType::User, + }; + let mut backend = MockIdentityMappingBackend::default(); + let sot_clone = sot.clone(); + backend + .expect_get_by_local_id() + .withf(|_, lid: &'_ str, did: &'_ str, _et: &IdMappingEntityType| { + lid == "lid" && did == "did" + }) + .returning(move |_, _, _, _| Ok(Some(sot_clone.clone()))); + let provider = IdentityMappingService { + backend_driver: Arc::new(backend), + }; + + let res: IdMapping = provider + .get_by_local_id(&state, "lid", "did", IdMappingEntityType::User) + .await + .unwrap() + .expect("id mapping should be there"); + assert_eq!(res, sot); + } + + #[tokio::test] + async fn test_get_by_public_id() { + let state = get_mocked_state(None, None); + let sot = IdMapping { + public_id: "pid".into(), + local_id: "lid".into(), + domain_id: "did".into(), + entity_type: IdMappingEntityType::User, + }; + let mut backend = MockIdentityMappingBackend::default(); + let sot_clone = sot.clone(); + backend + .expect_get_by_public_id() + .withf(|_, pid: &'_ str| pid == "pid") + .returning(move |_, _| Ok(Some(sot_clone.clone()))); + let provider = IdentityMappingService { + backend_driver: Arc::new(backend), + }; + + let res: IdMapping = provider + .get_by_public_id(&state, "pid") + .await + .unwrap() + .expect("id mapping should be there"); + assert_eq!(res, sot); + } +} diff --git a/crates/keystone/src/identity_mapping/types.rs b/crates/core/src/identity_mapping/types.rs similarity index 100% rename from crates/keystone/src/identity_mapping/types.rs rename to crates/core/src/identity_mapping/types.rs diff --git a/crates/keystone/src/identity_mapping/types/id_mapping.rs b/crates/core/src/identity_mapping/types/id_mapping.rs similarity index 100% rename from crates/keystone/src/identity_mapping/types/id_mapping.rs rename to crates/core/src/identity_mapping/types/id_mapping.rs diff --git a/crates/keystone/src/identity_mapping/types/provider_api.rs b/crates/core/src/identity_mapping/types/provider_api.rs similarity index 100% rename from crates/keystone/src/identity_mapping/types/provider_api.rs rename to crates/core/src/identity_mapping/types/provider_api.rs diff --git a/crates/core/src/k8s_auth/api.rs b/crates/core/src/k8s_auth/api.rs new file mode 100644 index 00000000..cfe81fd8 --- /dev/null +++ b/crates/core/src/k8s_auth/api.rs @@ -0,0 +1,15 @@ +// 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 +pub mod error; +pub mod types; diff --git a/crates/core/src/k8s_auth/api/error.rs b/crates/core/src/k8s_auth/api/error.rs new file mode 100644 index 00000000..e30232ba --- /dev/null +++ b/crates/core/src/k8s_auth/api/error.rs @@ -0,0 +1,59 @@ +// 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 +use tracing::error; + +use crate::api::error::KeystoneApiError; +use crate::k8s_auth::K8sAuthProviderError; + +/// Convert `K8sAuthProviderError` error into the [HTTP](KeystoneApiError) with +/// the expected +impl From for KeystoneApiError { + fn from(source: K8sAuthProviderError) -> Self { + error!( + "Error happened during request k8s_auth processing: {:#?}", + source + ); + match source { + K8sAuthProviderError::AudienceMismatch => Self::forbidden(source), + K8sAuthProviderError::CaCertificateUnknown => Self::forbidden(source), + K8sAuthProviderError::AuthInstanceNotActive(..) => Self::forbidden(source), + K8sAuthProviderError::AuthInstanceNotFound(x) => Self::NotFound { + resource: "k8s auth configuration".into(), + identifier: x, + }, + K8sAuthProviderError::Conflict(x) => Self::Conflict(x), + K8sAuthProviderError::FailedBoundServiceAccountName(..) => Self::forbidden(source), + K8sAuthProviderError::FailedBoundServiceAccountNamespace(..) => Self::forbidden(source), + K8sAuthProviderError::Jwt { .. } => Self::forbidden(source), + K8sAuthProviderError::ExpiredToken => Self::forbidden(source), + K8sAuthProviderError::InsecureAlgorithm => Self::forbidden(source), + K8sAuthProviderError::InvalidToken => Self::forbidden(source), + K8sAuthProviderError::RoleNotFound(x) => Self::NotFound { + resource: "k8s auth role".into(), + identifier: x, + }, + K8sAuthProviderError::RoleNotActive(..) => Self::forbidden(source), + K8sAuthProviderError::RoleInstanceOwnershipMismatch(..) => Self::forbidden(source), + K8sAuthProviderError::TokenRestrictionNotFound(x) => Self::NotFound { + resource: "token restriction".into(), + identifier: x, + }, + K8sAuthProviderError::UserNotFound(x) => Self::NotFound { + resource: "user/service account".into(), + identifier: x, + }, + other => Self::InternalError(other.to_string()), + } + } +} diff --git a/crates/core/src/k8s_auth/api/types.rs b/crates/core/src/k8s_auth/api/types.rs new file mode 100644 index 00000000..562114bb --- /dev/null +++ b/crates/core/src/k8s_auth/api/types.rs @@ -0,0 +1,17 @@ +// 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 + +mod auth; +mod instance; +mod role; diff --git a/crates/core/src/k8s_auth/api/types/auth.rs b/crates/core/src/k8s_auth/api/types/auth.rs new file mode 100644 index 00000000..9a66640d --- /dev/null +++ b/crates/core/src/k8s_auth/api/types/auth.rs @@ -0,0 +1,30 @@ +// 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 +//! K8s auth provider types. + +use openstack_keystone_api_types::k8s_auth::auth as api_auth; + +use secrecy::{ExposeSecret, SecretString}; + +use crate::k8s_auth::types; + +impl From<(api_auth::K8sAuthRequest, String)> for types::K8sAuthRequest { + fn from(value: (api_auth::K8sAuthRequest, String)) -> Self { + Self { + auth_instance_id: value.1, + jwt: SecretString::from(value.0.jwt.expose_secret()), + role_name: value.0.role_name, + } + } +} diff --git a/crates/core/src/k8s_auth/api/types/instance.rs b/crates/core/src/k8s_auth/api/types/instance.rs new file mode 100644 index 00000000..e2a9aabe --- /dev/null +++ b/crates/core/src/k8s_auth/api/types/instance.rs @@ -0,0 +1,84 @@ +// 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 +//! # Kubernetes auth instance types +use axum::{ + Json, + http::StatusCode, + response::{IntoResponse, Response}, +}; + +use openstack_keystone_api_types::k8s_auth::instance as api_instance; + +use crate::k8s_auth::types; + +impl From for api_instance::K8sAuthInstance { + fn from(value: types::K8sAuthInstance) -> Self { + Self { + ca_cert: value.ca_cert, + disable_local_ca_jwt: value.disable_local_ca_jwt, + domain_id: value.domain_id, + enabled: value.enabled, + host: value.host, + id: value.id, + name: value.name, + } + } +} + +impl From for types::K8sAuthInstanceCreate { + fn from(value: api_instance::K8sAuthInstanceCreateRequest) -> Self { + Self { + ca_cert: value.instance.ca_cert, + disable_local_ca_jwt: value.instance.disable_local_ca_jwt, + domain_id: value.instance.domain_id, + enabled: value.instance.enabled, + host: value.instance.host, + id: None, + name: value.instance.name, + } + } +} + +impl From for types::K8sAuthInstanceUpdate { + fn from(value: api_instance::K8sAuthInstanceUpdateRequest) -> Self { + Self { + ca_cert: value.instance.ca_cert, + disable_local_ca_jwt: value.instance.disable_local_ca_jwt, + enabled: value.instance.enabled, + host: value.instance.host, + name: value.instance.name, + } + } +} + +impl IntoResponse for types::K8sAuthInstance { + fn into_response(self) -> Response { + ( + StatusCode::OK, + Json(api_instance::K8sAuthInstanceResponse { + instance: api_instance::K8sAuthInstance::from(self), + }), + ) + .into_response() + } +} + +impl From for types::K8sAuthInstanceListParameters { + fn from(value: api_instance::K8sAuthInstanceListParameters) -> Self { + Self { + domain_id: value.domain_id, + name: value.name, + } + } +} diff --git a/crates/core/src/k8s_auth/api/types/role.rs b/crates/core/src/k8s_auth/api/types/role.rs new file mode 100644 index 00000000..cad3677f --- /dev/null +++ b/crates/core/src/k8s_auth/api/types/role.rs @@ -0,0 +1,90 @@ +// 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 +//! K8s auth role provider types. +use axum::{ + Json, + http::StatusCode, + response::{IntoResponse, Response}, +}; + +use openstack_keystone_api_types::k8s_auth::role as api_role; + +use crate::k8s_auth::types; + +impl From for api_role::K8sAuthRole { + fn from(value: types::K8sAuthRole) -> Self { + Self { + auth_instance_id: value.auth_instance_id, + bound_audience: value.bound_audience, + bound_service_account_names: value.bound_service_account_names, + bound_service_account_namespaces: value.bound_service_account_namespaces, + domain_id: value.domain_id, + enabled: value.enabled, + id: value.id, + name: value.name, + token_restriction_id: value.token_restriction_id, + } + } +} + +impl From<(api_role::K8sAuthRoleCreateRequest, String, String)> for types::K8sAuthRoleCreate { + fn from(value: (api_role::K8sAuthRoleCreateRequest, String, String)) -> Self { + Self { + auth_instance_id: value.1, + bound_audience: value.0.role.bound_audience, + bound_service_account_names: value.0.role.bound_service_account_names, + bound_service_account_namespaces: value.0.role.bound_service_account_namespaces, + domain_id: value.2, + enabled: value.0.role.enabled, + id: None, + name: value.0.role.name, + token_restriction_id: value.0.role.token_restriction_id, + } + } +} + +impl From for types::K8sAuthRoleUpdate { + fn from(value: api_role::K8sAuthRoleUpdateRequest) -> Self { + Self { + bound_audience: value.role.bound_audience, + bound_service_account_names: value.role.bound_service_account_names, + bound_service_account_namespaces: value.role.bound_service_account_namespaces, + enabled: value.role.enabled, + name: value.role.name, + token_restriction_id: value.role.token_restriction_id, + } + } +} + +impl IntoResponse for types::K8sAuthRole { + fn into_response(self) -> Response { + ( + StatusCode::OK, + Json(api_role::K8sAuthRoleResponse { + role: api_role::K8sAuthRole::from(self), + }), + ) + .into_response() + } +} + +impl From for types::K8sAuthRoleListParameters { + fn from(value: api_role::K8sAuthRoleListParameters) -> Self { + Self { + auth_instance_id: value.auth_instance_id, + domain_id: value.domain_id, + name: value.name, + } + } +} diff --git a/crates/keystone/src/k8s_auth/auth.rs b/crates/core/src/k8s_auth/auth.rs similarity index 97% rename from crates/keystone/src/k8s_auth/auth.rs rename to crates/core/src/k8s_auth/auth.rs index 89c2654a..a9bc05a4 100644 --- a/crates/keystone/src/k8s_auth/auth.rs +++ b/crates/core/src/k8s_auth/auth.rs @@ -26,7 +26,7 @@ use tracing::{debug, trace}; use crate::auth::AuthenticatedInfo; use crate::identity::IdentityApi; -use crate::k8s_auth::{K8sAuthProvider, K8sAuthProviderError, types::*}; +use crate::k8s_auth::{K8sAuthProviderError, service::K8sAuthService, types::*}; use crate::keystone::ServiceState; use crate::token::{TokenApi, types::TokenRestriction}; @@ -35,7 +35,7 @@ static SERVICE_ACCOUNT_CERT_PATH_STR: &str = "/var/run/secrets/kubernetes.io/ser static SERVICE_ACCOUNT_CERT_PATH: OnceLock = OnceLock::new(); //&str = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"; -impl K8sAuthProvider { +impl K8sAuthService { /// Get the [`Client`] for communication with the K8. /// /// # Arguments @@ -320,13 +320,11 @@ mod tests { use super::super::backend::MockK8sAuthBackend; use super::*; - use crate::api::tests::get_mocked_state; use crate::identity::{MockIdentityProvider, types::*}; use crate::keystone::Service; use crate::provider::Provider; - use crate::tests::get_state_mock; - use crate::token::MockTokenProvider; - use crate::token::types::TokenRestriction; + use crate::tests::get_mocked_state; + use crate::token::{MockTokenProvider, types::TokenRestriction}; /// fake cert valid till 2036 static CA_CERT: &str = r#" @@ -379,12 +377,12 @@ FPrC1HpT3dzIAiEAtEB0so+KoJb/2Opn1RycVzxke1CQrWgjS8ySnnFK5ok= async fn build_auth_test( token_mock: MockTokenProvider, identity_mock: MockIdentityProvider, - ) -> Result<(K8sAuthProvider, Arc, SecretString, MockServer)> { + ) -> Result<(K8sAuthService, Arc, SecretString, MockServer)> { let provider_mock = Provider::mocked_builder() - .token(token_mock) - .identity(identity_mock); + .mock_token(token_mock) + .mock_identity(identity_mock); let mock_srv = MockServer::start_async().await; - let state = get_mocked_state(provider_mock, true, None, Some(true)); + let state = get_mocked_state(None, Some(provider_mock)); let host = format!("http://{}:{}", mock_srv.host(), mock_srv.port()); let mut backend = MockK8sAuthBackend::default(); @@ -423,7 +421,7 @@ FPrC1HpT3dzIAiEAtEB0so+KoJb/2Opn1RycVzxke1CQrWgjS8ySnnFK5ok= }]) }); - let provider = K8sAuthProvider { + let provider = K8sAuthService { backend_driver: Arc::new(backend), http_clients: RwLock::new(HashMap::new()), }; @@ -447,7 +445,7 @@ FPrC1HpT3dzIAiEAtEB0so+KoJb/2Opn1RycVzxke1CQrWgjS8ySnnFK5ok= #[tokio::test] async fn test_get_or_create_client_with_ca() -> Result<()> { - let srv = K8sAuthProvider { + let srv = K8sAuthService { backend_driver: Arc::new(MockK8sAuthBackend::default()), http_clients: RwLock::new(HashMap::new()), }; @@ -484,7 +482,7 @@ FPrC1HpT3dzIAiEAtEB0so+KoJb/2Opn1RycVzxke1CQrWgjS8ySnnFK5ok= #[tokio::test] async fn test_get_or_create_client_k8s_ca() -> Result<()> { - let provider = K8sAuthProvider { + let provider = K8sAuthService { backend_driver: Arc::new(MockK8sAuthBackend::default()), http_clients: RwLock::new(HashMap::new()), }; @@ -510,7 +508,7 @@ FPrC1HpT3dzIAiEAtEB0so+KoJb/2Opn1RycVzxke1CQrWgjS8ySnnFK5ok= #[tokio::test] async fn test_get_or_create_client_error_no_ca() -> Result<()> { - let provider = K8sAuthProvider { + let provider = K8sAuthService { backend_driver: Arc::new(MockK8sAuthBackend::default()), http_clients: RwLock::new(HashMap::new()), }; @@ -534,7 +532,7 @@ FPrC1HpT3dzIAiEAtEB0so+KoJb/2Opn1RycVzxke1CQrWgjS8ySnnFK5ok= #[tokio::test] async fn test_get_or_create_client_disable_local_ca_jwt() -> Result<()> { - let provider = K8sAuthProvider { + let provider = K8sAuthService { backend_driver: Arc::new(MockK8sAuthBackend::default()), http_clients: RwLock::new(HashMap::new()), }; @@ -553,7 +551,7 @@ FPrC1HpT3dzIAiEAtEB0so+KoJb/2Opn1RycVzxke1CQrWgjS8ySnnFK5ok= #[tokio::test] async fn test_query_k8s_token_review_aud_mismatch() -> Result<()> { - let provider = K8sAuthProvider { + let provider = K8sAuthService { backend_driver: Arc::new(MockK8sAuthBackend::default()), http_clients: RwLock::new(HashMap::new()), }; @@ -601,7 +599,7 @@ FPrC1HpT3dzIAiEAtEB0so+KoJb/2Opn1RycVzxke1CQrWgjS8ySnnFK5ok= #[tokio::test] async fn test_query_k8s_token_review_expired() -> Result<()> { - let provider = K8sAuthProvider { + let provider = K8sAuthService { backend_driver: Arc::new(MockK8sAuthBackend::default()), http_clients: RwLock::new(HashMap::new()), }; @@ -649,7 +647,7 @@ FPrC1HpT3dzIAiEAtEB0so+KoJb/2Opn1RycVzxke1CQrWgjS8ySnnFK5ok= #[tokio::test] async fn test_query_k8s_token_review() -> Result<()> { - let provider = K8sAuthProvider { + let provider = K8sAuthService { backend_driver: Arc::new(MockK8sAuthBackend::default()), http_clients: RwLock::new(HashMap::new()), }; @@ -756,7 +754,7 @@ FPrC1HpT3dzIAiEAtEB0so+KoJb/2Opn1RycVzxke1CQrWgjS8ySnnFK5ok= #[test] fn test_check_k8s_token_review_response() { - let provider = K8sAuthProvider { + let provider = K8sAuthService { backend_driver: Arc::new(MockK8sAuthBackend::default()), http_clients: RwLock::new(HashMap::new()), }; @@ -857,13 +855,13 @@ FPrC1HpT3dzIAiEAtEB0so+KoJb/2Opn1RycVzxke1CQrWgjS8ySnnFK5ok= #[tokio::test] async fn test_auth_conf_not_found() { - let state = get_state_mock(); + let state = get_mocked_state(None, None); let mut backend = MockK8sAuthBackend::default(); backend .expect_get_auth_instance() .withf(|_, id: &'_ str| id == "cid") .returning(|_, _| Ok(None)); - let provider = K8sAuthProvider { + let provider = K8sAuthService { backend_driver: Arc::new(backend), http_clients: RwLock::new(HashMap::new()), }; @@ -887,7 +885,7 @@ FPrC1HpT3dzIAiEAtEB0so+KoJb/2Opn1RycVzxke1CQrWgjS8ySnnFK5ok= #[tokio::test] async fn test_auth_role_not_found() { - let state = get_state_mock(); + let state = get_mocked_state(None, None); let mut backend = MockK8sAuthBackend::default(); backend .expect_get_auth_instance() @@ -912,7 +910,7 @@ FPrC1HpT3dzIAiEAtEB0so+KoJb/2Opn1RycVzxke1CQrWgjS8ySnnFK5ok= }) .returning(|_, _| Ok(vec![])); - let provider = K8sAuthProvider { + let provider = K8sAuthService { backend_driver: Arc::new(backend), http_clients: RwLock::new(HashMap::new()), }; @@ -936,7 +934,7 @@ FPrC1HpT3dzIAiEAtEB0so+KoJb/2Opn1RycVzxke1CQrWgjS8ySnnFK5ok= #[tokio::test] async fn test_auth_conf_or_auth_role_disabled() { - let state = get_state_mock(); + let state = get_mocked_state(None, None); let mut backend = MockK8sAuthBackend::default(); backend .expect_get_auth_instance() @@ -1007,7 +1005,7 @@ FPrC1HpT3dzIAiEAtEB0so+KoJb/2Opn1RycVzxke1CQrWgjS8ySnnFK5ok= }]) }); - let provider = K8sAuthProvider { + let provider = K8sAuthService { backend_driver: Arc::new(backend), http_clients: RwLock::new(HashMap::new()), }; diff --git a/crates/core/src/k8s_auth/backend.rs b/crates/core/src/k8s_auth/backend.rs new file mode 100644 index 00000000..508b869d --- /dev/null +++ b/crates/core/src/k8s_auth/backend.rs @@ -0,0 +1,97 @@ +// 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 +//! # K8s auth: Backends. +use async_trait::async_trait; + +use crate::k8s_auth::{K8sAuthProviderError, types::*}; +use crate::keystone::ServiceState; + +/// K8s auth Backend trait. +/// +/// Backend driver interface expected by the revocation auth_instance. +#[cfg_attr(test, mockall::automock)] +#[async_trait] +pub trait K8sAuthBackend: Send + Sync { + /// Register new K8s auth auth_instance. + async fn create_auth_instance( + &self, + state: &ServiceState, + auth_instance: K8sAuthInstanceCreate, + ) -> Result; + + /// Register new K8s auth role. + async fn create_auth_role( + &self, + state: &ServiceState, + role: K8sAuthRoleCreate, + ) -> Result; + + /// Delete K8s auth auth_instance. + async fn delete_auth_instance<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result<(), K8sAuthProviderError>; + + /// Delete K8s auth role. + async fn delete_auth_role<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result<(), K8sAuthProviderError>; + + /// Register new K8s auth auth_instance. + async fn get_auth_instance<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result, K8sAuthProviderError>; + + /// Register new K8s auth role. + async fn get_auth_role<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result, K8sAuthProviderError>; + + /// List K8s auth auth_instances. + async fn list_auth_instances( + &self, + state: &ServiceState, + params: &K8sAuthInstanceListParameters, + ) -> Result, K8sAuthProviderError>; + + /// List K8s auth roles. + async fn list_auth_roles( + &self, + state: &ServiceState, + params: &K8sAuthRoleListParameters, + ) -> Result, K8sAuthProviderError>; + + /// Update K8s auth auth_instance. + async fn update_auth_instance<'a>( + &self, + state: &ServiceState, + id: &'a str, + data: K8sAuthInstanceUpdate, + ) -> Result; + + /// Update K8s auth role. + async fn update_auth_role<'a>( + &self, + state: &ServiceState, + id: &'a str, + data: K8sAuthRoleUpdate, + ) -> Result; +} diff --git a/crates/keystone/src/k8s_auth/error.rs b/crates/core/src/k8s_auth/error.rs similarity index 98% rename from crates/keystone/src/k8s_auth/error.rs rename to crates/core/src/k8s_auth/error.rs index 157f2b0a..7982861e 100644 --- a/crates/keystone/src/k8s_auth/error.rs +++ b/crates/core/src/k8s_auth/error.rs @@ -139,7 +139,7 @@ pub enum K8sAuthProviderError { TokenRestrictionMustSpecifyUserId, /// Unsupported driver. - #[error("unsupported driver {0}")] + #[error("unsupported driver `{0}` for the k8s provider")] UnsupportedDriver(String), /// User not found. diff --git a/crates/keystone/src/k8s_auth/mock.rs b/crates/core/src/k8s_auth/mock.rs similarity index 93% rename from crates/keystone/src/k8s_auth/mock.rs rename to crates/core/src/k8s_auth/mock.rs index 9bcb2a56..b8e609f8 100644 --- a/crates/keystone/src/k8s_auth/mock.rs +++ b/crates/core/src/k8s_auth/mock.rs @@ -13,21 +13,15 @@ // SPDX-License-Identifier: Apache-2.0 //! # K8s auth - internal mocking tools. use async_trait::async_trait; -#[cfg(test)] use mockall::mock; use crate::auth::AuthenticatedInfo; -use crate::config::Config; use crate::k8s_auth::{K8sAuthApi, K8sAuthProviderError, types::*}; use crate::keystone::ServiceState; -use crate::plugin_manager::PluginManager; use crate::token::types::TokenRestriction; -#[cfg(test)] mock! { - pub K8sAuthProvider { - pub fn new(cfg: &Config, plugin_manager: &PluginManager) -> Result; - } + pub K8sAuthProvider {} #[async_trait] impl K8sAuthApi for K8sAuthProvider { diff --git a/crates/core/src/k8s_auth/mod.rs b/crates/core/src/k8s_auth/mod.rs new file mode 100644 index 00000000..15a29eaa --- /dev/null +++ b/crates/core/src/k8s_auth/mod.rs @@ -0,0 +1,211 @@ +// 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 +//! # Kubernetes authentication. + +use async_trait::async_trait; + +pub mod api; +mod auth; +pub mod backend; +pub mod error; +#[cfg(any(test, feature = "mock"))] +mod mock; +pub mod service; +pub mod types; + +use crate::k8s_auth::service::K8sAuthService; +use crate::keystone::ServiceState; +use crate::plugin_manager::PluginManagerApi; +use crate::token::types::TokenRestriction; +use crate::{auth::AuthenticatedInfo, config::Config}; +use types::*; + +pub use error::K8sAuthProviderError; +#[cfg(any(test, feature = "mock"))] +pub use mock::MockK8sAuthProvider; +pub use types::K8sAuthApi; + +/// K8s Auth provider. +pub enum K8sAuthProvider { + Service(K8sAuthService), + #[cfg(any(test, feature = "mock"))] + Mock(MockK8sAuthProvider), +} + +impl K8sAuthProvider { + pub fn new( + config: &Config, + plugin_manager: &P, + ) -> Result { + Ok(Self::Service(K8sAuthService::new(config, plugin_manager)?)) + } +} + +#[async_trait] +impl K8sAuthApi for K8sAuthProvider { + /// Authenticate (exchange) the K8s Service account token. + async fn authenticate_by_k8s_sa_token( + &self, + state: &ServiceState, + req: &K8sAuthRequest, + ) -> Result<(AuthenticatedInfo, TokenRestriction), K8sAuthProviderError> { + match self { + Self::Service(provider) => provider.authenticate_by_k8s_sa_token(state, req).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.authenticate_by_k8s_sa_token(state, req).await, + } + } + + /// Register new K8s auth instance. + #[tracing::instrument(skip(self, state))] + async fn create_auth_instance( + &self, + state: &ServiceState, + instance: K8sAuthInstanceCreate, + ) -> Result { + match self { + Self::Service(provider) => provider.create_auth_instance(state, instance).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.create_auth_instance(state, instance).await, + } + } + + /// Register new K8s auth role. + #[tracing::instrument(skip(self, state))] + async fn create_auth_role( + &self, + state: &ServiceState, + role: K8sAuthRoleCreate, + ) -> Result { + match self { + Self::Service(provider) => provider.create_auth_role(state, role).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.create_auth_role(state, role).await, + } + } + + /// Delete K8s auth provider. + #[tracing::instrument(skip(self, state))] + async fn delete_auth_instance<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result<(), K8sAuthProviderError> { + match self { + Self::Service(provider) => provider.delete_auth_instance(state, id).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.delete_auth_instance(state, id).await, + } + } + + /// Delete K8s auth role. + #[tracing::instrument(skip(self, state))] + async fn delete_auth_role<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result<(), K8sAuthProviderError> { + match self { + Self::Service(provider) => provider.delete_auth_role(state, id).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.delete_auth_role(state, id).await, + } + } + + /// Register new K8s auth instance. + #[tracing::instrument(skip(self, state))] + async fn get_auth_instance<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result, K8sAuthProviderError> { + match self { + Self::Service(provider) => provider.get_auth_instance(state, id).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.get_auth_instance(state, id).await, + } + } + + /// Register new K8s auth role. + #[tracing::instrument(skip(self, state))] + async fn get_auth_role<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result, K8sAuthProviderError> { + match self { + Self::Service(provider) => provider.get_auth_role(state, id).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.get_auth_role(state, id).await, + } + } + + /// List K8s auth instances. + #[tracing::instrument(skip(self, state))] + async fn list_auth_instances( + &self, + state: &ServiceState, + params: &K8sAuthInstanceListParameters, + ) -> Result, K8sAuthProviderError> { + match self { + Self::Service(provider) => provider.list_auth_instances(state, params).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.list_auth_instances(state, params).await, + } + } + + /// List K8s auth roles. + #[tracing::instrument(skip(self, state))] + async fn list_auth_roles( + &self, + state: &ServiceState, + params: &K8sAuthRoleListParameters, + ) -> Result, K8sAuthProviderError> { + match self { + Self::Service(provider) => provider.list_auth_roles(state, params).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.list_auth_roles(state, params).await, + } + } + + /// Update K8s auth instance. + #[tracing::instrument(skip(self, state))] + async fn update_auth_instance<'a>( + &self, + state: &ServiceState, + id: &'a str, + data: K8sAuthInstanceUpdate, + ) -> Result { + match self { + Self::Service(provider) => provider.update_auth_instance(state, id, data).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.update_auth_instance(state, id, data).await, + } + } + + /// Update K8s auth role. + #[tracing::instrument(skip(self, state))] + async fn update_auth_role<'a>( + &self, + state: &ServiceState, + id: &'a str, + data: K8sAuthRoleUpdate, + ) -> Result { + match self { + Self::Service(provider) => provider.update_auth_role(state, id, data).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.update_auth_role(state, id, data).await, + } + } +} diff --git a/crates/core/src/k8s_auth/service.rs b/crates/core/src/k8s_auth/service.rs new file mode 100644 index 00000000..31b8d1f7 --- /dev/null +++ b/crates/core/src/k8s_auth/service.rs @@ -0,0 +1,248 @@ +// 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 +//! # Kubernetes authentication. + +use std::collections::HashMap; +use std::sync::Arc; + +use async_trait::async_trait; +use reqwest::Client; +use tokio::sync::RwLock; + +use crate::k8s_auth::{K8sAuthProviderError, backend::K8sAuthBackend, types::*}; +use crate::keystone::ServiceState; +use crate::plugin_manager::PluginManagerApi; +use crate::token::types::TokenRestriction; +use crate::{auth::AuthenticatedInfo, config::Config}; + +/// K8s Auth provider. +pub struct K8sAuthService { + /// Backend driver. + pub(super) backend_driver: Arc, + + /// Reqwest client. + pub(super) http_clients: RwLock>>, +} + +impl K8sAuthService { + pub fn new( + config: &Config, + plugin_manager: &P, + ) -> Result { + let backend_driver = plugin_manager + .get_k8s_auth_backend(config.k8s_auth.driver.clone())? + .clone(); + Ok(Self { + backend_driver, + http_clients: RwLock::new(HashMap::new()), + }) + } +} + +#[async_trait] +impl K8sAuthApi for K8sAuthService { + /// Authenticate (exchange) the K8s Service account token. + async fn authenticate_by_k8s_sa_token( + &self, + state: &ServiceState, + req: &K8sAuthRequest, + ) -> Result<(AuthenticatedInfo, TokenRestriction), K8sAuthProviderError> { + self.authenticate(state, req).await + } + + /// Register new K8s auth instance. + #[tracing::instrument(skip(self, state))] + async fn create_auth_instance( + &self, + state: &ServiceState, + instance: K8sAuthInstanceCreate, + ) -> Result { + let mut new = instance; + if new.id.is_none() { + new.id = Some(uuid::Uuid::new_v4().simple().to_string()); + } + self.backend_driver.create_auth_instance(state, new).await + } + + /// Register new K8s auth role. + #[tracing::instrument(skip(self, state))] + async fn create_auth_role( + &self, + state: &ServiceState, + role: K8sAuthRoleCreate, + ) -> Result { + let mut new = role; + if new.id.is_none() { + new.id = Some(uuid::Uuid::new_v4().simple().to_string()); + } + self.backend_driver.create_auth_role(state, new).await + } + + /// Delete K8s auth provider. + #[tracing::instrument(skip(self, state))] + async fn delete_auth_instance<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result<(), K8sAuthProviderError> { + self.backend_driver.delete_auth_instance(state, id).await + } + + /// Delete K8s auth role. + #[tracing::instrument(skip(self, state))] + async fn delete_auth_role<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result<(), K8sAuthProviderError> { + self.backend_driver.delete_auth_role(state, id).await + } + + /// Register new K8s auth instance. + #[tracing::instrument(skip(self, state))] + async fn get_auth_instance<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result, K8sAuthProviderError> { + self.backend_driver.get_auth_instance(state, id).await + } + + /// Register new K8s auth role. + #[tracing::instrument(skip(self, state))] + async fn get_auth_role<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result, K8sAuthProviderError> { + self.backend_driver.get_auth_role(state, id).await + } + + /// List K8s auth instances. + #[tracing::instrument(skip(self, state))] + async fn list_auth_instances( + &self, + state: &ServiceState, + params: &K8sAuthInstanceListParameters, + ) -> Result, K8sAuthProviderError> { + self.backend_driver.list_auth_instances(state, params).await + } + + /// List K8s auth roles. + #[tracing::instrument(skip(self, state))] + async fn list_auth_roles( + &self, + state: &ServiceState, + params: &K8sAuthRoleListParameters, + ) -> Result, K8sAuthProviderError> { + self.backend_driver.list_auth_roles(state, params).await + } + + /// Update K8s auth instance. + #[tracing::instrument(skip(self, state))] + async fn update_auth_instance<'a>( + &self, + state: &ServiceState, + id: &'a str, + data: K8sAuthInstanceUpdate, + ) -> Result { + self.backend_driver + .update_auth_instance(state, id, data) + .await + } + + /// Update K8s auth role. + #[tracing::instrument(skip(self, state))] + async fn update_auth_role<'a>( + &self, + state: &ServiceState, + id: &'a str, + data: K8sAuthRoleUpdate, + ) -> Result { + self.backend_driver.update_auth_role(state, id, data).await + } +} + +#[cfg(test)] +pub(crate) mod tests { + use std::sync::Arc; + + use super::*; + use crate::k8s_auth::backend::MockK8sAuthBackend; + use crate::tests::get_mocked_state; + + #[tokio::test] + async fn test_create_auth_instance() { + let state = get_mocked_state(None, None); + let mut backend = MockK8sAuthBackend::default(); + backend + .expect_create_auth_instance() + .returning(|_, _| Ok(K8sAuthInstance::default())); + let provider = K8sAuthService { + backend_driver: Arc::new(backend), + http_clients: RwLock::new(HashMap::new()), + }; + + assert!( + provider + .create_auth_instance( + &state, + K8sAuthInstanceCreate { + ca_cert: Some("ca".into()), + disable_local_ca_jwt: Some(true), + domain_id: "did".into(), + enabled: true, + host: "host".into(), + id: Some("id".into()), + name: Some("name".into()), + } + ) + .await + .is_ok() + ); + } + + #[tokio::test] + async fn test_create_auth_role() { + let state = get_mocked_state(None, None); + let mut backend = MockK8sAuthBackend::default(); + backend + .expect_create_auth_role() + .returning(|_, _| Ok(K8sAuthRole::default())); + let provider = K8sAuthService { + backend_driver: Arc::new(backend), + http_clients: RwLock::new(HashMap::new()), + }; + + assert!( + provider + .create_auth_role( + &state, + K8sAuthRoleCreate { + auth_instance_id: "cid".into(), + bound_audience: Some("aud".into()), + bound_service_account_names: vec!["a".into(), "b".into()], + bound_service_account_namespaces: vec!["na".into(), "nb".into()], + domain_id: "did".into(), + enabled: true, + id: Some("id".into()), + name: "name".into(), + token_restriction_id: "trid".into(), + } + ) + .await + .is_ok() + ); + } +} diff --git a/crates/keystone/src/k8s_auth/types.rs b/crates/core/src/k8s_auth/types.rs similarity index 100% rename from crates/keystone/src/k8s_auth/types.rs rename to crates/core/src/k8s_auth/types.rs diff --git a/crates/keystone/src/k8s_auth/types/auth.rs b/crates/core/src/k8s_auth/types/auth.rs similarity index 100% rename from crates/keystone/src/k8s_auth/types/auth.rs rename to crates/core/src/k8s_auth/types/auth.rs diff --git a/crates/keystone/src/k8s_auth/types/instance.rs b/crates/core/src/k8s_auth/types/instance.rs similarity index 100% rename from crates/keystone/src/k8s_auth/types/instance.rs rename to crates/core/src/k8s_auth/types/instance.rs diff --git a/crates/keystone/src/k8s_auth/types/provider_api.rs b/crates/core/src/k8s_auth/types/provider_api.rs similarity index 100% rename from crates/keystone/src/k8s_auth/types/provider_api.rs rename to crates/core/src/k8s_auth/types/provider_api.rs diff --git a/crates/keystone/src/k8s_auth/types/role.rs b/crates/core/src/k8s_auth/types/role.rs similarity index 100% rename from crates/keystone/src/k8s_auth/types/role.rs rename to crates/core/src/k8s_auth/types/role.rs diff --git a/crates/core/src/keystone.rs b/crates/core/src/keystone.rs new file mode 100644 index 00000000..1f80da50 --- /dev/null +++ b/crates/core/src/keystone.rs @@ -0,0 +1,67 @@ +// 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 +//! # Keystone state +use sea_orm::DatabaseConnection; +use std::sync::Arc; +use tracing::info; + +use crate::config::Config; +use crate::error::KeystoneError; +//#[double] +use crate::policy::PolicyEnforcer; +use crate::provider::Provider; + +// Placing ServiceState behind Arc is necessary to address DatabaseConnection +// not implementing Clone. +//#[derive(Clone)] +pub struct Service { + /// Config file. + pub config: Config, + + /// Database connection. + pub db: DatabaseConnection, + + /// Policy enforcer. + pub policy_enforcer: Arc, + + /// Service/resource Provider. + pub provider: Provider, + + /// Shutdown flag. + pub shutdown: bool, +} + +pub type ServiceState = Arc; + +impl Service { + pub fn new( + cfg: Config, + db: DatabaseConnection, + provider: Provider, + policy_enforcer: Arc, + ) -> Result { + Ok(Self { + config: cfg.clone(), + provider, + db, + policy_enforcer, + shutdown: false, + }) + } + + pub async fn terminate(&self) -> Result<(), KeystoneError> { + info!("Terminating Keystone"); + Ok(()) + } +} diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs new file mode 100644 index 00000000..48159301 --- /dev/null +++ b/crates/core/src/lib.rs @@ -0,0 +1,104 @@ +// 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 + +//! # OpenStack Keystone in Rust +//! +//! The legacy Keystone identity service (written in Python and maintained +//! upstream by OpenStack Foundation) has served the OpenStack ecosystem +//! reliably for years. It handles authentication, authorization, token +//! issuance, service catalog, project/tenant management, and federation +//! services across thousands of deployments. However, as we embarked on adding +//! next-generation identity features—such as native WebAuthn (“passkeys”), +//! modern federation flows, direct OIDC support, JWT login, workload +//! authorization, restricted tokens and service-accounts—it became clear that +//! certain design and performance limitations of the Python codebase would +//! hamper efficient implementation of these new features. +//! +//! Consequently, we initiated a project termed “Keystone-NG”: a Rust-based +//! component that augments rather than fully replaces the existing Keystone +//! service. The original plan was to implement only the new feature-set in Rust +//! and route those new API paths to the Rust component, while keeping the core +//! Python Keystone service in place for existing users and workflows. +//! +//! As development progressed, however, the breadth of new functionality (and +//! the opportunity to revisit some of the existing limitations) led to a +//! partial re-implementation of certain core identity flows in Rust. This +//! allows us to benefit from Rust's memory safety, concurrency model, +//! performance, and modern tooling, while still preserving the upstream +//! Keystone Python service as the canonical “master” identity service, routing +//! only the new endpoints and capabilities through the Rust component. +//! +//! In practice, this architecture means: +//! +//! - The upstream Python Keystone remains the main identity interface, +//! preserving backward compatibility, integration with other OpenStack +//! services, existing user workflows, catalogs, policies and plugins. +//! +//! - The Rust “Keystone-NG” component handles new functionality, specifically: +//! +//! - Native WebAuthN (passkeys) support for passwordless / phishing-resistant +//! MFA +//! +//! - A reworked federation service, enabling modern identity brokering and +//! advanced federation semantics OIDC (OpenID Connect) Direct in Keystone, +//! enabling Keystone to act as an OIDC Provider or integrate with external +//! OIDC identity providers natively JWT login flows, enabling stateless, +//! compact tokens suitable for new micro-services, CLI, SDK, and +//! workload-to-workload scenarios +//! +//! - Workload Authorization, designed for service-to-service authorization in +//! cloud native contexts (not just human users) +//! +//! - Restricted Tokens and Service Accounts, which allow fine-grained, +//! limited‐scope credentials for automation, agents, and service accounts, +//! with explicit constraints and expiry +//! +//! By routing only the new flows through the Rust component we preserve the +//! stability and ecosystem compatibility of Keystone, while enabling a +//! forward-looking identity architecture. Over time, additional identity flows +//! may be migrated or refactored into the Rust component as needed, but our +//! current objective is to retain the existing Keystone Python implementation +//! as the trusted, mature baseline and incrementally build the “Keystone-NG” +//! Rust service as the complement. + +//use application_credential::ApplicationCredentialApi; + +//pub trait ServiceState { +// fn get_application_credential_provider(&self) -> &impl ApplicationCredentialApi; +//} + +pub mod api; +pub mod application_credential; +pub mod assignment; +pub mod auth; +pub mod catalog; +pub mod common; +pub mod config; +pub mod error; +pub mod federation; +pub mod identity; +pub mod identity_mapping; +pub mod k8s_auth; +pub mod keystone; +pub mod plugin_manager; +pub mod policy; +pub mod provider; +pub mod resource; +pub mod revoke; +pub mod role; +pub mod token; +pub mod trust; + +#[cfg(test)] +pub mod tests; diff --git a/crates/core/src/plugin_manager.rs b/crates/core/src/plugin_manager.rs new file mode 100644 index 00000000..b851d4c9 --- /dev/null +++ b/crates/core/src/plugin_manager.rs @@ -0,0 +1,192 @@ +// 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 +//! # Plugin manager +//! +//! A driver, also known as a backend, is an important architectural component +//! of Keystone. It is an abstraction around the data access needed by a +//! particular subsystem. This pluggable implementation is not only how Keystone +//! implements its own data access, but how you can implement your own! +//! +//! The [PluginManagerApi] is responsible for picking the proper backend driver for +//! the provider. +use std::sync::Arc; + +use crate::application_credential::{ + ApplicationCredentialProviderError, backend::ApplicationCredentialBackend, +}; +use crate::assignment::backend::AssignmentBackend; +use crate::assignment::error::AssignmentProviderError; +use crate::catalog::backend::CatalogBackend; +use crate::catalog::error::CatalogProviderError; +use crate::federation::backend::FederationBackend; +use crate::federation::error::FederationProviderError; +use crate::identity::backend::IdentityBackend; +use crate::identity::error::IdentityProviderError; +use crate::identity_mapping::IdentityMappingProviderError; +use crate::identity_mapping::backend::IdentityMappingBackend; +use crate::k8s_auth::K8sAuthProviderError; +use crate::k8s_auth::backend::K8sAuthBackend; +use crate::resource::backend::ResourceBackend; +use crate::resource::error::ResourceProviderError; +use crate::revoke::RevokeProviderError; +use crate::revoke::backend::RevokeBackend; +use crate::role::RoleProviderError; +use crate::role::backend::RoleBackend; +use crate::token::TokenProviderError; +use crate::token::backend::TokenRestrictionBackend; +use crate::trust::TrustProviderError; +use crate::trust::backend::TrustBackend; + +/// Plugin manager trait. +pub trait PluginManagerApi { + /// Get registered application credential backend. + fn get_application_credential_backend>( + &self, + name: S, + ) -> Result<&Arc, ApplicationCredentialProviderError>; + + /// Get registered assignment backend. + fn get_assignment_backend>( + &self, + name: S, + ) -> Result<&Arc, AssignmentProviderError>; + + /// Get registered catalog backend. + fn get_catalog_backend>( + &self, + name: S, + ) -> Result<&Arc, CatalogProviderError>; + + /// Get registered federation backend. + fn get_federation_backend>( + &self, + name: S, + ) -> Result<&Arc, FederationProviderError>; + + /// Get registered identity backend. + fn get_identity_backend>( + &self, + name: S, + ) -> Result<&Arc, IdentityProviderError>; + + /// Get registered identity mapping backend. + fn get_identity_mapping_backend>( + &self, + name: S, + ) -> Result<&Arc, IdentityMappingProviderError>; + + /// Get registered k8s auth backend. + fn get_k8s_auth_backend>( + &self, + name: S, + ) -> Result<&Arc, K8sAuthProviderError>; + + /// Get registered resource backend. + fn get_resource_backend>( + &self, + name: S, + ) -> Result<&Arc, ResourceProviderError>; + + /// Get registered revoke backend. + fn get_revoke_backend>( + &self, + name: S, + ) -> Result<&Arc, RevokeProviderError>; + + /// Get role resource backend. + fn get_role_backend>( + &self, + name: S, + ) -> Result<&Arc, RoleProviderError>; + + /// Get registered token restriction backend. + fn get_token_restriction_backend>( + &self, + name: S, + ) -> Result<&Arc, TokenProviderError>; + + /// Get registered trust backend. + fn get_trust_backend>( + &self, + name: S, + ) -> Result<&Arc, TrustProviderError>; + + /// Register application credential backend. + fn register_application_credential_backend>( + &mut self, + name: S, + plugin: Arc, + ); + + /// Register assignment backend. + fn register_assignment_backend>( + &mut self, + name: S, + plugin: Arc, + ); + + /// Register catalog backend. + fn register_catalog_backend>(&mut self, name: S, plugin: Arc); + + /// Register federation backend. + fn register_federation_backend>( + &mut self, + name: S, + plugin: Arc, + ); + + /// Register identity backend. + fn register_identity_backend>( + &mut self, + name: S, + plugin: Arc, + ); + + /// Register identity mapping backend. + fn register_identity_mapping_backend>( + &mut self, + name: S, + plugin: Arc, + ); + + /// Register k8s_auth backend. + fn register_k8s_auth_backend>( + &mut self, + name: S, + plugin: Arc, + ); + + /// Register resource backend. + fn register_resource_backend>( + &mut self, + name: S, + plugin: Arc, + ); + + /// Register revoke backend. + fn register_revoke_backend>(&mut self, name: S, plugin: Arc); + + /// Register role backend. + fn register_role_backend>(&mut self, name: S, plugin: Arc); + + /// Register token restriction backend. + fn register_token_restriction_backend>( + &mut self, + name: S, + plugin: Arc, + ); + + /// Register trust backend. + fn register_trust_backend>(&mut self, name: S, plugin: Arc); +} diff --git a/crates/core/src/policy.rs b/crates/core/src/policy.rs new file mode 100644 index 00000000..4abf2c71 --- /dev/null +++ b/crates/core/src/policy.rs @@ -0,0 +1,217 @@ +// 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 +//! # Policy enforcement +//! +//! Policy enforcement in Keystone is delegated to the Open Policy Agent. It can +//! be invoked either with the HTTP request or as a WASM module. + +use async_trait::async_trait; +#[cfg(any(test, feature = "mock"))] +use mockall::mock; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use thiserror::Error; + +use crate::token::Token; + +/// Policy related error. +#[derive(Debug, Error)] +pub enum PolicyError { + /// Module compilation error. + #[error("module compilation task crashed")] + Compilation(#[from] eyre::Report), + + /// Dummy policy enforcer cannot be used. + #[error("dummy (empty) policy enforcer")] + Dummy, + + /// Forbidden error. + #[error("{}", .0.violations.as_ref().map( + |v| v.iter().cloned().map(|x| x.msg) + .reduce(|acc, s| format!("{acc}, {s}")) + .unwrap_or_default() + ).unwrap_or("The request you made requires authentication.".into()))] + Forbidden(PolicyEvaluationResult), + + #[error(transparent)] + IO(#[from] std::io::Error), + + #[error(transparent)] + Join(#[from] tokio::task::JoinError), + + /// Json serializaion error. + #[error(transparent)] + Json(#[from] serde_json::Error), + + /// HTTP client error. + #[error(transparent)] + Reqwest(#[from] reqwest::Error), + + /// Url parsing error. + #[error(transparent)] + UrlParse(#[from] url::ParseError), +} + +#[async_trait] +pub trait PolicyEnforcer: Send + Sync { + async fn enforce( + &self, + policy_name: &'static str, + credentials: &Token, + target: Value, + update: Option, + ) -> Result; +} + +#[cfg(any(test, feature = "mock"))] +mock! { + pub Policy {} + + #[async_trait] + impl PolicyEnforcer for Policy { + async fn enforce( + &self, + policy_name: &'static str, + credentials: &Token, + target: Value, + current: Option + ) -> Result; + } +} + +#[derive(Debug, Error)] +#[error("failed to evaluate policy")] +pub enum EvaluationError { + Serialization(#[from] serde_json::Error), + Evaluation(#[from] eyre::Report), +} + +/// OpenPolicyAgent `Credentials` object. +#[derive(Serialize, Debug)] +pub struct Credentials { + pub user_id: String, + pub roles: Vec, + #[serde(default)] + pub project_id: Option, + #[serde(default)] + pub domain_id: Option, + #[serde(default)] + pub system: Option, +} + +impl From<&Token> for Credentials { + fn from(token: &Token) -> Self { + Self { + user_id: token.user_id().clone(), + roles: token + .effective_roles() + .map(|x| { + x.iter() + .filter_map(|role| role.name.clone()) + .collect::>() + }) + .unwrap_or_default(), + project_id: token.project().map(|val| val.id.clone()), + domain_id: token.domain().map(|val| val.id.clone()), + system: None, + } + } +} + +/// A single violation of a policy. +#[derive(Clone, Deserialize, Debug, JsonSchema, Serialize)] +pub struct Violation { + pub msg: String, + pub field: Option, +} + +/// The OpenPolicyAgent response. +#[derive(Deserialize, Debug)] +pub struct OpaResponse { + pub result: PolicyEvaluationResult, +} + +/// The result of a policy evaluation. +#[derive(Clone, Deserialize, Debug, Serialize)] +pub struct PolicyEvaluationResult { + /// Whether the user is allowed to perform the request or not. + pub allow: bool, + /// Whether the user is allowed to see resources of other domains. + #[serde(default)] + pub can_see_other_domain_resources: Option, + /// List of violations. + #[serde(rename = "violation")] + pub violations: Option>, +} + +impl std::fmt::Display for PolicyEvaluationResult { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut first = true; + if let Some(violations) = &self.violations { + for violation in violations { + if first { + first = false; + } else { + write!(f, ", ")?; + } + write!(f, "{}", violation.msg)?; + } + } + Ok(()) + } +} + +impl PolicyEvaluationResult { + #[must_use] + pub fn allow(&self) -> bool { + self.allow + } + + /// Returns true if the policy evaluation was successful. + #[must_use] + pub fn valid(&self) -> bool { + self.violations + .as_deref() + .map(|x| x.is_empty()) + .unwrap_or(false) + } + + #[cfg(any(test, feature = "mock"))] + pub fn allowed() -> Self { + Self { + allow: true, + can_see_other_domain_resources: None, + violations: None, + } + } + + #[cfg(any(test, feature = "mock"))] + pub fn allowed_admin() -> Self { + Self { + allow: true, + can_see_other_domain_resources: Some(true), + violations: None, + } + } + + #[cfg(any(test, feature = "mock"))] + pub fn forbidden() -> Self { + Self { + allow: false, + can_see_other_domain_resources: Some(false), + violations: None, + } + } +} diff --git a/crates/core/src/provider.rs b/crates/core/src/provider.rs new file mode 100644 index 00000000..1119fa28 --- /dev/null +++ b/crates/core/src/provider.rs @@ -0,0 +1,304 @@ +// 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 +//! # Provider manager +//! +//! Provider manager provides access to the individual service providers. This +//! gives an easy interact for passing overall manager down to the individual +//! providers that might need to call other providers while also allowing an +//! easy injection of mocked providers. +use derive_builder::Builder; + +use crate::application_credential::ApplicationCredentialApi; +use crate::application_credential::ApplicationCredentialProvider; +#[cfg(any(test, feature = "mock"))] +use crate::application_credential::MockApplicationCredentialProvider; +use crate::assignment::AssignmentApi; +use crate::assignment::AssignmentProvider; +#[cfg(any(test, feature = "mock"))] +use crate::assignment::MockAssignmentProvider; +use crate::catalog::CatalogApi; +use crate::catalog::CatalogProvider; +#[cfg(any(test, feature = "mock"))] +use crate::catalog::MockCatalogProvider; +use crate::config::Config; +use crate::error::KeystoneError; +use crate::federation::FederationApi; +use crate::federation::FederationProvider; +#[cfg(any(test, feature = "mock"))] +use crate::federation::MockFederationProvider; +use crate::identity::IdentityApi; +use crate::identity::IdentityProvider; +#[cfg(any(test, feature = "mock"))] +use crate::identity::MockIdentityProvider; +use crate::identity_mapping::IdentityMappingApi; +use crate::identity_mapping::IdentityMappingProvider; +#[cfg(any(test, feature = "mock"))] +use crate::identity_mapping::MockIdentityMappingProvider; +use crate::k8s_auth::K8sAuthApi; +use crate::k8s_auth::K8sAuthProvider; +#[cfg(any(test, feature = "mock"))] +use crate::k8s_auth::MockK8sAuthProvider; +use crate::plugin_manager::PluginManagerApi; +#[cfg(any(test, feature = "mock"))] +use crate::resource::MockResourceProvider; +use crate::resource::ResourceApi; +use crate::resource::ResourceProvider; +#[cfg(any(test, feature = "mock"))] +use crate::revoke::MockRevokeProvider; +use crate::revoke::RevokeApi; +use crate::revoke::RevokeProvider; +#[cfg(any(test, feature = "mock"))] +use crate::role::MockRoleProvider; +use crate::role::RoleApi; +use crate::role::RoleProvider; +#[cfg(any(test, feature = "mock"))] +use crate::token::MockTokenProvider; +use crate::token::TokenApi; +use crate::token::TokenProvider; +#[cfg(any(test, feature = "mock"))] +use crate::trust::MockTrustProvider; +use crate::trust::TrustApi; +use crate::trust::TrustProvider; + +/// Global provider manager. +#[derive(Builder)] +// It is necessary to use the owned pattern since otherwise builder invokes clone which immediately +// confuses mockall used in tests +#[builder(pattern = "owned")] +pub struct Provider { + /// Configuration. + pub config: Config, + /// Application credential provider. + application_credential: ApplicationCredentialProvider, + /// Assignment provider. + assignment: AssignmentProvider, + /// Catalog provider. + catalog: CatalogProvider, + /// Federation provider. + federation: FederationProvider, + /// Identity provider. + identity: IdentityProvider, + /// Identity mapping provider. + identity_mapping: IdentityMappingProvider, + /// K8s auth provider. + k8s_auth: K8sAuthProvider, + /// Resource provider. + resource: ResourceProvider, + /// Revoke provider. + revoke: RevokeProvider, + /// Role provider. + role: RoleProvider, + /// Token provider. + token: TokenProvider, + /// Trust provider. + trust: TrustProvider, +} + +#[cfg(any(test, feature = "mock"))] +impl ProviderBuilder { + pub fn mock_application_credential(self, value: MockApplicationCredentialProvider) -> Self { + let mut new = self; + new.application_credential = Some(ApplicationCredentialProvider::Mock(value)); + new + } + + pub fn mock_assignment(self, value: MockAssignmentProvider) -> Self { + let mut new = self; + new.assignment = Some(AssignmentProvider::Mock(value)); + new + } + + pub fn mock_catalog(self, value: MockCatalogProvider) -> Self { + let mut new = self; + new.catalog = Some(CatalogProvider::Mock(value)); + new + } + + pub fn mock_federation(self, value: MockFederationProvider) -> Self { + let mut new = self; + new.federation = Some(FederationProvider::Mock(value)); + new + } + + pub fn mock_identity(self, value: MockIdentityProvider) -> Self { + let mut new = self; + new.identity = Some(IdentityProvider::Mock(value)); + new + } + pub fn mock_identity_mapping(self, value: MockIdentityMappingProvider) -> Self { + let mut new = self; + new.identity_mapping = Some(IdentityMappingProvider::Mock(value)); + new + } + pub fn mock_k8s_auth(self, value: MockK8sAuthProvider) -> Self { + let mut new = self; + new.k8s_auth = Some(K8sAuthProvider::Mock(value)); + new + } + pub fn mock_resource(self, value: MockResourceProvider) -> Self { + let mut new = self; + new.resource = Some(ResourceProvider::Mock(value)); + new + } + pub fn mock_revoke(self, value: MockRevokeProvider) -> Self { + let mut new = self; + new.revoke = Some(RevokeProvider::Mock(value)); + new + } + pub fn mock_role(self, value: MockRoleProvider) -> Self { + let mut new = self; + new.role = Some(RoleProvider::Mock(value)); + new + } + pub fn mock_token(self, value: MockTokenProvider) -> Self { + let mut new = self; + new.token = Some(TokenProvider::Mock(value)); + new + } + pub fn mock_trust(self, value: MockTrustProvider) -> Self { + let mut new = self; + new.trust = Some(TrustProvider::Mock(value)); + new + } +} + +impl Provider { + pub fn new( + cfg: Config, + plugin_manager: &P, + ) -> Result { + let application_credential_provider = + ApplicationCredentialProvider::new(&cfg, plugin_manager)?; + let assignment_provider = AssignmentProvider::new(&cfg, plugin_manager)?; + let catalog_provider = CatalogProvider::new(&cfg, plugin_manager)?; + let federation_provider = FederationProvider::new(&cfg, plugin_manager)?; + let identity_provider = IdentityProvider::new(&cfg, plugin_manager)?; + let identity_mapping_provider = IdentityMappingProvider::new(&cfg, plugin_manager)?; + let k8s_auth_provider = K8sAuthProvider::new(&cfg, plugin_manager)?; + let resource_provider = ResourceProvider::new(&cfg, plugin_manager)?; + let revoke_provider = RevokeProvider::new(&cfg, plugin_manager)?; + let role_provider = RoleProvider::new(&cfg, plugin_manager)?; + let token_provider = TokenProvider::new(&cfg, plugin_manager)?; + let trust_provider = TrustProvider::new(&cfg, plugin_manager)?; + + Ok(Self { + config: cfg, + application_credential: application_credential_provider, + assignment: assignment_provider, + catalog: catalog_provider, + federation: federation_provider, + identity: identity_provider, + identity_mapping: identity_mapping_provider, + k8s_auth: k8s_auth_provider, + resource: resource_provider, + revoke: revoke_provider, + role: role_provider, + token: token_provider, + trust: trust_provider, + }) + } + + #[cfg(any(test, feature = "mock"))] + pub fn mocked_builder() -> ProviderBuilder { + let config = Config::default(); + let application_credential_mock = + crate::application_credential::MockApplicationCredentialProvider::default(); + let assignment_mock = crate::assignment::MockAssignmentProvider::default(); + let catalog_mock = crate::catalog::MockCatalogProvider::default(); + let identity_mock = crate::identity::MockIdentityProvider::default(); + let identity_mapping_mock = crate::identity_mapping::MockIdentityMappingProvider::default(); + let federation_mock = crate::federation::MockFederationProvider::default(); + let k8s_auth_mock = crate::k8s_auth::MockK8sAuthProvider::default(); + let resource_mock = crate::resource::MockResourceProvider::default(); + let revoke_mock = crate::revoke::MockRevokeProvider::default(); + let role_mock = crate::role::MockRoleProvider::default(); + let token_mock = crate::token::MockTokenProvider::default(); + let trust_mock = crate::trust::MockTrustProvider::default(); + + ProviderBuilder::default() + .config(config.clone()) + .mock_application_credential(application_credential_mock) + .mock_assignment(assignment_mock) + .mock_catalog(catalog_mock) + .mock_identity(identity_mock) + .mock_identity_mapping(identity_mapping_mock) + .mock_federation(federation_mock) + .mock_k8s_auth(k8s_auth_mock) + .mock_resource(resource_mock) + .mock_revoke(revoke_mock) + .mock_role(role_mock) + .mock_token(token_mock) + .mock_trust(trust_mock) + } + + /// Get the application credential provider. + pub fn get_application_credential_provider(&self) -> &impl ApplicationCredentialApi { + &self.application_credential + } + + /// Get the assignment provider. + pub fn get_assignment_provider(&self) -> &impl AssignmentApi { + &self.assignment + } + + /// Get the catalog provider. + pub fn get_catalog_provider(&self) -> &impl CatalogApi { + &self.catalog + } + + /// Get the federation provider. + pub fn get_federation_provider(&self) -> &impl FederationApi { + &self.federation + } + + /// Get the identity provider. + pub fn get_identity_provider(&self) -> &impl IdentityApi { + &self.identity + } + + /// Get the identity mapping provider. + pub fn get_identity_mapping_provider(&self) -> &impl IdentityMappingApi { + &self.identity_mapping + } + + /// Get the resource provider. + pub fn get_k8s_auth_provider(&self) -> &impl K8sAuthApi { + &self.k8s_auth + } + + /// Get the resource provider. + pub fn get_resource_provider(&self) -> &impl ResourceApi { + &self.resource + } + + /// Get the revocation provider. + pub fn get_revoke_provider(&self) -> &impl RevokeApi { + &self.revoke + } + + /// Get the role provider. + pub fn get_role_provider(&self) -> &impl RoleApi { + &self.role + } + + /// Get the token provider. + pub fn get_token_provider(&self) -> &impl TokenApi { + &self.token + } + + /// Get the trust provider. + pub fn get_trust_provider(&self) -> &impl TrustApi { + &self.trust + } +} diff --git a/crates/core/src/resource/backend.rs b/crates/core/src/resource/backend.rs new file mode 100644 index 00000000..a5707c01 --- /dev/null +++ b/crates/core/src/resource/backend.rs @@ -0,0 +1,109 @@ +// 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 + +use async_trait::async_trait; + +use crate::keystone::ServiceState; +use crate::resource::ResourceProviderError; +use crate::resource::types::*; + +/// Resource driver interface. +#[cfg_attr(test, mockall::automock)] +#[async_trait] +pub trait ResourceBackend: Send + Sync { + /// Get `enabled` field of the domain. + async fn get_domain_enabled<'a>( + &self, + state: &ServiceState, + domain_id: &'a str, + ) -> Result; + + /// Create new domain. + async fn create_domain( + &self, + state: &ServiceState, + domain: DomainCreate, + ) -> Result; + + /// Create new project. + async fn create_project( + &self, + state: &ServiceState, + project: ProjectCreate, + ) -> Result; + + /// Delete domain by the ID + async fn delete_domain<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result<(), ResourceProviderError>; + + /// Delete project by the ID + async fn delete_project<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result<(), ResourceProviderError>; + + /// Get single domain by ID + async fn get_domain<'a>( + &self, + state: &ServiceState, + domain_id: &'a str, + ) -> Result, ResourceProviderError>; + + /// Get single domain by Name + async fn get_domain_by_name<'a>( + &self, + state: &ServiceState, + domain_name: &'a str, + ) -> Result, ResourceProviderError>; + + /// Get single project by ID + async fn get_project<'a>( + &self, + state: &ServiceState, + project_id: &'a str, + ) -> Result, ResourceProviderError>; + + /// Get single project by Name and Domain ID + async fn get_project_by_name<'a>( + &self, + state: &ServiceState, + name: &'a str, + domain_id: &'a str, + ) -> Result, ResourceProviderError>; + + /// Get project parents + async fn get_project_parents<'a>( + &self, + state: &ServiceState, + project_id: &'a str, + ) -> Result>, ResourceProviderError>; + + /// List domains. + async fn list_domains( + &self, + state: &ServiceState, + params: &DomainListParameters, + ) -> Result, ResourceProviderError>; + + /// List projects. + async fn list_projects( + &self, + state: &ServiceState, + params: &ProjectListParameters, + ) -> Result, ResourceProviderError>; +} diff --git a/crates/keystone/src/resource/error.rs b/crates/core/src/resource/error.rs similarity index 96% rename from crates/keystone/src/resource/error.rs rename to crates/core/src/resource/error.rs index 954356a6..feb5da80 100644 --- a/crates/keystone/src/resource/error.rs +++ b/crates/core/src/resource/error.rs @@ -49,7 +49,7 @@ pub enum ResourceProviderError { ProjectNotFound(String), /// Unsupported driver. - #[error("unsupported driver {0}")] + #[error("unsupported driver `{0}` for the resource provider")] UnsupportedDriver(String), /// Request validation error. diff --git a/crates/keystone/src/resource/mock.rs b/crates/core/src/resource/mock.rs similarity index 92% rename from crates/keystone/src/resource/mock.rs rename to crates/core/src/resource/mock.rs index b7772d98..211e8860 100644 --- a/crates/keystone/src/resource/mock.rs +++ b/crates/core/src/resource/mock.rs @@ -12,20 +12,14 @@ // // SPDX-License-Identifier: Apache-2.0 use async_trait::async_trait; -#[cfg(test)] use mockall::mock; -use crate::config::Config; use crate::keystone::ServiceState; -use crate::plugin_manager::PluginManager; use crate::resource::error::ResourceProviderError; use crate::resource::types::*; -#[cfg(test)] mock! { - pub ResourceProvider { - pub fn new(cfg: &Config, plugin_manager: &PluginManager) -> Result; - } + pub ResourceProvider {} #[async_trait] impl ResourceApi for ResourceProvider { diff --git a/crates/core/src/resource/mod.rs b/crates/core/src/resource/mod.rs new file mode 100644 index 00000000..58399a98 --- /dev/null +++ b/crates/core/src/resource/mod.rs @@ -0,0 +1,226 @@ +// 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 +//! # Resource provider +//! +//! Following Keystone concepts are covered by the provider: +//! +//! ## Domain +//! +//! An Identity service API v3 entity. Domains are a collection of projects and +//! users that define administrative boundaries for managing Identity entities. +//! Domains can represent an individual, company, or operator-owned space. They +//! expose administrative activities directly to system users. Users can be +//! granted the administrator role for a domain. A domain administrator can +//! create projects, users, and groups in a domain and assign roles to users and +//! groups in a domain. +//! +//! ## Project +//! +//! A container that groups or isolates resources or identity objects. Depending +//! on the service operator, a project might map to a customer, account, +//! organization, or tenant. +use async_trait::async_trait; + +pub mod backend; +pub mod error; +#[cfg(any(test, feature = "mock"))] +mod mock; +pub mod service; +pub mod types; + +use crate::config::Config; +use crate::keystone::ServiceState; +use crate::plugin_manager::PluginManagerApi; +use crate::resource::service::ResourceService; +use crate::resource::types::*; + +pub use crate::resource::error::ResourceProviderError; +#[cfg(any(test, feature = "mock"))] +pub use mock::MockResourceProvider; +pub use types::ResourceApi; + +pub enum ResourceProvider { + Service(ResourceService), + #[cfg(any(test, feature = "mock"))] + Mock(MockResourceProvider), +} + +impl ResourceProvider { + pub fn new( + config: &Config, + plugin_manager: &P, + ) -> Result { + Ok(Self::Service(ResourceService::new(config, plugin_manager)?)) + } +} + +#[async_trait] +impl ResourceApi for ResourceProvider { + /// Check whether the domain is enabled. + async fn get_domain_enabled<'a>( + &self, + state: &ServiceState, + domain_id: &'a str, + ) -> Result { + match self { + Self::Service(provider) => provider.get_domain_enabled(state, domain_id).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.get_domain_enabled(state, domain_id).await, + } + } + + /// Create new domain. + async fn create_domain( + &self, + state: &ServiceState, + domain: DomainCreate, + ) -> Result { + match self { + Self::Service(provider) => provider.create_domain(state, domain).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.create_domain(state, domain).await, + } + } + + /// Create new project. + async fn create_project( + &self, + state: &ServiceState, + project: ProjectCreate, + ) -> Result { + match self { + Self::Service(provider) => provider.create_project(state, project).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.create_project(state, project).await, + } + } + + /// Delete a domain by the ID. + async fn delete_domain<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result<(), ResourceProviderError> { + match self { + Self::Service(provider) => provider.delete_domain(state, id).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.delete_domain(state, id).await, + } + } + + /// Delete a project by the ID. + async fn delete_project<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result<(), ResourceProviderError> { + match self { + Self::Service(provider) => provider.delete_project(state, id).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.delete_project(state, id).await, + } + } + + /// Get single domain. + async fn get_domain<'a>( + &self, + state: &ServiceState, + domain_id: &'a str, + ) -> Result, ResourceProviderError> { + match self { + Self::Service(provider) => provider.get_domain(state, domain_id).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.get_domain(state, domain_id).await, + } + } + + /// Get single project. + async fn get_project<'a>( + &self, + state: &ServiceState, + project_id: &'a str, + ) -> Result, ResourceProviderError> { + match self { + Self::Service(provider) => provider.get_project(state, project_id).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.get_project(state, project_id).await, + } + } + + /// Get single project by Name and Domain ID. + async fn get_project_by_name<'a>( + &self, + state: &ServiceState, + name: &'a str, + domain_id: &'a str, + ) -> Result, ResourceProviderError> { + match self { + Self::Service(provider) => provider.get_project_by_name(state, name, domain_id).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.get_project_by_name(state, name, domain_id).await, + } + } + + /// Get project parents. + async fn get_project_parents<'a>( + &self, + state: &ServiceState, + project_id: &'a str, + ) -> Result>, ResourceProviderError> { + match self { + Self::Service(provider) => provider.get_project_parents(state, project_id).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.get_project_parents(state, project_id).await, + } + } + + /// Get single domain by its name. + async fn find_domain_by_name<'a>( + &self, + state: &ServiceState, + domain_name: &'a str, + ) -> Result, ResourceProviderError> { + match self { + Self::Service(provider) => provider.find_domain_by_name(state, domain_name).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.find_domain_by_name(state, domain_name).await, + } + } + + /// List domains. + async fn list_domains( + &self, + state: &ServiceState, + params: &DomainListParameters, + ) -> Result, ResourceProviderError> { + match self { + Self::Service(provider) => provider.list_domains(state, params).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.list_domains(state, params).await, + } + } + + /// List projects. + async fn list_projects( + &self, + state: &ServiceState, + params: &ProjectListParameters, + ) -> Result, ResourceProviderError> { + match self { + Self::Service(provider) => provider.list_projects(state, params).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.list_projects(state, params).await, + } + } +} diff --git a/crates/core/src/resource/service.rs b/crates/core/src/resource/service.rs new file mode 100644 index 00000000..4d9b3b11 --- /dev/null +++ b/crates/core/src/resource/service.rs @@ -0,0 +1,182 @@ +// 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 +//! # Resource provider +use async_trait::async_trait; +use std::sync::Arc; +use uuid::Uuid; +use validator::Validate; + +use crate::config::Config; +use crate::keystone::ServiceState; +use crate::plugin_manager::PluginManagerApi; +use crate::resource::{ResourceProviderError, backend::ResourceBackend, types::*}; + +pub struct ResourceService { + backend_driver: Arc, +} + +impl ResourceService { + pub fn new( + config: &Config, + plugin_manager: &P, + ) -> Result { + let backend_driver = plugin_manager + .get_resource_backend(config.resource.driver.clone())? + .clone(); + Ok(Self { backend_driver }) + } +} + +#[async_trait] +impl ResourceApi for ResourceService { + /// Check whether the domain is enabled. + async fn get_domain_enabled<'a>( + &self, + state: &ServiceState, + domain_id: &'a str, + ) -> Result { + self.backend_driver + .get_domain_enabled(state, domain_id) + .await + } + + /// Create new domain. + #[tracing::instrument(level = "info", skip(self, state))] + async fn create_domain( + &self, + state: &ServiceState, + domain: DomainCreate, + ) -> Result { + let mut new_domain = domain; + + if new_domain.id.is_none() { + new_domain.id = Some(Uuid::new_v4().simple().to_string()); + } + new_domain.validate()?; + self.backend_driver.create_domain(state, new_domain).await + } + + /// Create new project. + #[tracing::instrument(level = "info", skip(self, state))] + async fn create_project( + &self, + state: &ServiceState, + project: ProjectCreate, + ) -> Result { + let mut new_project = project; + + if new_project.id.is_none() { + new_project.id = Some(Uuid::new_v4().simple().to_string()); + } + new_project.validate()?; + self.backend_driver.create_project(state, new_project).await + } + + /// Delete a domain by the ID. + #[tracing::instrument(level = "info", skip(self, state))] + async fn delete_domain<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result<(), ResourceProviderError> { + self.backend_driver.delete_domain(state, id).await + } + + /// Delete a project by the ID. + #[tracing::instrument(level = "info", skip(self, state))] + async fn delete_project<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result<(), ResourceProviderError> { + self.backend_driver.delete_project(state, id).await + } + + /// Get single domain. + #[tracing::instrument(level = "info", skip(self, state))] + async fn get_domain<'a>( + &self, + state: &ServiceState, + domain_id: &'a str, + ) -> Result, ResourceProviderError> { + self.backend_driver.get_domain(state, domain_id).await + } + + /// Get single project. + #[tracing::instrument(level = "info", skip(self, state))] + async fn get_project<'a>( + &self, + state: &ServiceState, + project_id: &'a str, + ) -> Result, ResourceProviderError> { + self.backend_driver.get_project(state, project_id).await + } + + /// Get single project by Name and Domain ID. + #[tracing::instrument(level = "info", skip(self, state))] + async fn get_project_by_name<'a>( + &self, + state: &ServiceState, + name: &'a str, + domain_id: &'a str, + ) -> Result, ResourceProviderError> { + self.backend_driver + .get_project_by_name(state, name, domain_id) + .await + } + + /// Get project parents. + #[tracing::instrument(level = "info", skip(self, state))] + async fn get_project_parents<'a>( + &self, + state: &ServiceState, + project_id: &'a str, + ) -> Result>, ResourceProviderError> { + self.backend_driver + .get_project_parents(state, project_id) + .await + } + + /// Get single domain by its name. + #[tracing::instrument(level = "info", skip(self, state))] + async fn find_domain_by_name<'a>( + &self, + state: &ServiceState, + domain_name: &'a str, + ) -> Result, ResourceProviderError> { + self.backend_driver + .get_domain_by_name(state, domain_name) + .await + } + + /// List domains. + #[tracing::instrument(level = "info", skip(self, state))] + async fn list_domains( + &self, + state: &ServiceState, + params: &DomainListParameters, + ) -> Result, ResourceProviderError> { + self.backend_driver.list_domains(state, params).await + } + + /// List projects. + #[tracing::instrument(level = "info", skip(self, state))] + async fn list_projects( + &self, + state: &ServiceState, + params: &ProjectListParameters, + ) -> Result, ResourceProviderError> { + self.backend_driver.list_projects(state, params).await + } +} diff --git a/crates/keystone/src/resource/types.rs b/crates/core/src/resource/types.rs similarity index 100% rename from crates/keystone/src/resource/types.rs rename to crates/core/src/resource/types.rs diff --git a/crates/keystone/src/resource/types/domain.rs b/crates/core/src/resource/types/domain.rs similarity index 100% rename from crates/keystone/src/resource/types/domain.rs rename to crates/core/src/resource/types/domain.rs diff --git a/crates/keystone/src/resource/types/project.rs b/crates/core/src/resource/types/project.rs similarity index 100% rename from crates/keystone/src/resource/types/project.rs rename to crates/core/src/resource/types/project.rs diff --git a/crates/keystone/src/resource/types/provider_api.rs b/crates/core/src/resource/types/provider_api.rs similarity index 100% rename from crates/keystone/src/resource/types/provider_api.rs rename to crates/core/src/resource/types/provider_api.rs diff --git a/crates/core/src/revoke/backend.rs b/crates/core/src/revoke/backend.rs new file mode 100644 index 00000000..c548a5e1 --- /dev/null +++ b/crates/core/src/revoke/backend.rs @@ -0,0 +1,56 @@ +// 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 +//! Token revocation: Backends. +//! Revocation provider Backend trait. +use async_trait::async_trait; + +use crate::keystone::ServiceState; +use crate::revoke::{RevokeProviderError, types::*}; +use crate::token::types::Token; + +//pub mod error; + +/// RevokeBackend trait. +/// +/// Backend driver interface expected by the revocation provider. +#[cfg_attr(test, mockall::automock)] +#[async_trait] +pub trait RevokeBackend: Send + Sync { + /// Create revocation event. + async fn create_revocation_event( + &self, + state: &ServiceState, + event: RevocationEventCreate, + ) -> Result; + + /// Check token revocation. + /// + /// Check whether there are existing revocation records that invalidate the + /// token. + async fn is_token_revoked( + &self, + state: &ServiceState, + token: &Token, + ) -> Result; + + /// Revoke the token. + /// + /// Mark the token as revoked to prohibit from being used even while not + /// expired. + async fn revoke_token( + &self, + state: &ServiceState, + token: &Token, + ) -> Result<(), RevokeProviderError>; +} diff --git a/crates/keystone/src/revoke/error.rs b/crates/core/src/revoke/error.rs similarity index 58% rename from crates/keystone/src/revoke/error.rs rename to crates/core/src/revoke/error.rs index efd44ebc..ee91f497 100644 --- a/crates/keystone/src/revoke/error.rs +++ b/crates/core/src/revoke/error.rs @@ -15,18 +15,9 @@ use thiserror::Error; -use crate::revoke::backend::error::RevokeDatabaseError; - /// Revoke provider error. #[derive(Error, Debug)] pub enum RevokeProviderError { - /// SQL backend error. - #[error(transparent)] - Backend { - /// The source of the error. - source: RevokeDatabaseError, - }, - /// Conflict. #[error("conflict: {0}")] Conflict(String), @@ -40,21 +31,6 @@ pub enum RevokeProviderError { TokenHasNoAuditId, /// Unsupported driver. - #[error("unsupported driver {0}")] + #[error("unsupported driver `{0}` for the revoke provider")] UnsupportedDriver(String), } - -impl From for RevokeProviderError { - fn from(source: RevokeDatabaseError) -> Self { - match source { - RevokeDatabaseError::Database { source } => match source { - cfl @ crate::error::DatabaseError::Conflict { .. } => { - Self::Conflict(cfl.to_string()) - } - other => Self::Backend { - source: RevokeDatabaseError::Database { source: other }, - }, - }, - } - } -} diff --git a/crates/keystone/src/revoke/mock.rs b/crates/core/src/revoke/mock.rs similarity index 86% rename from crates/keystone/src/revoke/mock.rs rename to crates/core/src/revoke/mock.rs index af1d1ebd..9cdb3f45 100644 --- a/crates/keystone/src/revoke/mock.rs +++ b/crates/core/src/revoke/mock.rs @@ -13,21 +13,15 @@ // SPDX-License-Identifier: Apache-2.0 //! Token revocation - internal mocking tools. use async_trait::async_trait; -#[cfg(test)] use mockall::mock; -use crate::config::Config; -use crate::plugin_manager::PluginManager; use crate::revoke::{RevokeApi, RevokeProviderError, types::*}; use crate::token::types::Token; use crate::keystone::ServiceState; -#[cfg(test)] mock! { - pub RevokeProvider { - pub fn new(cfg: &Config, plugin_manager: &PluginManager) -> Result; - } + pub RevokeProvider {} #[async_trait] impl RevokeApi for RevokeProvider { diff --git a/crates/core/src/revoke/mod.rs b/crates/core/src/revoke/mod.rs new file mode 100644 index 00000000..2427231b --- /dev/null +++ b/crates/core/src/revoke/mod.rs @@ -0,0 +1,120 @@ +// 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 +//! # Token revocation provider. +//! +//! Token revocation may be implemented in different ways, but in most cases +//! would be represented by the presence of the revocation or the invalidation +//! record matching the certain token parameters. +//! +//! Default backend is the [`sql`](crate::revoke::backend::sql) and uses the +//! database [table](crate::db::entity::revocation_event::Model) for storing the +//! revocation events. They have their own expiration. +//! +//! Tokens are not invalidated by saving the exact value, but rather by saving +//! certain attributes of the token. +//! +//! Following attributes are used for matching of the regular fernet token: +//! +//! - `audit_id` +//! - `domain_id` +//! - `expires_at` +//! - `project_id` +//! - `user_id` +//! +//! Additionally the `token.issued_at` is compared to be lower than the +//! `issued_before` field of the revocation record. + +use async_trait::async_trait; + +pub mod backend; +pub mod error; +#[cfg(any(test, feature = "mock"))] +mod mock; +pub mod service; +pub mod types; + +use crate::config::Config; +use crate::keystone::ServiceState; +use crate::plugin_manager::PluginManagerApi; +use crate::revoke::service::RevokeService; +use crate::token::types::Token; + +pub use error::RevokeProviderError; +#[cfg(any(test, feature = "mock"))] +pub use mock::MockRevokeProvider; +pub use types::*; + +/// Revoke provider. +pub enum RevokeProvider { + Service(RevokeService), + #[cfg(any(test, feature = "mock"))] + Mock(MockRevokeProvider), +} + +impl RevokeProvider { + pub fn new( + config: &Config, + plugin_manager: &P, + ) -> Result { + Ok(Self::Service(RevokeService::new(config, plugin_manager)?)) + } +} + +#[async_trait] +impl RevokeApi for RevokeProvider { + /// Create revocation event. + async fn create_revocation_event( + &self, + state: &ServiceState, + event: RevocationEventCreate, + ) -> Result { + match self { + Self::Service(provider) => provider.create_revocation_event(state, event).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.create_revocation_event(state, event).await, + } + } + + /// Check whether the token has been revoked or not. + /// + /// Checks revocation events matching the token parameters and return + /// `false` if their count is more than `0`. + async fn is_token_revoked( + &self, + state: &ServiceState, + token: &Token, + ) -> Result { + match self { + Self::Service(provider) => provider.is_token_revoked(state, token).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.is_token_revoked(state, token).await, + } + } + + /// Revoke the token. + /// + /// Mark the token as revoked to prohibit from being used even while not + /// expired. + async fn revoke_token( + &self, + state: &ServiceState, + token: &Token, + ) -> Result<(), RevokeProviderError> { + match self { + Self::Service(provider) => provider.revoke_token(state, token).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.revoke_token(state, token).await, + } + } +} diff --git a/crates/core/src/revoke/service.rs b/crates/core/src/revoke/service.rs new file mode 100644 index 00000000..4adf0b19 --- /dev/null +++ b/crates/core/src/revoke/service.rs @@ -0,0 +1,110 @@ +// 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 +//! # Token revocation provider. + +use async_trait::async_trait; +use std::sync::Arc; + +use crate::config::Config; +use crate::keystone::ServiceState; +use crate::plugin_manager::PluginManagerApi; +use crate::revoke::{RevokeProviderError, backend::RevokeBackend, types::*}; +use crate::token::types::Token; + +/// Revoke provider. +pub struct RevokeService { + /// Backend driver. + backend_driver: Arc, +} + +impl RevokeService { + pub fn new( + config: &Config, + plugin_manager: &P, + ) -> Result { + let backend_driver = plugin_manager + .get_revoke_backend(config.revoke.driver.clone())? + .clone(); + Ok(Self { backend_driver }) + } +} + +#[async_trait] +impl RevokeApi for RevokeService { + /// Create revocation event. + #[tracing::instrument(level = "info", skip(self, state))] + async fn create_revocation_event( + &self, + state: &ServiceState, + event: RevocationEventCreate, + ) -> Result { + self.backend_driver + .create_revocation_event(state, event) + .await + } + + /// Check whether the token has been revoked or not. + /// + /// Checks revocation events matching the token parameters and return + /// `false` if their count is more than `0`. + #[tracing::instrument(level = "info", skip(self, state, token))] + async fn is_token_revoked( + &self, + state: &ServiceState, + token: &Token, + ) -> Result { + tracing::info!("Checking for the revocation events"); + self.backend_driver.is_token_revoked(state, token).await + } + + /// Revoke the token. + /// + /// Mark the token as revoked to prohibit from being used even while not + /// expired. + async fn revoke_token( + &self, + state: &ServiceState, + token: &Token, + ) -> Result<(), RevokeProviderError> { + self.backend_driver.revoke_token(state, token).await + } +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use super::*; + use crate::revoke::backend::MockRevokeBackend; + use crate::tests::get_mocked_state; + + #[tokio::test] + async fn test_create_revocation_event() { + let state = get_mocked_state(None, None); + let mut backend = MockRevokeBackend::default(); + backend + .expect_create_revocation_event() + .returning(|_, _| Ok(RevocationEvent::default())); + let provider = RevokeService { + backend_driver: Arc::new(backend), + }; + + assert!( + provider + .create_revocation_event(&state, RevocationEventCreate::default()) + .await + .is_ok() + ); + } +} diff --git a/crates/keystone/src/revoke/types.rs b/crates/core/src/revoke/types.rs similarity index 100% rename from crates/keystone/src/revoke/types.rs rename to crates/core/src/revoke/types.rs diff --git a/crates/keystone/src/revoke/types/provider_api.rs b/crates/core/src/revoke/types/provider_api.rs similarity index 100% rename from crates/keystone/src/revoke/types/provider_api.rs rename to crates/core/src/revoke/types/provider_api.rs diff --git a/crates/keystone/src/revoke/types/revocation_event.rs b/crates/core/src/revoke/types/revocation_event.rs similarity index 98% rename from crates/keystone/src/revoke/types/revocation_event.rs rename to crates/core/src/revoke/types/revocation_event.rs index 25709fc9..5ddb049a 100644 --- a/crates/keystone/src/revoke/types/revocation_event.rs +++ b/crates/core/src/revoke/types/revocation_event.rs @@ -159,7 +159,7 @@ impl TryFrom<&Token> for RevocationEventListParameters { value .audit_ids() .first() - .ok_or_else(|| RevokeProviderError::TokenHasNoAuditId)?, + .ok_or(RevokeProviderError::TokenHasNoAuditId)?, ) .cloned(), //consumer_id: None, @@ -214,7 +214,7 @@ impl TryFrom<&Token> for RevocationEventCreate { value .audit_ids() .first() - .ok_or_else(|| RevokeProviderError::TokenHasNoAuditId)?, + .ok_or(RevokeProviderError::TokenHasNoAuditId)?, ) .cloned(), consumer_id: None, diff --git a/crates/core/src/role/backend.rs b/crates/core/src/role/backend.rs new file mode 100644 index 00000000..e078e7f7 --- /dev/null +++ b/crates/core/src/role/backend.rs @@ -0,0 +1,65 @@ +// 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 + +use async_trait::async_trait; +use std::collections::{BTreeMap, BTreeSet}; + +use crate::keystone::ServiceState; +use crate::role::{RoleProviderError, types::role::*}; + +#[cfg_attr(test, mockall::automock)] +#[async_trait] +pub trait RoleBackend: Send + Sync { + /// Create Role. + async fn create_role( + &self, + state: &ServiceState, + params: RoleCreate, + ) -> Result; + + /// Delete a role by the ID. + async fn delete_role<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result<(), RoleProviderError>; + + /// Get single role by ID + async fn get_role<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result, RoleProviderError>; + + /// Expand implied roles. + async fn expand_implied_roles( + &self, + state: &ServiceState, + roles: &mut Vec, + ) -> Result<(), RoleProviderError>; + + /// List role imply rules. + async fn list_imply_rules( + &self, + state: &ServiceState, + resolve: bool, + ) -> Result>, RoleProviderError>; + + /// List Roles. + async fn list_roles( + &self, + state: &ServiceState, + params: &RoleListParameters, + ) -> Result, RoleProviderError>; +} diff --git a/crates/keystone/src/role/error.rs b/crates/core/src/role/error.rs similarity index 96% rename from crates/keystone/src/role/error.rs rename to crates/core/src/role/error.rs index 05dfade8..f3cf5658 100644 --- a/crates/keystone/src/role/error.rs +++ b/crates/core/src/role/error.rs @@ -45,7 +45,7 @@ pub enum RoleProviderError { }, /// Unsupported driver. - #[error("unsupported driver {0}")] + #[error("unsupported driver `{0}` for the role provider")] UnsupportedDriver(String), /// Validation error. diff --git a/crates/keystone/src/role/mock.rs b/crates/core/src/role/mock.rs similarity index 90% rename from crates/keystone/src/role/mock.rs rename to crates/core/src/role/mock.rs index 5d0a364e..883b7680 100644 --- a/crates/keystone/src/role/mock.rs +++ b/crates/core/src/role/mock.rs @@ -15,15 +15,11 @@ use async_trait::async_trait; use mockall::mock; use std::collections::{BTreeMap, BTreeSet}; -use crate::config::Config; use crate::keystone::ServiceState; -use crate::plugin_manager::PluginManager; use crate::role::{RoleApi, RoleProviderError, types::*}; mock! { - pub RoleProvider { - pub fn new(cfg: &Config, plugin_manager: &PluginManager) -> Result; - } + pub RoleProvider {} #[async_trait] impl RoleApi for RoleProvider { diff --git a/crates/core/src/role/mod.rs b/crates/core/src/role/mod.rs new file mode 100644 index 00000000..2dbff031 --- /dev/null +++ b/crates/core/src/role/mod.rs @@ -0,0 +1,152 @@ +// 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 +//! # Role provider +//! +//! Role provider provides possibility to manage roles (part of RBAC). +//! +//! Following Keystone concepts are covered by the provider: +//! +//! ## Role inference +//! +//! Roles in Keystone may imply other roles building an inference chain. For +//! example a role `manager` can imply the `member` role, which in turn implies +//! the `reader` role. As such with a single assignment of the `manager` role +//! the user will automatically get `manager`, `member` and `reader` roles. This +//! helps limiting number of necessary direct assignments. +//! +//! ## Role +//! +//! A personality with a defined set of user rights and privileges to perform a +//! specific set of operations. The Identity service issues a token to a user +//! that includes a list of roles. When a user calls a service, that service +//! interprets the user role set, and determines to which operations or +//! resources each role grants access. +use async_trait::async_trait; +use std::collections::{BTreeMap, BTreeSet}; + +pub mod backend; +pub mod error; +#[cfg(any(test, feature = "mock"))] +mod mock; +pub mod service; +pub mod types; + +use crate::config::Config; +use crate::keystone::ServiceState; +use crate::plugin_manager::PluginManagerApi; +use crate::role::{service::RoleService, types::*}; + +pub use error::RoleProviderError; +#[cfg(any(test, feature = "mock"))] +pub use mock::MockRoleProvider; +pub use types::RoleApi; + +pub enum RoleProvider { + Service(RoleService), + #[cfg(any(test, feature = "mock"))] + Mock(MockRoleProvider), +} + +impl RoleProvider { + pub fn new( + config: &Config, + plugin_manager: &P, + ) -> Result { + Ok(Self::Service(RoleService::new(config, plugin_manager)?)) + } +} + +#[async_trait] +impl RoleApi for RoleProvider { + /// Create role. + #[tracing::instrument(level = "info", skip(self, state))] + async fn create_role( + &self, + state: &ServiceState, + params: RoleCreate, + ) -> Result { + match self { + Self::Service(provider) => provider.create_role(state, params).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.create_role(state, params).await, + } + } + + /// Delete a role by the ID. + #[tracing::instrument(level = "info", skip(self, state))] + async fn delete_role<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result<(), RoleProviderError> { + match self { + Self::Service(provider) => provider.delete_role(state, id).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.delete_role(state, id).await, + } + } + + /// Get single role. + async fn get_role<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result, RoleProviderError> { + match self { + Self::Service(provider) => provider.get_role(state, id).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.get_role(state, id).await, + } + } + + /// Expand implied roles. + /// + /// Return list of the roles with the imply rules being considered. + async fn expand_implied_roles( + &self, + state: &ServiceState, + roles: &mut Vec, + ) -> Result<(), RoleProviderError> { + match self { + Self::Service(provider) => provider.expand_implied_roles(state, roles).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.expand_implied_roles(state, roles).await, + } + } + + /// List role imply rules. + async fn list_imply_rules( + &self, + state: &ServiceState, + resolve: bool, + ) -> Result>, RoleProviderError> { + match self { + Self::Service(provider) => provider.list_imply_rules(state, resolve).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.list_imply_rules(state, resolve).await, + } + } + + /// List roles. + async fn list_roles( + &self, + state: &ServiceState, + params: &RoleListParameters, + ) -> Result, RoleProviderError> { + match self { + Self::Service(provider) => provider.list_roles(state, params).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.list_roles(state, params).await, + } + } +} diff --git a/crates/core/src/role/service.rs b/crates/core/src/role/service.rs new file mode 100644 index 00000000..6445ec5f --- /dev/null +++ b/crates/core/src/role/service.rs @@ -0,0 +1,119 @@ +// 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 +//! # Role provider +use std::collections::{BTreeMap, BTreeSet}; +use std::sync::Arc; + +use async_trait::async_trait; +use uuid::Uuid; +use validator::Validate; + +use crate::config::Config; +use crate::keystone::ServiceState; +use crate::plugin_manager::PluginManagerApi; +use crate::role::{RoleProviderError, backend::RoleBackend, types::*}; + +pub struct RoleService { + backend_driver: Arc, +} + +impl RoleService { + pub fn new( + config: &Config, + plugin_manager: &P, + ) -> Result { + let backend_driver = plugin_manager + .get_role_backend(config.role.driver.clone())? + .clone(); + Ok(Self { backend_driver }) + } +} + +#[async_trait] +impl RoleApi for RoleService { + /// Create role. + #[tracing::instrument(level = "info", skip(self, state))] + async fn create_role( + &self, + state: &ServiceState, + params: RoleCreate, + ) -> Result { + params.validate()?; + + let mut new_params = params; + + if new_params.id.is_none() { + new_params.id = Some(Uuid::new_v4().simple().to_string()); + } + self.backend_driver.create_role(state, new_params).await + } + + /// Delete a role by the ID. + #[tracing::instrument(level = "info", skip(self, state))] + async fn delete_role<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result<(), RoleProviderError> { + self.backend_driver.delete_role(state, id).await + } + + /// Get single role. + #[tracing::instrument(level = "info", skip(self, state))] + async fn get_role<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result, RoleProviderError> { + self.backend_driver.get_role(state, id).await + } + + /// Expand implied roles. + /// + /// Return list of the roles with the imply rules being considered. + #[tracing::instrument(level = "info", skip(self, state))] + async fn expand_implied_roles( + &self, + state: &ServiceState, + roles: &mut Vec, + ) -> Result<(), RoleProviderError> { + // In most of the cases a logic for expanding the roles may be implemented by + // the provider itself, but some backend drivers may have more efficient + // methods. + self.backend_driver + .expand_implied_roles(state, roles) + .await?; + Ok(()) + } + + /// List role imply rules. + #[tracing::instrument(level = "info", skip(self, state))] + async fn list_imply_rules( + &self, + state: &ServiceState, + resolve: bool, + ) -> Result>, RoleProviderError> { + self.backend_driver.list_imply_rules(state, resolve).await + } + + /// List roles. + #[tracing::instrument(level = "info", skip(self, state))] + async fn list_roles( + &self, + state: &ServiceState, + params: &RoleListParameters, + ) -> Result, RoleProviderError> { + params.validate()?; + self.backend_driver.list_roles(state, params).await + } +} diff --git a/crates/keystone/src/role/types.rs b/crates/core/src/role/types.rs similarity index 100% rename from crates/keystone/src/role/types.rs rename to crates/core/src/role/types.rs diff --git a/crates/keystone/src/role/types/provider_api.rs b/crates/core/src/role/types/provider_api.rs similarity index 100% rename from crates/keystone/src/role/types/provider_api.rs rename to crates/core/src/role/types/provider_api.rs diff --git a/crates/keystone/src/role/types/role.rs b/crates/core/src/role/types/role.rs similarity index 99% rename from crates/keystone/src/role/types/role.rs rename to crates/core/src/role/types/role.rs index ba3536d2..fa4d70a4 100644 --- a/crates/keystone/src/role/types/role.rs +++ b/crates/core/src/role/types/role.rs @@ -12,12 +12,13 @@ // // SPDX-License-Identifier: Apache-2.0 -use crate::error::BuilderError; use derive_builder::Builder; use serde::{Deserialize, Serialize}; use serde_json::Value; use validator::Validate; +use crate::error::BuilderError; + /// Role representation. #[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize, Validate)] #[builder(build_fn(error = "BuilderError"))] diff --git a/crates/core/src/tests.rs b/crates/core/src/tests.rs new file mode 100644 index 00000000..9aa6e94d --- /dev/null +++ b/crates/core/src/tests.rs @@ -0,0 +1,41 @@ +// 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 +//! # Test related functionality +use sea_orm::DatabaseConnection; +use std::sync::Arc; + +use crate::config::Config; +use crate::keystone::{Service, ServiceState}; +use crate::policy::MockPolicy; +use crate::provider::{Provider, ProviderBuilder}; + +pub(crate) mod token; + +pub fn get_mocked_state( + config: Option, + provider_builder: Option, +) -> ServiceState { + Arc::new( + Service::new( + config.unwrap_or_default(), + DatabaseConnection::Disconnected, + provider_builder + .unwrap_or(Provider::mocked_builder()) + .build() + .unwrap(), + Arc::new(MockPolicy::default()), + ) + .unwrap(), + ) +} diff --git a/crates/core/src/tests/token.rs b/crates/core/src/tests/token.rs new file mode 100644 index 00000000..ae9dd425 --- /dev/null +++ b/crates/core/src/tests/token.rs @@ -0,0 +1,39 @@ +// 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 + +use std::fs::File; +use std::io::Write; +use tempfile::tempdir; + +use crate::config::Config; + +pub fn setup_config() -> Config { + let keys_dir = tempdir().unwrap(); + // write fernet key used to generate tokens in python + let file_path = keys_dir.path().join("0"); + let mut tmp_file = File::create(&file_path).unwrap(); + write!(tmp_file, "BFTs1CIVIBLTP4GOrQ26VETrJ7Zwz1O4wbEcCQ966eM=").unwrap(); + + let builder = config::Config::builder() + .set_override( + "auth.methods", + "password,token,openid,application_credential", + ) + .unwrap() + .set_override("database.connection", "dummy") + .unwrap(); + let mut config: Config = Config::try_from(builder).expect("can build a valid config"); + config.fernet_tokens.key_repository = keys_dir.keep(); + config +} diff --git a/crates/keystone/src/token/backend.rs b/crates/core/src/token/backend.rs similarity index 100% rename from crates/keystone/src/token/backend.rs rename to crates/core/src/token/backend.rs diff --git a/crates/keystone/src/token/backend/fernet.rs b/crates/core/src/token/backend/fernet.rs similarity index 100% rename from crates/keystone/src/token/backend/fernet.rs rename to crates/core/src/token/backend/fernet.rs diff --git a/crates/keystone/src/token/backend/fernet/application_credential.rs b/crates/core/src/token/backend/fernet/application_credential.rs similarity index 100% rename from crates/keystone/src/token/backend/fernet/application_credential.rs rename to crates/core/src/token/backend/fernet/application_credential.rs diff --git a/crates/keystone/src/token/backend/fernet/domain_scoped.rs b/crates/core/src/token/backend/fernet/domain_scoped.rs similarity index 100% rename from crates/keystone/src/token/backend/fernet/domain_scoped.rs rename to crates/core/src/token/backend/fernet/domain_scoped.rs diff --git a/crates/keystone/src/token/backend/fernet/federation_domain_scoped.rs b/crates/core/src/token/backend/fernet/federation_domain_scoped.rs similarity index 100% rename from crates/keystone/src/token/backend/fernet/federation_domain_scoped.rs rename to crates/core/src/token/backend/fernet/federation_domain_scoped.rs diff --git a/crates/keystone/src/token/backend/fernet/federation_project_scoped.rs b/crates/core/src/token/backend/fernet/federation_project_scoped.rs similarity index 100% rename from crates/keystone/src/token/backend/fernet/federation_project_scoped.rs rename to crates/core/src/token/backend/fernet/federation_project_scoped.rs diff --git a/crates/keystone/src/token/backend/fernet/federation_unscoped.rs b/crates/core/src/token/backend/fernet/federation_unscoped.rs similarity index 100% rename from crates/keystone/src/token/backend/fernet/federation_unscoped.rs rename to crates/core/src/token/backend/fernet/federation_unscoped.rs diff --git a/crates/keystone/src/token/backend/fernet/project_scoped.rs b/crates/core/src/token/backend/fernet/project_scoped.rs similarity index 100% rename from crates/keystone/src/token/backend/fernet/project_scoped.rs rename to crates/core/src/token/backend/fernet/project_scoped.rs diff --git a/crates/keystone/src/token/backend/fernet/restricted.rs b/crates/core/src/token/backend/fernet/restricted.rs similarity index 100% rename from crates/keystone/src/token/backend/fernet/restricted.rs rename to crates/core/src/token/backend/fernet/restricted.rs diff --git a/crates/keystone/src/token/backend/fernet/system_scoped.rs b/crates/core/src/token/backend/fernet/system_scoped.rs similarity index 100% rename from crates/keystone/src/token/backend/fernet/system_scoped.rs rename to crates/core/src/token/backend/fernet/system_scoped.rs diff --git a/crates/keystone/src/token/backend/fernet/trust.rs b/crates/core/src/token/backend/fernet/trust.rs similarity index 100% rename from crates/keystone/src/token/backend/fernet/trust.rs rename to crates/core/src/token/backend/fernet/trust.rs diff --git a/crates/keystone/src/token/backend/fernet/unscoped.rs b/crates/core/src/token/backend/fernet/unscoped.rs similarity index 100% rename from crates/keystone/src/token/backend/fernet/unscoped.rs rename to crates/core/src/token/backend/fernet/unscoped.rs diff --git a/crates/keystone/src/token/backend/fernet/utils.rs b/crates/core/src/token/backend/fernet/utils.rs similarity index 100% rename from crates/keystone/src/token/backend/fernet/utils.rs rename to crates/core/src/token/backend/fernet/utils.rs diff --git a/crates/keystone/src/token/error.rs b/crates/core/src/token/error.rs similarity index 96% rename from crates/keystone/src/token/error.rs rename to crates/core/src/token/error.rs index 8793ed72..7015623f 100644 --- a/crates/keystone/src/token/error.rs +++ b/crates/core/src/token/error.rs @@ -17,7 +17,7 @@ use std::num::TryFromIntError; use thiserror::Error; -use crate::error::{BuilderError, DatabaseError}; +use crate::error::BuilderError; /// Token provider error. #[derive(Error, Debug)] @@ -70,10 +70,9 @@ pub enum TokenProviderError { #[error("{message}")] Conflict { message: String, context: String }, - /// Database error. - #[error(transparent)] - Database(#[from] DatabaseError), - + ///// Database error. + //#[error(transparent)] + //Database(#[from] DatabaseError), /// The domain is disabled. #[error("domain is disabled")] DomainDisabled(String), @@ -229,9 +228,9 @@ pub enum TokenProviderError { #[error("unsupported authentication methods {0} in token payload")] UnsupportedAuthMethods(String), - /// Unsupported driver. - #[error("unsupported driver {0}")] - UnsupportedDriver(String), + /// Unsupported token restriction driver. + #[error("driver `{0}` is not supported for the token restriction provider")] + UnsupportedTRDriver(String), /// The user is disabled. #[error("user disabled")] diff --git a/crates/keystone/src/token/mock.rs b/crates/core/src/token/mock.rs similarity index 94% rename from crates/keystone/src/token/mock.rs rename to crates/core/src/token/mock.rs index 6f587fdb..87c90a8b 100644 --- a/crates/keystone/src/token/mock.rs +++ b/crates/core/src/token/mock.rs @@ -18,9 +18,7 @@ use mockall::mock; use super::error::TokenProviderError; use crate::auth::{AuthenticatedInfo, AuthzInfo}; -use crate::config::Config; use crate::keystone::ServiceState; -use crate::plugin_manager::PluginManager; use super::{ Token, TokenApi, TokenRestriction, TokenRestrictionCreate, TokenRestrictionListParameters, @@ -28,9 +26,7 @@ use super::{ }; mock! { - pub TokenProvider { - pub fn new(cfg: &Config, plugin_manager: &PluginManager) -> Result; - } + pub TokenProvider {} #[async_trait] impl TokenApi for TokenProvider { diff --git a/crates/core/src/token/mod.rs b/crates/core/src/token/mod.rs new file mode 100644 index 00000000..bb5c478c --- /dev/null +++ b/crates/core/src/token/mod.rs @@ -0,0 +1,275 @@ +// 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 +//! # Token provider. +//! +//! A Keystone token is an alpha-numeric text string that enables access to +//! OpenStack APIs and resources. A token may be revoked at any time and is +//! valid for a finite duration. OpenStack Identity is an integration service +//! that does not aspire to be a full-fledged identity store and management +//! solution. + +use async_trait::async_trait; + +pub mod backend; +pub mod error; +#[cfg(any(test, feature = "mock"))] +mod mock; +pub mod service; +//mod token_restriction; +pub mod types; + +use crate::auth::{AuthenticatedInfo, AuthzInfo}; +use crate::config::Config; +use crate::keystone::ServiceState; +use crate::plugin_manager::PluginManagerApi; +use crate::token::service::TokenService; +pub use error::TokenProviderError; + +pub use crate::token::types::*; +#[cfg(any(test, feature = "mock"))] +pub use mock::MockTokenProvider; + +pub enum TokenProvider { + Service(TokenService), + #[cfg(any(test, feature = "mock"))] + Mock(MockTokenProvider), +} + +impl TokenProvider { + pub fn new( + config: &Config, + plugin_manager: &P, + ) -> Result { + Ok(Self::Service(TokenService::new(config, plugin_manager)?)) + } +} + +#[async_trait] +impl TokenApi for TokenProvider { + /// Authenticate by token. + async fn authenticate_by_token<'a>( + &self, + state: &ServiceState, + credential: &'a str, + allow_expired: Option, + window_seconds: Option, + ) -> Result { + match self { + Self::Service(provider) => { + provider + .authenticate_by_token(state, credential, allow_expired, window_seconds) + .await + } + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => { + provider + .authenticate_by_token(state, credential, allow_expired, window_seconds) + .await + } + } + } + + /// Validate token. + async fn validate_token<'a>( + &self, + state: &ServiceState, + credential: &'a str, + allow_expired: Option, + window_seconds: Option, + ) -> Result { + match self { + Self::Service(provider) => { + provider + .validate_token(state, credential, allow_expired, window_seconds) + .await + } + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => { + provider + .validate_token(state, credential, allow_expired, window_seconds) + .await + } + } + } + + /// Issue the Keystone token. + fn issue_token( + &self, + authentication_info: AuthenticatedInfo, + authz_info: AuthzInfo, + token_restrictions: Option<&TokenRestriction>, + ) -> Result { + match self { + Self::Service(provider) => { + provider.issue_token(authentication_info, authz_info, token_restrictions) + } + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => { + provider.issue_token(authentication_info, authz_info, token_restrictions) + } + } + } + + /// Encode the token into a `String` representation. + /// + /// Encode the [`Token`] into the `String` to be used as a http header. + fn encode_token(&self, token: &Token) -> Result { + match self { + Self::Service(provider) => provider.encode_token(token), + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.encode_token(token), + } + } + + /// Populate role assignments in the token that support that information. + async fn populate_role_assignments( + &self, + state: &ServiceState, + token: &mut Token, + ) -> Result<(), TokenProviderError> { + match self { + Self::Service(provider) => provider.populate_role_assignments(state, token).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.populate_role_assignments(state, token).await, + } + } + + /// Expand the token information. + /// + /// Query and expand information about the user, scope and the role + /// assignments into the token. + async fn expand_token_information( + &self, + state: &ServiceState, + token: &Token, + ) -> Result { + match self { + Self::Service(provider) => provider.expand_token_information(state, token).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.expand_token_information(state, token).await, + } + } + + /// Get the token restriction by the ID. + async fn get_token_restriction<'a>( + &self, + state: &ServiceState, + id: &'a str, + expand_roles: bool, + ) -> Result, TokenProviderError> { + match self { + Self::Service(provider) => { + provider + .get_token_restriction(state, id, expand_roles) + .await + } + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => { + provider + .get_token_restriction(state, id, expand_roles) + .await + } + } + } + + /// Create new token restriction. + async fn create_token_restriction<'a>( + &self, + state: &ServiceState, + restriction: TokenRestrictionCreate, + ) -> Result { + match self { + Self::Service(provider) => provider.create_token_restriction(state, restriction).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.create_token_restriction(state, restriction).await, + } + } + + /// List token restrictions. + async fn list_token_restrictions<'a>( + &self, + state: &ServiceState, + params: &TokenRestrictionListParameters, + ) -> Result, TokenProviderError> { + match self { + Self::Service(provider) => provider.list_token_restrictions(state, params).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.list_token_restrictions(state, params).await, + } + } + + /// Update existing token restriction. + async fn update_token_restriction<'a>( + &self, + state: &ServiceState, + id: &'a str, + restriction: TokenRestrictionUpdate, + ) -> Result { + match self { + Self::Service(provider) => { + provider + .update_token_restriction(state, id, restriction) + .await + } + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => { + provider + .update_token_restriction(state, id, restriction) + .await + } + } + } + + /// Delete token restriction by the ID. + async fn delete_token_restriction<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result<(), TokenProviderError> { + match self { + Self::Service(provider) => provider.delete_token_restriction(state, id).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.delete_token_restriction(state, id).await, + } + } +} + +#[cfg(test)] +mod tests { + use std::fs::File; + use std::io::Write; + use tempfile::tempdir; + + use crate::config::Config; + + pub(super) fn setup_config() -> Config { + let keys_dir = tempdir().unwrap(); + // write fernet key used to generate tokens in python + let file_path = keys_dir.path().join("0"); + let mut tmp_file = File::create(file_path).unwrap(); + write!(tmp_file, "BFTs1CIVIBLTP4GOrQ26VETrJ7Zwz1O4wbEcCQ966eM=").unwrap(); + + let builder = config::Config::builder() + .set_override( + "auth.methods", + "password,token,openid,application_credential", + ) + .unwrap() + .set_override("database.connection", "dummy") + .unwrap(); + let mut config: Config = Config::try_from(builder).expect("can build a valid config"); + config.fernet_tokens.key_repository = keys_dir.keep(); + config + } +} diff --git a/crates/keystone/src/token/token_provider.rs b/crates/core/src/token/service.rs similarity index 58% rename from crates/keystone/src/token/token_provider.rs rename to crates/core/src/token/service.rs index c91e58b5..9095e13d 100644 --- a/crates/keystone/src/token/token_provider.rs +++ b/crates/core/src/token/service.rs @@ -19,45 +19,62 @@ //! that does not aspire to be a full-fledged identity store and management //! solution. +use async_trait::async_trait; +use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD}; use chrono::{DateTime, TimeDelta, Utc}; use std::collections::HashSet; use std::sync::Arc; -use tracing::debug; +use tracing::{debug, trace}; +use uuid::Uuid; -use crate::auth::{AuthenticatedInfo, AuthzInfo}; +use crate::auth::{AuthenticatedInfo, AuthenticationError, AuthzInfo}; use crate::config::{Config, TokenProviderDriver}; +use crate::identity::IdentityApi; use crate::keystone::ServiceState; +use crate::plugin_manager::PluginManagerApi; +use crate::resource::{ + ResourceApi, + types::{Domain, Project}, +}; +use crate::revoke::RevokeApi; use crate::token::{ - FernetTokenProvider, TokenProvider, TokenProviderError, types::*, + TokenProviderError, + backend::{TokenBackend, TokenRestrictionBackend, fernet::FernetTokenProvider}, }; use crate::{ application_credential::ApplicationCredentialApi, assignment::{ AssignmentApi, error::AssignmentProviderError, - types::{Role, RoleAssignmentListParameters, RoleAssignmentListParametersBuilder}, - }, - identity::IdentityApi, - resource::{ - ResourceApi, - types::{Domain, Project}, + types::{RoleAssignmentListParameters, RoleAssignmentListParametersBuilder}, }, + role::{RoleApi, types::RoleRef}, trust::{TrustApi, types::Trust}, }; -//pub struct TokenProvider { -// config: Config, -// backend_driver: Arc, -//} +pub use crate::token::types::*; + +pub struct TokenService { + config: Config, + backend_driver: Arc, + tr_backend_driver: Arc, +} -impl TokenProvider { - pub fn new(config: &Config) -> Result { +impl TokenService { + pub fn new( + config: &Config, + plugin_manager: &P, + ) -> Result { let backend_driver = match config.token.provider { TokenProviderDriver::Fernet => FernetTokenProvider::new(config.clone()), }; + let tr_backend_driver = plugin_manager + .get_token_restriction_backend(&config.token_restriction.driver)? + .clone(); Ok(Self { config: config.clone(), backend_driver: Arc::new(backend_driver), + tr_backend_driver, }) } @@ -90,7 +107,7 @@ impl TokenProvider { } /// Create project scoped token. - pub fn create_project_scope_token( + fn create_project_scope_token( &self, authentication_info: &AuthenticatedInfo, project: &Project, @@ -133,7 +150,7 @@ impl TokenProvider { } /// Create domain scoped token. - pub fn create_domain_scope_token( + fn create_domain_scope_token( &self, authentication_info: &AuthenticatedInfo, domain: &Domain, @@ -152,7 +169,7 @@ impl TokenProvider { } /// Create unscoped token with the identity provider bind. - pub fn create_federated_unscoped_token( + fn create_federated_unscoped_token( &self, authentication_info: &AuthenticatedInfo, ) -> Result { @@ -178,7 +195,7 @@ impl TokenProvider { } /// Create project scoped token with the identity provider bind. - pub fn create_federated_project_scope_token( + fn create_federated_project_scope_token( &self, authentication_info: &AuthenticatedInfo, project: &Project, @@ -214,7 +231,7 @@ impl TokenProvider { } /// Create domain scoped token with the identity provider bind. - pub fn create_federated_domain_scope_token( + fn create_federated_domain_scope_token( &self, authentication_info: &AuthenticatedInfo, domain: &Domain, @@ -250,7 +267,7 @@ impl TokenProvider { } /// Create token with the specified restrictions. - pub fn create_restricted_token( + fn create_restricted_token( &self, authentication_info: &AuthenticatedInfo, authz_info: &AuthzInfo, @@ -287,7 +304,7 @@ impl TokenProvider { } /// Create system scoped token. - pub fn create_system_scoped_token( + fn create_system_scoped_token( &self, authentication_info: &AuthenticatedInfo, ) -> Result { @@ -304,7 +321,7 @@ impl TokenProvider { } /// Create token based on the trust. - pub fn create_trust_token( + fn create_trust_token( &self, authentication_info: &AuthenticatedInfo, trust: &Trust, @@ -336,7 +353,7 @@ impl TokenProvider { } /// Expand user information in the token. - pub async fn expand_user_information( + async fn expand_user_information( &self, state: &ServiceState, token: &mut Token, @@ -394,7 +411,7 @@ impl TokenProvider { } /// Expand the target scope information in the token. - pub async fn expand_scope_information( + async fn expand_scope_information( &self, state: &ServiceState, token: &mut Token, @@ -504,7 +521,7 @@ impl TokenProvider { } /// Populate role assignments in the token that support that information. - pub async fn _populate_role_assignments( + async fn _populate_role_assignments( &self, state: &ServiceState, token: &mut Token, @@ -543,11 +560,19 @@ impl TokenProvider { .into_iter() .map(|x| x.role_id.clone()) .collect(); - // Filter out roles referred in the AC that the user does not have anymore. - ac.roles.retain(|role| user_role_ids.contains(&role.id)); - if ac.roles.is_empty() { + + // Gather all effective roles that the user have remaining should some of the + // AppCred assigned roles be revoked in the meanwhile. + let mut final_roles: Vec = Vec::new(); + for role in ac.roles.iter() { + if user_role_ids.contains(&role.id) { + final_roles.push(role.clone()); + } + } + if final_roles.is_empty() { return Err(TokenProviderError::ActorHasNoRolesOnTarget); } + data.roles = Some(final_roles); }; } Token::DomainScope(data) => { @@ -567,10 +592,10 @@ impl TokenProvider { ) .await? .into_iter() - .map(|x| Role { + .map(|x| RoleRef { id: x.role_id.clone(), - name: x.role_name.clone().unwrap_or_default(), - ..Default::default() + name: x.role_name.clone(), + domain_id: None, }) .collect(), ); @@ -595,10 +620,10 @@ impl TokenProvider { ) .await? .into_iter() - .map(|x| Role { + .map(|x| RoleRef { id: x.role_id.clone(), - name: x.role_name.clone().unwrap_or_default(), - ..Default::default() + name: x.role_name.clone(), + domain_id: None, }) .collect(), ); @@ -623,10 +648,10 @@ impl TokenProvider { ) .await? .into_iter() - .map(|x| Role { + .map(|x| RoleRef { id: x.role_id.clone(), - name: x.role_name.clone().unwrap_or_default(), - ..Default::default() + name: x.role_name.clone(), + domain_id: None, }) .collect(), ); @@ -651,10 +676,10 @@ impl TokenProvider { ) .await? .into_iter() - .map(|x| Role { + .map(|x| RoleRef { id: x.role_id.clone(), - name: x.role_name.clone().unwrap_or_default(), - ..Default::default() + name: x.role_name.clone(), + domain_id: None, }) .collect(), ); @@ -689,10 +714,10 @@ impl TokenProvider { ) .await? .into_iter() - .map(|x| Role { + .map(|x| RoleRef { id: x.role_id.clone(), - name: x.role_name.clone().unwrap_or_default(), - ..Default::default() + name: x.role_name.clone(), + domain_id: None, }) .collect(), ); @@ -728,7 +753,7 @@ impl TokenProvider { // Expand the implied roles state .provider - .get_assignment_provider() + .get_role_provider() .expand_implied_roles(state, trust_roles) .await?; if !trust_roles @@ -752,18 +777,630 @@ impl TokenProvider { } } +#[async_trait] +impl TokenApi for TokenService { + /// Authenticate by token. + #[tracing::instrument(level = "info", skip(self, state, credential))] + async fn authenticate_by_token<'a>( + &self, + state: &ServiceState, + credential: &'a str, + allow_expired: Option, + window_seconds: Option, + ) -> Result { + // TODO: is the expand really false? + let token = self + .validate_token(state, credential, allow_expired, window_seconds) + .await?; + if let Token::Restricted(restriction) = &token + && !restriction.allow_renew + { + return Err(AuthenticationError::TokenRenewalForbidden)?; + } + let mut auth_info_builder = AuthenticatedInfo::builder(); + auth_info_builder.user_id(token.user_id()); + auth_info_builder.methods(token.methods().clone()); + auth_info_builder.audit_ids(token.audit_ids().clone()); + auth_info_builder.expires_at(*token.expires_at()); + if let Token::Restricted(restriction) = &token { + auth_info_builder.token_restriction_id(restriction.token_restriction_id.clone()); + } + Ok(auth_info_builder + .build() + .map_err(AuthenticationError::from)?) + } + + /// Validate token. + #[tracing::instrument(level = "info", skip(self, state, credential))] + async fn validate_token<'a>( + &self, + state: &ServiceState, + credential: &'a str, + allow_expired: Option, + window_seconds: Option, + ) -> Result { + let mut token = self.backend_driver.decode(credential)?; + let latest_expiration_cutof = Utc::now() + .checked_add_signed(TimeDelta::seconds(window_seconds.unwrap_or(0))) + .unwrap_or(Utc::now()); + if !allow_expired.unwrap_or_default() && *token.expires_at() < latest_expiration_cutof { + trace!( + "Token has expired at {:?} with cutof: {:?}", + token.expires_at(), + latest_expiration_cutof + ); + return Err(TokenProviderError::Expired); + } + + // Expand the token unless `expand = Some(false)` + token = self.expand_token_information(state, &token).await?; + + if state + .provider + .get_revoke_provider() + .is_token_revoked(state, &token) + .await? + { + return Err(TokenProviderError::TokenRevoked); + } + + token.validate_subject(state).await?; + token.validate_scope(state).await?; + + Ok(token) + } + + /// Issue the Keystone token. + #[tracing::instrument(level = "debug", skip(self))] + fn issue_token( + &self, + authentication_info: AuthenticatedInfo, + authz_info: AuthzInfo, + token_restrictions: Option<&TokenRestriction>, + ) -> Result { + // This should be executed already, but let's better repeat it as last line of + // defence. It is also necessary to call this before to stop before we + // start to resolve authz info. + authentication_info.validate()?; + + // TODO: Check whether it is allowed to change the scope of the token if + // AuthenticatedInfo already contains scope it was issued for. + let mut authentication_info = authentication_info; + authentication_info + .audit_ids + .push(URL_SAFE_NO_PAD.encode(Uuid::new_v4().as_bytes())); + if let Some(token_restrictions) = &token_restrictions { + self.create_restricted_token(&authentication_info, &authz_info, token_restrictions) + } else if authentication_info.idp_id.is_some() && authentication_info.protocol_id.is_some() + { + match &authz_info { + AuthzInfo::Domain(domain) => { + self.create_federated_domain_scope_token(&authentication_info, domain) + } + AuthzInfo::Project(project) => { + self.create_federated_project_scope_token(&authentication_info, project) + } + AuthzInfo::Trust(_trust) => Err(TokenProviderError::Conflict { + message: "cannot create trust token with an identity provider in scope".into(), + context: "issuing token".into(), + }), + AuthzInfo::System => Err(TokenProviderError::Conflict { + message: "cannot create system scope token with an identity provider in scope" + .into(), + context: "issuing token".into(), + }), + AuthzInfo::Unscoped => self.create_federated_unscoped_token(&authentication_info), + } + } else { + match &authz_info { + AuthzInfo::Domain(domain) => { + self.create_domain_scope_token(&authentication_info, domain) + } + AuthzInfo::Project(project) => { + self.create_project_scope_token(&authentication_info, project) + } + AuthzInfo::Trust(trust) => self.create_trust_token(&authentication_info, trust), + AuthzInfo::System => self.create_system_scoped_token(&authentication_info), + AuthzInfo::Unscoped => self.create_unscoped_token(&authentication_info), + } + } + } + + /// Encode the token into a `String` representation. + /// + /// Encode the [`Token`] into the `String` to be used as a http header. + fn encode_token(&self, token: &Token) -> Result { + self.backend_driver.encode(token) + } + + /// Populate role assignments in the token that support that information. + async fn populate_role_assignments( + &self, + state: &ServiceState, + token: &mut Token, + ) -> Result<(), TokenProviderError> { + self._populate_role_assignments(state, token).await + } + + /// Expand the token information. + /// + /// Query and expand information about the user, scope and the role + /// assignments into the token. + async fn expand_token_information( + &self, + state: &ServiceState, + token: &Token, + ) -> Result { + let mut new_token = token.clone(); + self.expand_user_information(state, &mut new_token).await?; + self.expand_scope_information(state, &mut new_token).await?; + self.populate_role_assignments(state, &mut new_token) + .await?; + Ok(new_token) + } + + /// Get the token restriction by the ID. + async fn get_token_restriction<'a>( + &self, + state: &ServiceState, + id: &'a str, + expand_roles: bool, + ) -> Result, TokenProviderError> { + self.tr_backend_driver + .get_token_restriction(state, id, expand_roles) + .await + } + + /// Create new token restriction. + async fn create_token_restriction<'a>( + &self, + state: &ServiceState, + restriction: TokenRestrictionCreate, + ) -> Result { + let mut restriction = restriction; + if restriction.id.is_empty() { + restriction.id = Uuid::new_v4().simple().to_string(); + } + self.tr_backend_driver + .create_token_restriction(state, restriction) + .await + } + + /// List token restrictions. + async fn list_token_restrictions<'a>( + &self, + state: &ServiceState, + params: &TokenRestrictionListParameters, + ) -> Result, TokenProviderError> { + self.tr_backend_driver + .list_token_restrictions(state, params) + .await + } + + /// Update existing token restriction. + async fn update_token_restriction<'a>( + &self, + state: &ServiceState, + id: &'a str, + restriction: TokenRestrictionUpdate, + ) -> Result { + self.tr_backend_driver + .update_token_restriction(state, id, restriction) + .await + } + + /// Delete token restriction by the ID. + async fn delete_token_restriction<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result<(), TokenProviderError> { + self.tr_backend_driver + .delete_token_restriction(state, id) + .await + } +} + #[cfg(test)] mod tests { use chrono::Utc; + use eyre::{Result, eyre}; + use std::sync::Arc; + use tracing_test::traced_test; + use uuid::Uuid; + use super::super::tests::setup_config; use super::*; + use crate::application_credential::{ + MockApplicationCredentialProvider, types::ApplicationCredential, + }; + use crate::assignment::{ + MockAssignmentProvider, + types::{Assignment, AssignmentType, RoleAssignmentListParameters}, + }; use crate::auth::AuthenticatedInfoBuilder; use crate::config::Config; - use crate::resource::types::*; + use crate::identity::{MockIdentityProvider, types::UserResponseBuilder}; + use crate::provider::Provider; + use crate::resource::{MockResourceProvider, types::*}; + use crate::revoke::MockRevokeProvider; + use crate::tests::get_mocked_state; + use crate::token::backend::MockTokenRestrictionBackend; + use crate::trust::types::*; + + /// Generate test token to use for validation testing. + fn generate_token(validity: Option) -> Result { + Ok(Token::ProjectScope(ProjectScopePayload { + methods: vec!["password".into()], + user_id: Uuid::new_v4().simple().to_string(), + project_id: Uuid::new_v4().simple().to_string(), + audit_ids: vec!["Zm9vCg".into()], + expires_at: Utc::now() + .checked_add_signed(validity.unwrap_or_default()) + .ok_or(eyre!("timedelta apply failed"))?, + ..Default::default() + })) + } + + fn get_provider(config: &Config) -> TokenService { + TokenService { + config: config.clone(), + backend_driver: Arc::new(FernetTokenProvider::new(config.clone())), + tr_backend_driver: Arc::new(MockTokenRestrictionBackend::default()), + } + } + + #[tokio::test] + async fn test_populate_role_assignments() { + let token_provider = get_provider(&Config::default()); + let mut assignment_mock = MockAssignmentProvider::default(); + assignment_mock + .expect_list_role_assignments() + .withf(|_, q: &RoleAssignmentListParameters| { + q.project_id == Some("project_id".to_string()) + }) + .returning(|_, q: &RoleAssignmentListParameters| { + Ok(vec![Assignment { + role_id: "rid".into(), + role_name: Some("role_name".into()), + actor_id: q.user_id.clone().unwrap(), + target_id: q.project_id.clone().unwrap(), + r#type: AssignmentType::UserProject, + inherited: false, + implied_via: None, + }]) + }); + assignment_mock + .expect_list_role_assignments() + .withf(|_, q: &RoleAssignmentListParameters| { + q.domain_id == Some("domain_id".to_string()) + }) + .returning(|_, q: &RoleAssignmentListParameters| { + Ok(vec![Assignment { + role_id: "rid".into(), + role_name: Some("role_name".into()), + actor_id: q.user_id.clone().unwrap(), + target_id: q.domain_id.clone().unwrap(), + r#type: AssignmentType::UserProject, + inherited: false, + implied_via: None, + }]) + }); + let provider = Provider::mocked_builder().mock_assignment(assignment_mock); + + let state = get_mocked_state(None, Some(provider)); + + let mut ptoken = Token::ProjectScope(ProjectScopePayload { + user_id: "bar".into(), + project_id: "project_id".into(), + ..Default::default() + }); + token_provider + .populate_role_assignments(&state, &mut ptoken) + .await + .unwrap(); + + if let Token::ProjectScope(data) = ptoken { + assert_eq!( + data.roles.unwrap(), + vec![RoleRef { + id: "rid".into(), + name: Some("role_name".into()), + domain_id: None + }] + ); + } else { + panic!("Not project scope"); + } + + let mut dtoken = Token::DomainScope(DomainScopePayload { + user_id: "bar".into(), + domain_id: "domain_id".into(), + ..Default::default() + }); + token_provider + .populate_role_assignments(&state, &mut dtoken) + .await + .unwrap(); + + if let Token::DomainScope(data) = dtoken { + assert_eq!( + data.roles.unwrap(), + vec![RoleRef { + id: "rid".into(), + name: Some("role_name".into()), + domain_id: None + }] + ); + } else { + panic!("Not domain scope"); + } + + let mut utoken = Token::Unscoped(UnscopedPayload { + user_id: "bar".into(), + ..Default::default() + }); + assert!( + token_provider + .populate_role_assignments(&state, &mut utoken) + .await + .is_ok() + ); + } + + /// Test that a valid token with revocation events fails validation. + #[tokio::test] + #[traced_test] + async fn test_validate_token_revoked() { + let token = generate_token(Some(TimeDelta::hours(1))).unwrap(); + + let config = setup_config(); + let token_provider = get_provider(&config); + let mut revoke_mock = MockRevokeProvider::default(); + //let token_clone = token.clone(); + revoke_mock + .expect_is_token_revoked() + // TODO: in roundtrip the precision of expiry is reduced and issued_at is different + //.withf(move |_, t: &Token| { + // *t == token_clone + //}) + .returning(|_, _| Ok(true)); + + let mut identity_mock = MockIdentityProvider::default(); + let token_clone = token.clone(); + identity_mock + .expect_get_user() + .withf(move |_, id: &'_ str| id == token_clone.user_id()) + .returning(|_, id: &'_ str| { + Ok(Some( + UserResponseBuilder::default() + .domain_id("user_domain_id") + .enabled(true) + .name("name") + .id(id) + .build() + .unwrap(), + )) + }); + let mut resource_mock = MockResourceProvider::default(); + let token_clone2 = token.clone(); + resource_mock + .expect_get_project() + .withf(move |_, id: &'_ str| id == token_clone2.project_id().unwrap()) + .returning(|_, id: &'_ str| { + Ok(Some(Project { + id: id.to_string(), + name: "project".to_string(), + ..Default::default() + })) + }); + + let mut assignment_mock = MockAssignmentProvider::default(); + let token_clone3 = token.clone(); + assignment_mock + .expect_list_role_assignments() + .withf(move |_, q: &RoleAssignmentListParameters| { + q.project_id == token_clone3.project_id().cloned() + }) + .returning(|_, q: &RoleAssignmentListParameters| { + Ok(vec![Assignment { + role_id: "rid".into(), + role_name: Some("role_name".into()), + actor_id: q.user_id.clone().unwrap(), + target_id: q.project_id.clone().unwrap(), + r#type: AssignmentType::UserProject, + inherited: false, + implied_via: None, + }]) + }); + let provider = Provider::mocked_builder() + .mock_assignment(assignment_mock) + .mock_identity(identity_mock) + .mock_revoke(revoke_mock) + .mock_resource(resource_mock); + + let state = get_mocked_state(Some(config), Some(provider)); + + let credential = token_provider.encode_token(&token).unwrap(); + match token_provider + .validate_token(&state, &credential, Some(false), None) + .await + { + Err(TokenProviderError::TokenRevoked) => {} + _ => { + panic!("token must be revoked") + } + } + } + + #[tokio::test] + async fn test_populate_role_assignments_application_credential() { + let token_provider = get_provider(&Config::default()); + let mut assignment_mock = MockAssignmentProvider::default(); + assignment_mock + .expect_list_role_assignments() + .withf(|_, q: &RoleAssignmentListParameters| { + q.project_id == Some("project_id".to_string()) + && q.user_id == Some("bar".to_string()) + }) + .returning(|_, q: &RoleAssignmentListParameters| { + Ok(vec![Assignment { + role_id: "role_1".into(), + role_name: Some("role_name".into()), + actor_id: q.user_id.clone().unwrap(), + target_id: q.project_id.clone().unwrap(), + r#type: AssignmentType::UserProject, + inherited: false, + implied_via: None, + }]) + }); + assignment_mock + .expect_list_role_assignments() + .withf(|_, q: &RoleAssignmentListParameters| { + q.domain_id == Some("domain_id".to_string()) + }) + .returning(|_, q: &RoleAssignmentListParameters| { + Ok(vec![Assignment { + role_id: "rid".into(), + role_name: Some("role_name".into()), + actor_id: q.user_id.clone().unwrap(), + target_id: q.domain_id.clone().unwrap(), + r#type: AssignmentType::UserProject, + inherited: false, + implied_via: None, + }]) + }); + let mut ac_mock = MockApplicationCredentialProvider::default(); + ac_mock + .expect_get_application_credential() + .withf(|_, id: &'_ str| id == "app_cred_id") + .returning(|_, id: &'_ str| { + Ok(Some(ApplicationCredential { + access_rules: None, + description: None, + expires_at: None, + id: id.into(), + name: "foo".into(), + project_id: "project_id".into(), + roles: vec![ + RoleRef { + id: "role_1".into(), + name: Some("role_name_1".into()), + domain_id: None, + }, + RoleRef { + id: "role_2".into(), + name: Some("role_name_2".into()), + domain_id: None, + }, + ], + unrestricted: false, + user_id: "bar".into(), + })) + }); + ac_mock + .expect_get_application_credential() + .withf(|_, id: &'_ str| id == "app_cred_bad_roles") + .returning(|_, id: &'_ str| { + Ok(Some(ApplicationCredential { + access_rules: None, + description: None, + expires_at: None, + id: id.into(), + name: "foo".into(), + project_id: "project_id".into(), + roles: vec![ + RoleRef { + id: "-role_1".into(), + name: Some("-role_name_1".into()), + domain_id: None, + }, + RoleRef { + id: "-role_2".into(), + name: Some("-role_name_2".into()), + domain_id: None, + }, + ], + unrestricted: false, + user_id: "bar".into(), + })) + }); + ac_mock + .expect_get_application_credential() + .withf(|_, id: &'_ str| id == "missing") + .returning(|_, _| Ok(None)); + let provider = Provider::mocked_builder() + .mock_application_credential(ac_mock) + .mock_assignment(assignment_mock); + + let state = get_mocked_state(None, Some(provider)); + + let mut token = Token::ApplicationCredential(ApplicationCredentialPayload { + user_id: "bar".into(), + project_id: "project_id".into(), + application_credential_id: "app_cred_id".into(), + ..Default::default() + }); + token_provider + .populate_role_assignments(&state, &mut token) + .await + .unwrap(); + + if let Token::ApplicationCredential(..) = &token { + assert_eq!( + token.effective_roles().unwrap(), + &vec![RoleRef { + id: "role_1".into(), + name: Some("role_name_1".into()), + domain_id: None, + }], + "only still active role assignment is returned" + ); + } else { + panic!("Not application credential scope"); + } + + // Try populating role assignments for not existing appcred + if let Err(TokenProviderError::ApplicationCredentialNotFound(id)) = token_provider + .populate_role_assignments( + &state, + &mut Token::ApplicationCredential(ApplicationCredentialPayload { + user_id: "bar".into(), + project_id: "project_id".into(), + application_credential_id: "missing".into(), + ..Default::default() + }), + ) + .await + { + assert_eq!(id, "missing"); + } else { + panic!("role expansion for missing application credential should fail"); + } + + // No roles remain after subtracting current user roles + if let Err(TokenProviderError::ActorHasNoRolesOnTarget) = token_provider + .populate_role_assignments( + &state, + &mut Token::ApplicationCredential(ApplicationCredentialPayload { + user_id: "bar".into(), + project_id: "project_id".into(), + application_credential_id: "app_cred_bad_roles".into(), + ..Default::default() + }), + ) + .await + { + } else { + panic!( + "role expansion for application credential with roles the user does not have anymore should fail" + ); + } + } #[tokio::test] async fn test_create_unscoped_token() { - let token_provider = TokenProvider::new(&Config::default()).unwrap(); + let token_provider = get_provider(&Config::default()); let now = Utc::now(); let token = token_provider .create_unscoped_token( @@ -791,7 +1428,7 @@ mod tests { #[tokio::test] async fn test_create_project_scope_token() { - let token_provider = TokenProvider::new(&Config::default()).unwrap(); + let token_provider = get_provider(&Config::default()); let now = Utc::now(); let token = token_provider .create_project_scope_token( @@ -833,7 +1470,7 @@ mod tests { #[tokio::test] async fn test_create_domain_scope_token() { - let token_provider = TokenProvider::new(&Config::default()).unwrap(); + let token_provider = get_provider(&Config::default()); let now = Utc::now(); let token = token_provider .create_domain_scope_token( @@ -873,7 +1510,7 @@ mod tests { #[tokio::test] async fn test_create_system_token() { - let token_provider = TokenProvider::new(&Config::default()).unwrap(); + let token_provider = get_provider(&Config::default()); let now = Utc::now(); let token = token_provider .create_system_scoped_token( @@ -905,7 +1542,7 @@ mod tests { #[tokio::test] async fn test_create_trust_token() { - let token_provider = TokenProvider::new(&Config::default()).unwrap(); + let token_provider = get_provider(&Config::default()); let now = Utc::now(); let token = token_provider .create_trust_token( @@ -975,7 +1612,7 @@ mod tests { #[tokio::test] async fn test_create_restricted_token() { - let token_provider = TokenProvider::new(&Config::default()).unwrap(); + let token_provider = get_provider(&Config::default()); let now = Utc::now(); let token = token_provider .create_restricted_token( diff --git a/crates/keystone/src/token/types.rs b/crates/core/src/token/types.rs similarity index 100% rename from crates/keystone/src/token/types.rs rename to crates/core/src/token/types.rs diff --git a/crates/keystone/src/token/types/application_credential.rs b/crates/core/src/token/types/application_credential.rs similarity index 100% rename from crates/keystone/src/token/types/application_credential.rs rename to crates/core/src/token/types/application_credential.rs diff --git a/crates/keystone/src/token/types/common.rs b/crates/core/src/token/types/common.rs similarity index 100% rename from crates/keystone/src/token/types/common.rs rename to crates/core/src/token/types/common.rs diff --git a/crates/keystone/src/token/types/domain_scoped.rs b/crates/core/src/token/types/domain_scoped.rs similarity index 100% rename from crates/keystone/src/token/types/domain_scoped.rs rename to crates/core/src/token/types/domain_scoped.rs diff --git a/crates/keystone/src/token/types/federation_domain_scoped.rs b/crates/core/src/token/types/federation_domain_scoped.rs similarity index 100% rename from crates/keystone/src/token/types/federation_domain_scoped.rs rename to crates/core/src/token/types/federation_domain_scoped.rs diff --git a/crates/keystone/src/token/types/federation_project_scoped.rs b/crates/core/src/token/types/federation_project_scoped.rs similarity index 100% rename from crates/keystone/src/token/types/federation_project_scoped.rs rename to crates/core/src/token/types/federation_project_scoped.rs diff --git a/crates/keystone/src/token/types/federation_unscoped.rs b/crates/core/src/token/types/federation_unscoped.rs similarity index 100% rename from crates/keystone/src/token/types/federation_unscoped.rs rename to crates/core/src/token/types/federation_unscoped.rs diff --git a/crates/keystone/src/token/types/project_scoped.rs b/crates/core/src/token/types/project_scoped.rs similarity index 100% rename from crates/keystone/src/token/types/project_scoped.rs rename to crates/core/src/token/types/project_scoped.rs diff --git a/crates/keystone/src/token/types/provider_api.rs b/crates/core/src/token/types/provider_api.rs similarity index 100% rename from crates/keystone/src/token/types/provider_api.rs rename to crates/core/src/token/types/provider_api.rs diff --git a/crates/keystone/src/token/types/restricted.rs b/crates/core/src/token/types/restricted.rs similarity index 100% rename from crates/keystone/src/token/types/restricted.rs rename to crates/core/src/token/types/restricted.rs diff --git a/crates/keystone/src/token/types/system_scoped.rs b/crates/core/src/token/types/system_scoped.rs similarity index 100% rename from crates/keystone/src/token/types/system_scoped.rs rename to crates/core/src/token/types/system_scoped.rs diff --git a/crates/keystone/src/token/types/trust.rs b/crates/core/src/token/types/trust.rs similarity index 100% rename from crates/keystone/src/token/types/trust.rs rename to crates/core/src/token/types/trust.rs diff --git a/crates/keystone/src/token/types/unscoped.rs b/crates/core/src/token/types/unscoped.rs similarity index 100% rename from crates/keystone/src/token/types/unscoped.rs rename to crates/core/src/token/types/unscoped.rs diff --git a/crates/core/src/trust/api.rs b/crates/core/src/trust/api.rs new file mode 100644 index 00000000..aaa7e745 --- /dev/null +++ b/crates/core/src/trust/api.rs @@ -0,0 +1,17 @@ +// 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 +//! # Trust API + +pub mod error; +pub mod types; diff --git a/crates/keystone/src/trust/api/error.rs b/crates/core/src/trust/api/error.rs similarity index 100% rename from crates/keystone/src/trust/api/error.rs rename to crates/core/src/trust/api/error.rs diff --git a/crates/core/src/trust/api/types.rs b/crates/core/src/trust/api/types.rs new file mode 100644 index 00000000..dc5e78f3 --- /dev/null +++ b/crates/core/src/trust/api/types.rs @@ -0,0 +1,16 @@ +// 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 +//! # Trust API + +pub mod trust; diff --git a/crates/core/src/trust/api/types/trust.rs b/crates/core/src/trust/api/types/trust.rs new file mode 100644 index 00000000..3f6f3af2 --- /dev/null +++ b/crates/core/src/trust/api/types/trust.rs @@ -0,0 +1,35 @@ +// 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 +use crate::trust::types::Trust; + +pub use openstack_keystone_api_types::trust as api_trust; + +impl From<&Trust> for api_trust::TokenTrustRepr { + fn from(value: &Trust) -> Self { + Self { + expires_at: value.expires_at, + id: value.id.clone(), + impersonation: value.impersonation, + remaining_uses: value.remaining_uses, + redelegated_trust_id: value.redelegated_trust_id.clone(), + redelegation_count: value.redelegation_count, + trustor_user: api_trust::TokenTrustUser { + id: value.trustor_user_id.clone(), + }, + trustee_user: api_trust::TokenTrustUser { + id: value.trustee_user_id.clone(), + }, + } + } +} diff --git a/crates/core/src/trust/backend.rs b/crates/core/src/trust/backend.rs new file mode 100644 index 00000000..c51a27a0 --- /dev/null +++ b/crates/core/src/trust/backend.rs @@ -0,0 +1,46 @@ +// 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 +//! Trust provider Backend trait. +use async_trait::async_trait; + +use crate::keystone::ServiceState; +use crate::trust::{TrustProviderError, types::*}; + +/// TrustBackend trait. +/// +/// Backend driver interface expected by the trust provider. +#[cfg_attr(test, mockall::automock)] +#[async_trait] +pub trait TrustBackend: Send + Sync { + /// Get trust by ID. + async fn get_trust<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result, TrustProviderError>; + + /// Resolve trust chain by the trust ID. + async fn get_trust_delegation_chain<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result>, TrustProviderError>; + + /// List trusts. + async fn list_trusts( + &self, + state: &ServiceState, + params: &TrustListParameters, + ) -> Result, TrustProviderError>; +} diff --git a/crates/keystone/src/trust/error.rs b/crates/core/src/trust/error.rs similarity index 97% rename from crates/keystone/src/trust/error.rs rename to crates/core/src/trust/error.rs index d8b95cad..a038238b 100644 --- a/crates/keystone/src/trust/error.rs +++ b/crates/core/src/trust/error.rs @@ -90,6 +90,6 @@ pub enum TrustProviderError { }, /// Unsupported driver. - #[error("unsupported driver {0}")] + #[error("unsupported driver `{0}` for the trust provider")] UnsupportedDriver(String), } diff --git a/crates/keystone/src/trust/mock.rs b/crates/core/src/trust/mock.rs similarity index 87% rename from crates/keystone/src/trust/mock.rs rename to crates/core/src/trust/mock.rs index acbd648f..40e6a9c8 100644 --- a/crates/keystone/src/trust/mock.rs +++ b/crates/core/src/trust/mock.rs @@ -13,20 +13,14 @@ // SPDX-License-Identifier: Apache-2.0 //! Trust - internal mocking tools. use async_trait::async_trait; -#[cfg(test)] use mockall::mock; -use crate::config::Config; -use crate::plugin_manager::PluginManager; use crate::trust::{TrustApi, TrustProviderError, types::*}; use crate::keystone::ServiceState; -#[cfg(test)] mock! { - pub TrustProvider { - pub fn new(cfg: &Config, plugin_manager: &PluginManager) -> Result; - } + pub TrustProvider {} #[async_trait] impl TrustApi for TrustProvider { diff --git a/crates/core/src/trust/mod.rs b/crates/core/src/trust/mod.rs new file mode 100644 index 00000000..c8191e2d --- /dev/null +++ b/crates/core/src/trust/mod.rs @@ -0,0 +1,149 @@ +// 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 +//! # Trust provider. +//! +//! Trusts. +//! +//! A trust represents a user's (the trustor) authorization to delegate roles to +//! another user (the trustee), and optionally allow the trustee to impersonate +//! the trustor. After the trustor has created a trust, the trustee can specify +//! the trust's id attribute as part of an authentication request to then create +//! a token representing the delegated authority of the trustor. +//! +//! The trust contains constraints on the delegated attributes. A token created +//! based on a trust will convey a subset of the trustor's roles on the +//! specified project. Optionally, the trust may only be valid for a specified +//! time period, as defined by expires_at. If no expires_at is specified, then +//! the trust is valid until it is explicitly revoked. +//! +//! The impersonation flag allows the trustor to optionally delegate +//! impersonation abilities to the trustee. To services validating the token, +//! the trustee will appear as the trustor, although the token will also contain +//! the impersonation flag to indicate that this behavior is in effect. +//! +//! A project_id may not be specified without at least one role, and vice versa. +//! In other words, there is no way of implicitly delegating all roles to a +//! trustee, in order to prevent users accidentally creating trust that are much +//! more broad in scope than intended. A trust without a project_id or any +//! delegated roles is unscoped, and therefore does not represent authorization +//! on a specific resource. +//! +//! Trusts are immutable. If the trustee or trustor wishes to modify the +//! attributes of the trust, they should create a new trust and delete the old +//! trust. If a trust is deleted, any tokens generated based on the trust are +//! immediately revoked. +//! +//! If the trustor loses access to any delegated attributes, the trust becomes +//! immediately invalid and any tokens generated based on the trust are +//! immediately revoked. +//! +//! Trusts can also be chained, meaning, a trust can be created by using a trust +//! scoped token. + +use async_trait::async_trait; + +pub mod api; +pub mod backend; +pub mod error; +#[cfg(any(test, feature = "mock"))] +mod mock; +pub mod service; +pub mod types; + +use crate::config::Config; +use crate::keystone::ServiceState; +use crate::plugin_manager::PluginManagerApi; +use crate::trust::service::TrustService; + +pub use error::TrustProviderError; +#[cfg(any(test, feature = "mock"))] +pub use mock::MockTrustProvider; +pub use types::*; + +/// Trust provider. +pub enum TrustProvider { + Service(TrustService), + #[cfg(any(test, feature = "mock"))] + Mock(MockTrustProvider), +} + +impl TrustProvider { + pub fn new( + config: &Config, + plugin_manager: &P, + ) -> Result { + Ok(Self::Service(TrustService::new(config, plugin_manager)?)) + } +} + +#[async_trait] +impl TrustApi for TrustProvider { + /// Get trust by ID. + async fn get_trust<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result, TrustProviderError> { + match self { + Self::Service(provider) => provider.get_trust(state, id).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.get_trust(state, id).await, + } + } + + /// Resolve trust delegation chain by the trust ID. + async fn get_trust_delegation_chain<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result>, TrustProviderError> { + match self { + Self::Service(provider) => provider.get_trust_delegation_chain(state, id).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.get_trust_delegation_chain(state, id).await, + } + } + + /// List trusts. + async fn list_trusts( + &self, + state: &ServiceState, + params: &TrustListParameters, + ) -> Result, TrustProviderError> { + match self { + Self::Service(provider) => provider.list_trusts(state, params).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.list_trusts(state, params).await, + } + } + + /// Validate trust delegation chain. + /// + /// - redelegation deepness cannot exceed the global limit. + /// - redelegated trusts must not specify use limit. + /// - validate redelegated trust expiration is not later than of the + /// original. + /// - redelegated trust must not add new roles. + async fn validate_trust_delegation_chain( + &self, + state: &ServiceState, + trust: &Trust, + ) -> Result { + match self { + Self::Service(provider) => provider.validate_trust_delegation_chain(state, trust).await, + #[cfg(any(test, feature = "mock"))] + Self::Mock(provider) => provider.validate_trust_delegation_chain(state, trust).await, + } + } +} diff --git a/crates/core/src/trust/service.rs b/crates/core/src/trust/service.rs new file mode 100644 index 00000000..36a186e0 --- /dev/null +++ b/crates/core/src/trust/service.rs @@ -0,0 +1,804 @@ +// 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 +//! # Trust provider. + +use std::collections::{HashMap, HashSet}; +use std::hash::RandomState; +use std::sync::Arc; + +use async_trait::async_trait; +use chrono::{DateTime, Utc}; +use tracing::debug; + +use crate::config::Config; +use crate::keystone::ServiceState; +use crate::plugin_manager::PluginManagerApi; +use crate::role::{RoleApi, types::Role}; +use crate::trust::{TrustProviderError, backend::TrustBackend, types::*}; + +/// Trust provider. +pub struct TrustService { + /// Backend driver. + backend_driver: Arc, +} + +impl TrustService { + pub fn new( + config: &Config, + plugin_manager: &P, + ) -> Result { + let backend_driver = plugin_manager + .get_trust_backend(config.trust.driver.clone())? + .clone(); + Ok(Self { backend_driver }) + } +} + +#[async_trait] +impl TrustApi for TrustService { + /// Get trust by ID. + #[tracing::instrument(level = "debug", skip(self, state))] + async fn get_trust<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result, TrustProviderError> { + if let Some(mut trust) = self.backend_driver.get_trust(state, id).await? { + let all_roles: HashMap = HashMap::from_iter( + state + .provider + .get_role_provider() + .list_roles( + state, + &crate::role::types::RoleListParameters { + domain_id: Some(None), + ..Default::default() + }, + ) + .await? + .iter() + .map(|role| (role.id.clone(), role.to_owned())), + ); + if let Some(ref mut roles) = trust.roles { + for role in roles.iter_mut() { + if let Some(erole) = all_roles.get(&role.id) { + role.domain_id = erole.domain_id.clone(); + role.name = Some(erole.name.clone()); + } + } + // Drop all roles for which name is not set (it is a signal that the processing + // above has not found the role matching the parameters. + roles.retain_mut(|role| role.name.is_some()); + } + return Ok(Some(trust)); + } + Ok(None) + } + + /// Resolve trust delegation chain by the trust ID. + #[tracing::instrument(level = "debug", skip(self, state))] + async fn get_trust_delegation_chain<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result>, TrustProviderError> { + self.backend_driver + .get_trust_delegation_chain(state, id) + .await + } + + /// List trusts. + #[tracing::instrument(level = "debug", skip(self, state))] + async fn list_trusts( + &self, + state: &ServiceState, + params: &TrustListParameters, + ) -> Result, TrustProviderError> { + let mut trusts = self.backend_driver.list_trusts(state, params).await?; + + let all_roles: HashMap = HashMap::from_iter( + state + .provider + .get_role_provider() + .list_roles( + state, + &crate::role::types::RoleListParameters { + domain_id: Some(None), + ..Default::default() + }, + ) + .await? + .iter() + .map(|role| (role.id.clone(), role.to_owned())), + ); + for trust in trusts.iter_mut() { + if let Some(ref mut roles) = trust.roles { + for role in roles.iter_mut() { + if let Some(erole) = all_roles.get(&role.id) { + role.domain_id = erole.domain_id.clone(); + role.name = Some(erole.name.clone()); + } + } + // Drop all roles for which name is not set (it is a signal that the processing + // above has not found the role matching the parameters. + roles.retain_mut(|role| role.name.is_some()); + } + } + + Ok(trusts) + } + + /// Validate trust delegation chain. + /// + /// - redelegation deepness cannot exceed the global limit. + /// - redelegated trusts must not specify use limit. + /// - validate redelegated trust expiration is not later than of the + /// original. + /// - redelegated trust must not add new roles. + #[tracing::instrument(level = "debug", skip(self, state))] + async fn validate_trust_delegation_chain( + &self, + state: &ServiceState, + trust: &Trust, + ) -> Result { + if trust.redelegated_trust_id.is_some() + && let Some(chain) = self.get_trust_delegation_chain(state, &trust.id).await? + { + if chain.len() > state.config.trust.max_redelegation_count { + return Err(TrustProviderError::RedelegationDeepnessExceed { + length: chain.len(), + max_depth: state.config.trust.max_redelegation_count, + }); + } + let mut parent_trust: Option = None; + let mut parent_expiration: Option> = None; + for delegation in chain.iter().rev() { + // None of the trusts can specify the redelegation_count > delegation_count of + // the top level trust + if let Some(current_redelegation_count) = delegation.redelegation_count + && current_redelegation_count > state.config.trust.max_redelegation_count as u32 + { + return Err(TrustProviderError::RedelegationDeepnessExceed { + length: current_redelegation_count as usize, + max_depth: state.config.trust.max_redelegation_count, + }); + } + if delegation.remaining_uses.is_some() { + return Err(TrustProviderError::RemainingUsesMustBeUnset); + } + // Check that the parent trust is not expiring earlier than the redelegated + if let Some(trust_expiry) = delegation.expires_at { + if let Some(parent_expiry) = parent_trust + .as_ref() + .and_then(|x| x.expires_at) + .or(parent_expiration) + { + if trust_expiry > parent_expiry { + return Err(TrustProviderError::ExpirationImpossible); + } + // reset the parent_expiration to the one of the current delegation. + parent_expiration = Some(trust_expiry); + } + // Ensure we set the parent_expiration with the first met value. + if parent_expiration.is_none() { + parent_expiration = Some(trust_expiry); + } + } + // Check that the redelegation is not adding new roles + if let Some(parent_trust) = &parent_trust + && !HashSet::::from_iter( + delegation + .roles + .as_deref() + .unwrap_or_default() + .iter() + .map(|role| role.id.clone()), + ) + .is_subset(&HashSet::from_iter( + parent_trust + .roles + .as_deref() + .unwrap_or_default() + .iter() + .map(|role| role.id.clone()), + )) + { + debug!( + "Trust roles {:?} are missing for the trustor {:?}", + trust.roles, parent_trust.roles, + ); + return Err(TrustProviderError::RedelegatedRolesNotAvailable); + } + // Check the impersonation + if delegation.impersonation && !parent_trust.is_some_and(|x| x.impersonation) { + return Err(TrustProviderError::RedelegatedImpersonationNotAllowed); + } + parent_trust = Some(delegation.clone()); + } + } + + Ok(true) + } +} + +#[cfg(test)] +mod tests { + use chrono::{DateTime, Utc}; + use std::sync::Arc; + + use super::*; + use crate::provider::Provider; + use crate::role::{MockRoleProvider, types::*}; + use crate::tests::get_mocked_state; + use crate::trust::backend::MockTrustBackend; + + #[tokio::test] + async fn test_get_trust() { + let mut role_mock = MockRoleProvider::default(); + role_mock + .expect_list_roles() + .withf(|_, qp: &RoleListParameters| { + RoleListParameters { + domain_id: Some(None), + ..Default::default() + } == *qp + }) + .returning(|_, _| Ok(Vec::new())); + let provider_builder = Provider::mocked_builder().mock_role(role_mock); + let state = get_mocked_state(None, Some(provider_builder)); + + let mut backend = MockTrustBackend::new(); + backend + .expect_get_trust() + .withf(|_, id: &'_ str| id == "fake_trust") + .returning(|_, _| { + Ok(Some(Trust { + id: "fake_trust".into(), + ..Default::default() + })) + }); + + let trust_provider = TrustService { + backend_driver: Arc::new(backend), + }; + + let trust: Trust = trust_provider + .get_trust(&state, "fake_trust") + .await + .unwrap() + .expect("trust found"); + assert_eq!(trust.id, "fake_trust"); + } + + #[tokio::test] + async fn test_get_trust_delegation_chain() { + let mut role_mock = MockRoleProvider::default(); + role_mock + .expect_list_roles() + .withf(|_, qp: &RoleListParameters| { + RoleListParameters { + domain_id: Some(None), + ..Default::default() + } == *qp + }) + .returning(|_, _| Ok(Vec::new())); + let provider_builder = Provider::mocked_builder().mock_role(role_mock); + let state = get_mocked_state(None, Some(provider_builder)); + + let mut backend = MockTrustBackend::new(); + backend + .expect_get_trust_delegation_chain() + .withf(|_, id: &'_ str| id == "fake_trust") + .returning(|_, _| { + Ok(Some(vec![ + Trust { + id: "redelegated_trust".into(), + redelegated_trust_id: Some("trust_id".into()), + ..Default::default() + }, + Trust { + id: "trust_id".into(), + ..Default::default() + }, + ])) + }); + + let trust_provider = TrustService { + backend_driver: Arc::new(backend), + }; + + let chain = trust_provider + .get_trust_delegation_chain(&state, "fake_trust") + .await + .unwrap() + .expect("chain fetched"); + assert_eq!(chain.len(), 2); + } + + #[tokio::test] + async fn test_validate_trust_delegation_chain_not_redelegated() { + let mut role_mock = MockRoleProvider::default(); + role_mock + .expect_list_roles() + .withf(|_, qp: &RoleListParameters| { + RoleListParameters { + domain_id: Some(None), + ..Default::default() + } == *qp + }) + .returning(|_, _| Ok(Vec::new())); + let provider_builder = Provider::mocked_builder().mock_role(role_mock); + let state = get_mocked_state(None, Some(provider_builder)); + + let mut backend = MockTrustBackend::new(); + backend + .expect_get_trust() + .withf(|_, id: &'_ str| id == "fake_trust") + .returning(|_, _| { + Ok(Some(Trust { + id: "fake_trust".into(), + ..Default::default() + })) + }); + + let trust_provider = TrustService { + backend_driver: Arc::new(backend), + }; + let trust = trust_provider + .get_trust(&state, "fake_trust") + .await + .unwrap() + .expect("trust found"); + trust_provider + .validate_trust_delegation_chain(&state, &trust) + .await + .unwrap(); + } + + #[tokio::test] + async fn test_validate_trust_delegation_chain() { + let mut role_mock = MockRoleProvider::default(); + role_mock + .expect_list_roles() + .withf(|_, qp: &RoleListParameters| { + RoleListParameters { + domain_id: Some(None), + ..Default::default() + } == *qp + }) + .returning(|_, _| Ok(Vec::new())); + let provider_builder = Provider::mocked_builder().mock_role(role_mock); + let state = get_mocked_state(None, Some(provider_builder)); + let mut backend = MockTrustBackend::new(); + backend + .expect_get_trust() + .withf(|_, id: &'_ str| id == "redelegated_trust") + .returning(|_, _| { + Ok(Some(Trust { + id: "redelegated_trust".into(), + redelegated_trust_id: Some("trust_id".into()), + ..Default::default() + })) + }); + backend + .expect_get_trust_delegation_chain() + .withf(|_, id: &'_ str| id == "redelegated_trust") + .returning(|_, _| { + Ok(Some(vec![ + Trust { + id: "redelegated_trust".into(), + redelegated_trust_id: Some("trust_id".into()), + ..Default::default() + }, + Trust { + id: "trust_id".into(), + ..Default::default() + }, + ])) + }); + + let trust_provider = TrustService { + backend_driver: Arc::new(backend), + }; + let trust = trust_provider + .get_trust(&state, "redelegated_trust") + .await + .unwrap() + .expect("trust found"); + trust_provider + .validate_trust_delegation_chain(&state, &trust) + .await + .unwrap(); + } + + #[tokio::test] + async fn test_validate_trust_delegation_chain_expiration() { + let mut role_mock = MockRoleProvider::default(); + role_mock + .expect_list_roles() + .withf(|_, qp: &RoleListParameters| { + RoleListParameters { + domain_id: Some(None), + ..Default::default() + } == *qp + }) + .returning(|_, _| Ok(Vec::new())); + let provider_builder = Provider::mocked_builder().mock_role(role_mock); + let state = get_mocked_state(None, Some(provider_builder)); + let mut backend = MockTrustBackend::new(); + backend + .expect_get_trust() + .withf(|_, id: &'_ str| id == "redelegated_trust2") + .returning(|_, _| { + Ok(Some(Trust { + id: "redelegated_trust2".into(), + redelegated_trust_id: Some("redelegated_trust1".into()), + ..Default::default() + })) + }); + backend + .expect_get_trust_delegation_chain() + .withf(|_, id: &'_ str| id == "redelegated_trust2") + .returning(|_, _| { + Ok(Some(vec![ + Trust { + id: "redelegated_trust2".into(), + redelegated_trust_id: Some("redelegated_trust1".into()), + expires_at: Some(DateTime::::MAX_UTC), + ..Default::default() + }, + Trust { + id: "redelegated_trust1".into(), + redelegated_trust_id: Some("trust_id".into()), + ..Default::default() + }, + Trust { + id: "trust_id".into(), + expires_at: Some(Utc::now()), + ..Default::default() + }, + ])) + }); + + let trust_provider = TrustService { + backend_driver: Arc::new(backend), + }; + let trust = trust_provider + .get_trust(&state, "redelegated_trust2") + .await + .unwrap() + .expect("trust found"); + if let Err(TrustProviderError::ExpirationImpossible) = trust_provider + .validate_trust_delegation_chain(&state, &trust) + .await + { + } else { + panic!("redelegated trust cannot expire later than the parent"); + }; + } + + #[tokio::test] + async fn test_validate_trust_delegation_chain_no_new_roles() { + let mut role_mock = MockRoleProvider::default(); + role_mock + .expect_list_roles() + .withf(|_, qp: &RoleListParameters| { + RoleListParameters { + domain_id: Some(None), + ..Default::default() + } == *qp + }) + .returning(|_, _| Ok(Vec::new())); + let provider_builder = Provider::mocked_builder().mock_role(role_mock); + let state = get_mocked_state(None, Some(provider_builder)); + let mut backend = MockTrustBackend::new(); + backend + .expect_get_trust() + .withf(|_, id: &'_ str| id == "redelegated_trust") + .returning(|_, _| { + Ok(Some(Trust { + id: "redelegated_trust".into(), + redelegated_trust_id: Some("trust_id".into()), + ..Default::default() + })) + }); + backend + .expect_get_trust_delegation_chain() + .withf(|_, id: &'_ str| id == "redelegated_trust") + .returning(|_, _| { + Ok(Some(vec![ + Trust { + id: "redelegated_trust".into(), + redelegated_trust_id: Some("trust_id".into()), + roles: Some(vec![ + RoleRef { + id: "rid1".into(), + name: None, + domain_id: None, + }, + RoleRef { + id: "rid2".into(), + name: None, + domain_id: None, + }, + ]), + ..Default::default() + }, + Trust { + id: "trust_id".into(), + roles: Some(vec![RoleRef { + id: "rid1".into(), + name: None, + domain_id: None, + }]), + ..Default::default() + }, + ])) + }); + + let trust_provider = TrustService { + backend_driver: Arc::new(backend), + }; + let trust = trust_provider + .get_trust(&state, "redelegated_trust") + .await + .unwrap() + .expect("trust found"); + + if let Err(TrustProviderError::RedelegatedRolesNotAvailable) = trust_provider + .validate_trust_delegation_chain(&state, &trust) + .await + { + } else { + panic!("adding new roles on redelegation should be disallowed"); + }; + } + + #[tokio::test] + async fn test_validate_trust_delegation_chain_impersonation() { + let mut role_mock = MockRoleProvider::default(); + role_mock + .expect_list_roles() + .withf(|_, qp: &RoleListParameters| { + RoleListParameters { + domain_id: Some(None), + ..Default::default() + } == *qp + }) + .returning(|_, _| Ok(Vec::new())); + let provider_builder = Provider::mocked_builder().mock_role(role_mock); + let state = get_mocked_state(None, Some(provider_builder)); + let mut backend = MockTrustBackend::new(); + backend + .expect_get_trust() + .withf(|_, id: &'_ str| id == "redelegated_trust2") + .returning(|_, _| { + Ok(Some(Trust { + id: "redelegated_trust2".into(), + redelegated_trust_id: Some("redelegated_trust1".into()), + ..Default::default() + })) + }); + backend + .expect_get_trust_delegation_chain() + .withf(|_, id: &'_ str| id == "redelegated_trust2") + .returning(|_, _| { + Ok(Some(vec![ + Trust { + id: "redelegated_trust2".into(), + redelegated_trust_id: Some("redelegated_trust1".into()), + ..Default::default() + }, + Trust { + id: "redelegated_trust1".into(), + redelegated_trust_id: Some("trust_id".into()), + impersonation: true, + ..Default::default() + }, + Trust { + id: "trust_id".into(), + impersonation: false, + ..Default::default() + }, + ])) + }); + + let trust_provider = TrustService { + backend_driver: Arc::new(backend), + }; + let trust = trust_provider + .get_trust(&state, "redelegated_trust2") + .await + .unwrap() + .expect("trust found"); + match trust_provider + .validate_trust_delegation_chain(&state, &trust) + .await + { + Err(TrustProviderError::RedelegatedImpersonationNotAllowed) => {} + other => { + panic!( + "redelegated trust impersonation cannot be enabled, {:?}", + other + ); + } + } + } + + #[tokio::test] + async fn test_validate_trust_delegation_chain_deepness() { + let mut role_mock = MockRoleProvider::default(); + role_mock + .expect_list_roles() + .withf(|_, qp: &RoleListParameters| { + RoleListParameters { + domain_id: Some(None), + ..Default::default() + } == *qp + }) + .returning(|_, _| Ok(Vec::new())); + let provider_builder = Provider::mocked_builder().mock_role(role_mock); + let state = get_mocked_state(None, Some(provider_builder)); + let mut backend = MockTrustBackend::new(); + backend + .expect_get_trust() + .withf(|_, id: &'_ str| id == "redelegated_trust2") + .returning(|_, _| { + Ok(Some(Trust { + id: "redelegated_trust2".into(), + redelegated_trust_id: Some("redelegated_trust1".into()), + ..Default::default() + })) + }); + backend + .expect_get_trust() + .withf(|_, id: &'_ str| id == "redelegated_trust_long") + .returning(|_, _| { + Ok(Some(Trust { + id: "redelegated_trust_long".into(), + redelegated_trust_id: Some("redelegated_trust2".into()), + ..Default::default() + })) + }); + backend + .expect_get_trust_delegation_chain() + .withf(|_, id: &'_ str| id == "redelegated_trust2") + .returning(|_, _| { + Ok(Some(vec![ + Trust { + id: "redelegated_trust2".into(), + redelegated_trust_id: Some("redelegated_trust1".into()), + redelegation_count: Some(4), + ..Default::default() + }, + Trust { + id: "redelegated_trust1".into(), + redelegated_trust_id: Some("trust_id".into()), + ..Default::default() + }, + Trust { + id: "trust_id".into(), + ..Default::default() + }, + ])) + }); + + backend + .expect_get_trust_delegation_chain() + .withf(|_, id: &'_ str| id == "redelegated_trust_long") + .returning(|_, _| { + Ok(Some(vec![ + Trust { + id: "redelegated_trust_long".into(), + redelegated_trust_id: Some("redelegated_trust2".into()), + ..Default::default() + }, + Trust { + id: "redelegated_trust2".into(), + redelegated_trust_id: Some("redelegated_trust1".into()), + ..Default::default() + }, + Trust { + id: "redelegated_trust1".into(), + redelegated_trust_id: Some("trust_id".into()), + ..Default::default() + }, + Trust { + id: "trust_id".into(), + ..Default::default() + }, + ])) + }); + + let trust_provider = TrustService { + backend_driver: Arc::new(backend), + }; + let trust = trust_provider + .get_trust(&state, "redelegated_trust2") + .await + .unwrap() + .expect("trust found"); + match trust_provider + .validate_trust_delegation_chain(&state, &trust) + .await + { + Err(TrustProviderError::RedelegationDeepnessExceed { .. }) => {} + other => { + panic!( + "redelegated trust redelegation_count exceeds limit, but {:?}", + other + ); + } + } + + let trust = trust_provider + .get_trust(&state, "redelegated_trust_long") + .await + .unwrap() + .expect("trust found"); + match trust_provider + .validate_trust_delegation_chain(&state, &trust) + .await + { + Err(TrustProviderError::RedelegationDeepnessExceed { .. }) => {} + other => { + panic!("trust redelegation chain exceeds limit, but {:?}", other); + } + } + } + + #[tokio::test] + async fn test_list_trusts() { + let mut role_mock = MockRoleProvider::default(); + role_mock + .expect_list_roles() + .withf(|_, qp: &RoleListParameters| { + RoleListParameters { + domain_id: Some(None), + ..Default::default() + } == *qp + }) + .returning(|_, _| Ok(Vec::new())); + let provider_builder = Provider::mocked_builder().mock_role(role_mock); + let state = get_mocked_state(None, Some(provider_builder)); + + let mut backend = MockTrustBackend::new(); + backend + .expect_list_trusts() + .withf(|_, params: &TrustListParameters| *params == TrustListParameters::default()) + .returning(|_, _| { + Ok(vec![ + Trust { + id: "redelegated_trust".into(), + redelegated_trust_id: Some("trust_id".into()), + ..Default::default() + }, + Trust { + id: "trust_id".into(), + ..Default::default() + }, + ]) + }); + + let trust_provider = TrustService { + backend_driver: Arc::new(backend), + }; + + let list = trust_provider + .list_trusts(&state, &TrustListParameters::default()) + .await + .unwrap(); + assert_eq!(list.len(), 2); + } +} diff --git a/crates/keystone/src/trust/types.rs b/crates/core/src/trust/types.rs similarity index 100% rename from crates/keystone/src/trust/types.rs rename to crates/core/src/trust/types.rs diff --git a/crates/keystone/src/trust/types/provider.rs b/crates/core/src/trust/types/provider.rs similarity index 100% rename from crates/keystone/src/trust/types/provider.rs rename to crates/core/src/trust/types/provider.rs diff --git a/crates/keystone/src/trust/types/trust.rs b/crates/core/src/trust/types/trust.rs similarity index 100% rename from crates/keystone/src/trust/types/trust.rs rename to crates/core/src/trust/types/trust.rs diff --git a/crates/keystone/Cargo.toml b/crates/keystone/Cargo.toml index 853833a6..bb816d1f 100644 --- a/crates/keystone/Cargo.toml +++ b/crates/keystone/Cargo.toml @@ -28,38 +28,22 @@ harness = false async-trait.workspace = true axum = { workspace = true, features = ["http1", "http2", "macros", "matched-path", "original-uri", "query", "tokio", "tracing"] } base64.workspace = true -bcrypt = { workspace = true, features = ["alloc"] } -byteorder.workspace = true -bytes.workspace = true chrono.workspace = true clap = { workspace = true, features = ["derive"] } color-eyre.workspace = true -config = { workspace = true, features = ["async", "ini"] } derive_builder.workspace = true -dyn-clone.workspace = true eyre.workspace = true -fernet = { workspace = true, features = ["rustcrypto"] } -futures-util.workspace = true -itertools.workspace = true openstack-keystone-api-types = { version = "0.1", path = "../api-types/"} +openstack-keystone-core = { version = "0.1", path = "../core" } openstack-keystone-distributed-storage = { version = "0.1", path = "../storage/"} -mockall_double.workspace = true -nix = { workspace = true, features = ["fs", "user"] } openidconnect.workspace = true -rand.workspace = true -regex.workspace = true reqwest = { workspace = true, features = ["json", "http2", "gzip", "deflate"] } -rmp.workspace = true -schemars.workspace = true -scopeguard.workspace = true sea-orm = { workspace = true, features = ["debug-print", "sqlx-mysql", "sqlx-postgres", "runtime-tokio", "runtime-tokio-native-tls"] } sea-orm-migration = { workspace = true, features = ["sqlx-mysql", "sqlx-postgres", "runtime-tokio"] } secrecy = { workspace = true, features = ["serde"] } serde.workspace = true -serde_bytes.workspace = true serde_json.workspace = true serde_urlencoded.workspace = true -tempfile.workspace = true thiserror.workspace = true tokio = { workspace = true, features = ["fs", "macros", "signal", "rt-multi-thread"] } tokio-util.workspace = true @@ -69,14 +53,12 @@ tracing.workspace = true tracing-appender.workspace = true tracing-subscriber.workspace = true url = { workspace = true, features = ["serde"] } -url-macro.workspace = true utoipa = { workspace = true, features = ["axum_extras", "yaml"] } utoipa-axum.workspace = true utoipa-swagger-ui = { workspace = true, features = ["axum", "vendored"] } uuid = { workspace = true, features = ["v4"] } validator = { workspace = true, features = ["derive"] } webauthn-rs = { workspace = true, features = ["danger-allow-state-serialisation"] } -jsonwebtoken = { version = "10.3", features = ["rust_crypto"] } [dev-dependencies] base64urlsafedata.workspace = true @@ -85,10 +67,10 @@ http-body-util.workspace = true httpmock = { version = "0.8", features = ["http2"] } hyper = { workspace = true, features = ["http1"] } hyper-util = { workspace = true, features = ["tokio", "http1"] } +openstack-keystone-core = { version = "0.1", path = "../core", features = ["mock"] } mockall.workspace = true rstest.workspace = true sea-orm = { workspace = true, features = ["mock", "sqlx-sqlite" ]} -tempfile.workspace = true tracing-test = { workspace = true, features = ["no-env-filter"] } url.workspace = true webauthn-authenticator-rs = { workspace = true, features = ["softtoken"] } diff --git a/crates/keystone/src/api/common.rs b/crates/keystone/src/api/common.rs index 2c7c2d2f..8ee7abdb 100644 --- a/crates/keystone/src/api/common.rs +++ b/crates/keystone/src/api/common.rs @@ -255,7 +255,7 @@ mod tests { }); let state = get_mocked_state( - Provider::mocked_builder().resource(resource_mock), + Provider::mocked_builder().mock_resource(resource_mock), true, None, Some(false), diff --git a/crates/keystone/src/api/error.rs b/crates/keystone/src/api/error.rs index 205615ef..81974122 100644 --- a/crates/keystone/src/api/error.rs +++ b/crates/keystone/src/api/error.rs @@ -12,347 +12,349 @@ // // SPDX-License-Identifier: Apache-2.0 //! # Keystone API error. -use axum::{ - Json, - extract::rejection::JsonRejection, - http::StatusCode, - response::{IntoResponse, Response}, -}; -use serde_json::json; -use thiserror::Error; -use tracing::error; - -use crate::assignment::error::AssignmentProviderError; -use crate::auth::AuthenticationError; -use crate::catalog::error::CatalogProviderError; -use crate::error::BuilderError; -use crate::identity::error::IdentityProviderError; -use crate::policy::PolicyError; -use crate::resource::error::ResourceProviderError; -use crate::revoke::error::RevokeProviderError; -use crate::role::error::RoleProviderError; -use crate::token::error::TokenProviderError; - -/// Keystone API operation errors. -#[derive(Debug, Error)] -pub enum KeystoneApiError { - /// Selected authentication is forbidden. - #[error("changing current authentication scope is forbidden")] - AuthenticationRescopeForbidden, - - #[error("Attempted to authenticate with an unsupported method.")] - AuthMethodNotSupported, - - #[error("{0}.")] - BadRequest(String), - - /// Base64 decoding error. - #[error(transparent)] - Base64Decode(#[from] base64::DecodeError), - - #[error("conflict, resource already existing")] - Conflict(String), - - #[error("domain id or name must be present")] - DomainIdOrName, - - #[error("You are not authorized to perform the requested action.")] - Forbidden { - /// The source of the error. - #[source] - source: Box, - }, - - #[error("invalid header header")] - InvalidHeader, - - #[error("invalid token")] - InvalidToken, - - #[error(transparent)] - JsonExtractorRejection(#[from] JsonRejection), - - #[error("internal server error: {0}")] - InternalError(String), - - #[error("could not find {resource}: {identifier}")] - NotFound { - resource: String, - identifier: String, - }, - - /// Others. - #[error(transparent)] - Other(#[from] eyre::Report), - - #[error(transparent)] - Policy { - #[from] - source: PolicyError, - }, - - #[error("project id or name must be present")] - ProjectIdOrName, - - #[error("project domain must be present")] - ProjectDomain, - - /// Selected authentication is forbidden. - #[error("selected authentication is forbidden")] - SelectedAuthenticationForbidden, - - /// (de)serialization error. - #[error(transparent)] - Serde { - #[from] - source: serde_json::Error, - }, - - #[error("missing x-subject-token header")] - SubjectTokenMissing, - - #[error("The request you have made requires authentication.")] - UnauthorizedNoContext, - - #[error("{}", .context.clone().unwrap_or("The request you have made requires authentication.".to_string()))] - Unauthorized { - context: Option, - /// The source of the error. - #[source] - source: Box, - }, - - /// Request validation error. - #[error("request validation failed: {source}")] - Validator { - /// The source of the error. - #[from] - source: validator::ValidationErrors, - }, -} - -impl KeystoneApiError { - pub fn forbidden(error: E) -> Self - where - E: std::error::Error + Send + Sync + 'static, - { - Self::Forbidden { - source: Box::new(error), - } - } - - pub fn unauthorized(error: E, context: Option) -> Self - where - E: std::error::Error + Send + Sync + 'static, - C: Into, - { - Self::Unauthorized { - context: context.map(Into::into), - source: Box::new(error), - } - } -} - -impl IntoResponse for KeystoneApiError { - fn into_response(self) -> Response { - error!("Error happened during request processing: {:#?}", self); - - let status_code = match self { - KeystoneApiError::Conflict(_) => StatusCode::CONFLICT, - KeystoneApiError::NotFound { .. } => StatusCode::NOT_FOUND, - KeystoneApiError::BadRequest(..) => StatusCode::BAD_REQUEST, - KeystoneApiError::Unauthorized { .. } => StatusCode::UNAUTHORIZED, - KeystoneApiError::UnauthorizedNoContext => StatusCode::UNAUTHORIZED, - KeystoneApiError::Forbidden { .. } => StatusCode::FORBIDDEN, - KeystoneApiError::Policy { .. } => StatusCode::FORBIDDEN, - KeystoneApiError::SelectedAuthenticationForbidden - | KeystoneApiError::AuthenticationRescopeForbidden => StatusCode::BAD_REQUEST, - KeystoneApiError::InternalError(_) | KeystoneApiError::Other(..) => { - StatusCode::INTERNAL_SERVER_ERROR - } - _ => StatusCode::BAD_REQUEST, - }; - - ( - status_code, - Json(json!({"error": {"code": status_code.as_u16(), "message": self.to_string()}})), - ) - .into_response() - } -} - -impl From for KeystoneApiError { - fn from(value: AuthenticationError) -> Self { - match value { - AuthenticationError::DomainDisabled(..) => { - KeystoneApiError::unauthorized(value, None::) - } - AuthenticationError::ProjectDisabled(..) => { - KeystoneApiError::unauthorized(value, None::) - } - AuthenticationError::StructBuilder { source } => { - KeystoneApiError::InternalError(source.to_string()) - } - AuthenticationError::UserDisabled(ref user_id) => { - let uid = user_id.clone(); - KeystoneApiError::unauthorized( - value, - Some(format!("The account is disabled for the user: {uid}")), - ) - } - AuthenticationError::UserLocked(ref user_id) => { - let uid = user_id.clone(); - KeystoneApiError::unauthorized( - value, - Some(format!("The account is locked for the user: {uid}")), - ) - } - AuthenticationError::UserPasswordExpired(ref user_id) => { - let uid = user_id.clone(); - KeystoneApiError::unauthorized( - value, - Some(format!( - "The password is expired and need to be changed for user: {uid}" - )), - ) - } - AuthenticationError::UserNameOrPasswordWrong => KeystoneApiError::unauthorized( - value, - Some("Invalid username or password".to_string()), - ), - AuthenticationError::TokenRenewalForbidden => { - KeystoneApiError::SelectedAuthenticationForbidden - } - AuthenticationError::Unauthorized => { - KeystoneApiError::unauthorized(value, None::) - } - } - } -} - -impl From for KeystoneApiError { - fn from(source: AssignmentProviderError) -> Self { - match source { - AssignmentProviderError::AssignmentNotFound(x) => Self::NotFound { - resource: "assignment".into(), - identifier: x, - }, - AssignmentProviderError::RoleNotFound(x) => Self::NotFound { - resource: "role".into(), - identifier: x, - }, - ref err @ AssignmentProviderError::Conflict(..) => Self::Conflict(err.to_string()), - ref err @ AssignmentProviderError::Validation { .. } => { - Self::BadRequest(err.to_string()) - } - other => Self::InternalError(other.to_string()), - } - } -} - -impl From for KeystoneApiError { - fn from(value: crate::error::BuilderError) -> Self { - Self::InternalError(value.to_string()) - } -} - -impl From for KeystoneApiError { - fn from(value: openstack_keystone_api_types::error::BuilderError) -> Self { - Self::InternalError(value.to_string()) - } -} - -impl From for KeystoneApiError { - fn from(source: RoleProviderError) -> Self { - match source { - RoleProviderError::RoleNotFound(x) => Self::NotFound { - resource: "role".into(), - identifier: x, - }, - ref err @ RoleProviderError::Conflict(..) => Self::Conflict(err.to_string()), - ref err @ RoleProviderError::Validation { .. } => Self::BadRequest(err.to_string()), - other => Self::InternalError(other.to_string()), - } - } -} - -impl From for KeystoneApiError { - fn from(value: serde_urlencoded::ser::Error) -> Self { - Self::InternalError(value.to_string()) - } -} - -impl From for KeystoneApiError { - fn from(value: url::ParseError) -> Self { - Self::InternalError(value.to_string()) - } -} - -impl From for KeystoneApiError { - fn from(value: CatalogProviderError) -> Self { - match value { - ref err @ CatalogProviderError::Conflict(..) => Self::Conflict(err.to_string()), - other => Self::InternalError(other.to_string()), - } - } -} - -impl From for KeystoneApiError { - fn from(value: IdentityProviderError) -> Self { - match value { - IdentityProviderError::Authentication { source } => source.into(), - IdentityProviderError::UserNotFound(x) => Self::NotFound { - resource: "user".into(), - identifier: x, - }, - IdentityProviderError::GroupNotFound(x) => Self::NotFound { - resource: "group".into(), - identifier: x, - }, - other => Self::InternalError(other.to_string()), - } - } -} - -impl From for KeystoneApiError { - fn from(value: ResourceProviderError) -> Self { - match value { - ref err @ ResourceProviderError::Conflict(..) => Self::BadRequest(err.to_string()), - ResourceProviderError::DomainNotFound(x) => Self::NotFound { - resource: "domain".into(), - identifier: x, - }, - other => Self::InternalError(other.to_string()), - } - } -} - -impl From for KeystoneApiError { - fn from(value: RevokeProviderError) -> Self { - match value { - ref err @ RevokeProviderError::Conflict(..) => Self::BadRequest(err.to_string()), - other => Self::InternalError(other.to_string()), - } - } -} - -impl From for KeystoneApiError { - fn from(value: TokenProviderError) -> Self { - match value { - TokenProviderError::Authentication(source) => source.into(), - TokenProviderError::DomainDisabled(x) => Self::NotFound { - resource: "domain".into(), - identifier: x, - }, - TokenProviderError::TokenRestrictionNotFound(x) => Self::NotFound { - resource: "token restriction".into(), - identifier: x, - }, - TokenProviderError::ProjectDisabled(x) => Self::NotFound { - resource: "project".into(), - identifier: x, - }, - other => Self::InternalError(other.to_string()), - } - } -} +//use axum::{ +// Json, +// extract::rejection::JsonRejection, +// http::StatusCode, +// response::{IntoResponse, Response}, +//}; +//use serde_json::json; +//use thiserror::Error; +//use tracing::error; +// +//use crate::assignment::error::AssignmentProviderError; +//use crate::auth::AuthenticationError; +//use crate::catalog::error::CatalogProviderError; +//use crate::error::BuilderError; +//use crate::identity::error::IdentityProviderError; +//use crate::policy::PolicyError; +//use crate::resource::error::ResourceProviderError; +//use crate::revoke::error::RevokeProviderError; +//use crate::role::error::RoleProviderError; +//use crate::token::error::TokenProviderError; + +pub use openstack_keystone_core::api::KeystoneApiError; + +///// Keystone API operation errors. +//#[derive(Debug, Error)] +//pub enum KeystoneApiError { +// /// Selected authentication is forbidden. +// #[error("changing current authentication scope is forbidden")] +// AuthenticationRescopeForbidden, +// +// #[error("Attempted to authenticate with an unsupported method.")] +// AuthMethodNotSupported, +// +// #[error("{0}.")] +// BadRequest(String), +// +// /// Base64 decoding error. +// #[error(transparent)] +// Base64Decode(#[from] base64::DecodeError), +// +// #[error("conflict, resource already existing")] +// Conflict(String), +// +// #[error("domain id or name must be present")] +// DomainIdOrName, +// +// #[error("You are not authorized to perform the requested action.")] +// Forbidden { +// /// The source of the error. +// #[source] +// source: Box, +// }, +// +// #[error("invalid header header")] +// InvalidHeader, +// +// #[error("invalid token")] +// InvalidToken, +// +// #[error(transparent)] +// JsonExtractorRejection(#[from] JsonRejection), +// +// #[error("internal server error: {0}")] +// InternalError(String), +// +// #[error("could not find {resource}: {identifier}")] +// NotFound { +// resource: String, +// identifier: String, +// }, +// +// /// Others. +// #[error(transparent)] +// Other(#[from] eyre::Report), +// +// #[error(transparent)] +// Policy { +// #[from] +// source: PolicyError, +// }, +// +// #[error("project id or name must be present")] +// ProjectIdOrName, +// +// #[error("project domain must be present")] +// ProjectDomain, +// +// /// Selected authentication is forbidden. +// #[error("selected authentication is forbidden")] +// SelectedAuthenticationForbidden, +// +// /// (de)serialization error. +// #[error(transparent)] +// Serde { +// #[from] +// source: serde_json::Error, +// }, +// +// #[error("missing x-subject-token header")] +// SubjectTokenMissing, +// +// #[error("The request you have made requires authentication.")] +// UnauthorizedNoContext, +// +// #[error("{}", .context.clone().unwrap_or("The request you have made requires authentication.".to_string()))] +// Unauthorized { +// context: Option, +// /// The source of the error. +// #[source] +// source: Box, +// }, +// +// /// Request validation error. +// #[error("request validation failed: {source}")] +// Validator { +// /// The source of the error. +// #[from] +// source: validator::ValidationErrors, +// }, +//} +// +//impl KeystoneApiError { +// pub fn forbidden(error: E) -> Self +// where +// E: std::error::Error + Send + Sync + 'static, +// { +// Self::Forbidden { +// source: Box::new(error), +// } +// } +// +// pub fn unauthorized(error: E, context: Option) -> Self +// where +// E: std::error::Error + Send + Sync + 'static, +// C: Into, +// { +// Self::Unauthorized { +// context: context.map(Into::into), +// source: Box::new(error), +// } +// } +//} +// +//impl IntoResponse for KeystoneApiError { +// fn into_response(self) -> Response { +// error!("Error happened during request processing: {:#?}", self); +// +// let status_code = match self { +// KeystoneApiError::Conflict(_) => StatusCode::CONFLICT, +// KeystoneApiError::NotFound { .. } => StatusCode::NOT_FOUND, +// KeystoneApiError::BadRequest(..) => StatusCode::BAD_REQUEST, +// KeystoneApiError::Unauthorized { .. } => StatusCode::UNAUTHORIZED, +// KeystoneApiError::UnauthorizedNoContext => StatusCode::UNAUTHORIZED, +// KeystoneApiError::Forbidden { .. } => StatusCode::FORBIDDEN, +// KeystoneApiError::Policy { .. } => StatusCode::FORBIDDEN, +// KeystoneApiError::SelectedAuthenticationForbidden +// | KeystoneApiError::AuthenticationRescopeForbidden => StatusCode::BAD_REQUEST, +// KeystoneApiError::InternalError(_) | KeystoneApiError::Other(..) => { +// StatusCode::INTERNAL_SERVER_ERROR +// } +// _ => StatusCode::BAD_REQUEST, +// }; +// +// ( +// status_code, +// Json(json!({"error": {"code": status_code.as_u16(), "message": self.to_string()}})), +// ) +// .into_response() +// } +//} + +//impl From for KeystoneApiError { +// fn from(value: AuthenticationError) -> Self { +// match value { +// AuthenticationError::DomainDisabled(..) => { +// KeystoneApiError::unauthorized(value, None::) +// } +// AuthenticationError::ProjectDisabled(..) => { +// KeystoneApiError::unauthorized(value, None::) +// } +// AuthenticationError::StructBuilder { source } => { +// KeystoneApiError::InternalError(source.to_string()) +// } +// AuthenticationError::UserDisabled(ref user_id) => { +// let uid = user_id.clone(); +// KeystoneApiError::unauthorized( +// value, +// Some(format!("The account is disabled for the user: {uid}")), +// ) +// } +// AuthenticationError::UserLocked(ref user_id) => { +// let uid = user_id.clone(); +// KeystoneApiError::unauthorized( +// value, +// Some(format!("The account is locked for the user: {uid}")), +// ) +// } +// AuthenticationError::UserPasswordExpired(ref user_id) => { +// let uid = user_id.clone(); +// KeystoneApiError::unauthorized( +// value, +// Some(format!( +// "The password is expired and need to be changed for user: {uid}" +// )), +// ) +// } +// AuthenticationError::UserNameOrPasswordWrong => KeystoneApiError::unauthorized( +// value, +// Some("Invalid username or password".to_string()), +// ), +// AuthenticationError::TokenRenewalForbidden => { +// KeystoneApiError::SelectedAuthenticationForbidden +// } +// AuthenticationError::Unauthorized => { +// KeystoneApiError::unauthorized(value, None::) +// } +// } +// } +//} +// +//impl From for KeystoneApiError { +// fn from(source: AssignmentProviderError) -> Self { +// match source { +// AssignmentProviderError::AssignmentNotFound(x) => Self::NotFound { +// resource: "assignment".into(), +// identifier: x, +// }, +// AssignmentProviderError::RoleNotFound(x) => Self::NotFound { +// resource: "role".into(), +// identifier: x, +// }, +// ref err @ AssignmentProviderError::Conflict(..) => Self::Conflict(err.to_string()), +// ref err @ AssignmentProviderError::Validation { .. } => { +// Self::BadRequest(err.to_string()) +// } +// other => Self::InternalError(other.to_string()), +// } +// } +//} +// +//impl From for KeystoneApiError { +// fn from(value: crate::error::BuilderError) -> Self { +// Self::InternalError(value.to_string()) +// } +//} +// +//impl From for KeystoneApiError { +// fn from(value: openstack_keystone_api_types::error::BuilderError) -> Self { +// Self::InternalError(value.to_string()) +// } +//} +// +//impl From for KeystoneApiError { +// fn from(source: RoleProviderError) -> Self { +// match source { +// RoleProviderError::RoleNotFound(x) => Self::NotFound { +// resource: "role".into(), +// identifier: x, +// }, +// ref err @ RoleProviderError::Conflict(..) => Self::Conflict(err.to_string()), +// ref err @ RoleProviderError::Validation { .. } => Self::BadRequest(err.to_string()), +// other => Self::InternalError(other.to_string()), +// } +// } +//} +// +//impl From for KeystoneApiError { +// fn from(value: serde_urlencoded::ser::Error) -> Self { +// Self::InternalError(value.to_string()) +// } +//} +// +//impl From for KeystoneApiError { +// fn from(value: url::ParseError) -> Self { +// Self::InternalError(value.to_string()) +// } +//} +// +//impl From for KeystoneApiError { +// fn from(value: CatalogProviderError) -> Self { +// match value { +// ref err @ CatalogProviderError::Conflict(..) => Self::Conflict(err.to_string()), +// other => Self::InternalError(other.to_string()), +// } +// } +//} +// +//impl From for KeystoneApiError { +// fn from(value: IdentityProviderError) -> Self { +// match value { +// IdentityProviderError::Authentication { source } => source.into(), +// IdentityProviderError::UserNotFound(x) => Self::NotFound { +// resource: "user".into(), +// identifier: x, +// }, +// IdentityProviderError::GroupNotFound(x) => Self::NotFound { +// resource: "group".into(), +// identifier: x, +// }, +// other => Self::InternalError(other.to_string()), +// } +// } +//} +// +//impl From for KeystoneApiError { +// fn from(value: ResourceProviderError) -> Self { +// match value { +// ref err @ ResourceProviderError::Conflict(..) => Self::BadRequest(err.to_string()), +// ResourceProviderError::DomainNotFound(x) => Self::NotFound { +// resource: "domain".into(), +// identifier: x, +// }, +// other => Self::InternalError(other.to_string()), +// } +// } +//} +// +//impl From for KeystoneApiError { +// fn from(value: RevokeProviderError) -> Self { +// match value { +// ref err @ RevokeProviderError::Conflict(..) => Self::BadRequest(err.to_string()), +// other => Self::InternalError(other.to_string()), +// } +// } +//} +// +//impl From for KeystoneApiError { +// fn from(value: TokenProviderError) -> Self { +// match value { +// TokenProviderError::Authentication(source) => source.into(), +// TokenProviderError::DomainDisabled(x) => Self::NotFound { +// resource: "domain".into(), +// identifier: x, +// }, +// TokenProviderError::TokenRestrictionNotFound(x) => Self::NotFound { +// resource: "token restriction".into(), +// identifier: x, +// }, +// TokenProviderError::ProjectDisabled(x) => Self::NotFound { +// resource: "project".into(), +// identifier: x, +// }, +// other => Self::InternalError(other.to_string()), +// } +// } +//} diff --git a/crates/keystone/src/api/mod.rs b/crates/keystone/src/api/mod.rs index 5659ee06..aaf9c5e8 100644 --- a/crates/keystone/src/api/mod.rs +++ b/crates/keystone/src/api/mod.rs @@ -129,7 +129,7 @@ pub(crate) mod tests { use crate::config::Config; use crate::identity::types::UserResponseBuilder; use crate::keystone::{Service, ServiceState}; - use crate::policy::{MockPolicyEnforcer, PolicyError, PolicyEvaluationResult}; + use crate::policy::{MockPolicy, PolicyError, PolicyEvaluationResult}; use crate::provider::ProviderBuilder; use crate::token::{MockTokenProvider, Token, UnscopedPayload}; @@ -164,14 +164,14 @@ pub(crate) mod tests { ..Default::default() })) }); - provider_builder.token(token_mock) + provider_builder.mock_token(token_mock) } else { provider_builder } .build() .unwrap(); - let mut policy_enforcer_mock = MockPolicyEnforcer::default(); + let mut policy_enforcer_mock = MockPolicy::default(); policy_enforcer_mock .expect_enforce() @@ -192,7 +192,7 @@ pub(crate) mod tests { Config::default(), DatabaseConnection::Disconnected, provider, - policy_enforcer_mock, + Arc::new(policy_enforcer_mock), ) .unwrap(), ) diff --git a/crates/keystone/src/api/types.rs b/crates/keystone/src/api/types.rs index 8850fe0d..1a2b0940 100644 --- a/crates/keystone/src/api/types.rs +++ b/crates/keystone/src/api/types.rs @@ -17,9 +17,9 @@ pub use openstack_keystone_api_types::catalog::*; pub use openstack_keystone_api_types::scope::*; pub use openstack_keystone_api_types::version::*; -use crate::catalog::types::Endpoint as ProviderEndpoint; -use crate::common::types as provider_types; -use crate::resource::types as resource_provider_types; +//use crate::catalog::types::Endpoint as ProviderEndpoint; +//use crate::common::types as provider_types; +//use crate::resource::types as resource_provider_types; //impl From<(Service, Vec)> for CatalogService { // fn from(value: (Service, Vec)) -> Self { @@ -32,17 +32,17 @@ use crate::resource::types as resource_provider_types; // } //} -impl From for Endpoint { - fn from(value: ProviderEndpoint) -> Self { - Self { - id: value.id.clone(), - interface: value.interface.clone(), - url: value.url.clone(), - region: value.region_id.clone(), - region_id: value.region_id.clone(), - } - } -} +//impl From for Endpoint { +// fn from(value: ProviderEndpoint) -> Self { +// Self { +// id: value.id.clone(), +// interface: value.interface.clone(), +// url: value.url.clone(), +// region: value.region_id.clone(), +// region_id: value.region_id.clone(), +// } +// } +//} //impl From)>> for Catalog { // fn from(value: Vec<(Service, Vec)>) -> Self { @@ -55,80 +55,80 @@ impl From for Endpoint { // } //} -impl From for Domain { - fn from(value: resource_provider_types::Domain) -> Self { - Self { - id: Some(value.id), - name: Some(value.name), - } - } -} - -impl From<&resource_provider_types::Domain> for Domain { - fn from(value: &resource_provider_types::Domain) -> Self { - Self { - id: Some(value.id.clone()), - name: Some(value.name.clone()), - } - } -} - -impl From for provider_types::Domain { - fn from(value: Domain) -> Self { - Self { - id: value.id, - name: value.name, - } - } -} - -impl From for Domain { - fn from(value: provider_types::Domain) -> Self { - Self { - id: value.id, - name: value.name, - } - } -} - -impl From for provider_types::Project { - fn from(value: ScopeProject) -> Self { - Self { - id: value.id, - name: value.name, - domain: value.domain.map(Into::into), - } - } -} - -impl From for ScopeProject { - fn from(value: provider_types::Project) -> Self { - Self { - id: value.id, - name: value.name, - domain: value.domain.map(Into::into), - } - } -} - -impl From<&provider_types::Project> for ScopeProject { - fn from(value: &provider_types::Project) -> Self { - Self::from(value.clone()) - } -} - -impl From for provider_types::System { - fn from(value: System) -> Self { - Self { all: value.all } - } -} - -impl From for provider_types::Scope { - fn from(value: Scope) -> Self { - match value { - Scope::Project(scope) => Self::Project(scope.into()), - Scope::Domain(scope) => Self::Domain(scope.into()), - Scope::System(scope) => Self::System(scope.into()), - } - } -} +//impl From for Domain { +// fn from(value: resource_provider_types::Domain) -> Self { +// Self { +// id: Some(value.id), +// name: Some(value.name), +// } +// } +//} +// +//impl From<&resource_provider_types::Domain> for Domain { +// fn from(value: &resource_provider_types::Domain) -> Self { +// Self { +// id: Some(value.id.clone()), +// name: Some(value.name.clone()), +// } +// } +//} +// +//impl From for provider_types::Domain { +// fn from(value: Domain) -> Self { +// Self { +// id: value.id, +// name: value.name, +// } +// } +//} +// +//impl From for Domain { +// fn from(value: provider_types::Domain) -> Self { +// Self { +// id: value.id, +// name: value.name, +// } +// } +//} +// +//impl From for provider_types::Project { +// fn from(value: ScopeProject) -> Self { +// Self { +// id: value.id, +// name: value.name, +// domain: value.domain.map(Into::into), +// } +// } +//} +// +//impl From for ScopeProject { +// fn from(value: provider_types::Project) -> Self { +// Self { +// id: value.id, +// name: value.name, +// domain: value.domain.map(Into::into), +// } +// } +//} +// +//impl From<&provider_types::Project> for ScopeProject { +// fn from(value: &provider_types::Project) -> Self { +// Self::from(value.clone()) +// } +//} +// +//impl From for provider_types::System { +// fn from(value: System) -> Self { +// Self { all: value.all } +// } +//} +// +//impl From for provider_types::Scope { +// fn from(value: Scope) -> Self { +// match value { +// Scope::Project(scope) => Self::Project(scope.into()), +// Scope::Domain(scope) => Self::Domain(scope.into()), +// Scope::System(scope) => Self::System(scope.into()), +// } +// } +//} diff --git a/crates/keystone/src/api/v3/auth/project/list.rs b/crates/keystone/src/api/v3/auth/project/list.rs index b44537b9..a0fad162 100644 --- a/crates/keystone/src/api/v3/auth/project/list.rs +++ b/crates/keystone/src/api/v3/auth/project/list.rs @@ -194,8 +194,8 @@ mod tests { }); let provider_builder = Provider::mocked_builder() - .assignment(assignment_mock) - .resource(resource_mock); + .mock_assignment(assignment_mock) + .mock_resource(resource_mock); let state = get_mocked_state(provider_builder, true, None, None); let mut api = openapi_router() diff --git a/crates/keystone/src/api/v3/auth/token/common.rs b/crates/keystone/src/api/v3/auth/token/common.rs index 4b5b2188..dc3715eb 100644 --- a/crates/keystone/src/api/v3/auth/token/common.rs +++ b/crates/keystone/src/api/v3/auth/token/common.rs @@ -181,7 +181,7 @@ mod tests { }) .returning(move |_, _| Ok(auth_clone.clone())); - let provider = Provider::mocked_builder().identity(identity_mock); + let provider = Provider::mocked_builder().mock_identity(identity_mock); let state = get_mocked_state(provider, true, None, None); @@ -242,8 +242,8 @@ mod tests { }); let provider = Provider::mocked_builder() - .identity(identity_mock) - .token(token_mock); + .mock_identity(identity_mock) + .mock_token(token_mock); let state = get_mocked_state(provider, true, None, Some(true)); diff --git a/crates/keystone/src/api/v3/auth/token/create.rs b/crates/keystone/src/api/v3/auth/token/create.rs index c521ca70..bcdda2de 100644 --- a/crates/keystone/src/api/v3/auth/token/create.rs +++ b/crates/keystone/src/api/v3/auth/token/create.rs @@ -21,6 +21,7 @@ use axum::{ }; use validator::Validate; +use super::token_impl::build_api_token_v3; use crate::api::v3::auth::token::common::{authenticate_request, get_authz_info}; use crate::api::v3::auth::token::types::{AuthRequest, CreateTokenParameters, TokenResponse}; use crate::api::{Catalog, CatalogService, error::KeystoneApiError}; @@ -75,7 +76,7 @@ pub(super) async fn create( .await?; let mut api_token = TokenResponse { - token: token.build_api_token_v3(&state).await?, + token: build_api_token_v3(&token, &state).await?, }; if !query.nocatalog.is_some_and(|x| x) { let catalog: Catalog = Catalog( @@ -130,7 +131,7 @@ mod tests { types::{UserPasswordAuthRequest, UserResponseBuilder}, }; use crate::keystone::Service; - use crate::policy::MockPolicyEnforcer; + use crate::policy::MockPolicy; use crate::provider::Provider; use crate::resource::{ MockResourceProvider, @@ -258,11 +259,11 @@ mod tests { let provider = Provider::mocked_builder() .config(config.clone()) - .assignment(assignment_mock) - .catalog(catalog_mock) - .identity(identity_mock) - .resource(resource_mock) - .token(token_mock) + .mock_assignment(assignment_mock) + .mock_catalog(catalog_mock) + .mock_identity(identity_mock) + .mock_resource(resource_mock) + .mock_token(token_mock) .build() .unwrap(); @@ -271,7 +272,7 @@ mod tests { config, DatabaseConnection::Disconnected, provider, - MockPolicyEnforcer::new(), + Arc::new(MockPolicy::default()), ) .unwrap(), ); @@ -367,8 +368,8 @@ mod tests { let provider = Provider::mocked_builder() .config(config.clone()) - .identity(identity_mock) - .resource(resource_mock) + .mock_identity(identity_mock) + .mock_resource(resource_mock) .build() .unwrap(); @@ -377,7 +378,7 @@ mod tests { config, DatabaseConnection::Disconnected, provider, - MockPolicyEnforcer::new(), + Arc::new(MockPolicy::default()), ) .unwrap(), ); diff --git a/crates/keystone/src/api/v3/auth/token/delete.rs b/crates/keystone/src/api/v3/auth/token/delete.rs index 333ab13c..13a9e0bc 100644 --- a/crates/keystone/src/api/v3/auth/token/delete.rs +++ b/crates/keystone/src/api/v3/auth/token/delete.rs @@ -166,8 +166,8 @@ mod tests { .returning(|_, _| Ok(())); let provider = Provider::mocked_builder() - .token(token_mock) - .revoke(revoke_mock); + .mock_token(token_mock) + .mock_revoke(revoke_mock); let state = get_mocked_state(provider, true, None, Some(true)); @@ -201,7 +201,7 @@ mod tests { .withf(|_, token: &'_ str, _, _| token == "baz") .returning(move |_, _, _, _| Err(TokenProviderError::Expired)); - let provider = Provider::mocked_builder().token(token_mock); + let provider = Provider::mocked_builder().mock_token(token_mock); let state = get_mocked_state(provider, true, None, Some(true)); @@ -235,7 +235,7 @@ mod tests { .withf(|_, token: &'_ str, _, _| token == "baz") .returning(move |_, _, _, _| Err(TokenProviderError::TokenRevoked)); - let provider = Provider::mocked_builder().token(token_mock); + let provider = Provider::mocked_builder().mock_token(token_mock); let state = get_mocked_state(provider, true, None, Some(true)); let mut api = openapi_router() diff --git a/crates/keystone/src/api/v3/auth/token/show.rs b/crates/keystone/src/api/v3/auth/token/show.rs index 187ba9b3..441f1c4f 100644 --- a/crates/keystone/src/api/v3/auth/token/show.rs +++ b/crates/keystone/src/api/v3/auth/token/show.rs @@ -30,6 +30,7 @@ use axum::{ use serde_json::{json, to_value}; use tracing::error; +use super::token_impl::build_api_token_v3; use crate::api::v3::auth::token::types::{TokenResponse, ValidateTokenParameters}; use crate::api::{Catalog, CatalogService, auth::Auth, error::KeystoneApiError}; use crate::catalog::CatalogApi; @@ -96,7 +97,7 @@ pub(super) async fn show( .await?; //// Expand the token since we didn't expand it before. - let mut response_token = token.build_api_token_v3(&state).await?; + let mut response_token = build_api_token_v3(&token, &state).await?; if !query.nocatalog.is_some_and(|x| x) { let catalog: Catalog = Catalog( @@ -190,10 +191,10 @@ mod tests { .returning(|_, _| Ok(Vec::new())); let provider = Provider::mocked_builder() - .identity(identity_mock) - .resource(resource_mock) - .token(token_mock) - .catalog(catalog_mock); + .mock_identity(identity_mock) + .mock_resource(resource_mock) + .mock_token(token_mock) + .mock_catalog(catalog_mock); let state = get_mocked_state(provider, true, None, Some(true)); @@ -294,10 +295,10 @@ mod tests { .returning(|_, _| Ok(Vec::new())); let provider = Provider::mocked_builder() - .identity(identity_mock) - .resource(resource_mock) - .token(token_mock) - .catalog(catalog_mock); + .mock_identity(identity_mock) + .mock_resource(resource_mock) + .mock_token(token_mock) + .mock_catalog(catalog_mock); let state = get_mocked_state(provider, true, None, Some(true)); @@ -338,7 +339,7 @@ mod tests { .withf(|_, token: &'_ str, _, _| token == "baz") .returning(|_, _, _, _| Err(TokenProviderError::Expired)); - let provider = Provider::mocked_builder().token(token_mock); + let provider = Provider::mocked_builder().mock_token(token_mock); let state = get_mocked_state(provider, true, None, Some(true)); let mut api = openapi_router() @@ -378,7 +379,7 @@ mod tests { .withf(|_, token: &'_ str, _, _| token == "baz") .returning(|_, _, _, _| Err(TokenProviderError::TokenRevoked)); - let provider = Provider::mocked_builder().token(token_mock); + let provider = Provider::mocked_builder().mock_token(token_mock); let state = get_mocked_state(provider, true, None, Some(true)); diff --git a/crates/keystone/src/api/v3/auth/token/token_impl.rs b/crates/keystone/src/api/v3/auth/token/token_impl.rs index f083df67..0fa03cf0 100644 --- a/crates/keystone/src/api/v3/auth/token/token_impl.rs +++ b/crates/keystone/src/api/v3/auth/token/token_impl.rs @@ -26,184 +26,181 @@ use crate::trust::TrustApi; use super::common::*; -impl ProviderToken { - pub async fn build_api_token_v3( - &self, - state: &ServiceState, - ) -> Result { - let mut response = TokenBuilder::default(); - let mut project: Option = self.project().cloned(); - let mut domain: Option = self.domain().cloned(); - response.audit_ids(self.audit_ids().clone()); - response.methods(self.methods().clone()); - response.expires_at(*self.expires_at()); - response.issued_at(*self.issued_at()); - - let user = if let Some(user) = self.user() { - user - } else { - &state - .provider - .get_identity_provider() - .get_user(state, self.user_id()) - .await? - .ok_or_else(|| KeystoneApiError::NotFound { - resource: "user".into(), - identifier: self.user_id().clone(), - })? - }; - - let user_domain = common::get_domain(state, Some(&user.domain_id), None::<&str>).await?; - - let mut user_response: UserBuilder = UserBuilder::default(); - user_response.id(user.id.clone()); - user_response.name(user.name.clone()); - if let Some(val) = user.password_expires_at { - user_response.password_expires_at(val); - } - user_response.domain(user_domain.clone()); - response.user(user_response.build()?); - - if let Some(roles) = self.effective_roles() { - response.roles( - roles - .clone() - .into_iter() - .map(Into::into) - .collect::>(), - ); - } +pub async fn build_api_token_v3( + token: &ProviderToken, + state: &ServiceState, +) -> Result { + let mut response = TokenBuilder::default(); + let mut project: Option = token.project().cloned(); + let mut domain: Option = token.domain().cloned(); + response.audit_ids(token.audit_ids().clone()); + response.methods(token.methods().clone()); + response.expires_at(*token.expires_at()); + response.issued_at(*token.issued_at()); + + let user = if let Some(user) = token.user() { + user + } else { + &state + .provider + .get_identity_provider() + .get_user(state, token.user_id()) + .await? + .ok_or_else(|| KeystoneApiError::NotFound { + resource: "user".into(), + identifier: token.user_id().clone(), + })? + }; + + let user_domain = common::get_domain(state, Some(&user.domain_id), None::<&str>).await?; + + let mut user_response: UserBuilder = UserBuilder::default(); + user_response.id(user.id.clone()); + user_response.name(user.name.clone()); + if let Some(val) = user.password_expires_at { + user_response.password_expires_at(val); + } + user_response.domain(user_domain.clone()); + response.user(user_response.build()?); + + if let Some(roles) = token.effective_roles() { + response.roles( + roles + .clone() + .into_iter() + .map(Into::into) + .collect::>(), + ); + } - match self { - ProviderToken::ApplicationCredential(token) => { - if project.is_none() { - project = Some( - state - .provider - .get_resource_provider() - .get_project(state, &token.project_id) - .await? - .ok_or_else(|| KeystoneApiError::NotFound { - resource: "project".into(), - identifier: token.project_id.clone(), - })?, - ); - } + match token { + ProviderToken::ApplicationCredential(token) => { + if project.is_none() { + project = Some( + state + .provider + .get_resource_provider() + .get_project(state, &token.project_id) + .await? + .ok_or_else(|| KeystoneApiError::NotFound { + resource: "project".into(), + identifier: token.project_id.clone(), + })?, + ); } - ProviderToken::DomainScope(token) => { - if domain.is_none() { - domain = Some( - common::get_domain(state, Some(&token.domain_id), None::<&str>).await?, - ); - } + } + ProviderToken::DomainScope(token) => { + if domain.is_none() { + domain = + Some(common::get_domain(state, Some(&token.domain_id), None::<&str>).await?); } - ProviderToken::FederationUnscoped(_token) => {} - ProviderToken::FederationDomainScope(token) => { - if domain.is_none() { - domain = Some( - common::get_domain(state, Some(&token.domain_id), None::<&str>).await?, - ); - } + } + ProviderToken::FederationUnscoped(_token) => {} + ProviderToken::FederationDomainScope(token) => { + if domain.is_none() { + domain = + Some(common::get_domain(state, Some(&token.domain_id), None::<&str>).await?); } - ProviderToken::FederationProjectScope(token) => { - if project.is_none() { - project = Some( - state - .provider - .get_resource_provider() - .get_project(state, &token.project_id) - .await? - .ok_or_else(|| KeystoneApiError::NotFound { - resource: "project".into(), - identifier: token.project_id.clone(), - })?, - ); - } + } + ProviderToken::FederationProjectScope(token) => { + if project.is_none() { + project = Some( + state + .provider + .get_resource_provider() + .get_project(state, &token.project_id) + .await? + .ok_or_else(|| KeystoneApiError::NotFound { + resource: "project".into(), + identifier: token.project_id.clone(), + })?, + ); } - ProviderToken::ProjectScope(token) => { - if project.is_none() { - project = Some( - state - .provider - .get_resource_provider() - .get_project(state, &token.project_id) - .await? - .ok_or_else(|| KeystoneApiError::NotFound { - resource: "project".into(), - identifier: token.project_id.clone(), - })?, - ); - } + } + ProviderToken::ProjectScope(token) => { + if project.is_none() { + project = Some( + state + .provider + .get_resource_provider() + .get_project(state, &token.project_id) + .await? + .ok_or_else(|| KeystoneApiError::NotFound { + resource: "project".into(), + identifier: token.project_id.clone(), + })?, + ); } - ProviderToken::Restricted(token) => { - if project.is_none() { - project = Some( - state - .provider - .get_resource_provider() - .get_project(state, &token.project_id) - .await? - .ok_or_else(|| KeystoneApiError::NotFound { - resource: "project".into(), - identifier: token.project_id.clone(), - })?, - ); - } + } + ProviderToken::Restricted(token) => { + if project.is_none() { + project = Some( + state + .provider + .get_resource_provider() + .get_project(state, &token.project_id) + .await? + .ok_or_else(|| KeystoneApiError::NotFound { + resource: "project".into(), + identifier: token.project_id.clone(), + })?, + ); } - ProviderToken::SystemScope(_token) => { - response.system(System { all: true }); + } + ProviderToken::SystemScope(_token) => { + response.system(System { all: true }); + } + ProviderToken::Trust(token) => { + if project.is_none() { + project = Some( + state + .provider + .get_resource_provider() + .get_project(state, &token.project_id) + .await? + .ok_or_else(|| KeystoneApiError::NotFound { + resource: "project".into(), + identifier: token.project_id.clone(), + })?, + ); } - ProviderToken::Trust(token) => { - if project.is_none() { - project = Some( - state - .provider - .get_resource_provider() - .get_project(state, &token.project_id) - .await? - .ok_or_else(|| KeystoneApiError::NotFound { - resource: "project".into(), - identifier: token.project_id.clone(), - })?, - ); - } - - if let Some(trust) = &token.trust { - response.trust(trust); - } else { - response.trust( - &state - .provider - .get_trust_provider() - .get_trust(state, &token.trust_id) - .await? - .ok_or_else(|| KeystoneApiError::NotFound { - resource: "trust".into(), - identifier: token.trust_id.clone(), - })?, - ); - } + + if let Some(trust) = &token.trust { + response.trust(trust); + } else { + response.trust( + &state + .provider + .get_trust_provider() + .get_trust(state, &token.trust_id) + .await? + .ok_or_else(|| KeystoneApiError::NotFound { + resource: "trust".into(), + identifier: token.trust_id.clone(), + })?, + ); } - ProviderToken::Unscoped(_token) => {} } + ProviderToken::Unscoped(_token) => {} + } - if let Some(domain) = domain { - response.domain(domain.clone()); - } - if let Some(project) = project { - response.project( - get_project_info_builder(state, &project, &user_domain) - .await? - .build()?, - ); - } - Ok(response.build()?) + if let Some(domain) = domain { + response.domain(domain.clone()); } + if let Some(project) = project { + response.project( + get_project_info_builder(state, &project, &user_domain) + .await? + .build()?, + ); + } + Ok(response.build()?) } #[cfg(test)] mod tests { + use super::*; use crate::api::tests::get_mocked_state; use crate::api::v3::role::types::RoleRef; use crate::identity::{MockIdentityProvider, types::UserResponseBuilder}; @@ -248,16 +245,18 @@ mod tests { })) }); let provider = Provider::mocked_builder() - .identity(identity_mock) - .resource(resource_mock); + .mock_identity(identity_mock) + .mock_resource(resource_mock); let state = get_mocked_state(provider, true, None, Some(true)); - let api_token = ProviderToken::Unscoped(UnscopedPayload { - user_id: "bar".into(), - ..Default::default() - }) - .build_api_token_v3(&state) + let api_token = build_api_token_v3( + &ProviderToken::Unscoped(UnscopedPayload { + user_id: "bar".into(), + ..Default::default() + }), + &state, + ) .await .unwrap(); assert_eq!("bar", api_token.user.id); @@ -294,17 +293,19 @@ mod tests { })) }); let provider = Provider::mocked_builder() - .identity(identity_mock) - .resource(resource_mock); + .mock_identity(identity_mock) + .mock_resource(resource_mock); let state = get_mocked_state(provider, true, None, None); - let api_token = ProviderToken::DomainScope(DomainScopePayload { - user_id: "bar".into(), - domain_id: "domain_id".into(), - ..Default::default() - }) - .build_api_token_v3(&state) + let api_token = build_api_token_v3( + &ProviderToken::DomainScope(DomainScopePayload { + user_id: "bar".into(), + domain_id: "domain_id".into(), + ..Default::default() + }), + &state, + ) .await .unwrap(); @@ -354,8 +355,8 @@ mod tests { })) }); let provider = Provider::mocked_builder() - .identity(identity_mock) - .resource(resource_mock); + .mock_identity(identity_mock) + .mock_resource(resource_mock); let state = get_mocked_state(provider, true, None, None); @@ -370,7 +371,7 @@ mod tests { ..Default::default() }); - let api_token = token.build_api_token_v3(&state).await.unwrap(); + let api_token = build_api_token_v3(&token, &state).await.unwrap(); assert_eq!("bar", api_token.user.id); assert_eq!(Some("user_domain_id"), api_token.user.domain.id.as_deref()); @@ -425,8 +426,8 @@ mod tests { })) }); let provider = Provider::mocked_builder() - .identity(identity_mock) - .resource(resource_mock); + .mock_identity(identity_mock) + .mock_resource(resource_mock); let state = get_mocked_state(provider, true, None, None); @@ -448,7 +449,7 @@ mod tests { ..Default::default() }); - let api_token = token.build_api_token_v3(&state).await.unwrap(); + let api_token = build_api_token_v3(&token, &state).await.unwrap(); assert_eq!("bar", api_token.user.id); assert_eq!(Some("user_domain_id"), api_token.user.domain.id.as_deref()); diff --git a/crates/keystone/src/api/v3/auth/token/types.rs b/crates/keystone/src/api/v3/auth/token/types.rs index 82570910..4d2a30eb 100644 --- a/crates/keystone/src/api/v3/auth/token/types.rs +++ b/crates/keystone/src/api/v3/auth/token/types.rs @@ -14,45 +14,45 @@ pub use openstack_keystone_api_types::v3::auth::token::*; -use crate::error::BuilderError; -use crate::identity::types as identity_types; -use crate::token::Token as BackendToken; +//use crate::error::BuilderError; +//use crate::identity::types as identity_types; +//use crate::token::Token as BackendToken; -impl TryFrom for identity_types::UserPasswordAuthRequest { - type Error = BuilderError; - - fn try_from(value: UserPassword) -> Result { - let mut upa = identity_types::UserPasswordAuthRequestBuilder::default(); - if let Some(id) = &value.id { - upa.id(id); - } - if let Some(name) = &value.name { - upa.name(name); - } - if let Some(domain) = &value.domain { - let mut domain_builder = identity_types::DomainBuilder::default(); - if let Some(id) = &domain.id { - domain_builder.id(id); - } - if let Some(name) = &domain.name { - domain_builder.name(name); - } - upa.domain(domain_builder.build()?); - } - upa.password(value.password.clone()); - upa.build() - } -} - -impl TryFrom<&BackendToken> for Token { - type Error = openstack_keystone_api_types::error::BuilderError; +//impl TryFrom for identity_types::UserPasswordAuthRequest { +// type Error = BuilderError; +// +// fn try_from(value: UserPassword) -> Result { +// let mut upa = identity_types::UserPasswordAuthRequestBuilder::default(); +// if let Some(id) = &value.id { +// upa.id(id); +// } +// if let Some(name) = &value.name { +// upa.name(name); +// } +// if let Some(domain) = &value.domain { +// let mut domain_builder = identity_types::DomainBuilder::default(); +// if let Some(id) = &domain.id { +// domain_builder.id(id); +// } +// if let Some(name) = &domain.name { +// domain_builder.name(name); +// } +// upa.domain(domain_builder.build()?); +// } +// upa.password(value.password.clone()); +// upa.build() +// } +//} - fn try_from(value: &BackendToken) -> Result { - let mut token = TokenBuilder::default(); - token.user(UserBuilder::default().id(value.user_id()).build()?); - token.methods(value.methods().clone()); - token.audit_ids(value.audit_ids().clone()); - token.expires_at(*value.expires_at()); - token.build() - } -} +//impl TryFrom<&BackendToken> for Token { +// type Error = openstack_keystone_api_types::error::BuilderError; +// +// fn try_from(value: &BackendToken) -> Result { +// let mut token = TokenBuilder::default(); +// token.user(UserBuilder::default().id(value.user_id()).build()?); +// token.methods(value.methods().clone()); +// token.audit_ids(value.audit_ids().clone()); +// token.expires_at(*value.expires_at()); +// token.build() +// } +//} diff --git a/crates/keystone/src/api/v3/group/mod.rs b/crates/keystone/src/api/v3/group/mod.rs index c4e39dd9..8a7af69a 100644 --- a/crates/keystone/src/api/v3/group/mod.rs +++ b/crates/keystone/src/api/v3/group/mod.rs @@ -186,7 +186,7 @@ mod tests { }); let state = get_mocked_state( - Provider::mocked_builder().identity(identity_mock), + Provider::mocked_builder().mock_identity(identity_mock), true, None, None, @@ -239,7 +239,7 @@ mod tests { .returning(|_, _| Ok(Vec::new())); let state = get_mocked_state( - Provider::mocked_builder().identity(identity_mock), + Provider::mocked_builder().mock_identity(identity_mock), true, None, None, @@ -303,7 +303,7 @@ mod tests { }); let state = get_mocked_state( - Provider::mocked_builder().identity(identity_mock), + Provider::mocked_builder().mock_identity(identity_mock), true, None, None, @@ -369,7 +369,7 @@ mod tests { }); let state = get_mocked_state( - Provider::mocked_builder().identity(identity_mock), + Provider::mocked_builder().mock_identity(identity_mock), true, None, None, @@ -423,7 +423,7 @@ mod tests { .returning(|_, _| Ok(())); let state = get_mocked_state( - Provider::mocked_builder().identity(identity_mock), + Provider::mocked_builder().mock_identity(identity_mock), true, None, None, diff --git a/crates/keystone/src/api/v3/group/types.rs b/crates/keystone/src/api/v3/group/types.rs index cfc059da..720cc96d 100644 --- a/crates/keystone/src/api/v3/group/types.rs +++ b/crates/keystone/src/api/v3/group/types.rs @@ -12,58 +12,58 @@ // // SPDX-License-Identifier: Apache-2.0 -use axum::{ - Json, - http::StatusCode, - response::{IntoResponse, Response}, -}; +//use axum::{ +// Json, +// http::StatusCode, +// response::{IntoResponse, Response}, +//}; pub use openstack_keystone_api_types::v3::group::*; -use crate::identity::types; - -impl From for Group { - fn from(value: types::Group) -> Self { - Self { - id: value.id, - domain_id: value.domain_id, - name: value.name, - description: value.description, - extra: value.extra, - } - } -} - -impl From for types::GroupCreate { - fn from(value: GroupCreateRequest) -> Self { - let group = value.group; - Self { - id: None, - name: group.name, - domain_id: group.domain_id, - extra: group.extra, - description: group.description, - } - } -} - -impl IntoResponse for types::Group { - fn into_response(self) -> Response { - ( - StatusCode::OK, - Json(GroupResponse { - group: Group::from(self), - }), - ) - .into_response() - } -} - -impl From for types::GroupListParameters { - fn from(value: GroupListParameters) -> Self { - Self { - domain_id: value.domain_id, - name: value.name, - } - } -} +// use crate::identity::types; +// +// impl From for Group { +// fn from(value: types::Group) -> Self { +// Self { +// id: value.id, +// domain_id: value.domain_id, +// name: value.name, +// description: value.description, +// extra: value.extra, +// } +// } +// } +// +// impl From for types::GroupCreate { +// fn from(value: GroupCreateRequest) -> Self { +// let group = value.group; +// Self { +// id: None, +// name: group.name, +// domain_id: group.domain_id, +// extra: group.extra, +// description: group.description, +// } +// } +// } +// +// impl IntoResponse for types::Group { +// fn into_response(self) -> Response { +// ( +// StatusCode::OK, +// Json(GroupResponse { +// group: Group::from(self), +// }), +// ) +// .into_response() +// } +// } +// +// impl From for types::GroupListParameters { +// fn from(value: GroupListParameters) -> Self { +// Self { +// domain_id: value.domain_id, +// name: value.name, +// } +// } +// } diff --git a/crates/keystone/src/api/v3/project/create.rs b/crates/keystone/src/api/v3/project/create.rs index 7d5bb408..33983f30 100644 --- a/crates/keystone/src/api/v3/project/create.rs +++ b/crates/keystone/src/api/v3/project/create.rs @@ -110,7 +110,7 @@ mod tests { }) }); - let provider_builder = Provider::mocked_builder().resource(resource_mock); + let provider_builder = Provider::mocked_builder().mock_resource(resource_mock); let state = get_mocked_state(provider_builder, true, None, None); let mut api = openapi_router() diff --git a/crates/keystone/src/api/v3/project/delete.rs b/crates/keystone/src/api/v3/project/delete.rs index 7fdbf48d..e05c68b0 100644 --- a/crates/keystone/src/api/v3/project/delete.rs +++ b/crates/keystone/src/api/v3/project/delete.rs @@ -97,7 +97,7 @@ mod tests { .withf(|_, id: &'_ str| id == "bar") .returning(|_, _| Ok(())); - provider = provider.resource(mock); + provider = provider.mock_resource(mock); let state = get_mocked_state(provider, true, None, None); let mut api = openapi_router() diff --git a/crates/keystone/src/api/v3/project/types.rs b/crates/keystone/src/api/v3/project/types.rs index 7072587f..6543704c 100644 --- a/crates/keystone/src/api/v3/project/types.rs +++ b/crates/keystone/src/api/v3/project/types.rs @@ -15,56 +15,56 @@ pub use openstack_keystone_api_types::v3::project::*; -use crate::resource::types as provider_types; - -impl From for ProjectShort { - fn from(value: provider_types::Project) -> Self { - Self { - domain_id: value.domain_id, - enabled: value.enabled, - id: value.id, - name: value.name, - } - } -} - -impl From<&provider_types::Project> for ProjectShort { - fn from(value: &provider_types::Project) -> Self { - Self { - domain_id: value.domain_id.clone(), - enabled: value.enabled, - id: value.id.clone(), - name: value.name.clone(), - } - } -} - -impl From for Project { - fn from(value: provider_types::Project) -> Self { - Self { - description: value.description, - domain_id: value.domain_id, - enabled: value.enabled, - extra: value.extra, - id: value.id, - is_domain: value.is_domain, - name: value.name, - parent_id: value.parent_id, - } - } -} - -impl From for provider_types::ProjectCreate { - fn from(value: ProjectCreate) -> Self { - Self { - description: value.description, - domain_id: value.domain_id, - enabled: value.enabled, - extra: value.extra, - id: None, - is_domain: value.is_domain, - name: value.name, - parent_id: value.parent_id, - } - } -} +//use crate::resource::types as provider_types; +// +//impl From for ProjectShort { +// fn from(value: provider_types::Project) -> Self { +// Self { +// domain_id: value.domain_id, +// enabled: value.enabled, +// id: value.id, +// name: value.name, +// } +// } +//} +// +//impl From<&provider_types::Project> for ProjectShort { +// fn from(value: &provider_types::Project) -> Self { +// Self { +// domain_id: value.domain_id.clone(), +// enabled: value.enabled, +// id: value.id.clone(), +// name: value.name.clone(), +// } +// } +//} +// +//impl From for Project { +// fn from(value: provider_types::Project) -> Self { +// Self { +// description: value.description, +// domain_id: value.domain_id, +// enabled: value.enabled, +// extra: value.extra, +// id: value.id, +// is_domain: value.is_domain, +// name: value.name, +// parent_id: value.parent_id, +// } +// } +//} +// +//impl From for provider_types::ProjectCreate { +// fn from(value: ProjectCreate) -> Self { +// Self { +// description: value.description, +// domain_id: value.domain_id, +// enabled: value.enabled, +// extra: value.extra, +// id: None, +// is_domain: value.is_domain, +// name: value.name, +// parent_id: value.parent_id, +// } +// } +//} diff --git a/crates/keystone/src/api/v3/role/create.rs b/crates/keystone/src/api/v3/role/create.rs index 8ed53919..27338064 100644 --- a/crates/keystone/src/api/v3/role/create.rs +++ b/crates/keystone/src/api/v3/role/create.rs @@ -105,7 +105,12 @@ mod tests { }) }); - let state = get_mocked_state(Provider::mocked_builder().role(role_mock), true, None, None); + let state = get_mocked_state( + Provider::mocked_builder().mock_role(role_mock), + true, + None, + None, + ); let mut api = openapi_router() .layer(TraceLayer::new_for_http()) diff --git a/crates/keystone/src/api/v3/role/list.rs b/crates/keystone/src/api/v3/role/list.rs index 42b3039f..99ecc2df 100644 --- a/crates/keystone/src/api/v3/role/list.rs +++ b/crates/keystone/src/api/v3/role/list.rs @@ -91,7 +91,12 @@ mod tests { }]) }); - let state = get_mocked_state(Provider::mocked_builder().role(role_mock), true, None, None); + let state = get_mocked_state( + Provider::mocked_builder().mock_role(role_mock), + true, + None, + None, + ); let mut api = openapi_router() .layer(TraceLayer::new_for_http()) @@ -140,7 +145,12 @@ mod tests { }) .returning(|_, _| Ok(Vec::new())); - let state = get_mocked_state(Provider::mocked_builder().role(role_mock), true, None, None); + let state = get_mocked_state( + Provider::mocked_builder().mock_role(role_mock), + true, + None, + None, + ); let mut api = openapi_router() .layer(TraceLayer::new_for_http()) diff --git a/crates/keystone/src/api/v3/role/show.rs b/crates/keystone/src/api/v3/role/show.rs index 3eb92358..c3817f8f 100644 --- a/crates/keystone/src/api/v3/role/show.rs +++ b/crates/keystone/src/api/v3/role/show.rs @@ -89,7 +89,12 @@ mod tests { })) }); - let state = get_mocked_state(Provider::mocked_builder().role(role_mock), true, None, None); + let state = get_mocked_state( + Provider::mocked_builder().mock_role(role_mock), + true, + None, + None, + ); let mut api = openapi_router() .layer(TraceLayer::new_for_http()) diff --git a/crates/keystone/src/api/v3/role/types.rs b/crates/keystone/src/api/v3/role/types.rs index 9eefd2ba..45351ee6 100644 --- a/crates/keystone/src/api/v3/role/types.rs +++ b/crates/keystone/src/api/v3/role/types.rs @@ -12,67 +12,67 @@ // // SPDX-License-Identifier: Apache-2.0 -use axum::{ - Json, - http::StatusCode, - response::{IntoResponse, Response}, -}; +//use axum::{ +// Json, +// http::StatusCode, +// response::{IntoResponse, Response}, +//}; pub use openstack_keystone_api_types::v3::role::*; -use crate::role::types; +//use crate::role::types; -impl From for Role { - fn from(value: types::Role) -> Self { - Self { - id: value.id, - domain_id: value.domain_id, - name: value.name, - description: value.description, - extra: value.extra, - } - } -} - -impl From for RoleRef { - fn from(value: types::RoleRef) -> Self { - Self { - id: value.id, - domain_id: value.domain_id, - name: value.name.unwrap_or_default(), - } - } -} - -impl IntoResponse for types::Role { - fn into_response(self) -> Response { - ( - StatusCode::OK, - Json(RoleResponse { - role: Role::from(self), - }), - ) - .into_response() - } -} - -impl From for types::RoleListParameters { - fn from(value: RoleListParameters) -> Self { - Self { - domain_id: Some(value.domain_id), - name: value.name, - } - } -} - -impl From for types::RoleCreate { - fn from(value: RoleCreate) -> Self { - Self { - description: value.description, - domain_id: value.domain_id, - extra: value.extra, - id: None, - name: value.name, - } - } -} +//impl From for Role { +// fn from(value: types::Role) -> Self { +// Self { +// id: value.id, +// domain_id: value.domain_id, +// name: value.name, +// description: value.description, +// extra: value.extra, +// } +// } +//} +// +//impl From for RoleRef { +// fn from(value: types::RoleRef) -> Self { +// Self { +// id: value.id, +// domain_id: value.domain_id, +// name: value.name.unwrap_or_default(), +// } +// } +//} +// +//impl IntoResponse for types::Role { +// fn into_response(self) -> Response { +// ( +// StatusCode::OK, +// Json(RoleResponse { +// role: Role::from(self), +// }), +// ) +// .into_response() +// } +//} +// +//impl From for types::RoleListParameters { +// fn from(value: RoleListParameters) -> Self { +// Self { +// domain_id: Some(value.domain_id), +// name: value.name, +// } +// } +//} +// +//impl From for types::RoleCreate { +// fn from(value: RoleCreate) -> Self { +// Self { +// description: value.description, +// domain_id: value.domain_id, +// extra: value.extra, +// id: None, +// name: value.name, +// } +// } +//} diff --git a/crates/keystone/src/api/v3/role_assignment/list.rs b/crates/keystone/src/api/v3/role_assignment/list.rs index b92c3797..d6eebd27 100644 --- a/crates/keystone/src/api/v3/role_assignment/list.rs +++ b/crates/keystone/src/api/v3/role_assignment/list.rs @@ -101,7 +101,7 @@ mod tests { }); let state = get_mocked_state( - Provider::mocked_builder().assignment(assignment_mock), + Provider::mocked_builder().mock_assignment(assignment_mock), true, None, None, @@ -212,7 +212,7 @@ mod tests { }); let state = get_mocked_state( - Provider::mocked_builder().assignment(assignment_mock), + Provider::mocked_builder().mock_assignment(assignment_mock), true, None, None, diff --git a/crates/keystone/src/api/v3/role_assignment/project/user/role/check.rs b/crates/keystone/src/api/v3/role_assignment/project/user/role/check.rs index ee917703..8b7d4dbf 100644 --- a/crates/keystone/src/api/v3/role_assignment/project/user/role/check.rs +++ b/crates/keystone/src/api/v3/role_assignment/project/user/role/check.rs @@ -215,10 +215,10 @@ mod tests { })) }); let provider_builder = Provider::mocked_builder() - .assignment(assignment_mock) - .identity(identity_mock) - .resource(resource_mock) - .role(role_mock); + .mock_assignment(assignment_mock) + .mock_identity(identity_mock) + .mock_resource(resource_mock) + .mock_role(role_mock); let state = get_mocked_state(provider_builder, true, None, None); let mut api = openapi_router() .layer(TraceLayer::new_for_http()) @@ -305,10 +305,10 @@ mod tests { })) }); let provider_builder = Provider::mocked_builder() - .assignment(assignment_mock) - .identity(identity_mock) - .resource(resource_mock) - .role(role_mock); + .mock_assignment(assignment_mock) + .mock_identity(identity_mock) + .mock_resource(resource_mock) + .mock_role(role_mock); let state = get_mocked_state(provider_builder, true, None, None); let mut api = openapi_router() .layer(TraceLayer::new_for_http()) @@ -395,10 +395,10 @@ mod tests { })) }); let provider_builder = Provider::mocked_builder() - .assignment(assignment_mock) - .identity(identity_mock) - .resource(resource_mock) - .role(role_mock); + .mock_assignment(assignment_mock) + .mock_identity(identity_mock) + .mock_resource(resource_mock) + .mock_role(role_mock); let state = get_mocked_state(provider_builder, false, None, None); let mut api = openapi_router() .layer(TraceLayer::new_for_http()) @@ -465,10 +465,10 @@ mod tests { })) }); let provider_builder = Provider::mocked_builder() - .assignment(assignment_mock) - .identity(identity_mock) - .resource(resource_mock) - .role(role_mock); + .mock_assignment(assignment_mock) + .mock_identity(identity_mock) + .mock_resource(resource_mock) + .mock_role(role_mock); let state = get_mocked_state(provider_builder, true, None, None); let mut api = openapi_router() .layer(TraceLayer::new_for_http()) @@ -539,10 +539,10 @@ mod tests { .withf(|_, pid: &'_ str| pid == "project_id") .returning(|_, _| Ok(None)); let provider_builder = Provider::mocked_builder() - .assignment(assignment_mock) - .identity(identity_mock) - .resource(resource_mock) - .role(role_mock); + .mock_assignment(assignment_mock) + .mock_identity(identity_mock) + .mock_resource(resource_mock) + .mock_role(role_mock); let state = get_mocked_state(provider_builder, true, None, None); let mut api = openapi_router() .layer(TraceLayer::new_for_http()) @@ -613,10 +613,10 @@ mod tests { })) }); let provider_builder = Provider::mocked_builder() - .assignment(assignment_mock) - .identity(identity_mock) - .resource(resource_mock) - .role(role_mock); + .mock_assignment(assignment_mock) + .mock_identity(identity_mock) + .mock_resource(resource_mock) + .mock_role(role_mock); let state = get_mocked_state(provider_builder, true, None, None); let mut api = openapi_router() .layer(TraceLayer::new_for_http()) diff --git a/crates/keystone/src/api/v3/role_assignment/project/user/role/grant.rs b/crates/keystone/src/api/v3/role_assignment/project/user/role/grant.rs index 5a6cfcee..eb700d03 100644 --- a/crates/keystone/src/api/v3/role_assignment/project/user/role/grant.rs +++ b/crates/keystone/src/api/v3/role_assignment/project/user/role/grant.rs @@ -202,10 +202,10 @@ mod tests { })) }); let provider_builder = Provider::mocked_builder() - .assignment(assignment_mock) - .identity(identity_mock) - .resource(resource_mock) - .role(role_mock); + .mock_assignment(assignment_mock) + .mock_identity(identity_mock) + .mock_resource(resource_mock) + .mock_role(role_mock); let state = get_mocked_state(provider_builder, true, None, None); let mut api = openapi_router() .layer(TraceLayer::new_for_http()) @@ -269,9 +269,9 @@ mod tests { })) }); let provider_builder = Provider::mocked_builder() - .identity(identity_mock) - .resource(resource_mock) - .role(role_mock); + .mock_identity(identity_mock) + .mock_resource(resource_mock) + .mock_role(role_mock); let state = get_mocked_state(provider_builder, false, None, None); let mut api = openapi_router() .layer(TraceLayer::new_for_http()) @@ -325,9 +325,9 @@ mod tests { })) }); let provider_builder = Provider::mocked_builder() - .identity(identity_mock) - .resource(resource_mock) - .role(role_mock); + .mock_identity(identity_mock) + .mock_resource(resource_mock) + .mock_role(role_mock); let state = get_mocked_state(provider_builder, true, None, None); let mut api = openapi_router() .layer(TraceLayer::new_for_http()) @@ -384,9 +384,9 @@ mod tests { .withf(|_, pid: &'_ str| pid == "project_id") .returning(|_, _| Ok(None)); let provider_builder = Provider::mocked_builder() - .identity(identity_mock) - .resource(resource_mock) - .role(role_mock); + .mock_identity(identity_mock) + .mock_resource(resource_mock) + .mock_role(role_mock); let state = get_mocked_state(provider_builder, true, None, None); let mut api = openapi_router() .layer(TraceLayer::new_for_http()) @@ -444,9 +444,9 @@ mod tests { })) }); let provider_builder = Provider::mocked_builder() - .identity(identity_mock) - .resource(resource_mock) - .role(role_mock); + .mock_identity(identity_mock) + .mock_resource(resource_mock) + .mock_role(role_mock); let state = get_mocked_state(provider_builder, true, None, None); let mut api = openapi_router() .layer(TraceLayer::new_for_http()) diff --git a/crates/keystone/src/api/v3/role_assignment/project/user/role/revoke.rs b/crates/keystone/src/api/v3/role_assignment/project/user/role/revoke.rs index 263b5417..c057e8d9 100644 --- a/crates/keystone/src/api/v3/role_assignment/project/user/role/revoke.rs +++ b/crates/keystone/src/api/v3/role_assignment/project/user/role/revoke.rs @@ -200,10 +200,10 @@ mod tests { }); let provider_builder = Provider::mocked_builder() - .assignment(assignment_mock) - .identity(identity_mock) - .resource(resource_mock) - .role(role_mock); + .mock_assignment(assignment_mock) + .mock_identity(identity_mock) + .mock_resource(resource_mock) + .mock_role(role_mock); let state = get_mocked_state(provider_builder, true, None, None); let mut api = openapi_router() .layer(TraceLayer::new_for_http()) @@ -268,9 +268,9 @@ mod tests { }); let provider_builder = Provider::mocked_builder() - .identity(identity_mock) - .resource(resource_mock) - .role(role_mock); + .mock_identity(identity_mock) + .mock_resource(resource_mock) + .mock_role(role_mock); let state = get_mocked_state(provider_builder, false, None, None); // Policy NOT allowed let mut api = openapi_router() .layer(TraceLayer::new_for_http()) @@ -326,9 +326,9 @@ mod tests { }); let provider_builder = Provider::mocked_builder() - .identity(identity_mock) - .resource(resource_mock) - .role(role_mock); + .mock_identity(identity_mock) + .mock_resource(resource_mock) + .mock_role(role_mock); let state = get_mocked_state(provider_builder, true, None, None); let mut api = openapi_router() .layer(TraceLayer::new_for_http()) @@ -388,9 +388,9 @@ mod tests { .returning(|_, _| Ok(None)); // Project not found let provider_builder = Provider::mocked_builder() - .identity(identity_mock) - .resource(resource_mock) - .role(role_mock); + .mock_identity(identity_mock) + .mock_resource(resource_mock) + .mock_role(role_mock); let state = get_mocked_state(provider_builder, true, None, None); let mut api = openapi_router() .layer(TraceLayer::new_for_http()) @@ -450,9 +450,9 @@ mod tests { }); let provider_builder = Provider::mocked_builder() - .identity(identity_mock) - .resource(resource_mock) - .role(role_mock); + .mock_identity(identity_mock) + .mock_resource(resource_mock) + .mock_role(role_mock); let state = get_mocked_state(provider_builder, true, None, None); let mut api = openapi_router() .layer(TraceLayer::new_for_http()) diff --git a/crates/keystone/src/api/v3/role_assignment/types.rs b/crates/keystone/src/api/v3/role_assignment/types.rs index 05677997..ef39eb94 100644 --- a/crates/keystone/src/api/v3/role_assignment/types.rs +++ b/crates/keystone/src/api/v3/role_assignment/types.rs @@ -14,192 +14,192 @@ pub use openstack_keystone_api_types::v3::role_assignment::*; -use crate::api::error::KeystoneApiError; -use crate::assignment::types; - -impl TryFrom for Assignment { - type Error = KeystoneApiError; - - fn try_from(value: types::Assignment) -> Result { - let mut builder = AssignmentBuilder::default(); - builder.role(Role { - id: value.role_id, - name: value.role_name, - }); - match value.r#type { - types::AssignmentType::GroupDomain => { - builder.group(Group { id: value.actor_id }); - builder.scope(Scope::Domain(Domain { - id: value.target_id, - })); - } - types::AssignmentType::GroupProject => { - builder.group(Group { id: value.actor_id }); - builder.scope(Scope::Project(Project { - id: value.target_id, - })); - } - types::AssignmentType::UserDomain => { - builder.user(User { id: value.actor_id }); - builder.scope(Scope::Domain(Domain { - id: value.target_id, - })); - } - types::AssignmentType::UserProject => { - builder.user(User { id: value.actor_id }); - builder.scope(Scope::Project(Project { - id: value.target_id, - })); - } - types::AssignmentType::UserSystem => { - builder.user(User { id: value.actor_id }); - builder.scope(Scope::System(System { - id: value.target_id, - })); - } - types::AssignmentType::GroupSystem => { - builder.group(Group { id: value.actor_id }); - builder.scope(Scope::System(System { - id: value.target_id, - })); - } - } - Ok(builder.build()?) - } -} - -impl TryFrom for types::RoleAssignmentListParameters { - type Error = KeystoneApiError; - - fn try_from(value: RoleAssignmentListParameters) -> Result { - let mut builder = types::RoleAssignmentListParametersBuilder::default(); - // Filter by role - if let Some(val) = &value.role_id { - builder.role_id(val); - } - - // Filter by actor - if let Some(val) = &value.user_id { - builder.user_id(val); - } else if let Some(val) = &value.group_id { - builder.group_id(val); - } - - // Filter by target - if let Some(val) = &value.project_id { - builder.project_id(val); - } else if let Some(val) = &value.domain_id { - builder.domain_id(val); - } - - if let Some(val) = value.effective { - builder.effective(val); - } - if let Some(val) = value.include_names { - builder.include_names(val); - } - Ok(builder.build()?) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::assignment::types; - - #[test] - fn test_assignment_conversion() { - assert_eq!( - Assignment { - role: Role { - id: "role".into(), - name: Some("role_name".into()) - }, - user: Some(User { id: "actor".into() }), - scope: Scope::Project(Project { - id: "target".into() - }), - group: None, - }, - Assignment::try_from(types::Assignment { - role_id: "role".into(), - role_name: Some("role_name".into()), - actor_id: "actor".into(), - target_id: "target".into(), - r#type: types::AssignmentType::UserProject, - inherited: false, - implied_via: None, - }) - .unwrap() - ); - assert_eq!( - Assignment { - role: Role { - id: "role".into(), - name: None - }, - user: Some(User { id: "actor".into() }), - scope: Scope::Domain(Domain { - id: "target".into() - }), - group: None, - }, - Assignment::try_from(types::Assignment { - role_id: "role".into(), - role_name: None, - actor_id: "actor".into(), - target_id: "target".into(), - r#type: types::AssignmentType::UserDomain, - inherited: false, - implied_via: None, - }) - .unwrap() - ); - assert_eq!( - Assignment { - role: Role { - id: "role".into(), - name: None - }, - group: Some(Group { id: "actor".into() }), - scope: Scope::Project(Project { - id: "target".into() - }), - user: None, - }, - Assignment::try_from(types::Assignment { - role_id: "role".into(), - role_name: None, - actor_id: "actor".into(), - target_id: "target".into(), - r#type: types::AssignmentType::GroupProject, - inherited: false, - implied_via: None, - }) - .unwrap() - ); - assert_eq!( - Assignment { - role: Role { - id: "role".into(), - name: None - }, - group: Some(Group { id: "actor".into() }), - scope: Scope::Domain(Domain { - id: "target".into() - }), - user: None, - }, - Assignment::try_from(types::Assignment { - role_id: "role".into(), - role_name: None, - actor_id: "actor".into(), - target_id: "target".into(), - r#type: types::AssignmentType::GroupDomain, - inherited: false, - implied_via: None, - }) - .unwrap() - ); - } -} +//use crate::api::error::KeystoneApiError; +//use crate::assignment::types; +// +//impl TryFrom for Assignment { +// type Error = KeystoneApiError; +// +// fn try_from(value: types::Assignment) -> Result { +// let mut builder = AssignmentBuilder::default(); +// builder.role(Role { +// id: value.role_id, +// name: value.role_name, +// }); +// match value.r#type { +// types::AssignmentType::GroupDomain => { +// builder.group(Group { id: value.actor_id }); +// builder.scope(Scope::Domain(Domain { +// id: value.target_id, +// })); +// } +// types::AssignmentType::GroupProject => { +// builder.group(Group { id: value.actor_id }); +// builder.scope(Scope::Project(Project { +// id: value.target_id, +// })); +// } +// types::AssignmentType::UserDomain => { +// builder.user(User { id: value.actor_id }); +// builder.scope(Scope::Domain(Domain { +// id: value.target_id, +// })); +// } +// types::AssignmentType::UserProject => { +// builder.user(User { id: value.actor_id }); +// builder.scope(Scope::Project(Project { +// id: value.target_id, +// })); +// } +// types::AssignmentType::UserSystem => { +// builder.user(User { id: value.actor_id }); +// builder.scope(Scope::System(System { +// id: value.target_id, +// })); +// } +// types::AssignmentType::GroupSystem => { +// builder.group(Group { id: value.actor_id }); +// builder.scope(Scope::System(System { +// id: value.target_id, +// })); +// } +// } +// Ok(builder.build()?) +// } +//} +// +//impl TryFrom for types::RoleAssignmentListParameters { +// type Error = KeystoneApiError; +// +// fn try_from(value: RoleAssignmentListParameters) -> Result { +// let mut builder = types::RoleAssignmentListParametersBuilder::default(); +// // Filter by role +// if let Some(val) = &value.role_id { +// builder.role_id(val); +// } +// +// // Filter by actor +// if let Some(val) = &value.user_id { +// builder.user_id(val); +// } else if let Some(val) = &value.group_id { +// builder.group_id(val); +// } +// +// // Filter by target +// if let Some(val) = &value.project_id { +// builder.project_id(val); +// } else if let Some(val) = &value.domain_id { +// builder.domain_id(val); +// } +// +// if let Some(val) = value.effective { +// builder.effective(val); +// } +// if let Some(val) = value.include_names { +// builder.include_names(val); +// } +// Ok(builder.build()?) +// } +//} +// +//#[cfg(test)] +//mod tests { +// use super::*; +// use crate::assignment::types; +// +// #[test] +// fn test_assignment_conversion() { +// assert_eq!( +// Assignment { +// role: Role { +// id: "role".into(), +// name: Some("role_name".into()) +// }, +// user: Some(User { id: "actor".into() }), +// scope: Scope::Project(Project { +// id: "target".into() +// }), +// group: None, +// }, +// Assignment::try_from(types::Assignment { +// role_id: "role".into(), +// role_name: Some("role_name".into()), +// actor_id: "actor".into(), +// target_id: "target".into(), +// r#type: types::AssignmentType::UserProject, +// inherited: false, +// implied_via: None, +// }) +// .unwrap() +// ); +// assert_eq!( +// Assignment { +// role: Role { +// id: "role".into(), +// name: None +// }, +// user: Some(User { id: "actor".into() }), +// scope: Scope::Domain(Domain { +// id: "target".into() +// }), +// group: None, +// }, +// Assignment::try_from(types::Assignment { +// role_id: "role".into(), +// role_name: None, +// actor_id: "actor".into(), +// target_id: "target".into(), +// r#type: types::AssignmentType::UserDomain, +// inherited: false, +// implied_via: None, +// }) +// .unwrap() +// ); +// assert_eq!( +// Assignment { +// role: Role { +// id: "role".into(), +// name: None +// }, +// group: Some(Group { id: "actor".into() }), +// scope: Scope::Project(Project { +// id: "target".into() +// }), +// user: None, +// }, +// Assignment::try_from(types::Assignment { +// role_id: "role".into(), +// role_name: None, +// actor_id: "actor".into(), +// target_id: "target".into(), +// r#type: types::AssignmentType::GroupProject, +// inherited: false, +// implied_via: None, +// }) +// .unwrap() +// ); +// assert_eq!( +// Assignment { +// role: Role { +// id: "role".into(), +// name: None +// }, +// group: Some(Group { id: "actor".into() }), +// scope: Scope::Domain(Domain { +// id: "target".into() +// }), +// user: None, +// }, +// Assignment::try_from(types::Assignment { +// role_id: "role".into(), +// role_name: None, +// actor_id: "actor".into(), +// target_id: "target".into(), +// r#type: types::AssignmentType::GroupDomain, +// inherited: false, +// implied_via: None, +// }) +// .unwrap() +// ); +// } +//} diff --git a/crates/keystone/src/api/v3/user/mod.rs b/crates/keystone/src/api/v3/user/mod.rs index cebe4b8d..e524355a 100644 --- a/crates/keystone/src/api/v3/user/mod.rs +++ b/crates/keystone/src/api/v3/user/mod.rs @@ -222,7 +222,7 @@ mod tests { }); let state = get_mocked_state( - Provider::mocked_builder().identity(identity_mock), + Provider::mocked_builder().mock_identity(identity_mock), true, None, None, @@ -277,7 +277,7 @@ mod tests { .returning(|_, _| Ok(Vec::new())); let state = get_mocked_state( - Provider::mocked_builder().identity(identity_mock), + Provider::mocked_builder().mock_identity(identity_mock), true, None, None, @@ -339,7 +339,7 @@ mod tests { }); let state = get_mocked_state( - Provider::mocked_builder().identity(identity_mock), + Provider::mocked_builder().mock_identity(identity_mock), true, None, None, @@ -402,7 +402,7 @@ mod tests { }); let state = get_mocked_state( - Provider::mocked_builder().identity(identity_mock), + Provider::mocked_builder().mock_identity(identity_mock), true, None, None, @@ -468,7 +468,7 @@ mod tests { .returning(|_, _| Ok(())); let state = get_mocked_state( - Provider::mocked_builder().identity(identity_mock), + Provider::mocked_builder().mock_identity(identity_mock), true, None, None, @@ -524,7 +524,7 @@ mod tests { }); let state = get_mocked_state( - Provider::mocked_builder().identity(identity_mock), + Provider::mocked_builder().mock_identity(identity_mock), true, None, None, diff --git a/crates/keystone/src/api/v3/user/types.rs b/crates/keystone/src/api/v3/user/types.rs index 44b6919b..19bc1e30 100644 --- a/crates/keystone/src/api/v3/user/types.rs +++ b/crates/keystone/src/api/v3/user/types.rs @@ -12,130 +12,122 @@ // // SPDX-License-Identifier: Apache-2.0 -use axum::{ - Json, - http::StatusCode, - response::{IntoResponse, Response}, -}; - pub use openstack_keystone_api_types::v3::user::*; -use crate::identity::types as identity_types; - -impl From for UserOptions { - fn from(value: identity_types::UserOptions) -> Self { - Self { - ignore_change_password_upon_first_use: value.ignore_change_password_upon_first_use, - ignore_password_expiry: value.ignore_password_expiry, - ignore_lockout_failure_attempts: value.ignore_lockout_failure_attempts, - lock_password: value.lock_password, - ignore_user_inactivity: value.ignore_user_inactivity, - multi_factor_auth_rules: value.multi_factor_auth_rules, - multi_factor_auth_enabled: value.multi_factor_auth_enabled, - } - } -} - -impl From for identity_types::UserOptions { - fn from(value: UserOptions) -> Self { - Self { - ignore_change_password_upon_first_use: value.ignore_change_password_upon_first_use, - ignore_password_expiry: value.ignore_password_expiry, - ignore_lockout_failure_attempts: value.ignore_lockout_failure_attempts, - lock_password: value.lock_password, - ignore_user_inactivity: value.ignore_user_inactivity, - multi_factor_auth_rules: value.multi_factor_auth_rules, - multi_factor_auth_enabled: value.multi_factor_auth_enabled, - is_service_account: None, - } - } -} - -impl From for User { - fn from(value: identity_types::UserResponse) -> Self { - let opts: UserOptions = value.options.clone().into(); - // We only want to see user options if there is at least 1 option set - let opts = if opts.ignore_change_password_upon_first_use.is_some() - || opts.ignore_password_expiry.is_some() - || opts.ignore_lockout_failure_attempts.is_some() - || opts.lock_password.is_some() - || opts.ignore_user_inactivity.is_some() - || opts.multi_factor_auth_rules.is_some() - || opts.multi_factor_auth_enabled.is_some() - { - Some(opts) - } else { - None - }; - Self { - default_project_id: value.default_project_id, - domain_id: value.domain_id, - enabled: value.enabled, - extra: value.extra, - federated: value - .federated - .map(|val| val.into_iter().map(Into::into).collect()), - id: value.id, - name: value.name, - options: opts, - password_expires_at: value.password_expires_at, - } - } -} - -impl From for identity_types::UserCreate { - fn from(value: UserCreateRequest) -> Self { - let user = value.user; - Self { - default_project_id: user.default_project_id, - domain_id: user.domain_id, - enabled: Some(user.enabled), - extra: user.extra, - id: None, - federated: None, - name: user.name, - options: user.options.map(Into::into), - password: user.password, - } - } -} - -impl From for Federation { - fn from(value: identity_types::Federation) -> Self { - Self { - idp_id: value.idp_id, - protocols: value.protocols.into_iter().map(Into::into).collect(), - } - } -} -impl From for FederationProtocol { - fn from(value: identity_types::FederationProtocol) -> Self { - Self { - protocol_id: value.protocol_id, - unique_id: value.unique_id, - } - } -} - -impl IntoResponse for identity_types::UserResponse { - fn into_response(self) -> Response { - ( - StatusCode::OK, - Json(UserResponse { - user: User::from(self), - }), - ) - .into_response() - } -} - -impl From for identity_types::UserListParameters { - fn from(value: UserListParameters) -> Self { - Self { - domain_id: value.domain_id, - name: value.name, - unique_id: value.unique_id, - ..Default::default() // limit: value.limit, - } - } -} +//impl From for UserOptions { +// fn from(value: identity_types::UserOptions) -> Self { +// Self { +// ignore_change_password_upon_first_use: value.ignore_change_password_upon_first_use, +// ignore_password_expiry: value.ignore_password_expiry, +// ignore_lockout_failure_attempts: value.ignore_lockout_failure_attempts, +// lock_password: value.lock_password, +// ignore_user_inactivity: value.ignore_user_inactivity, +// multi_factor_auth_rules: value.multi_factor_auth_rules, +// multi_factor_auth_enabled: value.multi_factor_auth_enabled, +// } +// } +//} +// +//impl From for identity_types::UserOptions { +// fn from(value: UserOptions) -> Self { +// Self { +// ignore_change_password_upon_first_use: value.ignore_change_password_upon_first_use, +// ignore_password_expiry: value.ignore_password_expiry, +// ignore_lockout_failure_attempts: value.ignore_lockout_failure_attempts, +// lock_password: value.lock_password, +// ignore_user_inactivity: value.ignore_user_inactivity, +// multi_factor_auth_rules: value.multi_factor_auth_rules, +// multi_factor_auth_enabled: value.multi_factor_auth_enabled, +// is_service_account: None, +// } +// } +//} +// +//impl From for User { +// fn from(value: identity_types::UserResponse) -> Self { +// let opts: UserOptions = value.options.clone().into(); +// // We only want to see user options if there is at least 1 option set +// let opts = if opts.ignore_change_password_upon_first_use.is_some() +// || opts.ignore_password_expiry.is_some() +// || opts.ignore_lockout_failure_attempts.is_some() +// || opts.lock_password.is_some() +// || opts.ignore_user_inactivity.is_some() +// || opts.multi_factor_auth_rules.is_some() +// || opts.multi_factor_auth_enabled.is_some() +// { +// Some(opts) +// } else { +// None +// }; +// Self { +// default_project_id: value.default_project_id, +// domain_id: value.domain_id, +// enabled: value.enabled, +// extra: value.extra, +// federated: value +// .federated +// .map(|val| val.into_iter().map(Into::into).collect()), +// id: value.id, +// name: value.name, +// options: opts, +// password_expires_at: value.password_expires_at, +// } +// } +//} +// +//impl From for identity_types::UserCreate { +// fn from(value: UserCreateRequest) -> Self { +// let user = value.user; +// Self { +// default_project_id: user.default_project_id, +// domain_id: user.domain_id, +// enabled: Some(user.enabled), +// extra: user.extra, +// id: None, +// federated: None, +// name: user.name, +// options: user.options.map(Into::into), +// password: user.password, +// } +// } +//} +// +//impl From for Federation { +// fn from(value: identity_types::Federation) -> Self { +// Self { +// idp_id: value.idp_id, +// protocols: value.protocols.into_iter().map(Into::into).collect(), +// } +// } +//} +//impl From for FederationProtocol { +// fn from(value: identity_types::FederationProtocol) -> Self { +// Self { +// protocol_id: value.protocol_id, +// unique_id: value.unique_id, +// } +// } +//} +// +//impl IntoResponse for identity_types::UserResponse { +// fn into_response(self) -> Response { +// ( +// StatusCode::OK, +// Json(UserResponse { +// user: User::from(self), +// }), +// ) +// .into_response() +// } +//} +// +//impl From for identity_types::UserListParameters { +// fn from(value: UserListParameters) -> Self { +// Self { +// domain_id: value.domain_id, +// name: value.name, +// unique_id: value.unique_id, +// ..Default::default() // limit: value.limit, +// } +// } +//} diff --git a/crates/keystone/src/api/v4/auth/token/mod.rs b/crates/keystone/src/api/v4/auth/token/mod.rs index 635b952c..37c44fdf 100644 --- a/crates/keystone/src/api/v4/auth/token/mod.rs +++ b/crates/keystone/src/api/v4/auth/token/mod.rs @@ -18,7 +18,7 @@ use crate::api::v3::auth::token as v3_token; use crate::keystone::ServiceState; mod common; -mod token_impl; +pub mod token_impl; pub mod types; pub(super) fn openapi_router() -> OpenApiRouter { diff --git a/crates/keystone/src/api/v4/auth/token/token_impl.rs b/crates/keystone/src/api/v4/auth/token/token_impl.rs index 7657cabf..25e3d4b2 100644 --- a/crates/keystone/src/api/v4/auth/token/token_impl.rs +++ b/crates/keystone/src/api/v4/auth/token/token_impl.rs @@ -27,185 +27,184 @@ use crate::trust::TrustApi; use super::common::*; -impl ProviderToken { - pub async fn build_api_token_v4( - &self, - state: &ServiceState, - ) -> Result { - let mut response = TokenBuilder::default(); - let mut project: Option = self.project().cloned(); - let mut domain: Option = self.domain().cloned(); - response.audit_ids(self.audit_ids().clone()); - response.methods(self.methods().clone()); - response.expires_at(*self.expires_at()); - response.issued_at(*self.issued_at()); +//impl ProviderToken { +pub async fn build_api_token_v4( + token: &ProviderToken, + state: &ServiceState, +) -> Result { + let mut response = TokenBuilder::default(); + let mut project: Option = token.project().cloned(); + let mut domain: Option = token.domain().cloned(); + response.audit_ids(token.audit_ids().clone()); + response.methods(token.methods().clone()); + response.expires_at(*token.expires_at()); + response.issued_at(*token.issued_at()); - let user = if let Some(user) = self.user() { - user - } else { - &state - .provider - .get_identity_provider() - .get_user(state, self.user_id()) - .await? - .ok_or_else(|| KeystoneApiError::NotFound { - resource: "user".into(), - identifier: self.user_id().clone(), - })? - }; + let user = if let Some(user) = token.user() { + user + } else { + &state + .provider + .get_identity_provider() + .get_user(state, token.user_id()) + .await? + .ok_or_else(|| KeystoneApiError::NotFound { + resource: "user".into(), + identifier: token.user_id().clone(), + })? + }; - let user_domain = common::get_domain(state, Some(&user.domain_id), None::<&str>).await?; + let user_domain = common::get_domain(state, Some(&user.domain_id), None::<&str>).await?; - let mut user_response: UserBuilder = UserBuilder::default(); - user_response.id(user.id.clone()); - user_response.name(user.name.clone()); - if let Some(val) = user.password_expires_at { - user_response.password_expires_at(val); - } - user_response.domain(user_domain.clone()); - response.user(user_response.build()?); + let mut user_response: UserBuilder = UserBuilder::default(); + user_response.id(user.id.clone()); + user_response.name(user.name.clone()); + if let Some(val) = user.password_expires_at { + user_response.password_expires_at(val); + } + user_response.domain(user_domain.clone()); + response.user(user_response.build()?); - if let Some(roles) = self.effective_roles() { - response.roles( - roles - .clone() - .into_iter() - .map(Into::into) - .collect::>(), - ); - } + if let Some(roles) = token.effective_roles() { + response.roles( + roles + .clone() + .into_iter() + .map(Into::into) + .collect::>(), + ); + } - match self { - ProviderToken::ApplicationCredential(token) => { - if project.is_none() { - project = Some( - state - .provider - .get_resource_provider() - .get_project(state, &token.project_id) - .await? - .ok_or_else(|| KeystoneApiError::NotFound { - resource: "project".into(), - identifier: token.project_id.clone(), - })?, - ); - } + match token { + ProviderToken::ApplicationCredential(token) => { + if project.is_none() { + project = Some( + state + .provider + .get_resource_provider() + .get_project(state, &token.project_id) + .await? + .ok_or_else(|| KeystoneApiError::NotFound { + resource: "project".into(), + identifier: token.project_id.clone(), + })?, + ); } - ProviderToken::DomainScope(token) => { - if domain.is_none() { - domain = Some( - common::get_domain(state, Some(&token.domain_id), None::<&str>).await?, - ); - } + } + ProviderToken::DomainScope(token) => { + if domain.is_none() { + domain = + Some(common::get_domain(state, Some(&token.domain_id), None::<&str>).await?); } - ProviderToken::FederationUnscoped(_token) => {} - ProviderToken::FederationDomainScope(token) => { - if domain.is_none() { - domain = Some( - common::get_domain(state, Some(&token.domain_id), None::<&str>).await?, - ); - } + } + ProviderToken::FederationUnscoped(_token) => {} + ProviderToken::FederationDomainScope(token) => { + if domain.is_none() { + domain = + Some(common::get_domain(state, Some(&token.domain_id), None::<&str>).await?); } - ProviderToken::FederationProjectScope(token) => { - if project.is_none() { - project = Some( - state - .provider - .get_resource_provider() - .get_project(state, &token.project_id) - .await? - .ok_or_else(|| KeystoneApiError::NotFound { - resource: "project".into(), - identifier: token.project_id.clone(), - })?, - ); - } + } + ProviderToken::FederationProjectScope(token) => { + if project.is_none() { + project = Some( + state + .provider + .get_resource_provider() + .get_project(state, &token.project_id) + .await? + .ok_or_else(|| KeystoneApiError::NotFound { + resource: "project".into(), + identifier: token.project_id.clone(), + })?, + ); } - ProviderToken::ProjectScope(token) => { - if project.is_none() { - project = Some( - state - .provider - .get_resource_provider() - .get_project(state, &token.project_id) - .await? - .ok_or_else(|| KeystoneApiError::NotFound { - resource: "project".into(), - identifier: token.project_id.clone(), - })?, - ); - } + } + ProviderToken::ProjectScope(token) => { + if project.is_none() { + project = Some( + state + .provider + .get_resource_provider() + .get_project(state, &token.project_id) + .await? + .ok_or_else(|| KeystoneApiError::NotFound { + resource: "project".into(), + identifier: token.project_id.clone(), + })?, + ); } - ProviderToken::Restricted(token) => { - if project.is_none() { - project = Some( - state - .provider - .get_resource_provider() - .get_project(state, &token.project_id) - .await? - .ok_or_else(|| KeystoneApiError::NotFound { - resource: "project".into(), - identifier: token.project_id.clone(), - })?, - ); - } + } + ProviderToken::Restricted(token) => { + if project.is_none() { + project = Some( + state + .provider + .get_resource_provider() + .get_project(state, &token.project_id) + .await? + .ok_or_else(|| KeystoneApiError::NotFound { + resource: "project".into(), + identifier: token.project_id.clone(), + })?, + ); } - ProviderToken::SystemScope(_token) => { - response.system(System { all: true }); + } + ProviderToken::SystemScope(_token) => { + response.system(System { all: true }); + } + ProviderToken::Trust(token) => { + if project.is_none() { + project = Some( + state + .provider + .get_resource_provider() + .get_project(state, &token.project_id) + .await? + .ok_or_else(|| KeystoneApiError::NotFound { + resource: "project".into(), + identifier: token.project_id.clone(), + })?, + ); } - ProviderToken::Trust(token) => { - if project.is_none() { - project = Some( - state - .provider - .get_resource_provider() - .get_project(state, &token.project_id) - .await? - .ok_or_else(|| KeystoneApiError::NotFound { - resource: "project".into(), - identifier: token.project_id.clone(), - })?, - ); - } - if let Some(trust) = &token.trust { - response.trust(trust); - } else { - response.trust( - &state - .provider - .get_trust_provider() - .get_trust(state, &token.trust_id) - .await? - .ok_or_else(|| KeystoneApiError::NotFound { - resource: "trust".into(), - identifier: token.trust_id.clone(), - })?, - ); - } + if let Some(trust) = &token.trust { + response.trust(trust); + } else { + response.trust( + &state + .provider + .get_trust_provider() + .get_trust(state, &token.trust_id) + .await? + .ok_or_else(|| KeystoneApiError::NotFound { + resource: "trust".into(), + identifier: token.trust_id.clone(), + })?, + ); } - ProviderToken::Unscoped(_token) => {} } + ProviderToken::Unscoped(_token) => {} + } - if let Some(domain) = domain { - response.domain(domain.clone()); - } - if let Some(project) = project { - response.project( - get_project_info_builder(state, &project, &user_domain) - .await? - .build()?, - ); - } - let token = response.build()?; - token.validate()?; - Ok(token) + if let Some(domain) = domain { + response.domain(domain.clone()); } + if let Some(project) = project { + response.project( + get_project_info_builder(state, &project, &user_domain) + .await? + .build()?, + ); + } + let token_v4 = response.build()?; + token_v4.validate()?; + Ok(token_v4) } +//} #[cfg(test)] mod tests { + use super::*; use crate::api::tests::get_mocked_state; use crate::api::v3::role::types::RoleRef; use crate::identity::{MockIdentityProvider, types::UserResponseBuilder}; @@ -250,15 +249,17 @@ mod tests { })) }); let provider = Provider::mocked_builder() - .identity(identity_mock) - .resource(resource_mock); + .mock_identity(identity_mock) + .mock_resource(resource_mock); let state = get_mocked_state(provider, true, None, None); - let api_token = ProviderToken::Unscoped(UnscopedPayload { - user_id: "bar".into(), - ..Default::default() - }) - .build_api_token_v4(&state) + let api_token = build_api_token_v4( + &ProviderToken::Unscoped(UnscopedPayload { + user_id: "bar".into(), + ..Default::default() + }), + &state, + ) .await .unwrap(); assert_eq!("bar", api_token.user.id); @@ -295,17 +296,19 @@ mod tests { })) }); let provider = Provider::mocked_builder() - .identity(identity_mock) - .resource(resource_mock); + .mock_identity(identity_mock) + .mock_resource(resource_mock); let state = get_mocked_state(provider, true, None, None); - let api_token = ProviderToken::DomainScope(DomainScopePayload { - user_id: "bar".into(), - domain_id: "domain_id".into(), - ..Default::default() - }) - .build_api_token_v4(&state) + let api_token = build_api_token_v4( + &ProviderToken::DomainScope(DomainScopePayload { + user_id: "bar".into(), + domain_id: "domain_id".into(), + ..Default::default() + }), + &state, + ) .await .unwrap(); @@ -355,8 +358,8 @@ mod tests { })) }); let provider = Provider::mocked_builder() - .identity(identity_mock) - .resource(resource_mock); + .mock_identity(identity_mock) + .mock_resource(resource_mock); let state = get_mocked_state(provider, true, None, None); @@ -371,7 +374,7 @@ mod tests { ..Default::default() }); - let api_token = token.build_api_token_v3(&state).await.unwrap(); + let api_token = build_api_token_v4(&token, &state).await.unwrap(); assert_eq!("bar", api_token.user.id); assert_eq!(Some("user_domain_id"), api_token.user.domain.id.as_deref()); @@ -426,8 +429,8 @@ mod tests { })) }); let provider = Provider::mocked_builder() - .identity(identity_mock) - .resource(resource_mock); + .mock_identity(identity_mock) + .mock_resource(resource_mock); let state = get_mocked_state(provider, true, None, None); @@ -451,7 +454,7 @@ mod tests { ..Default::default() }); - let api_token = token.build_api_token_v4(&state).await.unwrap(); + let api_token = build_api_token_v4(&token, &state).await.unwrap(); assert_eq!("bar", api_token.user.id); assert_eq!(Some("user_domain_id"), api_token.user.domain.id.as_deref()); diff --git a/crates/keystone/src/api/v4/auth/token/types.rs b/crates/keystone/src/api/v4/auth/token/types.rs index 3bb01d4e..12745184 100644 --- a/crates/keystone/src/api/v4/auth/token/types.rs +++ b/crates/keystone/src/api/v4/auth/token/types.rs @@ -14,45 +14,45 @@ pub use openstack_keystone_api_types::v4::auth::token::*; -use crate::error::BuilderError; -use crate::identity::types as identity_types; -use crate::token::Token as BackendToken; - -impl TryFrom for identity_types::UserPasswordAuthRequest { - type Error = BuilderError; - - fn try_from(value: UserPassword) -> Result { - let mut upa = identity_types::UserPasswordAuthRequestBuilder::default(); - if let Some(id) = &value.id { - upa.id(id); - } - if let Some(name) = &value.name { - upa.name(name); - } - if let Some(domain) = &value.domain { - let mut domain_builder = identity_types::DomainBuilder::default(); - if let Some(id) = &domain.id { - domain_builder.id(id); - } - if let Some(name) = &domain.name { - domain_builder.name(name); - } - upa.domain(domain_builder.build()?); - } - upa.password(value.password.clone()); - upa.build() - } -} - -impl TryFrom<&BackendToken> for Token { - type Error = openstack_keystone_api_types::error::BuilderError; - - fn try_from(value: &BackendToken) -> Result { - let mut token = TokenBuilder::default(); - token.user(UserBuilder::default().id(value.user_id()).build()?); - token.methods(value.methods().clone()); - token.audit_ids(value.audit_ids().clone()); - token.expires_at(*value.expires_at()); - token.build() - } -} +//use crate::error::BuilderError; +//use crate::identity::types as identity_types; +//use crate::token::Token as BackendToken; +// +//impl TryFrom for identity_types::UserPasswordAuthRequest { +// type Error = BuilderError; +// +// fn try_from(value: UserPassword) -> Result { +// let mut upa = identity_types::UserPasswordAuthRequestBuilder::default(); +// if let Some(id) = &value.id { +// upa.id(id); +// } +// if let Some(name) = &value.name { +// upa.name(name); +// } +// if let Some(domain) = &value.domain { +// let mut domain_builder = identity_types::DomainBuilder::default(); +// if let Some(id) = &domain.id { +// domain_builder.id(id); +// } +// if let Some(name) = &domain.name { +// domain_builder.name(name); +// } +// upa.domain(domain_builder.build()?); +// } +// upa.password(value.password.clone()); +// upa.build() +// } +//} +// +//impl TryFrom<&BackendToken> for Token { +// type Error = openstack_keystone_api_types::error::BuilderError; +// +// fn try_from(value: &BackendToken) -> Result { +// let mut token = TokenBuilder::default(); +// token.user(UserBuilder::default().id(value.user_id()).build()?); +// token.methods(value.methods().clone()); +// token.audit_ids(value.audit_ids().clone()); +// token.expires_at(*value.expires_at()); +// token.build() +// } +//} diff --git a/crates/keystone/src/api/v4/group/mod.rs b/crates/keystone/src/api/v4/group/mod.rs index 4cd22760..fea04388 100644 --- a/crates/keystone/src/api/v4/group/mod.rs +++ b/crates/keystone/src/api/v4/group/mod.rs @@ -61,7 +61,7 @@ mod tests { }); let state = get_mocked_state( - Provider::mocked_builder().identity(identity_mock), + Provider::mocked_builder().mock_identity(identity_mock), true, None, None, @@ -114,7 +114,7 @@ mod tests { .returning(|_, _| Ok(Vec::new())); let state = get_mocked_state( - Provider::mocked_builder().identity(identity_mock), + Provider::mocked_builder().mock_identity(identity_mock), true, None, None, @@ -178,7 +178,7 @@ mod tests { }); let state = get_mocked_state( - Provider::mocked_builder().identity(identity_mock), + Provider::mocked_builder().mock_identity(identity_mock), true, None, None, @@ -244,7 +244,7 @@ mod tests { }); let state = get_mocked_state( - Provider::mocked_builder().identity(identity_mock), + Provider::mocked_builder().mock_identity(identity_mock), true, None, None, @@ -298,7 +298,7 @@ mod tests { .returning(|_, _| Ok(())); let state = get_mocked_state( - Provider::mocked_builder().identity(identity_mock), + Provider::mocked_builder().mock_identity(identity_mock), true, None, None, diff --git a/crates/keystone/src/api/v4/token/restriction/create.rs b/crates/keystone/src/api/v4/token/restriction/create.rs index 5a3923db..b075f3b9 100644 --- a/crates/keystone/src/api/v4/token/restriction/create.rs +++ b/crates/keystone/src/api/v4/token/restriction/create.rs @@ -122,7 +122,7 @@ mod tests { }); let state = get_mocked_state( - Provider::mocked_builder().token(token_mock), + Provider::mocked_builder().mock_token(token_mock), true, None, Some(true), diff --git a/crates/keystone/src/api/v4/token/restriction/delete.rs b/crates/keystone/src/api/v4/token/restriction/delete.rs index 0943a3a3..18016abc 100644 --- a/crates/keystone/src/api/v4/token/restriction/delete.rs +++ b/crates/keystone/src/api/v4/token/restriction/delete.rs @@ -143,7 +143,7 @@ mod tests { .returning(|_, _| Ok(())); let state = get_mocked_state( - Provider::mocked_builder().token(token_mock), + Provider::mocked_builder().mock_token(token_mock), true, None, Some(true), diff --git a/crates/keystone/src/api/v4/token/restriction/list.rs b/crates/keystone/src/api/v4/token/restriction/list.rs index 1fb9cdf5..fa3d92c8 100644 --- a/crates/keystone/src/api/v4/token/restriction/list.rs +++ b/crates/keystone/src/api/v4/token/restriction/list.rs @@ -130,7 +130,7 @@ mod tests { }]) }); let state = get_mocked_state( - Provider::mocked_builder().token(token_mock), + Provider::mocked_builder().mock_token(token_mock), true, None, Some(true), @@ -217,7 +217,7 @@ mod tests { }]) }); let state = get_mocked_state( - Provider::mocked_builder().token(token_mock), + Provider::mocked_builder().mock_token(token_mock), true, None, Some(true), diff --git a/crates/keystone/src/api/v4/token/restriction/show.rs b/crates/keystone/src/api/v4/token/restriction/show.rs index 747cec10..edc7e9a4 100644 --- a/crates/keystone/src/api/v4/token/restriction/show.rs +++ b/crates/keystone/src/api/v4/token/restriction/show.rs @@ -131,7 +131,7 @@ mod tests { }); let state = get_mocked_state( - Provider::mocked_builder().token(token_mock), + Provider::mocked_builder().mock_token(token_mock), true, None, Some(true), @@ -226,7 +226,7 @@ mod tests { })) }); let state = get_mocked_state( - Provider::mocked_builder().token(token_mock), + Provider::mocked_builder().mock_token(token_mock), false, None, Some(true), diff --git a/crates/keystone/src/api/v4/token/restriction/update.rs b/crates/keystone/src/api/v4/token/restriction/update.rs index 1c0820fb..30a410dd 100644 --- a/crates/keystone/src/api/v4/token/restriction/update.rs +++ b/crates/keystone/src/api/v4/token/restriction/update.rs @@ -147,7 +147,7 @@ mod tests { }); let state = get_mocked_state( - Provider::mocked_builder().token(token_mock), + Provider::mocked_builder().mock_token(token_mock), true, None, Some(true), diff --git a/crates/keystone/src/api/v4/token/types/restriction.rs b/crates/keystone/src/api/v4/token/types/restriction.rs index 54ba4cde..c83bdd6e 100644 --- a/crates/keystone/src/api/v4/token/types/restriction.rs +++ b/crates/keystone/src/api/v4/token/types/restriction.rs @@ -12,99 +12,99 @@ // // SPDX-License-Identifier: Apache-2.0 //! Token restriction types. -use axum::{ - Json, - http::StatusCode, - response::{IntoResponse, Response}, -}; +//use axum::{ +// Json, +// http::StatusCode, +// response::{IntoResponse, Response}, +//}; pub use openstack_keystone_api_types::v4::token_restriction::*; -use crate::token::types::{ - self as types, TokenRestriction as ProviderTokenRestriction, - TokenRestrictionCreate as ProviderTokenRestrictionCreate, - TokenRestrictionUpdate as ProviderTokenRestrictionUpdate, -}; - -impl From for types::TokenRestrictionListParameters { - fn from(value: TokenRestrictionListParameters) -> Self { - Self { - domain_id: value.domain_id, - user_id: value.user_id, - project_id: value.project_id, - } - } -} - -impl From for TokenRestriction { - fn from(value: ProviderTokenRestriction) -> Self { - Self { - allow_rescope: value.allow_rescope, - allow_renew: value.allow_renew, - id: value.id, - domain_id: value.domain_id, - project_id: value.project_id, - user_id: value.user_id, - roles: value - .roles - .map(|roles| roles.into_iter().map(Into::into).collect()) - .unwrap_or_default(), - } - } -} - -impl From for ProviderTokenRestrictionCreate { - fn from(value: TokenRestrictionCreateRequest) -> Self { - Self { - allow_rescope: value.restriction.allow_rescope, - allow_renew: value.restriction.allow_renew, - id: String::new(), - domain_id: value.restriction.domain_id, - project_id: value.restriction.project_id, - user_id: value.restriction.user_id, - role_ids: value - .restriction - .roles - .into_iter() - .map(|role| role.id) - .collect(), - } - } -} - -impl From for ProviderTokenRestrictionUpdate { - fn from(value: TokenRestrictionUpdateRequest) -> Self { - Self { - allow_rescope: value.restriction.allow_rescope, - allow_renew: value.restriction.allow_renew, - project_id: value.restriction.project_id, - user_id: value.restriction.user_id, - role_ids: value - .restriction - .roles - .map(|roles| roles.into_iter().map(|role| role.id).collect()), - } - } -} - -//impl From for RoleRef { -// fn from(value: crate::role::types::RoleRef) -> Self { +//use crate::token::types::{ +// self as types, TokenRestriction as ProviderTokenRestriction, +// TokenRestrictionCreate as ProviderTokenRestrictionCreate, +// TokenRestrictionUpdate as ProviderTokenRestrictionUpdate, +//}; +// +//impl From for types::TokenRestrictionListParameters { +// fn from(value: TokenRestrictionListParameters) -> Self { +// Self { +// domain_id: value.domain_id, +// user_id: value.user_id, +// project_id: value.project_id, +// } +// } +//} +// +//impl From for TokenRestriction { +// fn from(value: ProviderTokenRestriction) -> Self { // Self { +// allow_rescope: value.allow_rescope, +// allow_renew: value.allow_renew, // id: value.id, -// name: value.name.unwrap_or_default(), // domain_id: value.domain_id, +// project_id: value.project_id, +// user_id: value.user_id, +// roles: value +// .roles +// .map(|roles| roles.into_iter().map(Into::into).collect()) +// .unwrap_or_default(), // } // } //} - -impl IntoResponse for ProviderTokenRestriction { - fn into_response(self) -> Response { - ( - StatusCode::OK, - Json(TokenRestrictionResponse { - restriction: TokenRestriction::from(self), - }), - ) - .into_response() - } -} +// +//impl From for ProviderTokenRestrictionCreate { +// fn from(value: TokenRestrictionCreateRequest) -> Self { +// Self { +// allow_rescope: value.restriction.allow_rescope, +// allow_renew: value.restriction.allow_renew, +// id: String::new(), +// domain_id: value.restriction.domain_id, +// project_id: value.restriction.project_id, +// user_id: value.restriction.user_id, +// role_ids: value +// .restriction +// .roles +// .into_iter() +// .map(|role| role.id) +// .collect(), +// } +// } +//} +// +//impl From for ProviderTokenRestrictionUpdate { +// fn from(value: TokenRestrictionUpdateRequest) -> Self { +// Self { +// allow_rescope: value.restriction.allow_rescope, +// allow_renew: value.restriction.allow_renew, +// project_id: value.restriction.project_id, +// user_id: value.restriction.user_id, +// role_ids: value +// .restriction +// .roles +// .map(|roles| roles.into_iter().map(|role| role.id).collect()), +// } +// } +//} +// +////impl From for RoleRef { +//// fn from(value: crate::role::types::RoleRef) -> Self { +//// Self { +//// id: value.id, +//// name: value.name.unwrap_or_default(), +//// domain_id: value.domain_id, +//// } +//// } +////} +// +//impl IntoResponse for ProviderTokenRestriction { +// fn into_response(self) -> Response { +// ( +// StatusCode::OK, +// Json(TokenRestrictionResponse { +// restriction: TokenRestriction::from(self), +// }), +// ) +// .into_response() +// } +//} diff --git a/crates/keystone/src/api/v4/user/mod.rs b/crates/keystone/src/api/v4/user/mod.rs index 5df3c5d8..3ed928ca 100644 --- a/crates/keystone/src/api/v4/user/mod.rs +++ b/crates/keystone/src/api/v4/user/mod.rs @@ -218,7 +218,7 @@ mod tests { }); let state = get_mocked_state( - Provider::mocked_builder().identity(identity_mock), + Provider::mocked_builder().mock_identity(identity_mock), true, None, None, @@ -273,7 +273,7 @@ mod tests { .returning(|_, _| Ok(Vec::new())); let state = get_mocked_state( - Provider::mocked_builder().identity(identity_mock), + Provider::mocked_builder().mock_identity(identity_mock), true, None, None, @@ -335,7 +335,7 @@ mod tests { }); let state = get_mocked_state( - Provider::mocked_builder().identity(identity_mock), + Provider::mocked_builder().mock_identity(identity_mock), true, None, None, @@ -399,7 +399,7 @@ mod tests { }); let state = get_mocked_state( - Provider::mocked_builder().identity(identity_mock), + Provider::mocked_builder().mock_identity(identity_mock), true, None, None, @@ -465,7 +465,7 @@ mod tests { .returning(|_, _| Ok(())); let state = get_mocked_state( - Provider::mocked_builder().identity(identity_mock), + Provider::mocked_builder().mock_identity(identity_mock), true, None, None, @@ -521,7 +521,7 @@ mod tests { }); let state = get_mocked_state( - Provider::mocked_builder().identity(identity_mock), + Provider::mocked_builder().mock_identity(identity_mock), true, None, None, diff --git a/crates/keystone/src/application_credential/backend.rs b/crates/keystone/src/application_credential/backend.rs index 83b8861f..17283258 100644 --- a/crates/keystone/src/application_credential/backend.rs +++ b/crates/keystone/src/application_credential/backend.rs @@ -14,36 +14,5 @@ //! # Application credential provider backend pub mod sql; -use async_trait::async_trait; - -use crate::application_credential::ApplicationCredentialProviderError; -use crate::application_credential::types::*; -use crate::keystone::ServiceState; - +pub use openstack_keystone_core::application_credential::backend::ApplicationCredentialBackend; pub use sql::SqlBackend; - -/// Application Credential backend driver interface. -#[cfg_attr(test, mockall::automock)] -#[async_trait] -pub trait ApplicationCredentialBackend: Send + Sync { - /// Create a new application credential. - async fn create_application_credential( - &self, - state: &ServiceState, - rec: ApplicationCredentialCreate, - ) -> Result; - - /// Get a single application credential by ID. - async fn get_application_credential<'a>( - &self, - state: &ServiceState, - id: &'a str, - ) -> Result, ApplicationCredentialProviderError>; - - /// List application credentials. - async fn list_application_credentials( - &self, - state: &ServiceState, - params: &ApplicationCredentialListParameters, - ) -> Result, ApplicationCredentialProviderError>; -} diff --git a/crates/keystone/src/application_credential/mod.rs b/crates/keystone/src/application_credential/mod.rs index 00df1bbb..4aa39586 100644 --- a/crates/keystone/src/application_credential/mod.rs +++ b/crates/keystone/src/application_credential/mod.rs @@ -111,177 +111,5 @@ //! } //! ] //! ``` -use std::collections::BTreeMap; -use std::sync::Arc; - -use async_trait::async_trait; -use base64::{Engine as _, engine::general_purpose}; -use rand::{RngExt, rng}; -use secrecy::SecretString; -use uuid::Uuid; -use validator::Validate; - +pub use openstack_keystone_core::application_credential::*; pub mod backend; -pub mod error; -#[cfg(test)] -mod mock; -pub mod types; - -use crate::config::Config; -use crate::keystone::ServiceState; -use crate::plugin_manager::PluginManager; -use crate::role::{ - RoleApi, - types::{Role, RoleListParameters}, -}; -use backend::{ApplicationCredentialBackend, SqlBackend}; -use types::*; - -pub use error::ApplicationCredentialProviderError; -#[cfg(test)] -pub use mock::MockApplicationCredentialProvider; -pub use types::ApplicationCredentialApi; - -/// Application Credential Provider. -pub struct ApplicationCredentialProvider { - backend_driver: Arc, -} - -impl ApplicationCredentialProvider { - pub fn new( - config: &Config, - plugin_manager: &PluginManager, - ) -> Result { - let backend_driver = if let Some(driver) = plugin_manager - .get_application_credential_backend(config.application_credential.driver.clone()) - { - driver.clone() - } else { - match config.application_credential.driver.as_str() { - "sql" => Arc::new(SqlBackend::default()), - other => { - return Err(ApplicationCredentialProviderError::UnsupportedDriver( - other.to_string(), - )); - } - } - }; - Ok(Self { backend_driver }) - } -} - -#[async_trait] -impl ApplicationCredentialApi for ApplicationCredentialProvider { - /// Create a new application credential. - async fn create_application_credential( - &self, - state: &ServiceState, - rec: ApplicationCredentialCreate, - ) -> Result { - rec.validate()?; - // TODO: Check app creds count - let mut new_rec = rec; - if new_rec.id.is_none() { - new_rec.id = Some(Uuid::new_v4().simple().to_string()); - } - if let Some(ref mut rules) = new_rec.access_rules { - for rule in rules { - if rule.id.is_none() { - rule.id = Some(Uuid::new_v4().simple().to_string()); - } - } - } - if new_rec.secret.is_none() { - new_rec.secret = Some(generate_secret()); - } - self.backend_driver - .create_application_credential(state, new_rec) - .await - } - - /// Get a single application credential by ID. - #[tracing::instrument(level = "info", skip(self, state))] - async fn get_application_credential<'a>( - &self, - state: &ServiceState, - id: &'a str, - ) -> Result, ApplicationCredentialProviderError> { - if let Some(mut app_cred) = self - .backend_driver - .get_application_credential(state, id) - .await? - { - let roles: BTreeMap = state - .provider - .get_role_provider() - .list_roles(state, &RoleListParameters::default()) - .await? - .into_iter() - .map(|x| (x.id.clone(), x)) - .collect(); - for cred_role in app_cred.roles.iter_mut() { - if let Some(role) = roles.get(&cred_role.id) { - cred_role.name = Some(role.name.clone()); - cred_role.domain_id = role.domain_id.clone(); - } - } - Ok(Some(app_cred)) - } else { - Ok(None) - } - } - - /// List application credentials. - #[tracing::instrument(level = "info", skip(self, state))] - async fn list_application_credentials( - &self, - state: &ServiceState, - params: &ApplicationCredentialListParameters, - ) -> Result, ApplicationCredentialProviderError> - { - params.validate()?; - let mut creds = self - .backend_driver - .list_application_credentials(state, params) - .await?; - - let roles: BTreeMap = state - .provider - .get_role_provider() - .list_roles(state, &RoleListParameters::default()) - .await? - .into_iter() - .map(|x| (x.id.clone(), x)) - .collect(); - for cred in creds.iter_mut() { - for cred_role in cred.roles.iter_mut() { - if let Some(role) = roles.get(&cred_role.id) { - cred_role.name = Some(role.name.clone()); - cred_role.domain_id = role.domain_id.clone(); - } - } - } - Ok(creds) - } -} - -/// Generate application credential secret. -/// -/// Use the same algorithm as the python Keystone uses: -/// -/// - use random 64 bytes -/// - apply base64 encoding with no padding -pub fn generate_secret() -> SecretString { - const LENGTH: usize = 64; - - // 1. Generate 64 cryptographically secure random bytes (Analogous to - // `secrets.token_bytes(length)`) - let mut secret_bytes = [0u8; LENGTH]; - rng().fill(&mut secret_bytes[..]); - - // 2. Base64 URL-safe encoding (Analogous to `base64.urlsafe_b64encode(secret)`) - // with stripping padding handled automatically by `URL_SAFE_NO_PAD` engine. - let encoded_secret = general_purpose::URL_SAFE_NO_PAD.encode(secret_bytes); - - SecretString::new(encoded_secret.into()) -} diff --git a/crates/keystone/src/assignment/backend.rs b/crates/keystone/src/assignment/backend.rs index f4886bb2..34267d69 100644 --- a/crates/keystone/src/assignment/backend.rs +++ b/crates/keystone/src/assignment/backend.rs @@ -14,54 +14,5 @@ pub mod sql; -use async_trait::async_trait; - -use crate::assignment::AssignmentProviderError; -use crate::keystone::ServiceState; - -use crate::assignment::types::assignment::*; +pub use openstack_keystone_core::assignment::backend::AssignmentBackend; pub use sql::SqlBackend; - -#[cfg_attr(test, mockall::automock)] -#[async_trait] -pub trait AssignmentBackend: Send + Sync { - /// Check assignment grant. - async fn check_grant( - &self, - state: &ServiceState, - params: &Assignment, - ) -> Result; - - /// Create assignment grant. - async fn create_grant( - &self, - state: &ServiceState, - params: AssignmentCreate, - ) -> Result; - - /// List Role assignments - async fn list_assignments( - &self, - state: &ServiceState, - params: &RoleAssignmentListParameters, - ) -> Result, AssignmentProviderError>; - - /// List all role assignments for multiple actors on multiple targets - /// - /// It is a naive interpretation of the effective role assignments where we - /// check all roles assigned to the user (including groups) on a - /// concrete target (including all higher targets the role can be - /// inherited from) - async fn list_assignments_for_multiple_actors_and_targets( - &self, - state: &ServiceState, - params: &RoleAssignmentListForMultipleActorTargetParameters, - ) -> Result, AssignmentProviderError>; - - /// Revoke assignment grant. - async fn revoke_grant( - &self, - state: &ServiceState, - params: &Assignment, - ) -> Result<(), AssignmentProviderError>; -} diff --git a/crates/keystone/src/assignment/backend/sql.rs b/crates/keystone/src/assignment/backend/sql.rs index 1295f412..06894104 100644 --- a/crates/keystone/src/assignment/backend/sql.rs +++ b/crates/keystone/src/assignment/backend/sql.rs @@ -154,7 +154,7 @@ mod tests { use super::*; use crate::config::Config; use crate::keystone::Service; - use crate::policy::MockPolicyEnforcer; + use crate::policy::MockPolicy; use crate::provider::Provider; use crate::role::{MockRoleProvider, types::Role}; @@ -164,7 +164,7 @@ mod tests { Config::default(), db, provider, - MockPolicyEnforcer::default(), + Arc::new(MockPolicy::default()), ) .unwrap(), ) @@ -188,7 +188,10 @@ mod tests { ..Default::default() }]) }); - let provider = Provider::mocked_builder().role(role_mock).build().unwrap(); + let provider = Provider::mocked_builder() + .mock_role(role_mock) + .build() + .unwrap(); let state = get_mock_state(db, provider); @@ -263,7 +266,10 @@ mod tests { }, ]) }); - let provider = Provider::mocked_builder().role(role_mock).build().unwrap(); + let provider = Provider::mocked_builder() + .mock_role(role_mock) + .build() + .unwrap(); let state = get_mock_state(db, provider); @@ -335,7 +341,10 @@ mod tests { }, ]) }); - let provider = Provider::mocked_builder().role(role_mock).build().unwrap(); + let provider = Provider::mocked_builder() + .mock_role(role_mock) + .build() + .unwrap(); let state = get_mock_state(db, provider); diff --git a/crates/keystone/src/assignment/backend/sql/assignment.rs b/crates/keystone/src/assignment/backend/sql/assignment.rs index a9bacabf..92a2305a 100644 --- a/crates/keystone/src/assignment/backend/sql/assignment.rs +++ b/crates/keystone/src/assignment/backend/sql/assignment.rs @@ -15,7 +15,7 @@ use crate::assignment::{AssignmentProviderError, types::*}; use crate::db::entity::{ - assignment as db_assignment, role as db_role, sea_orm_active_enums::Type as DbAssignmentType, + assignment as db_assignment, sea_orm_active_enums::Type as DbAssignmentType, system_assignment as db_system_assignment, }; @@ -65,40 +65,6 @@ impl From<&db_assignment::Model> for Assignment { } } -impl From<(&db_assignment::Model, Option<&String>)> for Assignment { - fn from(value: (&db_assignment::Model, Option<&String>)) -> Self { - let mut assignment = Assignment::from(value.0.clone()); - if let Some(val) = value.1 { - assignment.role_name = Some(val.clone()); - } - assignment - } -} - -impl From<(db_assignment::Model, Option)> for Assignment { - fn from(value: (db_assignment::Model, Option)) -> Self { - let mut assignment = Assignment::from(value.0); - if let Some(val) = value.1 { - assignment.role_name = Some(val.name); - } - assignment - } -} - -impl TryFrom<(db_system_assignment::Model, Option)> for Assignment { - type Error = AssignmentProviderError; - - fn try_from( - value: (db_system_assignment::Model, Option), - ) -> Result { - let mut assignment = Assignment::try_from(value.0)?; - if let Some(val) = value.1 { - assignment.role_name = Some(val.name); - } - Ok(assignment) - } -} - impl From for AssignmentType { fn from(value: DbAssignmentType) -> Self { match value { @@ -128,21 +94,6 @@ impl TryFrom<&AssignmentType> for DbAssignmentType { } } -impl TryFrom<&str> for AssignmentType { - type Error = AssignmentProviderError; - fn try_from(value: &str) -> Result { - match value { - "GroupDomain" => Ok(Self::GroupDomain), - "GroupProject" => Ok(Self::GroupProject), - "GroupSystem" => Ok(Self::GroupSystem), - "UserDomain" => Ok(Self::UserDomain), - "UserProject" => Ok(Self::UserProject), - "UserSystem" => Ok(Self::UserSystem), - _ => Err(AssignmentProviderError::InvalidAssignmentType(value.into())), - } - } -} - #[cfg(test)] pub mod tests { use crate::db::entity::{assignment, sea_orm_active_enums, system_assignment}; diff --git a/crates/keystone/src/assignment/mod.rs b/crates/keystone/src/assignment/mod.rs index 9ef2cf7e..6d4ec17b 100644 --- a/crates/keystone/src/assignment/mod.rs +++ b/crates/keystone/src/assignment/mod.rs @@ -35,189 +35,5 @@ //! itself. This way for an assignment on the domain level the actor //! will get the role on the every project of the domain, but not the domain //! itself. -use async_trait::async_trait; -use std::sync::Arc; -use validator::Validate; - +pub use openstack_keystone_core::assignment::*; pub mod backend; -pub mod error; -#[cfg(test)] -mod mock; -pub mod types; - -use crate::config::Config; -use crate::identity::IdentityApi; -use crate::keystone::ServiceState; -use crate::plugin_manager::PluginManager; -use crate::resource::ResourceApi; -use crate::revoke::{RevokeApi, types::RevocationEventCreate}; -use backend::{AssignmentBackend, SqlBackend}; -use error::AssignmentProviderError; -use types::*; - -#[cfg(test)] -pub use mock::MockAssignmentProvider; -pub use types::AssignmentApi; - -pub struct AssignmentProvider { - backend_driver: Arc, -} - -impl AssignmentProvider { - pub fn new( - config: &Config, - plugin_manager: &PluginManager, - ) -> Result { - let backend_driver = if let Some(driver) = - plugin_manager.get_assignment_backend(config.assignment.driver.clone()) - { - driver.clone() - } else { - match config.assignment.driver.as_str() { - "sql" => Arc::new(SqlBackend::default()), - other => { - return Err(AssignmentProviderError::UnsupportedDriver( - other.to_string(), - )); - } - } - }; - Ok(Self { backend_driver }) - } -} - -#[async_trait] -impl AssignmentApi for AssignmentProvider { - /// Create assignment grant. - #[tracing::instrument(level = "info", skip(self, state))] - async fn create_grant( - &self, - state: &ServiceState, - grant: AssignmentCreate, - ) -> Result { - self.backend_driver.create_grant(state, grant).await - } - - /// List role assignments - #[tracing::instrument(level = "info", skip(self, state))] - async fn list_role_assignments( - &self, - state: &ServiceState, - params: &RoleAssignmentListParameters, - ) -> Result, AssignmentProviderError> { - params.validate()?; - let mut request = RoleAssignmentListForMultipleActorTargetParametersBuilder::default(); - let mut actors: Vec = Vec::new(); - let mut targets: Vec = Vec::new(); - if let Some(role_id) = ¶ms.role_id { - request.role_id(role_id); - } - if let Some(uid) = ¶ms.user_id { - actors.push(uid.into()); - } - if let Some(true) = ¶ms.effective - && let Some(uid) = ¶ms.user_id - { - let users = state - .provider - .get_identity_provider() - .list_groups_of_user(state, uid) - .await?; - actors.extend(users.into_iter().map(|x| x.id)); - }; - if let Some(val) = ¶ms.project_id { - targets.push(RoleAssignmentTarget { - id: val.clone(), - r#type: RoleAssignmentTargetType::Project, - inherited: Some(false), - }); - if let Some(parents) = state - .provider - .get_resource_provider() - .get_project_parents(state, val) - .await? - { - parents.iter().for_each(|parent_project| { - targets.push(RoleAssignmentTarget { - id: parent_project.id.clone(), - r#type: RoleAssignmentTargetType::Project, - inherited: Some(true), - }); - }); - } - } else if let Some(val) = ¶ms.domain_id { - targets.push(RoleAssignmentTarget { - id: val.clone(), - r#type: RoleAssignmentTargetType::Domain, - inherited: Some(false), - }); - } else if let Some(val) = ¶ms.system_id { - targets.push(RoleAssignmentTarget { - id: val.clone(), - r#type: RoleAssignmentTargetType::System, - inherited: Some(false), - }) - } - request.targets(targets); - request.actors(actors); - self.backend_driver - .list_assignments_for_multiple_actors_and_targets(state, &request.build()?) - .await - } - - /// Revoke grant - #[tracing::instrument(level = "info", skip(self, state))] - async fn revoke_grant( - &self, - state: &ServiceState, - grant: Assignment, - ) -> Result<(), AssignmentProviderError> { - // Call backend with reference (no move) - self.backend_driver.revoke_grant(state, &grant).await?; - - // Determine user_id or group_id - let user_id = match &grant.r#type { - AssignmentType::UserDomain - | AssignmentType::UserProject - | AssignmentType::UserSystem => Some(grant.actor_id.clone()), - - AssignmentType::GroupDomain - | AssignmentType::GroupProject - | AssignmentType::GroupSystem => None, - }; - - // Determine project_id or domain_id - let (project_id, domain_id) = match &grant.r#type { - AssignmentType::UserProject | AssignmentType::GroupProject => { - (Some(grant.target_id.clone()), None) - } - AssignmentType::UserDomain | AssignmentType::GroupDomain => { - (None, Some(grant.target_id.clone())) - } - AssignmentType::UserSystem | AssignmentType::GroupSystem => (None, None), - }; - - let revocation_event = RevocationEventCreate { - domain_id, - project_id, - user_id, - role_id: Some(grant.role_id.clone()), - trust_id: None, - consumer_id: None, - access_token_id: None, - issued_before: chrono::Utc::now(), - expires_at: None, - audit_id: None, - audit_chain_id: None, - revoked_at: chrono::Utc::now(), - }; - - state - .provider - .get_revoke_provider() - .create_revocation_event(state, revocation_event) - .await?; - - Ok(()) - } -} diff --git a/crates/keystone/src/auth/mod.rs b/crates/keystone/src/auth/mod.rs index c9cf9022..54521f03 100644 --- a/crates/keystone/src/auth/mod.rs +++ b/crates/keystone/src/auth/mod.rs @@ -20,320 +20,4 @@ //! present here to be shared across different authentication methods. The //! same is valid for the authorization validation (project/domain must exist //! and be enabled). - -use chrono::{DateTime, Utc}; -use derive_builder::Builder; -use serde::{Deserialize, Serialize}; -use thiserror::Error; -use tracing::warn; - -use crate::application_credential::types::ApplicationCredential; -use crate::error::BuilderError; -use crate::identity::types::{Group, UserResponse}; -use crate::resource::types::{Domain, Project}; -use crate::trust::types::Trust; - -#[derive(Error, Debug)] -pub enum AuthenticationError { - /// Domain is disabled. - #[error("The domain is disabled.")] - DomainDisabled(String), - - /// Project is disabled. - #[error("The project is disabled.")] - ProjectDisabled(String), - - /// Structures builder error. - #[error(transparent)] - StructBuilder { - /// The source of the error. - #[from] - source: BuilderError, - }, - - /// Token renewal is forbidden. - #[error("Token renewal (getting token from token) is prohibited.")] - TokenRenewalForbidden, - - /// Unauthorized. - #[error("The request you have made requires authentication.")] - Unauthorized, - - /// User is disabled. - #[error("The account is disabled for user: {0}")] - UserDisabled(String), - - /// User is locked due to the multiple failed attempts. - #[error("The account is temporarily disabled for user: {0}")] - UserLocked(String), - - /// User name password combination is wrong. - #[error("wrong username or password")] - UserNameOrPasswordWrong, - - /// User password is expired. - #[error("The password is expired for user: {0}")] - UserPasswordExpired(String), -} - -/// Information about successful authentication. -#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize)] -#[builder(build_fn(error = "BuilderError"))] -#[builder(setter(into, strip_option))] -pub struct AuthenticatedInfo { - /// Application credential. - #[builder(default)] - pub application_credential: Option, - - /// Audit IDs. - #[builder(default)] - pub audit_ids: Vec, - - /// Authentication expiration. - #[builder(default)] - pub expires_at: Option>, - - /// Federated IDP id. - #[builder(default)] - pub idp_id: Option, - - /// Authentication methods. - #[builder(default)] - pub methods: Vec, - - /// Federated protocol id. - #[builder(default)] - pub protocol_id: Option, - - /// Token restriction. - #[builder(default)] - pub token_restriction_id: Option, - - /// Resolved user object. - #[builder(default)] - pub user: Option, - - /// Resolved user domain information. - #[builder(default)] - pub user_domain: Option, - - /// Resolved user object. - #[builder(default)] - pub user_groups: Vec, - - /// User id. - pub user_id: String, -} - -impl AuthenticatedInfo { - pub fn builder() -> AuthenticatedInfoBuilder { - AuthenticatedInfoBuilder::default() - } - - /// Validate the authentication information: - /// - /// - User attribute must be set - /// - User must be enabled - /// - User object id must match user_id - pub fn validate(&self) -> Result<(), AuthenticationError> { - // TODO: all validations (disabled user, locked, etc) should be placed here - // since every authentication method goes different way and we risk - // missing validations - if let Some(user) = &self.user { - if user.id != self.user_id { - warn!( - "User data does not match the user_id attribute: {} vs {}", - self.user_id, user.id - ); - return Err(AuthenticationError::Unauthorized); - } - if !user.enabled { - return Err(AuthenticationError::UserDisabled(self.user_id.clone())); - } - } else { - warn!( - "User data must be resolved in the AuthenticatedInfo before validating: {:?}", - self - ); - return Err(AuthenticationError::Unauthorized); - } - - Ok(()) - } -} - -/// Authorization information. -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] -pub enum AuthzInfo { - /// Domain scope. - Domain(Domain), - /// Project scope. - Project(Project), - /// System scope. - System, - /// Trust scope. - Trust(Trust), - /// Unscoped. - Unscoped, -} - -impl AuthzInfo { - /// Validate the authorization information: - /// - /// - Unscoped: always valid - /// - Project: check if the project is enabled - /// - Domain: check if the domain is enabled - pub fn validate(&self) -> Result<(), AuthenticationError> { - match self { - AuthzInfo::Domain(domain) => { - if !domain.enabled { - return Err(AuthenticationError::DomainDisabled(domain.id.clone())); - } - } - AuthzInfo::Project(project) => { - if !project.enabled { - return Err(AuthenticationError::ProjectDisabled(project.id.clone())); - } - } - AuthzInfo::System => {} - AuthzInfo::Trust(_) => {} - AuthzInfo::Unscoped => {} - } - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - use tracing_test::traced_test; - - use crate::identity::types::{UserOptions, UserResponse}; - - #[test] - fn test_authn_validate_no_user() { - let authn = AuthenticatedInfo::builder().user_id("uid").build().unwrap(); - if let Err(AuthenticationError::Unauthorized) = authn.validate() { - } else { - panic!("should be unauthorized"); - } - } - - #[test] - #[traced_test] - fn test_authn_validate_user_disabled() { - let authn = AuthenticatedInfo::builder() - .user_id("uid") - .user(UserResponse { - id: "uid".to_string(), - enabled: false, - default_project_id: None, - domain_id: "did".into(), - extra: None, - name: "foo".into(), - options: UserOptions::default(), - federated: None, - password_expires_at: None, - }) - .build() - .unwrap(); - if let Err(AuthenticationError::UserDisabled(uid)) = authn.validate() { - assert_eq!("uid", uid); - } else { - panic!("should fail for disabled user"); - } - } - - #[test] - #[traced_test] - fn test_authn_validate_user_mismatch() { - let authn = AuthenticatedInfo::builder() - .user_id("uid1") - .user(UserResponse { - id: "uid2".to_string(), - enabled: false, - default_project_id: None, - domain_id: "did".into(), - extra: None, - name: "foo".into(), - options: UserOptions::default(), - federated: None, - password_expires_at: None, - }) - .build() - .unwrap(); - if let Err(AuthenticationError::Unauthorized) = authn.validate() { - } else { - panic!("should fail when user_id != user.id"); - } - } - - #[test] - #[traced_test] - fn test_authz_validate_project() { - let authz = AuthzInfo::Project(Project { - id: "pid".into(), - domain_id: "pdid".into(), - enabled: true, - ..Default::default() - }); - assert!(authz.validate().is_ok()); - } - - #[test] - #[traced_test] - fn test_authz_validate_project_disabled() { - let authz = AuthzInfo::Project(Project { - id: "pid".into(), - domain_id: "pdid".into(), - enabled: false, - ..Default::default() - }); - if let Err(AuthenticationError::ProjectDisabled(..)) = authz.validate() { - } else { - panic!("should fail when project is not enabled"); - } - } - - #[test] - #[traced_test] - fn test_authz_validate_domain() { - let authz = AuthzInfo::Domain(Domain { - id: "id".into(), - name: "name".into(), - enabled: true, - ..Default::default() - }); - assert!(authz.validate().is_ok()); - } - - #[test] - #[traced_test] - fn test_authz_validate_domain_disabled() { - let authz = AuthzInfo::Domain(Domain { - id: "id".into(), - name: "name".into(), - enabled: false, - ..Default::default() - }); - if let Err(AuthenticationError::DomainDisabled(..)) = authz.validate() { - } else { - panic!("should fail when domain is not enabled"); - } - } - - #[test] - #[traced_test] - fn test_authz_validate_system() { - let authz = AuthzInfo::System; - assert!(authz.validate().is_ok()); - } - - #[test] - #[traced_test] - fn test_authz_validate_unscoped() { - let authz = AuthzInfo::Unscoped; - assert!(authz.validate().is_ok()); - } -} +pub use openstack_keystone_core::auth::*; diff --git a/crates/keystone/src/bin/keystone.rs b/crates/keystone/src/bin/keystone.rs index b0b6f668..d19bd0b7 100644 --- a/crates/keystone/src/bin/keystone.rs +++ b/crates/keystone/src/bin/keystone.rs @@ -51,7 +51,7 @@ use openstack_keystone::config::Config; use openstack_keystone::federation::FederationApi; use openstack_keystone::keystone::{Service, ServiceState}; use openstack_keystone::plugin_manager::PluginManager; -use openstack_keystone::policy::PolicyEnforcer; +use openstack_keystone::policy::HttpPolicyEnforcer; use openstack_keystone::provider::Provider; use openstack_keystone::webauthn; use openstack_keystone_distributed_storage::app::get_app_server; @@ -195,18 +195,12 @@ async fn main() -> Result<(), Report> { .await .wrap_err("Database connection failed")?; - let mut plugin_manager = PluginManager::default(); + let plugin_manager = PluginManager::default(); + let provider = Provider::new(cfg.clone(), &plugin_manager)?; - plugin_manager.register_token_restriction_backend( - "sql", - Arc::new(openstack_keystone::token::token_restriction::SqlBackend::default()), - ); + let policy = HttpPolicyEnforcer::new(cfg.api_policy.opa_base_url.clone()).await?; - let provider = Provider::new(cfg.clone(), plugin_manager)?; - - let policy = PolicyEnforcer::http(cfg.api_policy.opa_base_url.clone()).await?; - - let shared_state = Arc::new(Service::new(cfg.clone(), conn, provider, policy)?); + let shared_state = Arc::new(Service::new(cfg.clone(), conn, provider, Arc::new(policy))?); spawn(cleanup(cloned_token, shared_state.clone())); diff --git a/crates/keystone/src/catalog/backend.rs b/crates/keystone/src/catalog/backend.rs index fbcd064b..00c45d8e 100644 --- a/crates/keystone/src/catalog/backend.rs +++ b/crates/keystone/src/catalog/backend.rs @@ -12,49 +12,5 @@ // // SPDX-License-Identifier: Apache-2.0 -use async_trait::async_trait; - pub mod sql; - -use crate::catalog::error::CatalogProviderError; -use crate::catalog::types::{Endpoint, EndpointListParameters, Service, ServiceListParameters}; -use crate::keystone::ServiceState; - -#[cfg_attr(test, mockall::automock)] -#[async_trait] -pub trait CatalogBackend: Send + Sync { - /// List services - async fn list_services( - &self, - state: &ServiceState, - params: &ServiceListParameters, - ) -> Result, CatalogProviderError>; - - /// Get single service by ID - async fn get_service<'a>( - &self, - state: &ServiceState, - id: &'a str, - ) -> Result, CatalogProviderError>; - - /// List Endpoints - async fn list_endpoints( - &self, - state: &ServiceState, - params: &EndpointListParameters, - ) -> Result, CatalogProviderError>; - - /// Get single endpoint by ID - async fn get_endpoint<'a>( - &self, - state: &ServiceState, - id: &'a str, - ) -> Result, CatalogProviderError>; - - /// Get Catalog (Services with Endpoints) - async fn get_catalog( - &self, - state: &ServiceState, - enabled: bool, - ) -> Result)>, CatalogProviderError>; -} +pub use openstack_keystone_core::catalog::backend::CatalogBackend; diff --git a/crates/keystone/src/catalog/mod.rs b/crates/keystone/src/catalog/mod.rs index 428b1ea3..8b481d2f 100644 --- a/crates/keystone/src/catalog/mod.rs +++ b/crates/keystone/src/catalog/mod.rs @@ -30,104 +30,5 @@ //! An OpenStack service, such as Compute (nova), Object Storage (swift), or //! Image service (glance), that provides one or more endpoints through which //! users can access resources and perform operations. -use async_trait::async_trait; -use std::sync::Arc; - +pub use openstack_keystone_core::catalog::*; pub mod backend; -pub mod error; -#[cfg(test)] -mod mock; -pub mod types; - -use crate::catalog::backend::CatalogBackend; -use crate::catalog::backend::sql::SqlBackend; -use crate::catalog::error::CatalogProviderError; -use crate::config::Config; -use crate::keystone::ServiceState; -use crate::plugin_manager::PluginManager; - -#[cfg(test)] -pub use mock::MockCatalogProvider; -pub use types::CatalogApi; - -use types::*; - -pub struct CatalogProvider { - backend_driver: Arc, -} - -impl CatalogProvider { - pub fn new( - config: &Config, - plugin_manager: &PluginManager, - ) -> Result { - let backend_driver = if let Some(driver) = - plugin_manager.get_catalog_backend(config.catalog.driver.clone()) - { - driver.clone() - } else { - match config.resource.driver.as_str() { - "sql" => Arc::new(SqlBackend::default()), - _ => { - return Err(CatalogProviderError::UnsupportedDriver( - config.resource.driver.clone(), - )); - } - } - }; - Ok(Self { backend_driver }) - } -} - -#[async_trait] -impl CatalogApi for CatalogProvider { - /// List services - #[tracing::instrument(level = "info", skip(self, state))] - async fn list_services( - &self, - state: &ServiceState, - params: &ServiceListParameters, - ) -> Result, CatalogProviderError> { - self.backend_driver.list_services(state, params).await - } - - /// Get single service by ID - #[tracing::instrument(level = "info", skip(self, state))] - async fn get_service<'a>( - &self, - state: &ServiceState, - id: &'a str, - ) -> Result, CatalogProviderError> { - self.backend_driver.get_service(state, id).await - } - - /// List Endpoints - #[tracing::instrument(level = "info", skip(self, state))] - async fn list_endpoints( - &self, - state: &ServiceState, - params: &EndpointListParameters, - ) -> Result, CatalogProviderError> { - self.backend_driver.list_endpoints(state, params).await - } - - /// Get single endpoint by ID - #[tracing::instrument(level = "info", skip(self, state))] - async fn get_endpoint<'a>( - &self, - state: &ServiceState, - id: &'a str, - ) -> Result, CatalogProviderError> { - self.backend_driver.get_endpoint(state, id).await - } - - /// Get catalog - #[tracing::instrument(level = "info", skip(self, state))] - async fn get_catalog( - &self, - state: &ServiceState, - enabled: bool, - ) -> Result)>, CatalogProviderError> { - self.backend_driver.get_catalog(state, enabled).await - } -} diff --git a/crates/keystone/src/common.rs b/crates/keystone/src/common.rs index c58376de..b1f1843e 100644 --- a/crates/keystone/src/common.rs +++ b/crates/keystone/src/common.rs @@ -12,5 +12,4 @@ // // SPDX-License-Identifier: Apache-2.0 //! # Common functionality -pub mod password_hashing; -pub mod types; +pub use openstack_keystone_core::common::*; diff --git a/crates/keystone/src/config.rs b/crates/keystone/src/config.rs index a6837912..8b879053 100644 --- a/crates/keystone/src/config.rs +++ b/crates/keystone/src/config.rs @@ -11,174 +11,4 @@ // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 -//! # Keystone configuration -//! -//! Parsing of the Keystone configuration file implementation. -use config::{File, FileFormat}; -use eyre::{Report, WrapErr}; -use serde::Deserialize; -use std::path::PathBuf; - -mod application_credentials; -mod assignment; -mod auth; -mod catalog; -mod common; -mod database; -mod default; -mod distributed_storage; -mod federation; -mod fernet_token; -mod identity; -mod identity_mapping; -mod k8s_auth; -mod policy; -mod resource; -mod revoke; -mod role; -mod security_compliance; -mod token; -mod token_restriction; -mod trust; -mod webauthn; - -use application_credentials::ApplicationCredentialProvider; -use assignment::AssignmentProvider; -use auth::AuthProvider; -use catalog::CatalogProvider; -use database::DatabaseSection; -pub use default::DefaultSection; -use distributed_storage::DistributedStorageConfiguration; -use federation::FederationProvider; -pub use fernet_token::FernetTokenProvider; -pub use identity::*; -use identity_mapping::IdentityMappingProvider; -use k8s_auth::K8sAuthProvider; -use policy::PolicyProvider; -use resource::ResourceProvider; -use revoke::RevokeProvider; -use role::RoleProvider; -use security_compliance::SecurityComplianceProvider; -use token::TokenProvider; -pub use token::TokenProviderDriver; -use token_restriction::TokenRestrictionProvider; -use trust::TrustProvider; -use webauthn::WebauthnSection; - -/// Keystone configuration. -#[derive(Debug, Default, Deserialize, Clone)] -pub struct Config { - /// Application credentials provider configuration. - #[serde(default)] - pub application_credential: ApplicationCredentialProvider, - - /// API policy enforcement. - #[serde(default)] - pub api_policy: PolicyProvider, - - /// Assignments (roles) provider configuration. - #[serde(default)] - pub assignment: AssignmentProvider, - - /// Authentication configuration. - pub auth: AuthProvider, - - /// Catalog provider configuration. - #[serde(default)] - pub catalog: CatalogProvider, - - /// Database configuration. - //#[serde(default)] - pub database: DatabaseSection, - - /// Global configuration options. - #[serde(rename = "DEFAULT", default)] - pub default: DefaultSection, - - /// Distributed storage configuration. - #[serde(default)] - pub distributed_storage: Option, - - /// Federation provider configuration. - #[serde(default)] - pub federation: FederationProvider, - - /// Fernet tokens provider configuration. - #[serde(default)] - pub fernet_tokens: FernetTokenProvider, - - /// Identity provider configuration. - #[serde(default)] - pub identity: IdentityProvider, - - /// Identity mapping provider configuration. - #[serde(default)] - pub identity_mapping: IdentityMappingProvider, - - /// K8s Auth provider configuration. - #[serde(default)] - pub k8s_auth: K8sAuthProvider, - - /// Resource provider configuration. - #[serde(default)] - pub resource: ResourceProvider, - - /// Revoke provider configuration. - #[serde(default)] - pub revoke: RevokeProvider, - - /// Role provider configuration. - #[serde(default)] - pub role: RoleProvider, - - /// Security compliance configuration. - #[serde(default)] - pub security_compliance: SecurityComplianceProvider, - - /// Token provider configuration. - #[serde(default)] - pub token: TokenProvider, - - /// Token restriction provider configuration. - #[serde(default)] - pub token_restriction: TokenRestrictionProvider, - - /// Trust provider configuration. - #[serde(default)] - pub trust: TrustProvider, - - /// Webauthn configuration. - #[serde(default)] - pub webauthn: WebauthnSection, -} - -impl Config { - pub fn new(path: PathBuf) -> Result { - let mut builder = config::Config::builder(); - - if std::path::Path::new(&path).is_file() { - builder = builder - .add_source(File::from(path).format(FileFormat::Ini)) - .add_source( - config::Environment::with_prefix("OS") - .prefix_separator("_") - .separator("__"), - ); - } - - builder.try_into() - } -} - -impl TryFrom> for Config { - type Error = Report; - fn try_from( - builder: config::ConfigBuilder, - ) -> Result { - builder - .build() - .wrap_err("Failed to read configuration file")? - .try_deserialize() - .wrap_err("Failed to parse configuration file") - } -} +pub use openstack_keystone_core::config::*; diff --git a/crates/keystone/src/error.rs b/crates/keystone/src/error.rs index dd4b6d54..95e08c25 100644 --- a/crates/keystone/src/error.rs +++ b/crates/keystone/src/error.rs @@ -14,204 +14,205 @@ //! # Error //! //! Diverse errors that can occur during the Keystone processing (not the API). +pub use openstack_keystone_core::error::*; use thiserror::Error; - -use crate::application_credential::error::ApplicationCredentialProviderError; -use crate::assignment::error::AssignmentProviderError; -use crate::catalog::error::CatalogProviderError; -use crate::federation::error::FederationProviderError; -use crate::identity::error::IdentityProviderError; -use crate::identity_mapping::error::IdentityMappingProviderError; -use crate::k8s_auth::error::K8sAuthProviderError; -use crate::policy::PolicyError; -use crate::resource::error::ResourceProviderError; -use crate::revoke::error::RevokeProviderError; -use crate::role::error::RoleProviderError; -use crate::token::TokenProviderError; -use crate::trust::TrustProviderError; -use crate::webauthn::WebauthnError; - -/// Keystone error. -#[derive(Debug, Error)] -pub enum KeystoneError { - /// Application credential provider. - #[error(transparent)] - ApplicationCredential { - /// The source of the error. - #[from] - source: ApplicationCredentialProviderError, - }, - - /// Assignment provider. - #[error(transparent)] - AssignmentProvider { - /// The source of the error. - #[from] - source: AssignmentProviderError, - }, - - /// Catalog provider. - #[error(transparent)] - CatalogProvider { - /// The source of the error. - #[from] - source: CatalogProviderError, - }, - - /// Federation provider. - #[error(transparent)] - FederationProvider { - /// The source of the error. - #[from] - source: FederationProviderError, - }, - - /// Identity provider. - #[error(transparent)] - IdentityProvider { - /// The source of the error. - #[from] - source: IdentityProviderError, - }, - - /// Identity mapping provider. - #[error(transparent)] - IdentityMapping { - /// The source of the error. - #[from] - source: IdentityMappingProviderError, - }, - - /// IO error. - #[error(transparent)] - IO { - /// The source of the error. - #[from] - source: std::io::Error, - }, - - /// Json serialization error. - #[error("json serde error: {}", source)] - Json { - /// The source of the error. - #[from] - source: serde_json::Error, - }, - - /// K8s auth provider. - #[error(transparent)] - K8sAuthProvider { - /// The source of the error. - #[from] - source: K8sAuthProviderError, - }, - - /// Policy engine. - #[error(transparent)] - Policy { - /// The source of the error. - #[from] - source: PolicyError, - }, - - /// Policy engine is not available. - #[error("policy enforcement is requested, but not available with the enabled features")] - PolicyEnforcementNotAvailable, - - /// Resource provider. - #[error(transparent)] - ResourceProvider { - /// The source of the error. - #[from] - source: ResourceProviderError, - }, - - /// Revoke provider error. - #[error(transparent)] - RevokeProvider { - /// The source of the error. - #[from] - source: RevokeProviderError, - }, - - /// Role provider. - #[error(transparent)] - RoleProvider { - /// The source of the error. - #[from] - source: RoleProviderError, - }, - - /// Token provider. - #[error(transparent)] - TokenProvider { - /// The source of the error. - #[from] - source: TokenProviderError, - }, - - /// Trust provider. - #[error(transparent)] - TrustProvider { - /// The source of the error. - #[from] - source: TrustProviderError, - }, - - /// Url parsing error. - #[error(transparent)] - UrlParse { - #[from] - source: url::ParseError, - }, - - /// WebauthN error. - #[error(transparent)] - Webauthn { - /// The source of the error. - #[from] - source: WebauthnError, - }, -} - -/// Builder error. -/// -/// A wrapper error that is used instead of the error generated by the -/// `derive_builder`. -#[derive(Debug, Error)] -#[non_exhaustive] -pub enum BuilderError { - /// Uninitialized field. - #[error("{0}")] - UninitializedField(String), - /// Custom validation error. - #[error("{0}")] - Validation(String), -} - -impl From for BuilderError { - fn from(s: String) -> Self { - Self::Validation(s) - } -} - -impl From for BuilderError { - fn from(value: openstack_keystone_api_types::error::BuilderError) -> Self { - match value { - openstack_keystone_api_types::error::BuilderError::UninitializedField(e) => { - Self::UninitializedField(e) - } - openstack_keystone_api_types::error::BuilderError::Validation(e) => Self::Validation(e), - } - } -} - -impl From for BuilderError { - fn from(ufe: derive_builder::UninitializedFieldError) -> Self { - Self::UninitializedField(ufe.to_string()) - } -} - +// +//use crate::application_credential::error::ApplicationCredentialProviderError; +//use crate::assignment::error::AssignmentProviderError; +//use crate::catalog::error::CatalogProviderError; +//use crate::federation::error::FederationProviderError; +//use crate::identity::error::IdentityProviderError; +//use crate::identity_mapping::error::IdentityMappingProviderError; +//use crate::k8s_auth::error::K8sAuthProviderError; +//use crate::policy::PolicyError; +//use crate::resource::error::ResourceProviderError; +//use crate::revoke::error::RevokeProviderError; +//use crate::role::error::RoleProviderError; +//use crate::token::TokenProviderError; +//use crate::trust::TrustProviderError; +//use crate::webauthn::WebauthnError; +// +///// Keystone error. +//#[derive(Debug, Error)] +//pub enum KeystoneError { +// /// Application credential provider. +// #[error(transparent)] +// ApplicationCredential { +// /// The source of the error. +// #[from] +// source: ApplicationCredentialProviderError, +// }, +// +// /// Assignment provider. +// #[error(transparent)] +// AssignmentProvider { +// /// The source of the error. +// #[from] +// source: AssignmentProviderError, +// }, +// +// /// Catalog provider. +// #[error(transparent)] +// CatalogProvider { +// /// The source of the error. +// #[from] +// source: CatalogProviderError, +// }, +// +// /// Federation provider. +// #[error(transparent)] +// FederationProvider { +// /// The source of the error. +// #[from] +// source: FederationProviderError, +// }, +// +// /// Identity provider. +// #[error(transparent)] +// IdentityProvider { +// /// The source of the error. +// #[from] +// source: IdentityProviderError, +// }, +// +// /// Identity mapping provider. +// #[error(transparent)] +// IdentityMapping { +// /// The source of the error. +// #[from] +// source: IdentityMappingProviderError, +// }, +// +// /// IO error. +// #[error(transparent)] +// IO { +// /// The source of the error. +// #[from] +// source: std::io::Error, +// }, +// +// /// Json serialization error. +// #[error("json serde error: {}", source)] +// Json { +// /// The source of the error. +// #[from] +// source: serde_json::Error, +// }, +// +// /// K8s auth provider. +// #[error(transparent)] +// K8sAuthProvider { +// /// The source of the error. +// #[from] +// source: K8sAuthProviderError, +// }, +// +// /// Policy engine. +// #[error(transparent)] +// Policy { +// /// The source of the error. +// #[from] +// source: PolicyError, +// }, +// +// /// Policy engine is not available. +// #[error("policy enforcement is requested, but not available with the enabled features")] +// PolicyEnforcementNotAvailable, +// +// /// Resource provider. +// #[error(transparent)] +// ResourceProvider { +// /// The source of the error. +// #[from] +// source: ResourceProviderError, +// }, +// +// /// Revoke provider error. +// #[error(transparent)] +// RevokeProvider { +// /// The source of the error. +// #[from] +// source: RevokeProviderError, +// }, +// +// /// Role provider. +// #[error(transparent)] +// RoleProvider { +// /// The source of the error. +// #[from] +// source: RoleProviderError, +// }, +// +// /// Token provider. +// #[error(transparent)] +// TokenProvider { +// /// The source of the error. +// #[from] +// source: TokenProviderError, +// }, +// +// /// Trust provider. +// #[error(transparent)] +// TrustProvider { +// /// The source of the error. +// #[from] +// source: TrustProviderError, +// }, +// +// /// Url parsing error. +// #[error(transparent)] +// UrlParse { +// #[from] +// source: url::ParseError, +// }, +// +// /// WebauthN error. +// #[error(transparent)] +// Webauthn { +// /// The source of the error. +// #[from] +// source: WebauthnError, +// }, +//} +// +///// Builder error. +///// +///// A wrapper error that is used instead of the error generated by the +///// `derive_builder`. +//#[derive(Debug, Error)] +//#[non_exhaustive] +//pub enum BuilderError { +// /// Uninitialized field. +// #[error("{0}")] +// UninitializedField(String), +// /// Custom validation error. +// #[error("{0}")] +// Validation(String), +//} +// +//impl From for BuilderError { +// fn from(s: String) -> Self { +// Self::Validation(s) +// } +//} +// +//impl From for BuilderError { +// fn from(value: openstack_keystone_api_types::error::BuilderError) -> Self { +// match value { +// openstack_keystone_api_types::error::BuilderError::UninitializedField(e) => { +// Self::UninitializedField(e) +// } +// openstack_keystone_api_types::error::BuilderError::Validation(e) => Self::Validation(e), +// } +// } +//} +// +//impl From for BuilderError { +// fn from(ufe: derive_builder::UninitializedFieldError) -> Self { +// Self::UninitializedField(ufe.to_string()) +// } +//} +// /// Context aware database error. #[derive(Debug, Error)] pub enum DatabaseError { diff --git a/crates/keystone/src/federation/api/error.rs b/crates/keystone/src/federation/api/error.rs index 1668c4c3..03e98fc8 100644 --- a/crates/keystone/src/federation/api/error.rs +++ b/crates/keystone/src/federation/api/error.rs @@ -17,7 +17,7 @@ use tracing::{Level, error, instrument}; use crate::api::error::KeystoneApiError; use crate::federation::api::types::*; -use crate::federation::error::FederationProviderError; +//use crate::federation::error::FederationProviderError; #[derive(Error, Debug)] pub enum OidcError { @@ -247,19 +247,19 @@ impl From for KeystoneApiError { } } -impl From for KeystoneApiError { - fn from(source: FederationProviderError) -> Self { - match source { - FederationProviderError::IdentityProviderNotFound(x) => Self::NotFound { - resource: "identity provider".into(), - identifier: x, - }, - FederationProviderError::MappingNotFound(x) => Self::NotFound { - resource: "mapping provider".into(), - identifier: x, - }, - FederationProviderError::Conflict(x) => Self::Conflict(x), - other => Self::InternalError(other.to_string()), - } - } -} +//impl From for KeystoneApiError { +// fn from(source: FederationProviderError) -> Self { +// match source { +// FederationProviderError::IdentityProviderNotFound(x) => Self::NotFound { +// resource: "identity provider".into(), +// identifier: x, +// }, +// FederationProviderError::MappingNotFound(x) => Self::NotFound { +// resource: "mapping provider".into(), +// identifier: x, +// }, +// FederationProviderError::Conflict(x) => Self::Conflict(x), +// other => Self::InternalError(other.to_string()), +// } +// } +//} diff --git a/crates/keystone/src/federation/api/identity_provider/create.rs b/crates/keystone/src/federation/api/identity_provider/create.rs index bc7027a2..b9e9c35c 100644 --- a/crates/keystone/src/federation/api/identity_provider/create.rs +++ b/crates/keystone/src/federation/api/identity_provider/create.rs @@ -102,7 +102,7 @@ mod tests { }); let state = get_mocked_state( - Provider::mocked_builder().federation(federation_mock), + Provider::mocked_builder().mock_federation(federation_mock), true, None, None, diff --git a/crates/keystone/src/federation/api/identity_provider/delete.rs b/crates/keystone/src/federation/api/identity_provider/delete.rs index 038f975d..fb43e011 100644 --- a/crates/keystone/src/federation/api/identity_provider/delete.rs +++ b/crates/keystone/src/federation/api/identity_provider/delete.rs @@ -142,7 +142,7 @@ mod tests { .returning(|_, _| Ok(())); let state = get_mocked_state( - Provider::mocked_builder().federation(federation_mock), + Provider::mocked_builder().mock_federation(federation_mock), true, None, None, diff --git a/crates/keystone/src/federation/api/identity_provider/list.rs b/crates/keystone/src/federation/api/identity_provider/list.rs index 0ed2104a..50397479 100644 --- a/crates/keystone/src/federation/api/identity_provider/list.rs +++ b/crates/keystone/src/federation/api/identity_provider/list.rs @@ -147,7 +147,7 @@ mod tests { }]) }); let state = get_mocked_state( - Provider::mocked_builder().federation(federation_mock), + Provider::mocked_builder().mock_federation(federation_mock), true, None, None, @@ -217,7 +217,7 @@ mod tests { }); let state = get_mocked_state( - Provider::mocked_builder().federation(federation_mock), + Provider::mocked_builder().mock_federation(federation_mock), true, None, None, @@ -250,7 +250,7 @@ mod tests { async fn test_list_forbidden() { let federation_mock = MockFederationProvider::default(); let state = get_mocked_state( - Provider::mocked_builder().federation(federation_mock), + Provider::mocked_builder().mock_federation(federation_mock), false, None, None, @@ -299,7 +299,7 @@ mod tests { }); let state = get_mocked_state( - Provider::mocked_builder().federation(federation_mock), + Provider::mocked_builder().mock_federation(federation_mock), true, Some(false), None, @@ -353,7 +353,7 @@ mod tests { }); let state = get_mocked_state( - Provider::mocked_builder().federation(federation_mock), + Provider::mocked_builder().mock_federation(federation_mock), true, Some(true), None, @@ -404,7 +404,7 @@ mod tests { }); let state = get_mocked_state( - Provider::mocked_builder().federation(federation_mock), + Provider::mocked_builder().mock_federation(federation_mock), true, Some(false), None, diff --git a/crates/keystone/src/federation/api/identity_provider/show.rs b/crates/keystone/src/federation/api/identity_provider/show.rs index 22d69319..f1a3e98c 100644 --- a/crates/keystone/src/federation/api/identity_provider/show.rs +++ b/crates/keystone/src/federation/api/identity_provider/show.rs @@ -116,7 +116,7 @@ mod tests { }); let state = get_mocked_state( - Provider::mocked_builder().federation(federation_mock), + Provider::mocked_builder().mock_federation(federation_mock), true, None, None, @@ -194,7 +194,7 @@ mod tests { }); let state = get_mocked_state( - Provider::mocked_builder().federation(federation_mock), + Provider::mocked_builder().mock_federation(federation_mock), false, None, None, diff --git a/crates/keystone/src/federation/api/identity_provider/update.rs b/crates/keystone/src/federation/api/identity_provider/update.rs index 15503e57..9eaeede5 100644 --- a/crates/keystone/src/federation/api/identity_provider/update.rs +++ b/crates/keystone/src/federation/api/identity_provider/update.rs @@ -129,7 +129,7 @@ mod tests { }); let state = get_mocked_state( - Provider::mocked_builder().federation(federation_mock), + Provider::mocked_builder().mock_federation(federation_mock), true, None, None, diff --git a/crates/keystone/src/federation/api/jwt.rs b/crates/keystone/src/federation/api/jwt.rs index 42c37c81..045a3fbb 100644 --- a/crates/keystone/src/federation/api/jwt.rs +++ b/crates/keystone/src/federation/api/jwt.rs @@ -339,7 +339,7 @@ pub async fn login( .map_err(KeystoneApiError::forbidden)?; let mut api_token = KeystoneTokenResponse { - token: token.build_api_token_v4(&state).await?, + token: crate::api::v4::auth::token::token_impl::build_api_token_v4(&token, &state).await?, }; let catalog: Catalog = Catalog( state diff --git a/crates/keystone/src/federation/api/mapping/create.rs b/crates/keystone/src/federation/api/mapping/create.rs index b59b58fd..c7afd0a6 100644 --- a/crates/keystone/src/federation/api/mapping/create.rs +++ b/crates/keystone/src/federation/api/mapping/create.rs @@ -92,7 +92,7 @@ mod tests { }); let state = get_mocked_state( - Provider::mocked_builder().federation(federation_mock), + Provider::mocked_builder().mock_federation(federation_mock), true, None, None, diff --git a/crates/keystone/src/federation/api/mapping/delete.rs b/crates/keystone/src/federation/api/mapping/delete.rs index 691321d2..4adb6222 100644 --- a/crates/keystone/src/federation/api/mapping/delete.rs +++ b/crates/keystone/src/federation/api/mapping/delete.rs @@ -121,7 +121,7 @@ mod tests { .returning(|_, _| Ok(())); let state = get_mocked_state( - Provider::mocked_builder().federation(federation_mock), + Provider::mocked_builder().mock_federation(federation_mock), true, None, None, diff --git a/crates/keystone/src/federation/api/mapping/list.rs b/crates/keystone/src/federation/api/mapping/list.rs index 3ce93baf..cc02cc3b 100644 --- a/crates/keystone/src/federation/api/mapping/list.rs +++ b/crates/keystone/src/federation/api/mapping/list.rs @@ -116,7 +116,7 @@ mod tests { }); let state = get_mocked_state( - Provider::mocked_builder().federation(federation_mock), + Provider::mocked_builder().mock_federation(federation_mock), true, None, None, @@ -203,7 +203,7 @@ mod tests { }); let state = get_mocked_state( - Provider::mocked_builder().federation(federation_mock), + Provider::mocked_builder().mock_federation(federation_mock), true, None, None, @@ -251,7 +251,7 @@ mod tests { }); let state = get_mocked_state( - Provider::mocked_builder().federation(federation_mock), + Provider::mocked_builder().mock_federation(federation_mock), true, None, None, diff --git a/crates/keystone/src/federation/api/mapping/show.rs b/crates/keystone/src/federation/api/mapping/show.rs index 0dfc5b01..73d5973d 100644 --- a/crates/keystone/src/federation/api/mapping/show.rs +++ b/crates/keystone/src/federation/api/mapping/show.rs @@ -118,7 +118,7 @@ mod tests { }); let state = get_mocked_state( - Provider::mocked_builder().federation(federation_mock), + Provider::mocked_builder().mock_federation(federation_mock), true, None, None, diff --git a/crates/keystone/src/federation/api/mapping/update.rs b/crates/keystone/src/federation/api/mapping/update.rs index 3c5e95e2..89649aef 100644 --- a/crates/keystone/src/federation/api/mapping/update.rs +++ b/crates/keystone/src/federation/api/mapping/update.rs @@ -126,7 +126,7 @@ mod tests { }); let state = get_mocked_state( - Provider::mocked_builder().federation(federation_mock), + Provider::mocked_builder().mock_federation(federation_mock), true, None, None, diff --git a/crates/keystone/src/federation/api/oidc.rs b/crates/keystone/src/federation/api/oidc.rs index b11c91da..3169ad24 100644 --- a/crates/keystone/src/federation/api/oidc.rs +++ b/crates/keystone/src/federation/api/oidc.rs @@ -329,7 +329,7 @@ pub async fn callback( .map_err(KeystoneApiError::forbidden)?; let mut api_token = KeystoneTokenResponse { - token: token.build_api_token_v4(&state).await?, + token: crate::api::v4::auth::token::token_impl::build_api_token_v4(&token, &state).await?, }; let catalog: Catalog = Catalog( state diff --git a/crates/keystone/src/federation/api/types/identity_provider.rs b/crates/keystone/src/federation/api/types/identity_provider.rs index c13c9170..ceb41138 100644 --- a/crates/keystone/src/federation/api/types/identity_provider.rs +++ b/crates/keystone/src/federation/api/types/identity_provider.rs @@ -12,11 +12,11 @@ // // SPDX-License-Identifier: Apache-2.0 //! Federated identity provider types. -use axum::{ - Json, - http::StatusCode, - response::{IntoResponse, Response}, -}; +//use axum::{ +// Json, +// http::StatusCode, +// response::{IntoResponse, Response}, +//}; use openstack_keystone_api_types::federation::identity_provider; @@ -29,92 +29,92 @@ pub use identity_provider::IdentityProviderResponse; pub use identity_provider::IdentityProviderUpdate; pub use identity_provider::IdentityProviderUpdateRequest; -use crate::federation::types; +//use crate::federation::types; use crate::api::common::{QueryParameterPagination, ResourceIdentifier}; -impl From for IdentityProvider { - fn from(value: types::IdentityProvider) -> Self { - Self { - id: value.id, - name: value.name, - domain_id: value.domain_id, - enabled: value.enabled, - oidc_discovery_url: value.oidc_discovery_url, - oidc_client_id: value.oidc_client_id, - oidc_response_mode: value.oidc_response_mode, - oidc_response_types: value.oidc_response_types, - jwks_url: value.jwks_url, - jwt_validation_pubkeys: value.jwt_validation_pubkeys, - bound_issuer: value.bound_issuer, - default_mapping_name: value.default_mapping_name, - provider_config: value.provider_config, - } - } -} +//impl From for IdentityProvider { +// fn from(value: types::IdentityProvider) -> Self { +// Self { +// id: value.id, +// name: value.name, +// domain_id: value.domain_id, +// enabled: value.enabled, +// oidc_discovery_url: value.oidc_discovery_url, +// oidc_client_id: value.oidc_client_id, +// oidc_response_mode: value.oidc_response_mode, +// oidc_response_types: value.oidc_response_types, +// jwks_url: value.jwks_url, +// jwt_validation_pubkeys: value.jwt_validation_pubkeys, +// bound_issuer: value.bound_issuer, +// default_mapping_name: value.default_mapping_name, +// provider_config: value.provider_config, +// } +// } +//} -impl From for types::IdentityProviderCreate { - fn from(value: IdentityProviderCreateRequest) -> Self { - Self { - id: None, - name: value.identity_provider.name, - domain_id: value.identity_provider.domain_id, - enabled: value.identity_provider.enabled, - oidc_discovery_url: value.identity_provider.oidc_discovery_url, - oidc_client_id: value.identity_provider.oidc_client_id, - oidc_client_secret: value.identity_provider.oidc_client_secret, - oidc_response_mode: value.identity_provider.oidc_response_mode, - oidc_response_types: value.identity_provider.oidc_response_types, - jwks_url: value.identity_provider.jwks_url, - jwt_validation_pubkeys: value.identity_provider.jwt_validation_pubkeys, - bound_issuer: value.identity_provider.bound_issuer, - default_mapping_name: value.identity_provider.default_mapping_name, - provider_config: value.identity_provider.provider_config, - } - } -} +//impl From for types::IdentityProviderCreate { +// fn from(value: IdentityProviderCreateRequest) -> Self { +// Self { +// id: None, +// name: value.identity_provider.name, +// domain_id: value.identity_provider.domain_id, +// enabled: value.identity_provider.enabled, +// oidc_discovery_url: value.identity_provider.oidc_discovery_url, +// oidc_client_id: value.identity_provider.oidc_client_id, +// oidc_client_secret: value.identity_provider.oidc_client_secret, +// oidc_response_mode: value.identity_provider.oidc_response_mode, +// oidc_response_types: value.identity_provider.oidc_response_types, +// jwks_url: value.identity_provider.jwks_url, +// jwt_validation_pubkeys: value.identity_provider.jwt_validation_pubkeys, +// bound_issuer: value.identity_provider.bound_issuer, +// default_mapping_name: value.identity_provider.default_mapping_name, +// provider_config: value.identity_provider.provider_config, +// } +// } +//} -impl From for types::IdentityProviderUpdate { - fn from(value: IdentityProviderUpdateRequest) -> Self { - Self { - name: value.identity_provider.name, - enabled: value.identity_provider.enabled, - oidc_discovery_url: value.identity_provider.oidc_discovery_url, - oidc_client_id: value.identity_provider.oidc_client_id, - oidc_client_secret: value.identity_provider.oidc_client_secret, - oidc_response_mode: value.identity_provider.oidc_response_mode, - oidc_response_types: value.identity_provider.oidc_response_types, - jwks_url: value.identity_provider.jwks_url, - jwt_validation_pubkeys: value.identity_provider.jwt_validation_pubkeys, - bound_issuer: value.identity_provider.bound_issuer, - default_mapping_name: value.identity_provider.default_mapping_name, - provider_config: value.identity_provider.provider_config, - } - } -} +//impl From for types::IdentityProviderUpdate { +// fn from(value: IdentityProviderUpdateRequest) -> Self { +// Self { +// name: value.identity_provider.name, +// enabled: value.identity_provider.enabled, +// oidc_discovery_url: value.identity_provider.oidc_discovery_url, +// oidc_client_id: value.identity_provider.oidc_client_id, +// oidc_client_secret: value.identity_provider.oidc_client_secret, +// oidc_response_mode: value.identity_provider.oidc_response_mode, +// oidc_response_types: value.identity_provider.oidc_response_types, +// jwks_url: value.identity_provider.jwks_url, +// jwt_validation_pubkeys: value.identity_provider.jwt_validation_pubkeys, +// bound_issuer: value.identity_provider.bound_issuer, +// default_mapping_name: value.identity_provider.default_mapping_name, +// provider_config: value.identity_provider.provider_config, +// } +// } +//} -impl IntoResponse for types::IdentityProvider { - fn into_response(self) -> Response { - ( - StatusCode::OK, - Json(IdentityProviderResponse { - identity_provider: IdentityProvider::from(self), - }), - ) - .into_response() - } -} +//impl IntoResponse for types::IdentityProvider { +// fn into_response(self) -> Response { +// ( +// StatusCode::OK, +// Json(IdentityProviderResponse { +// identity_provider: IdentityProvider::from(self), +// }), +// ) +// .into_response() +// } +//} -impl From for types::IdentityProviderListParameters { - fn from(value: IdentityProviderListParameters) -> Self { - Self { - name: value.name, - domain_ids: None, //value.domain_id, - limit: value.limit, - marker: value.marker, - } - } -} +//impl From for types::IdentityProviderListParameters { +// fn from(value: IdentityProviderListParameters) -> Self { +// Self { +// name: value.name, +// domain_ids: None, //value.domain_id, +// limit: value.limit, +// marker: value.marker, +// } +// } +//} impl ResourceIdentifier for IdentityProvider { fn get_id(&self) -> String { diff --git a/crates/keystone/src/federation/api/types/mapping.rs b/crates/keystone/src/federation/api/types/mapping.rs index 66c2bc05..cc5252ec 100644 --- a/crates/keystone/src/federation/api/types/mapping.rs +++ b/crates/keystone/src/federation/api/types/mapping.rs @@ -12,20 +12,20 @@ // // SPDX-License-Identifier: Apache-2.0 //! Federated attribute mapping types. -use axum::{ - Json, - http::StatusCode, - response::{IntoResponse, Response}, -}; -use uuid::Uuid; +//use axum::{ +// Json, +// http::StatusCode, +// response::{IntoResponse, Response}, +//}; +//use uuid::Uuid; use openstack_keystone_api_types::federation::mapping; use crate::api::{ - KeystoneApiError, + // KeystoneApiError, common::{QueryParameterPagination, ResourceIdentifier}, }; -use crate::federation::types; +//use crate::federation::types; pub use mapping::Mapping; pub use mapping::MappingCreate; @@ -37,120 +37,120 @@ pub use mapping::MappingType; pub use mapping::MappingUpdate; pub use mapping::MappingUpdateRequest; -impl From for Mapping { - fn from(value: types::Mapping) -> Self { - Self { - id: value.id, - name: value.name, - domain_id: value.domain_id, - idp_id: value.idp_id, - r#type: value.r#type.into(), - enabled: value.enabled, - allowed_redirect_uris: value.allowed_redirect_uris, - user_id_claim: value.user_id_claim, - user_name_claim: value.user_name_claim, - domain_id_claim: value.domain_id_claim, - groups_claim: value.groups_claim, - bound_audiences: value.bound_audiences, - bound_subject: value.bound_subject, - bound_claims: value.bound_claims, - oidc_scopes: value.oidc_scopes, - token_project_id: value.token_project_id, - token_restriction_id: value.token_restriction_id, - } - } -} +//impl From for Mapping { +// fn from(value: types::Mapping) -> Self { +// Self { +// id: value.id, +// name: value.name, +// domain_id: value.domain_id, +// idp_id: value.idp_id, +// r#type: value.r#type.into(), +// enabled: value.enabled, +// allowed_redirect_uris: value.allowed_redirect_uris, +// user_id_claim: value.user_id_claim, +// user_name_claim: value.user_name_claim, +// domain_id_claim: value.domain_id_claim, +// groups_claim: value.groups_claim, +// bound_audiences: value.bound_audiences, +// bound_subject: value.bound_subject, +// bound_claims: value.bound_claims, +// oidc_scopes: value.oidc_scopes, +// token_project_id: value.token_project_id, +// token_restriction_id: value.token_restriction_id, +// } +// } +//} -impl From for types::Mapping { - fn from(value: MappingCreateRequest) -> Self { - Self { - id: value.mapping.id.unwrap_or_else(|| Uuid::new_v4().into()), - name: value.mapping.name, - domain_id: value.mapping.domain_id, - idp_id: value.mapping.idp_id, - r#type: value.mapping.r#type.unwrap_or_default().into(), - enabled: value.mapping.enabled, - allowed_redirect_uris: value.mapping.allowed_redirect_uris, - user_id_claim: value.mapping.user_id_claim, - user_name_claim: value.mapping.user_name_claim, - domain_id_claim: value.mapping.domain_id_claim, - groups_claim: value.mapping.groups_claim, - bound_audiences: value.mapping.bound_audiences, - bound_subject: value.mapping.bound_subject, - bound_claims: value.mapping.bound_claims, - oidc_scopes: value.mapping.oidc_scopes, - token_project_id: value.mapping.token_project_id, - token_restriction_id: value.mapping.token_restriction_id, - } - } -} +//impl From for types::Mapping { +// fn from(value: MappingCreateRequest) -> Self { +// Self { +// id: value.mapping.id.unwrap_or_else(|| Uuid::new_v4().into()), +// name: value.mapping.name, +// domain_id: value.mapping.domain_id, +// idp_id: value.mapping.idp_id, +// r#type: value.mapping.r#type.unwrap_or_default().into(), +// enabled: value.mapping.enabled, +// allowed_redirect_uris: value.mapping.allowed_redirect_uris, +// user_id_claim: value.mapping.user_id_claim, +// user_name_claim: value.mapping.user_name_claim, +// domain_id_claim: value.mapping.domain_id_claim, +// groups_claim: value.mapping.groups_claim, +// bound_audiences: value.mapping.bound_audiences, +// bound_subject: value.mapping.bound_subject, +// bound_claims: value.mapping.bound_claims, +// oidc_scopes: value.mapping.oidc_scopes, +// token_project_id: value.mapping.token_project_id, +// token_restriction_id: value.mapping.token_restriction_id, +// } +// } +//} -impl From for types::MappingUpdate { - fn from(value: MappingUpdateRequest) -> Self { - Self { - name: value.mapping.name, - idp_id: value.mapping.idp_id, - r#type: value.mapping.r#type.map(Into::into), - enabled: value.mapping.enabled, - allowed_redirect_uris: value.mapping.allowed_redirect_uris, - user_id_claim: value.mapping.user_id_claim, - user_name_claim: value.mapping.user_name_claim, - domain_id_claim: value.mapping.domain_id_claim, - groups_claim: value.mapping.groups_claim, - bound_audiences: value.mapping.bound_audiences, - bound_subject: value.mapping.bound_subject, - bound_claims: value.mapping.bound_claims, - oidc_scopes: value.mapping.oidc_scopes, - token_project_id: value.mapping.token_project_id, - token_restriction_id: value.mapping.token_restriction_id, - } - } -} - -impl IntoResponse for types::Mapping { - fn into_response(self) -> Response { - ( - StatusCode::OK, - Json(MappingResponse { - mapping: Mapping::from(self), - }), - ) - .into_response() - } -} - -impl From for MappingType { - fn from(value: types::MappingType) -> MappingType { - match value { - types::MappingType::Oidc => MappingType::Oidc, - types::MappingType::Jwt => MappingType::Jwt, - } - } -} - -impl From for types::MappingType { - fn from(value: MappingType) -> types::MappingType { - match value { - MappingType::Oidc => types::MappingType::Oidc, - MappingType::Jwt => types::MappingType::Jwt, - } - } -} - -impl TryFrom for types::MappingListParameters { - type Error = KeystoneApiError; - - fn try_from(value: MappingListParameters) -> Result { - Ok(Self { - domain_id: value.domain_id, - idp_id: value.idp_id, - limit: value.limit, - marker: value.marker, - name: value.name, - r#type: value.r#type.map(Into::into), - }) - } -} +//impl From for types::MappingUpdate { +// fn from(value: MappingUpdateRequest) -> Self { +// Self { +// name: value.mapping.name, +// idp_id: value.mapping.idp_id, +// r#type: value.mapping.r#type.map(Into::into), +// enabled: value.mapping.enabled, +// allowed_redirect_uris: value.mapping.allowed_redirect_uris, +// user_id_claim: value.mapping.user_id_claim, +// user_name_claim: value.mapping.user_name_claim, +// domain_id_claim: value.mapping.domain_id_claim, +// groups_claim: value.mapping.groups_claim, +// bound_audiences: value.mapping.bound_audiences, +// bound_subject: value.mapping.bound_subject, +// bound_claims: value.mapping.bound_claims, +// oidc_scopes: value.mapping.oidc_scopes, +// token_project_id: value.mapping.token_project_id, +// token_restriction_id: value.mapping.token_restriction_id, +// } +// } +//} +// +//impl IntoResponse for types::Mapping { +// fn into_response(self) -> Response { +// ( +// StatusCode::OK, +// Json(MappingResponse { +// mapping: Mapping::from(self), +// }), +// ) +// .into_response() +// } +//} +// +//impl From for MappingType { +// fn from(value: types::MappingType) -> MappingType { +// match value { +// types::MappingType::Oidc => MappingType::Oidc, +// types::MappingType::Jwt => MappingType::Jwt, +// } +// } +//} +// +//impl From for types::MappingType { +// fn from(value: MappingType) -> types::MappingType { +// match value { +// MappingType::Oidc => types::MappingType::Oidc, +// MappingType::Jwt => types::MappingType::Jwt, +// } +// } +//} +// +//impl TryFrom for types::MappingListParameters { +// type Error = KeystoneApiError; +// +// fn try_from(value: MappingListParameters) -> Result { +// Ok(Self { +// domain_id: value.domain_id, +// idp_id: value.idp_id, +// limit: value.limit, +// marker: value.marker, +// name: value.name, +// r#type: value.r#type.map(Into::into), +// }) +// } +//} impl ResourceIdentifier for Mapping { fn get_id(&self) -> String { diff --git a/crates/keystone/src/federation/backend.rs b/crates/keystone/src/federation/backend.rs index 679791fd..441ab2fb 100644 --- a/crates/keystone/src/federation/backend.rs +++ b/crates/keystone/src/federation/backend.rs @@ -12,113 +12,6 @@ // // SPDX-License-Identifier: Apache-2.0 -use async_trait::async_trait; - -use crate::federation::FederationProviderError; -use crate::federation::types::*; -use crate::keystone::ServiceState; - pub mod sql; pub use sql::SqlBackend; - -/// Backend driver interface for the Federation Provider. -#[cfg_attr(test, mockall::automock)] -#[async_trait] -pub trait FederationBackend: Send + Sync { - /// Cleanup expired resources. - async fn cleanup(&self, state: &ServiceState) -> Result<(), FederationProviderError>; - - /// Create new authentication state. - async fn create_auth_state( - &self, - state: &ServiceState, - auth_state: AuthState, - ) -> Result; - - /// Create Identity provider. - async fn create_identity_provider( - &self, - state: &ServiceState, - idp: IdentityProviderCreate, - ) -> Result; - - /// Create mapping. - async fn create_mapping( - &self, - state: &ServiceState, - idp: Mapping, - ) -> Result; - - /// Delete authentication state. - async fn delete_auth_state<'a>( - &self, - state: &ServiceState, - id: &'a str, - ) -> Result<(), FederationProviderError>; - - /// Delete identity provider. - async fn delete_identity_provider<'a>( - &self, - state: &ServiceState, - id: &'a str, - ) -> Result<(), FederationProviderError>; - - /// Delete mapping. - async fn delete_mapping<'a>( - &self, - state: &ServiceState, - id: &'a str, - ) -> Result<(), FederationProviderError>; - - /// Get authentication state. - async fn get_auth_state<'a>( - &self, - state: &ServiceState, - id: &'a str, - ) -> Result, FederationProviderError>; - - /// Get single identity provider by ID. - async fn get_identity_provider<'a>( - &self, - state: &ServiceState, - id: &'a str, - ) -> Result, FederationProviderError>; - - /// Get single mapping by ID. - async fn get_mapping<'a>( - &self, - state: &ServiceState, - id: &'a str, - ) -> Result, FederationProviderError>; - - /// List Identity Providers. - async fn list_identity_providers( - &self, - state: &ServiceState, - params: &IdentityProviderListParameters, - ) -> Result, FederationProviderError>; - - /// List Identity Providers. - async fn list_mappings( - &self, - state: &ServiceState, - params: &MappingListParameters, - ) -> Result, FederationProviderError>; - - /// Update Identity provider. - async fn update_identity_provider<'a>( - &self, - state: &ServiceState, - id: &'a str, - idp: IdentityProviderUpdate, - ) -> Result; - - /// Update mapping. - async fn update_mapping<'a>( - &self, - state: &ServiceState, - id: &'a str, - idp: MappingUpdate, - ) -> Result; -} diff --git a/crates/keystone/src/federation/backend/sql.rs b/crates/keystone/src/federation/backend/sql.rs index e00fe7e8..b7114989 100644 --- a/crates/keystone/src/federation/backend/sql.rs +++ b/crates/keystone/src/federation/backend/sql.rs @@ -14,8 +14,10 @@ use async_trait::async_trait; +use openstack_keystone_core::federation::backend::FederationBackend; + use super::super::types::*; -use crate::federation::{FederationProviderError, backend::FederationBackend}; +use crate::federation::FederationProviderError; use crate::keystone::ServiceState; mod auth_state; diff --git a/crates/keystone/src/federation/backend/sql/mapping/list.rs b/crates/keystone/src/federation/backend/sql/mapping/list.rs index 632e7b37..2c62b278 100644 --- a/crates/keystone/src/federation/backend/sql/mapping/list.rs +++ b/crates/keystone/src/federation/backend/sql/mapping/list.rs @@ -78,7 +78,7 @@ mod tests { use super::super::tests::get_mapping_mock; use super::*; - use crate::federation::mapping::MappingType; + use crate::federation::types::mapping::MappingType; #[tokio::test] async fn test_query_all() { diff --git a/crates/keystone/src/federation/backend/sql/mapping/update.rs b/crates/keystone/src/federation/backend/sql/mapping/update.rs index e7840e0d..db5eb6bf 100644 --- a/crates/keystone/src/federation/backend/sql/mapping/update.rs +++ b/crates/keystone/src/federation/backend/sql/mapping/update.rs @@ -98,7 +98,7 @@ mod tests { use super::super::tests::get_mapping_mock; use super::*; - use crate::federation::mapping::MappingType; + use crate::federation::types::mapping::MappingType; #[tokio::test] async fn test_update() { diff --git a/crates/keystone/src/federation/mod.rs b/crates/keystone/src/federation/mod.rs index 8c8a762f..48376a33 100644 --- a/crates/keystone/src/federation/mod.rs +++ b/crates/keystone/src/federation/mod.rs @@ -15,239 +15,7 @@ //! //! Federation provider implements the functionality necessary for the user //! federation. -use async_trait::async_trait; -use std::sync::Arc; -use uuid::Uuid; - pub mod api; pub mod backend; -pub mod error; -#[cfg(test)] -pub mod mock; -pub mod types; - -use crate::config::Config; -use crate::federation::backend::{FederationBackend, SqlBackend}; -use crate::federation::error::FederationProviderError; -use crate::keystone::ServiceState; -use crate::plugin_manager::PluginManager; -use types::*; - -#[cfg(test)] -pub use mock::MockFederationProvider; -pub use types::FederationApi; - -pub struct FederationProvider { - backend_driver: Arc, -} - -impl FederationProvider { - pub fn new( - config: &Config, - plugin_manager: &PluginManager, - ) -> Result { - let backend_driver = if let Some(driver) = - plugin_manager.get_federation_backend(config.federation.driver.clone()) - { - driver.clone() - } else { - match config.federation.driver.as_str() { - "sql" => Arc::new(SqlBackend::default()), - _ => { - return Err(FederationProviderError::UnsupportedDriver( - config.resource.driver.clone(), - )); - } - } - }; - Ok(Self { backend_driver }) - } -} - -#[async_trait] -impl FederationApi for FederationProvider { - /// Cleanup expired resources. - #[tracing::instrument(level = "info", skip(self, state))] - async fn cleanup(&self, state: &ServiceState) -> Result<(), FederationProviderError> { - self.backend_driver.cleanup(state).await - } - - /// Create new auth state. - #[tracing::instrument(level = "debug", skip(self, state))] - async fn create_auth_state( - &self, - state: &ServiceState, - auth_state: AuthState, - ) -> Result { - self.backend_driver - .create_auth_state(state, auth_state) - .await - } - - /// Create Identity provider. - #[tracing::instrument(level = "debug", skip(self, state))] - async fn create_identity_provider( - &self, - state: &ServiceState, - idp: IdentityProviderCreate, - ) -> Result { - let mut mod_idp = idp; - if mod_idp.id.is_none() { - mod_idp.id = Some(Uuid::new_v4().simple().to_string()); - } - - self.backend_driver - .create_identity_provider(state, mod_idp) - .await - } - - /// Create mapping. - #[tracing::instrument(level = "debug", skip(self, state))] - async fn create_mapping( - &self, - state: &ServiceState, - mapping: Mapping, - ) -> Result { - let mut mod_mapping = mapping; - mod_mapping.id = Uuid::new_v4().into(); - if let Some(_pid) = &mod_mapping.token_project_id { - // ensure domain_id is set and matches the one of the project_id. - if let Some(_did) = &mod_mapping.domain_id { - // TODO: Get the project_id and compare the domain_id - } else { - return Err(FederationProviderError::MappingTokenProjectDomainUnset); - } - // TODO: ensure current user has access to the project - } - - self.backend_driver.create_mapping(state, mod_mapping).await - } - - /// Delete auth state. - #[tracing::instrument(level = "debug", skip(self, state))] - async fn delete_auth_state<'a>( - &self, - state: &ServiceState, - id: &'a str, - ) -> Result<(), FederationProviderError> { - self.backend_driver.delete_auth_state(state, id).await - } - - /// Delete identity provider. - #[tracing::instrument(level = "debug", skip(self, state))] - async fn delete_identity_provider<'a>( - &self, - state: &ServiceState, - id: &'a str, - ) -> Result<(), FederationProviderError> { - self.backend_driver - .delete_identity_provider(state, id) - .await - } - - /// Delete identity provider. - #[tracing::instrument(level = "debug", skip(self, state))] - async fn delete_mapping<'a>( - &self, - state: &ServiceState, - id: &'a str, - ) -> Result<(), FederationProviderError> { - self.backend_driver.delete_mapping(state, id).await - } - - /// Get auth state by ID. - #[tracing::instrument(level = "debug", skip(self, state))] - async fn get_auth_state<'a>( - &self, - state: &ServiceState, - id: &'a str, - ) -> Result, FederationProviderError> { - self.backend_driver.get_auth_state(state, id).await - } - - /// Get single IDP by ID. - #[tracing::instrument(level = "info", skip(self, state))] - async fn get_identity_provider<'a>( - &self, - state: &ServiceState, - id: &'a str, - ) -> Result, FederationProviderError> { - self.backend_driver.get_identity_provider(state, id).await - } - - /// Get single mapping by ID. - #[tracing::instrument(level = "info", skip(self, state))] - async fn get_mapping<'a>( - &self, - state: &ServiceState, - id: &'a str, - ) -> Result, FederationProviderError> { - self.backend_driver.get_mapping(state, id).await - } - - /// List IDP. - #[tracing::instrument(level = "info", skip(self, state))] - async fn list_identity_providers( - &self, - state: &ServiceState, - params: &IdentityProviderListParameters, - ) -> Result, FederationProviderError> { - self.backend_driver - .list_identity_providers(state, params) - .await - } - - /// List mappings. - #[tracing::instrument(level = "info", skip(self, state))] - async fn list_mappings( - &self, - state: &ServiceState, - params: &MappingListParameters, - ) -> Result, FederationProviderError> { - self.backend_driver.list_mappings(state, params).await - } - - /// Update Identity provider. - #[tracing::instrument(level = "debug", skip(self, state))] - async fn update_identity_provider<'a>( - &self, - state: &ServiceState, - id: &'a str, - idp: IdentityProviderUpdate, - ) -> Result { - self.backend_driver - .update_identity_provider(state, id, idp) - .await - } - - /// Update mapping - #[tracing::instrument(level = "debug", skip(self, state))] - async fn update_mapping<'a>( - &self, - state: &ServiceState, - id: &'a str, - mapping: MappingUpdate, - ) -> Result { - let current = self - .backend_driver - .get_mapping(state, id) - .await? - .ok_or_else(|| FederationProviderError::MappingNotFound(id.to_string()))?; - - if let Some(_new_idp_id) = &mapping.idp_id { - // TODO: Check the new idp_id domain escaping - } - if let Some(_pid) = &mapping.token_project_id { - // ensure domain_id is set and matches the one of the project_id. - if let Some(_did) = ¤t.domain_id { - // TODO: Get the project_id and compare the domain_id - } else { - return Err(FederationProviderError::MappingTokenProjectDomainUnset); - } - // TODO: ensure current user has access to the project - } - // TODO: Pass current to the backend to skip re-fetching - self.backend_driver.update_mapping(state, id, mapping).await - } -} +pub use openstack_keystone_core::federation::*; diff --git a/crates/keystone/src/identity/backend.rs b/crates/keystone/src/identity/backend.rs index a4f80f97..ab9ca402 100644 --- a/crates/keystone/src/identity/backend.rs +++ b/crates/keystone/src/identity/backend.rs @@ -12,200 +12,5 @@ // // SPDX-License-Identifier: Apache-2.0 -use async_trait::async_trait; -use chrono::{DateTime, Utc}; -use std::collections::HashSet; - -use crate::auth::AuthenticatedInfo; -use crate::identity::IdentityProviderError; -use crate::identity::types::*; -use crate::keystone::ServiceState; - pub mod sql; - -#[cfg_attr(test, mockall::automock)] -#[async_trait] -pub trait IdentityBackend: Send + Sync { - /// Add the user to the group. - async fn add_user_to_group<'a>( - &self, - state: &ServiceState, - user_id: &'a str, - group_id: &'a str, - ) -> Result<(), IdentityProviderError>; - - /// Add the user to the group with expiration. - async fn add_user_to_group_expiring<'a>( - &self, - state: &ServiceState, - user_id: &'a str, - group_id: &'a str, - idp_id: &'a str, - ) -> Result<(), IdentityProviderError>; - - /// Add user group membership relations. - async fn add_users_to_groups<'a>( - &self, - state: &ServiceState, - memberships: Vec<(&'a str, &'a str)>, - ) -> Result<(), IdentityProviderError>; - - /// Add expiring user group membership relations. - async fn add_users_to_groups_expiring<'a>( - &self, - state: &ServiceState, - memberships: Vec<(&'a str, &'a str)>, - idp_id: &'a str, - ) -> Result<(), IdentityProviderError>; - - /// Authenticate a user by a password. - async fn authenticate_by_password( - &self, - state: &ServiceState, - auth: &UserPasswordAuthRequest, - ) -> Result; - - /// Create group. - async fn create_group( - &self, - state: &ServiceState, - group: GroupCreate, - ) -> Result; - - /// Create service account. - async fn create_service_account( - &self, - state: &ServiceState, - sa: ServiceAccountCreate, - ) -> Result; - - /// Create user. - async fn create_user( - &self, - state: &ServiceState, - user: UserCreate, - ) -> Result; - - /// Delete group by ID. - async fn delete_group<'a>( - &self, - state: &ServiceState, - group_id: &'a str, - ) -> Result<(), IdentityProviderError>; - - /// Delete user. - async fn delete_user<'a>( - &self, - state: &ServiceState, - user_id: &'a str, - ) -> Result<(), IdentityProviderError>; - - /// Get single group by ID. - async fn get_group<'a>( - &self, - state: &ServiceState, - group_id: &'a str, - ) -> Result, IdentityProviderError>; - - /// Get single service account by ID. - async fn get_service_account<'a>( - &self, - state: &ServiceState, - user_id: &'a str, - ) -> Result, IdentityProviderError>; - - /// Get single user by ID. - async fn get_user<'a>( - &self, - state: &ServiceState, - user_id: &'a str, - ) -> Result, IdentityProviderError>; - - /// Get single user by ID. - async fn get_user_domain_id<'a>( - &self, - state: &ServiceState, - user_id: &'a str, - ) -> Result; - - /// Find federated user by IDP and Unique ID. - async fn find_federated_user<'a>( - &self, - state: &ServiceState, - idp_id: &'a str, - unique_id: &'a str, - ) -> Result, IdentityProviderError>; - - /// List groups. - async fn list_groups( - &self, - state: &ServiceState, - params: &GroupListParameters, - ) -> Result, IdentityProviderError>; - - /// List Users. - async fn list_users( - &self, - state: &ServiceState, - params: &UserListParameters, - ) -> Result, IdentityProviderError>; - - /// List groups a user is member of. - async fn list_groups_of_user<'a>( - &self, - state: &ServiceState, - user_id: &'a str, - ) -> Result, IdentityProviderError>; - - /// Remove the user from the group. - async fn remove_user_from_group<'a>( - &self, - state: &ServiceState, - user_id: &'a str, - group_id: &'a str, - ) -> Result<(), IdentityProviderError>; - - /// Remove the user from the group with expiration. - async fn remove_user_from_group_expiring<'a>( - &self, - state: &ServiceState, - user_id: &'a str, - group_id: &'a str, - idp_id: &'a str, - ) -> Result<(), IdentityProviderError>; - - /// Remove the user from multiple groups. - async fn remove_user_from_groups<'a>( - &self, - state: &ServiceState, - user_id: &'a str, - group_ids: HashSet<&'a str>, - ) -> Result<(), IdentityProviderError>; - - /// Remove the user from multiple expiring groups. - async fn remove_user_from_groups_expiring<'a>( - &self, - state: &ServiceState, - user_id: &'a str, - group_ids: HashSet<&'a str>, - idp_id: &'a str, - ) -> Result<(), IdentityProviderError>; - - /// Set group memberships for the user. - async fn set_user_groups<'a>( - &self, - state: &ServiceState, - user_id: &'a str, - group_ids: HashSet<&'a str>, - ) -> Result<(), IdentityProviderError>; - - /// Set expiring group memberships for the user. - async fn set_user_groups_expiring<'a>( - &self, - state: &ServiceState, - user_id: &'a str, - group_ids: HashSet<&'a str>, - idp_id: &'a str, - last_verified: Option<&'a DateTime>, - ) -> Result<(), IdentityProviderError>; -} +pub use sql::SqlBackend; diff --git a/crates/keystone/src/identity/backend/sql.rs b/crates/keystone/src/identity/backend/sql.rs index 94718efc..5b80e401 100644 --- a/crates/keystone/src/identity/backend/sql.rs +++ b/crates/keystone/src/identity/backend/sql.rs @@ -16,6 +16,8 @@ use async_trait::async_trait; use chrono::{DateTime, Utc}; use std::collections::HashSet; +use openstack_keystone_core::identity::backend::IdentityBackend; + mod authenticate; mod federated_user; mod group; @@ -30,7 +32,6 @@ mod user_option; use super::super::types::*; use crate::auth::AuthenticatedInfo; use crate::identity::IdentityProviderError; -use crate::identity::backend::IdentityBackend; use crate::keystone::ServiceState; #[derive(Default)] diff --git a/crates/keystone/src/identity/backend/sql/authenticate.rs b/crates/keystone/src/identity/backend/sql/authenticate.rs index da7b43f8..0ac814ac 100644 --- a/crates/keystone/src/identity/backend/sql/authenticate.rs +++ b/crates/keystone/src/identity/backend/sql/authenticate.rs @@ -23,6 +23,9 @@ use crate::auth::{AuthenticatedInfo, AuthenticationError}; use crate::common::password_hashing; use crate::config::Config; use crate::db::entity::{local_user as db_local_user, password as db_password}; +use crate::identity::backend::sql::local_user::MergeLocalUserData; +use crate::identity::backend::sql::password::MergePasswordData; +use crate::identity::backend::sql::user::MergeUserData; use crate::identity::{ IdentityProviderError, backend::sql::password, diff --git a/crates/keystone/src/identity/backend/sql/federated_user.rs b/crates/keystone/src/identity/backend/sql/federated_user.rs index 9200e0cf..841bf1cf 100644 --- a/crates/keystone/src/identity/backend/sql/federated_user.rs +++ b/crates/keystone/src/identity/backend/sql/federated_user.rs @@ -21,8 +21,14 @@ mod find; pub use create::create; pub use find::find_by_idp_and_unique_id; -impl UserResponseBuilder { - pub fn merge_federated_user_data(&mut self, data: I) -> &mut Self +pub trait MergeFederatedUserData { + fn merge_federated_user_data(&mut self, data: I) -> &mut Self + where + I: IntoIterator; +} + +impl MergeFederatedUserData for UserResponseBuilder { + fn merge_federated_user_data(&mut self, data: I) -> &mut Self where I: IntoIterator, { diff --git a/crates/keystone/src/identity/backend/sql/local_user.rs b/crates/keystone/src/identity/backend/sql/local_user.rs index 7b81691b..c30855f7 100644 --- a/crates/keystone/src/identity/backend/sql/local_user.rs +++ b/crates/keystone/src/identity/backend/sql/local_user.rs @@ -12,11 +12,8 @@ // // SPDX-License-Identifier: Apache-2.0 -use sea_orm::entity::*; - -use crate::db::entity::{local_user as db_local_user, user as db_user}; +use crate::db::entity::local_user as db_local_user; use crate::identity::types::*; -use crate::{config::Config, identity::IdentityProviderError}; mod create; mod get; @@ -28,37 +25,14 @@ pub use load::load_local_user_with_passwords; pub use load::load_local_users_passwords; pub use set::reset_failed_auth; -impl UserResponseBuilder { - pub fn merge_local_user_data(&mut self, data: &db_local_user::Model) -> &mut Self { - self.name(data.name.clone()); - self - } +pub trait MergeLocalUserData { + fn merge_local_user_data(&mut self, data: &db_local_user::Model) -> &mut Self; } -impl UserCreate { - /// Get `local_user::ActiveModel` from the `UserCreate` request. - pub(in super::super) fn to_local_user_active_model( - &self, - config: &Config, - main_record: &db_user::Model, - ) -> Result { - Ok(db_local_user::ActiveModel { - id: NotSet, - user_id: Set(main_record.id.clone()), - domain_id: Set(main_record.domain_id.clone()), - name: Set(self.name.clone()), - failed_auth_count: if main_record.enabled.is_some_and(|x| x) - && config - .security_compliance - .disable_user_account_days_inactive - .is_some() - { - Set(Some(0)) - } else { - NotSet - }, - failed_auth_at: NotSet, - }) +impl MergeLocalUserData for UserResponseBuilder { + fn merge_local_user_data(&mut self, data: &db_local_user::Model) -> &mut Self { + self.name(data.name.clone()); + self } } diff --git a/crates/keystone/src/identity/backend/sql/local_user/create.rs b/crates/keystone/src/identity/backend/sql/local_user/create.rs index ffb98b1b..45568aa1 100644 --- a/crates/keystone/src/identity/backend/sql/local_user/create.rs +++ b/crates/keystone/src/identity/backend/sql/local_user/create.rs @@ -30,9 +30,26 @@ pub async fn create( where C: ConnectionTrait, { - Ok(user - .to_local_user_active_model(conf, main_record)? - .insert(db) - .await - .context("inserting new user record")?) + Ok(local_user::ActiveModel { + id: NotSet, + user_id: Set(main_record.id.clone()), + domain_id: Set(main_record.domain_id.clone()), + name: Set(user.name.clone()), + failed_auth_count: if main_record.enabled.is_some_and(|x| x) + && conf + .security_compliance + .disable_user_account_days_inactive + .is_some() + { + Set(Some(0)) + } else { + NotSet + }, + failed_auth_at: NotSet, + } + //user + //.to_local_user_active_model(conf, main_record)? + .insert(db) + .await + .context("inserting new user record")?) } diff --git a/crates/keystone/src/identity/backend/sql/nonlocal_user.rs b/crates/keystone/src/identity/backend/sql/nonlocal_user.rs index 678dfb14..9fa173d0 100644 --- a/crates/keystone/src/identity/backend/sql/nonlocal_user.rs +++ b/crates/keystone/src/identity/backend/sql/nonlocal_user.rs @@ -21,8 +21,12 @@ mod get; pub use create::create; pub use get::*; -impl UserResponseBuilder { - pub fn merge_nonlocal_user_data(&mut self, data: &db_nonlocal_user::Model) -> &mut Self { +pub trait MergeNonlocalUserData { + fn merge_nonlocal_user_data(&mut self, data: &db_nonlocal_user::Model) -> &mut Self; +} + +impl MergeNonlocalUserData for UserResponseBuilder { + fn merge_nonlocal_user_data(&mut self, data: &db_nonlocal_user::Model) -> &mut Self { self.name(data.name.clone()); self } diff --git a/crates/keystone/src/identity/backend/sql/password.rs b/crates/keystone/src/identity/backend/sql/password.rs index 4f15deb4..432dbe82 100644 --- a/crates/keystone/src/identity/backend/sql/password.rs +++ b/crates/keystone/src/identity/backend/sql/password.rs @@ -34,8 +34,14 @@ pub(super) fn is_password_expired( Ok(false) } -impl UserResponseBuilder { - pub fn merge_passwords_data(&mut self, passwords: I) -> &mut Self +pub trait MergePasswordData { + fn merge_passwords_data(&mut self, passwords: I) -> &mut Self + where + I: IntoIterator; +} + +impl MergePasswordData for UserResponseBuilder { + fn merge_passwords_data(&mut self, passwords: I) -> &mut Self where I: IntoIterator, { diff --git a/crates/keystone/src/identity/backend/sql/service_account/create.rs b/crates/keystone/src/identity/backend/sql/service_account/create.rs index 701a8e44..0108302f 100644 --- a/crates/keystone/src/identity/backend/sql/service_account/create.rs +++ b/crates/keystone/src/identity/backend/sql/service_account/create.rs @@ -16,14 +16,44 @@ use chrono::{DateTime, Utc}; use sea_orm::DatabaseConnection; use sea_orm::TransactionTrait; use sea_orm::entity::*; +use uuid::Uuid; use crate::config::Config; +use crate::db::entity::user as db_user; use crate::error::DbContextExt; use crate::identity::{ IdentityProviderError, backend::sql::{nonlocal_user, user_option}, types::*, }; +use openstack_keystone_core::identity::types::get_user_last_active_at; + +impl db_user::ActiveModel { + fn from_sa_create( + user: &ServiceAccountCreate, + conf: &Config, + created_at: Option>, + ) -> Result { + let created_at = created_at.unwrap_or_else(Utc::now).naive_utc(); + + Ok(db_user::ActiveModel { + id: Set(user + .id + .clone() + .unwrap_or(Uuid::new_v4().simple().to_string())), + enabled: Set(Some(user.enabled.unwrap_or(true))), + extra: Set(Some("{}".to_string())), + default_project_id: NotSet, + // Set last_active to now if compliance disabling is on + last_active_at: get_user_last_active_at(conf, user.enabled, created_at) + .map(Set) + .unwrap_or(NotSet) + .into(), + created_at: Set(Some(created_at)), + domain_id: Set(user.domain_id.clone()), + }) + } +} /// Create a service account. /// @@ -43,8 +73,7 @@ pub async fn create( .await .context("starting transaction for persisting service account")?; - let main_entry = sa - .to_user_active_model(conf, created_at)? + let main_entry = db_user::ActiveModel::from_sa_create(&sa, conf, created_at)? .insert(&txn) .await .context("inserting main user for the service account entry")?; @@ -80,6 +109,25 @@ mod tests { use super::*; use crate::db::entity::{nonlocal_user, user}; + #[test] + fn test_active_record_from_sa_create() { + let now = Utc::now(); + let req = ServiceAccountCreate { + domain_id: "did".into(), + enabled: Some(true), + id: Some("said".into()), + name: "sa_name".into(), + }; + let cfg = Config::default(); + let sot = db_user::ActiveModel::from_sa_create(&req, &cfg, Some(now)).unwrap(); + assert_eq!(sot.default_project_id, NotSet); + assert_eq!(sot.domain_id, Set("did".into())); + assert_eq!(sot.enabled, Set(Some(true))); + assert_eq!(sot.extra, Set(Some("{}".into()))); + assert_eq!(sot.id, Set("said".into())); + assert_eq!(sot.last_active_at, NotSet); + } + #[tokio::test] async fn test_create() { // Create MockDatabase with mock query results diff --git a/crates/keystone/src/identity/backend/sql/user.rs b/crates/keystone/src/identity/backend/sql/user.rs index e9875959..b1e21a81 100644 --- a/crates/keystone/src/identity/backend/sql/user.rs +++ b/crates/keystone/src/identity/backend/sql/user.rs @@ -12,15 +12,19 @@ // // SPDX-License-Identifier: Apache-2.0 -use chrono::{DateTime, NaiveDate, Utc}; -use sea_orm::entity::*; -use serde_json::{Value, json}; +use chrono::NaiveDate; +//use chrono::{DateTime, NaiveDate, Utc}; +//use sea_orm::entity::*; +use serde_json::Value; use tracing::error; -use uuid::Uuid; +//use uuid::Uuid; -use crate::config::Config; +//use crate::config::Config; use crate::db::entity::user as db_user; -use crate::identity::{IdentityProviderError, types::*}; +use crate::identity::{ + //IdentityProviderError, + types::*, +}; mod create; mod delete; @@ -35,7 +39,15 @@ pub use get::{get, get_user_domain_id}; pub use list::list; pub use set::reset_last_active; -impl UserResponseBuilder { +pub trait MergeUserData { + fn merge_user_data( + &mut self, + user: &db_user::Model, + options: &UserOptions, + last_activity_cutof_date: Option<&NaiveDate>, + ) -> &mut Self; +} +impl MergeUserData for UserResponseBuilder { /// Merge the `user` table entry with corresponding user options into the /// [`UserResponseBuilder`]. /// @@ -52,7 +64,7 @@ impl UserResponseBuilder { /// [`user.last_active_at`](field@db_user::Model::last_active_at) `> /// last_activity_cutof_date`. Returns `true` when one or both are unset. /// - Defaults to `false` - pub(super) fn merge_user_data( + fn merge_user_data( &mut self, user: &db_user::Model, options: &UserOptions, @@ -90,70 +102,6 @@ impl UserResponseBuilder { } } -impl UserCreate { - /// Get `user::ActiveModel` from the `UserCreate` request. - pub(super) fn to_user_active_model( - &self, - config: &Config, - created_at: Option>, - ) -> Result { - let created_at = created_at.unwrap_or_else(Utc::now).naive_utc(); - - Ok(db_user::ActiveModel { - id: Set(self - .id - .clone() - .unwrap_or(Uuid::new_v4().simple().to_string())), - enabled: Set(Some(self.enabled.unwrap_or(true))), - extra: Set(Some(serde_json::to_string( - // For keystone it is important to have at least "{}" - &self.extra.as_ref().or(Some(&json!({}))), - )?)), - default_project_id: self - .default_project_id - .clone() - .map(Set) - .unwrap_or(NotSet) - .into(), - // Set last_active to now if compliance disabling is on - last_active_at: get_user_last_active_at(config, self.enabled, created_at) - .map(Set) - .unwrap_or(NotSet) - .into(), - created_at: Set(Some(created_at)), - domain_id: Set(self.domain_id.clone()), - }) - } -} - -impl ServiceAccountCreate { - /// Get a `db_user::ActiveModel` from the `ServiceAccountCreate` request. - pub(super) fn to_user_active_model( - &self, - conf: &Config, - created_at: Option>, - ) -> Result { - let created_at = created_at.unwrap_or_else(Utc::now).naive_utc(); - - Ok(db_user::ActiveModel { - id: Set(self - .id - .clone() - .unwrap_or(Uuid::new_v4().simple().to_string())), - enabled: Set(Some(self.enabled.unwrap_or(true))), - extra: Set(Some("{}".to_string())), - default_project_id: NotSet, - // Set last_active to now if compliance disabling is on - last_active_at: get_user_last_active_at(conf, self.enabled, created_at) - .map(Set) - .unwrap_or(NotSet) - .into(), - created_at: Set(Some(created_at)), - domain_id: Set(self.domain_id.clone()), - }) - } -} - #[cfg(test)] pub(super) mod tests { use chrono::{DateTime, Utc}; @@ -300,59 +248,59 @@ pub(super) mod tests { ); } - #[test] - fn test_active_record_from_user_create() { - let now = Utc::now(); - let req = UserCreateBuilder::default() - .default_project_id("dpid") - .domain_id("did") - .id("1") - .name("foo") - .enabled(true) - .build() - .unwrap(); - let cfg = Config::default(); - let sot = req.to_user_active_model(&cfg, Some(now)).unwrap(); - assert_eq!(sot.default_project_id, Set(Some("dpid".into()))); - assert_eq!(sot.domain_id, Set("did".into())); - assert_eq!(sot.enabled, Set(Some(true))); - assert_eq!(sot.extra, Set(Some("{}".into()))); - assert_eq!(sot.id, Set("1".into())); - assert_eq!(sot.last_active_at, NotSet); - } + //#[test] + //fn test_active_record_from_user_create() { + // let now = Utc::now(); + // let req = UserCreateBuilder::default() + // .default_project_id("dpid") + // .domain_id("did") + // .id("1") + // .name("foo") + // .enabled(true) + // .build() + // .unwrap(); + // let cfg = Config::default(); + // let sot = req.to_user_active_model(&cfg, Some(now)).unwrap(); + // assert_eq!(sot.default_project_id, Set(Some("dpid".into()))); + // assert_eq!(sot.domain_id, Set("did".into())); + // assert_eq!(sot.enabled, Set(Some(true))); + // assert_eq!(sot.extra, Set(Some("{}".into()))); + // assert_eq!(sot.id, Set("1".into())); + // assert_eq!(sot.last_active_at, NotSet); + //} - #[test] - fn test_active_record_from_user_create_track_user_activity() { - let now = Utc::now(); - let req = UserCreateBuilder::default() - .domain_id("did") - .id("1") - .name("foo") - .enabled(true) - .build() - .unwrap(); - let mut cfg = Config::default(); - cfg.security_compliance.disable_user_account_days_inactive = Some(1); - let sot = req.to_user_active_model(&cfg, Some(now)).unwrap(); - assert_eq!(sot.last_active_at, Set(Some(now.naive_utc().date()))); - } + //#[test] + //fn test_active_record_from_user_create_track_user_activity() { + // let now = Utc::now(); + // let req = UserCreateBuilder::default() + // .domain_id("did") + // .id("1") + // .name("foo") + // .enabled(true) + // .build() + // .unwrap(); + // let mut cfg = Config::default(); + // cfg.security_compliance.disable_user_account_days_inactive = Some(1); + // let sot = req.to_user_active_model(&cfg, Some(now)).unwrap(); + // assert_eq!(sot.last_active_at, Set(Some(now.naive_utc().date()))); + //} - #[test] - fn test_active_record_from_sa_create() { - let now = Utc::now(); - let req = ServiceAccountCreate { - domain_id: "did".into(), - enabled: Some(true), - id: Some("said".into()), - name: "sa_name".into(), - }; - let cfg = Config::default(); - let sot = req.to_user_active_model(&cfg, Some(now)).unwrap(); - assert_eq!(sot.default_project_id, NotSet); - assert_eq!(sot.domain_id, Set("did".into())); - assert_eq!(sot.enabled, Set(Some(true))); - assert_eq!(sot.extra, Set(Some("{}".into()))); - assert_eq!(sot.id, Set("said".into())); - assert_eq!(sot.last_active_at, NotSet); - } + //#[test] + //fn test_active_record_from_sa_create() { + // let now = Utc::now(); + // let req = ServiceAccountCreate { + // domain_id: "did".into(), + // enabled: Some(true), + // id: Some("said".into()), + // name: "sa_name".into(), + // }; + // let cfg = Config::default(); + // let sot = req.to_user_active_model(&cfg, Some(now)).unwrap(); + // assert_eq!(sot.default_project_id, NotSet); + // assert_eq!(sot.domain_id, Set("did".into())); + // assert_eq!(sot.enabled, Set(Some(true))); + // assert_eq!(sot.extra, Set(Some("{}".into()))); + // assert_eq!(sot.id, Set("said".into())); + // assert_eq!(sot.last_active_at, NotSet); + //} } diff --git a/crates/keystone/src/identity/backend/sql/user/create.rs b/crates/keystone/src/identity/backend/sql/user/create.rs index b60b9d81..8e57b629 100644 --- a/crates/keystone/src/identity/backend/sql/user/create.rs +++ b/crates/keystone/src/identity/backend/sql/user/create.rs @@ -14,8 +14,13 @@ use chrono::{DateTime, Utc}; use sea_orm::DatabaseConnection; +//use sea_orm::Iden; use sea_orm::entity::*; use sea_orm::{ConnectionTrait, TransactionTrait}; +use serde_json::json; +use uuid::Uuid; + +use openstack_keystone_core::identity::types::get_user_last_active_at; use crate::common::password_hashing; use crate::config::Config; @@ -23,6 +28,10 @@ use crate::db::entity::{ federated_user as db_federated_user, password as db_password, user as db_user, }; use crate::error::DbContextExt; +use crate::identity::backend::sql::federated_user::MergeFederatedUserData; +use crate::identity::backend::sql::local_user::MergeLocalUserData; +use crate::identity::backend::sql::password::MergePasswordData; +use crate::identity::backend::sql::user::MergeUserData; use crate::identity::{ IdentityProviderError, types::{UserCreate, UserOptions, UserResponse, UserResponseBuilder}, @@ -33,6 +42,41 @@ use super::super::local_user; use super::super::password; use super::super::user_option; +impl db_user::ActiveModel { + fn from_user_create( + user: &UserCreate, + config: &Config, + created_at: Option>, + ) -> Result { + let created_at = created_at.unwrap_or_else(Utc::now).naive_utc(); + + Ok(Self { + id: Set(user + .id + .clone() + .unwrap_or(Uuid::new_v4().simple().to_string())), + enabled: Set(Some(user.enabled.unwrap_or(true))), + extra: Set(Some(serde_json::to_string( + // For keystone it is important to have at least "{}" + &user.extra.as_ref().or(Some(&json!({}))), + )?)), + default_project_id: user + .default_project_id + .clone() + .map(Set) + .unwrap_or(NotSet) + .into(), + // Set last_active to now if compliance disabling is on + last_active_at: get_user_last_active_at(config, user.enabled, created_at) + .map(Set) + .unwrap_or(NotSet) + .into(), + created_at: Set(Some(created_at)), + domain_id: Set(user.domain_id.clone()), + }) + } +} + #[tracing::instrument(skip_all)] pub async fn create_main( conf: &Config, @@ -43,11 +87,14 @@ pub async fn create_main( where C: ConnectionTrait, { - Ok(user - .to_user_active_model(conf, created_at)? - .insert(db) - .await - .context("inserting user entry")?) + Ok( + db_user::ActiveModel::from_user_create(user, conf, created_at)? + // user + // .to_user_active_model(conf, created_at)? + .insert(db) + .await + .context("inserting user entry")?, + ) } #[tracing::instrument(skip(conf, db))] @@ -160,6 +207,43 @@ mod tests { user::{FederationBuilder, FederationProtocol}, }; + #[test] + fn test_active_record_from_user_create() { + let now = Utc::now(); + let req = UserCreateBuilder::default() + .default_project_id("dpid") + .domain_id("did") + .id("1") + .name("foo") + .enabled(true) + .build() + .unwrap(); + let cfg = Config::default(); + let sot = db_user::ActiveModel::from_user_create(&req, &cfg, Some(now)).unwrap(); //at)req.to_user_active_model(&cfg, Some(now)).unwrap(); + assert_eq!(sot.default_project_id, Set(Some("dpid".into()))); + assert_eq!(sot.domain_id, Set("did".into())); + assert_eq!(sot.enabled, Set(Some(true))); + assert_eq!(sot.extra, Set(Some("{}".into()))); + assert_eq!(sot.id, Set("1".into())); + assert_eq!(sot.last_active_at, NotSet); + } + + #[test] + fn test_active_record_from_user_create_track_user_activity() { + let now = Utc::now(); + let req = UserCreateBuilder::default() + .domain_id("did") + .id("1") + .name("foo") + .enabled(true) + .build() + .unwrap(); + let mut cfg = Config::default(); + cfg.security_compliance.disable_user_account_days_inactive = Some(1); + let sot = db_user::ActiveModel::from_user_create(&req, &cfg, Some(now)).unwrap(); //at)req.to_user_active_model(&cfg, Some(now)).unwrap(); + assert_eq!(sot.last_active_at, Set(Some(now.naive_utc().date()))); + } + #[tokio::test] async fn test_create_main() { let sot_db_res = db_user::Model { diff --git a/crates/keystone/src/identity/backend/sql/user/get.rs b/crates/keystone/src/identity/backend/sql/user/get.rs index 375e1a94..d5bbc97f 100644 --- a/crates/keystone/src/identity/backend/sql/user/get.rs +++ b/crates/keystone/src/identity/backend/sql/user/get.rs @@ -24,6 +24,11 @@ use crate::db::entity::{ user as db_user, }; use crate::error::DbContextExt; +use crate::identity::backend::sql::federated_user::MergeFederatedUserData; +use crate::identity::backend::sql::local_user::MergeLocalUserData; +use crate::identity::backend::sql::nonlocal_user::MergeNonlocalUserData; +use crate::identity::backend::sql::password::MergePasswordData; +use crate::identity::backend::sql::user::MergeUserData; use crate::identity::{ IdentityProviderError, types::{UserOptions, UserResponse, UserResponseBuilder}, diff --git a/crates/keystone/src/identity/backend/sql/user/list.rs b/crates/keystone/src/identity/backend/sql/user/list.rs index c6b5605d..29a23558 100644 --- a/crates/keystone/src/identity/backend/sql/user/list.rs +++ b/crates/keystone/src/identity/backend/sql/user/list.rs @@ -25,6 +25,11 @@ use crate::db::entity::{ user as db_user, }; use crate::error::DbContextExt; +use crate::identity::backend::sql::federated_user::MergeFederatedUserData; +use crate::identity::backend::sql::local_user::MergeLocalUserData; +use crate::identity::backend::sql::nonlocal_user::MergeNonlocalUserData; +use crate::identity::backend::sql::password::MergePasswordData; +use crate::identity::backend::sql::user::MergeUserData; use crate::identity::{ IdentityProviderError, types::{UserListParameters, UserOptions, UserResponse, UserResponseBuilder, UserType}, diff --git a/crates/keystone/src/identity/backend/sql/user_option.rs b/crates/keystone/src/identity/backend/sql/user_option.rs index c1cb6461..1e669f12 100644 --- a/crates/keystone/src/identity/backend/sql/user_option.rs +++ b/crates/keystone/src/identity/backend/sql/user_option.rs @@ -57,8 +57,15 @@ impl FromIterator for UserOptions { } } -impl UserOptions { - pub(super) fn to_model_iter>( +pub trait UserOptionIntoModelIterator { + fn to_model_iter>( + &self, + user_id: U, + ) -> Result, IdentityProviderError>; +} + +impl UserOptionIntoModelIterator for UserOptions { + fn to_model_iter>( &self, user_id: U, ) -> Result, IdentityProviderError> { @@ -126,6 +133,7 @@ impl UserOptions { #[cfg(test)] pub(crate) mod tests { + use super::*; use crate::db::entity::user_option; use crate::identity::types::UserOptions; diff --git a/crates/keystone/src/identity/backend/sql/user_option/create.rs b/crates/keystone/src/identity/backend/sql/user_option/create.rs index 9df3b67e..a20bd191 100644 --- a/crates/keystone/src/identity/backend/sql/user_option/create.rs +++ b/crates/keystone/src/identity/backend/sql/user_option/create.rs @@ -18,6 +18,7 @@ use sea_orm::entity::*; use crate::db::entity::prelude::UserOption as DbUserOption; use crate::db::entity::user_option as db_user_option; use crate::error::DbContextExt; +use crate::identity::backend::sql::user_option::UserOptionIntoModelIterator; use crate::identity::{IdentityProviderError, types::UserOptions}; /// Persist user options. diff --git a/crates/keystone/src/identity/mod.rs b/crates/keystone/src/identity/mod.rs index f1bf84f5..97508ca0 100644 --- a/crates/keystone/src/identity/mod.rs +++ b/crates/keystone/src/identity/mod.rs @@ -32,567 +32,5 @@ //! login and can access resources by using assigned tokens. Users can be //! directly assigned to a particular project and behave as if they are //! contained in that project. - -use async_trait::async_trait; -use chrono::{DateTime, Utc}; -use std::collections::{HashMap, HashSet}; -use std::sync::Arc; -use tokio::sync::RwLock; -use uuid::Uuid; -use validator::Validate; - +pub use openstack_keystone_core::identity::*; pub mod backend; -pub mod error; -#[cfg(test)] -pub mod mock; -pub mod types; -#[cfg(test)] -pub use mock::MockIdentityProvider; - -use crate::auth::AuthenticatedInfo; -use crate::config::Config; -use crate::identity::backend::{IdentityBackend, sql::SqlBackend}; -use crate::identity::{error::IdentityProviderError, types::*}; -use crate::keystone::ServiceState; -use crate::plugin_manager::PluginManager; -use crate::resource::{ResourceApi, error::ResourceProviderError}; - -pub use types::IdentityApi; - -/// Identity provider. -pub struct IdentityProvider { - backend_driver: Arc, - /// Caching flag. When enabled certain data can be cached (i.e. `domain_id` - /// by `user_id`). - caching: bool, - /// Internal cache of `user_id` to `domain_id` mappings. This information if - /// fully static and can never change (well, except with a direct SQL - /// update). - user_id_domain_id_cache: RwLock>, -} - -impl IdentityProvider { - pub fn new( - config: &Config, - plugin_manager: &PluginManager, - ) -> Result { - let backend_driver = if let Some(driver) = - plugin_manager.get_identity_backend(config.identity.driver.clone()) - { - driver.clone() - } else { - match config.identity.driver.as_str() { - "sql" => Arc::new(SqlBackend::default()), - _ => { - return Err(IdentityProviderError::UnsupportedDriver( - config.identity.driver.clone(), - )); - } - } - }; - Ok(Self { - backend_driver, - caching: config.identity.caching, - user_id_domain_id_cache: HashMap::new().into(), - }) - } - - pub fn from_driver(driver: I) -> Self { - Self { - backend_driver: Arc::new(driver), - caching: false, - user_id_domain_id_cache: HashMap::new().into(), - } - } -} - -#[async_trait] -impl IdentityApi for IdentityProvider { - #[tracing::instrument(skip(self, state))] - async fn add_user_to_group<'a>( - &self, - state: &ServiceState, - user_id: &'a str, - group_id: &'a str, - ) -> Result<(), IdentityProviderError> { - self.backend_driver - .add_user_to_group(state, user_id, group_id) - .await - } - - #[tracing::instrument(skip(self, state))] - async fn add_user_to_group_expiring<'a>( - &self, - state: &ServiceState, - user_id: &'a str, - group_id: &'a str, - idp_id: &'a str, - ) -> Result<(), IdentityProviderError> { - self.backend_driver - .add_user_to_group_expiring(state, user_id, group_id, idp_id) - .await - } - - #[tracing::instrument(skip(self, state))] - async fn add_users_to_groups<'a>( - &self, - state: &ServiceState, - memberships: Vec<(&'a str, &'a str)>, - ) -> Result<(), IdentityProviderError> { - self.backend_driver - .add_users_to_groups(state, memberships) - .await - } - - #[tracing::instrument(skip(self, state))] - async fn add_users_to_groups_expiring<'a>( - &self, - state: &ServiceState, - memberships: Vec<(&'a str, &'a str)>, - idp_id: &'a str, - ) -> Result<(), IdentityProviderError> { - self.backend_driver - .add_users_to_groups_expiring(state, memberships, idp_id) - .await - } - - /// Authenticate user with the password auth method. - #[tracing::instrument(skip(self, state, auth))] - async fn authenticate_by_password( - &self, - state: &ServiceState, - auth: &UserPasswordAuthRequest, - ) -> Result { - let mut auth = auth.clone(); - if auth.id.is_none() { - if auth.name.is_none() { - return Err(IdentityProviderError::UserIdOrNameWithDomain); - } - - if let Some(ref mut domain) = auth.domain { - if let Some(dname) = &domain.name { - let d = state - .provider - .get_resource_provider() - .find_domain_by_name(state, dname) - .await? - .ok_or(ResourceProviderError::DomainNotFound(dname.clone()))?; - domain.id = Some(d.id); - } else if domain.id.is_none() { - return Err(IdentityProviderError::UserIdOrNameWithDomain); - } - } else { - return Err(IdentityProviderError::UserIdOrNameWithDomain); - } - } - - self.backend_driver - .authenticate_by_password(state, &auth) - .await - } - - /// Create group. - #[tracing::instrument(skip(self, state))] - async fn create_group( - &self, - state: &ServiceState, - group: GroupCreate, - ) -> Result { - let mut res = group; - if res.id.is_none() { - res.id = Some(Uuid::new_v4().simple().to_string()); - } - self.backend_driver.create_group(state, res).await - } - - /// Create service account. - #[tracing::instrument(skip(self, state))] - async fn create_service_account( - &self, - state: &ServiceState, - sa: ServiceAccountCreate, - ) -> Result { - let mut mod_sa = sa; - if mod_sa.id.is_none() { - mod_sa.id = Some(Uuid::new_v4().simple().to_string()); - } - if mod_sa.enabled.is_none() { - mod_sa.enabled = Some(true); - } - mod_sa.validate()?; - self.backend_driver - .create_service_account(state, mod_sa) - .await - } - - /// Create user. - #[tracing::instrument(skip(self, state))] - async fn create_user( - &self, - state: &ServiceState, - user: UserCreate, - ) -> Result { - let mut mod_user = user; - if mod_user.id.is_none() { - mod_user.id = Some(Uuid::new_v4().simple().to_string()); - } - if mod_user.enabled.is_none() { - mod_user.enabled = Some(true); - } - mod_user.validate()?; - self.backend_driver.create_user(state, mod_user).await - } - - /// Delete group. - #[tracing::instrument(skip(self, state))] - async fn delete_group<'a>( - &self, - state: &ServiceState, - group_id: &'a str, - ) -> Result<(), IdentityProviderError> { - self.backend_driver.delete_group(state, group_id).await - } - - /// Delete user. - #[tracing::instrument(skip(self, state))] - async fn delete_user<'a>( - &self, - state: &ServiceState, - user_id: &'a str, - ) -> Result<(), IdentityProviderError> { - self.backend_driver.delete_user(state, user_id).await?; - if self.caching { - self.user_id_domain_id_cache.write().await.remove(user_id); - } - Ok(()) - } - - /// Get a service account by ID. - #[tracing::instrument(skip(self, state))] - async fn get_service_account<'a>( - &self, - state: &ServiceState, - user_id: &'a str, - ) -> Result, IdentityProviderError> { - self.backend_driver - .get_service_account(state, user_id) - .await - } - - /// Get single user. - #[tracing::instrument(skip(self, state))] - async fn get_user<'a>( - &self, - state: &ServiceState, - user_id: &'a str, - ) -> Result, IdentityProviderError> { - let user = self.backend_driver.get_user(state, user_id).await?; - if self.caching - && let Some(user) = &user - { - self.user_id_domain_id_cache - .write() - .await - .insert(user_id.to_string(), user.domain_id.clone()); - } - Ok(user) - } - - /// Get `domain_id` of a user. - /// - /// When the caching is enabled check for the cached value there. When no - /// data is present for the key - invoke the backend driver and place - /// the new value into the cache. Other operations (`get_user`, - /// `delete_user`) update the cache with `delete_user` purging the value - /// from the cache. - async fn get_user_domain_id<'a>( - &self, - state: &ServiceState, - user_id: &'a str, - ) -> Result { - if self.caching { - if let Some(domain_id) = self.user_id_domain_id_cache.read().await.get(user_id) { - return Ok(domain_id.clone()); - } else { - let domain_id = self - .backend_driver - .get_user_domain_id(state, user_id) - .await?; - self.user_id_domain_id_cache - .write() - .await - .insert(user_id.to_string(), domain_id.clone()); - return Ok(domain_id); - } - } else { - Ok(self - .backend_driver - .get_user_domain_id(state, user_id) - .await?) - } - } - - /// Find federated user by `idp_id` and `unique_id`. - #[tracing::instrument(skip(self, state))] - async fn find_federated_user<'a>( - &self, - state: &ServiceState, - idp_id: &'a str, - unique_id: &'a str, - ) -> Result, IdentityProviderError> { - self.backend_driver - .find_federated_user(state, idp_id, unique_id) - .await - } - - /// List users. - #[tracing::instrument(skip(self, state))] - async fn list_users( - &self, - state: &ServiceState, - params: &UserListParameters, - ) -> Result, IdentityProviderError> { - self.backend_driver.list_users(state, params).await - } - - /// List groups. - #[tracing::instrument(skip(self, state))] - async fn list_groups( - &self, - state: &ServiceState, - params: &GroupListParameters, - ) -> Result, IdentityProviderError> { - self.backend_driver.list_groups(state, params).await - } - - /// Get single group. - #[tracing::instrument(skip(self, state))] - async fn get_group<'a>( - &self, - state: &ServiceState, - group_id: &'a str, - ) -> Result, IdentityProviderError> { - self.backend_driver.get_group(state, group_id).await - } - - /// List groups a user is a member of. - #[tracing::instrument(skip(self, state))] - async fn list_groups_of_user<'a>( - &self, - state: &ServiceState, - user_id: &'a str, - ) -> Result, IdentityProviderError> { - self.backend_driver - .list_groups_of_user(state, user_id) - .await - } - - #[tracing::instrument(skip(self, state))] - async fn remove_user_from_group<'a>( - &self, - state: &ServiceState, - user_id: &'a str, - group_id: &'a str, - ) -> Result<(), IdentityProviderError> { - self.backend_driver - .remove_user_from_group(state, user_id, group_id) - .await - } - - #[tracing::instrument(skip(self, state))] - async fn remove_user_from_group_expiring<'a>( - &self, - state: &ServiceState, - user_id: &'a str, - group_id: &'a str, - idp_id: &'a str, - ) -> Result<(), IdentityProviderError> { - self.backend_driver - .remove_user_from_group_expiring(state, user_id, group_id, idp_id) - .await - } - - #[tracing::instrument(skip(self, state))] - async fn remove_user_from_groups<'a>( - &self, - state: &ServiceState, - user_id: &'a str, - group_ids: HashSet<&'a str>, - ) -> Result<(), IdentityProviderError> { - self.backend_driver - .remove_user_from_groups(state, user_id, group_ids) - .await - } - - #[tracing::instrument(skip(self, state))] - async fn remove_user_from_groups_expiring<'a>( - &self, - state: &ServiceState, - user_id: &'a str, - group_ids: HashSet<&'a str>, - idp_id: &'a str, - ) -> Result<(), IdentityProviderError> { - self.backend_driver - .remove_user_from_groups_expiring(state, user_id, group_ids, idp_id) - .await - } - - #[tracing::instrument(skip(self, state))] - async fn set_user_groups<'a>( - &self, - state: &ServiceState, - user_id: &'a str, - group_ids: HashSet<&'a str>, - ) -> Result<(), IdentityProviderError> { - self.backend_driver - .set_user_groups(state, user_id, group_ids) - .await - } - - #[tracing::instrument(skip(self, state))] - async fn set_user_groups_expiring<'a>( - &self, - state: &ServiceState, - user_id: &'a str, - group_ids: HashSet<&'a str>, - idp_id: &'a str, - last_verified: Option<&'a DateTime>, - ) -> Result<(), IdentityProviderError> { - self.backend_driver - .set_user_groups_expiring(state, user_id, group_ids, idp_id, last_verified) - .await - } -} - -#[cfg(test)] -mod tests { - use super::backend::MockIdentityBackend; - use super::types::user::{UserCreateBuilder, UserResponseBuilder}; - use super::*; - use crate::tests::get_state_mock; - - #[tokio::test] - async fn test_create_user() { - let state = get_state_mock(); - let mut backend = MockIdentityBackend::default(); - backend.expect_create_user().returning(|_, _| { - Ok(UserResponseBuilder::default() - .id("id") - .domain_id("domain_id") - .enabled(true) - .name("name") - .build() - .unwrap()) - }); - let provider = IdentityProvider::from_driver(backend); - - assert_eq!( - provider - .create_user( - &state, - UserCreateBuilder::default() - .name("uname") - .domain_id("did") - .build() - .unwrap() - ) - .await - .unwrap(), - UserResponseBuilder::default() - .domain_id("domain_id") - .enabled(true) - .id("id") - .name("name") - .build() - .unwrap() - ); - } - - #[tokio::test] - async fn test_get_user() { - let state = get_state_mock(); - let mut backend = MockIdentityBackend::default(); - backend - .expect_get_user() - .withf(|_, uid: &'_ str| uid == "uid") - .returning(|_, _| { - Ok(Some( - UserResponseBuilder::default() - .id("id") - .domain_id("domain_id") - .enabled(true) - .name("name") - .build() - .unwrap(), - )) - }); - let provider = IdentityProvider::from_driver(backend); - - assert_eq!( - provider - .get_user(&state, "uid") - .await - .unwrap() - .expect("user should be there"), - UserResponseBuilder::default() - .domain_id("domain_id") - .enabled(true) - .id("id") - .name("name") - .build() - .unwrap(), - ); - } - - #[tokio::test] - async fn test_get_user_domain_id() { - let state = get_state_mock(); - let mut backend = MockIdentityBackend::default(); - backend - .expect_get_user_domain_id() - .withf(|_, uid: &'_ str| uid == "uid") - .times(2) // only 2 times - .returning(|_, _| Ok("did".into())); - backend - .expect_get_user_domain_id() - .withf(|_, uid: &'_ str| uid == "missing") - .returning(|_, _| Err(IdentityProviderError::UserNotFound("missing".into()))); - let mut provider = IdentityProvider::from_driver(backend); - provider.caching = true; - - assert_eq!( - provider.get_user_domain_id(&state, "uid").await.unwrap(), - "did" - ); - assert_eq!( - provider.get_user_domain_id(&state, "uid").await.unwrap(), - "did", - "second time data extracted from cache" - ); - assert!( - provider - .get_user_domain_id(&state, "missing") - .await - .is_err() - ); - provider.caching = false; - assert_eq!( - provider.get_user_domain_id(&state, "uid").await.unwrap(), - "did", - "third time backend is again triggered causing total of 2 invocations" - ); - } - - #[tokio::test] - async fn test_delete_user() { - let state = get_state_mock(); - let mut backend = MockIdentityBackend::default(); - backend - .expect_delete_user() - .withf(|_, uid: &'_ str| uid == "uid") - .returning(|_, _| Ok(())); - let provider = IdentityProvider::from_driver(backend); - - assert!(provider.delete_user(&state, "uid").await.is_ok()); - } -} diff --git a/crates/keystone/src/identity_mapping/backend.rs b/crates/keystone/src/identity_mapping/backend.rs index de9a9426..cac88266 100644 --- a/crates/keystone/src/identity_mapping/backend.rs +++ b/crates/keystone/src/identity_mapping/backend.rs @@ -12,29 +12,5 @@ // // SPDX-License-Identifier: Apache-2.0 -use async_trait::async_trait; - -use crate::identity_mapping::{IdentityMappingProviderError, types::*}; -use crate::keystone::ServiceState; - pub mod sql; - -#[cfg_attr(test, mockall::automock)] -#[async_trait] -pub trait IdentityMappingBackend: Send + Sync { - /// Get the `IdMapping` by the local data. - async fn get_by_local_id<'a>( - &self, - state: &ServiceState, - local_id: &'a str, - domain_id: &'a str, - entity_type: IdMappingEntityType, - ) -> Result, IdentityMappingProviderError>; - - /// Get the IdMapping by the public_id. - async fn get_by_public_id<'a>( - &self, - state: &ServiceState, - public_id: &'a str, - ) -> Result, IdentityMappingProviderError>; -} +pub use openstack_keystone_core::identity_mapping::backend::IdentityMappingBackend; diff --git a/crates/keystone/src/identity_mapping/mod.rs b/crates/keystone/src/identity_mapping/mod.rs index 9a2b6297..a2d24fd2 100644 --- a/crates/keystone/src/identity_mapping/mod.rs +++ b/crates/keystone/src/identity_mapping/mod.rs @@ -16,158 +16,5 @@ //! //! Identity mapping provider provides a mapping of the entity ID between //! Keystone and the remote system (i.e. LDAP, IdP, OpenFGA, SCIM, etc). - -use async_trait::async_trait; -use std::sync::Arc; - +pub use openstack_keystone_core::identity_mapping::*; pub mod backend; -pub mod error; -#[cfg(test)] -pub mod mock; -pub mod types; - -use crate::config::Config; -use crate::identity_mapping::backend::{IdentityMappingBackend, sql::SqlBackend}; -use crate::keystone::ServiceState; -use crate::plugin_manager::PluginManager; -pub use error::IdentityMappingProviderError; -use types::*; - -#[cfg(test)] -pub use mock::MockIdentityMappingProvider; -pub use types::IdentityMappingApi; - -pub struct IdentityMappingProvider { - /// Backend driver. - backend_driver: Arc, -} - -impl IdentityMappingProvider { - pub fn new( - config: &Config, - plugin_manager: &PluginManager, - ) -> Result { - let backend_driver = if let Some(driver) = - plugin_manager.get_identity_mapping_backend(config.identity_mapping.driver.clone()) - { - driver.clone() - } else { - match config.identity_mapping.driver.as_str() { - "sql" => Arc::new(SqlBackend::default()), - _ => { - return Err(IdentityMappingProviderError::UnsupportedDriver( - config.identity_mapping.driver.clone(), - )); - } - } - }; - Ok(Self { backend_driver }) - } -} - -#[async_trait] -impl IdentityMappingApi for IdentityMappingProvider { - /// Get the `IdMapping` by the local data. - async fn get_by_local_id<'a>( - &self, - state: &ServiceState, - local_id: &'a str, - domain_id: &'a str, - entity_type: IdMappingEntityType, - ) -> Result, IdentityMappingProviderError> { - self.backend_driver - .get_by_local_id(state, local_id, domain_id, entity_type) - .await - } - - /// Get the IdMapping by the public_id. - #[tracing::instrument(level = "info", skip(self, state))] - async fn get_by_public_id<'a>( - &self, - state: &ServiceState, - public_id: &'a str, - ) -> Result, IdentityMappingProviderError> { - self.backend_driver.get_by_public_id(state, public_id).await - } -} - -#[cfg(test)] -mod tests { - use sea_orm::DatabaseConnection; - use std::sync::Arc; - - use super::backend::MockIdentityMappingBackend; - use super::*; - use crate::config::Config; - use crate::keystone::Service; - use crate::policy::MockPolicyEnforcer; - use crate::provider::Provider; - - fn get_state_mock() -> Arc { - Arc::new( - Service::new( - Config::default(), - DatabaseConnection::Disconnected, - Provider::mocked_builder().build().unwrap(), - MockPolicyEnforcer::default(), - ) - .unwrap(), - ) - } - - #[tokio::test] - async fn test_get_by_local_id() { - let state = get_state_mock(); - let sot = IdMapping { - public_id: "pid".into(), - local_id: "lid".into(), - domain_id: "did".into(), - entity_type: IdMappingEntityType::User, - }; - let mut backend = MockIdentityMappingBackend::default(); - let sot_clone = sot.clone(); - backend - .expect_get_by_local_id() - .withf(|_, lid: &'_ str, did: &'_ str, _et: &IdMappingEntityType| { - lid == "lid" && did == "did" - }) - .returning(move |_, _, _, _| Ok(Some(sot_clone.clone()))); - let provider = IdentityMappingProvider { - backend_driver: Arc::new(backend), - }; - - let res: IdMapping = provider - .get_by_local_id(&state, "lid", "did", IdMappingEntityType::User) - .await - .unwrap() - .expect("id mapping should be there"); - assert_eq!(res, sot); - } - - #[tokio::test] - async fn test_get_by_public_id() { - let state = get_state_mock(); - let sot = IdMapping { - public_id: "pid".into(), - local_id: "lid".into(), - domain_id: "did".into(), - entity_type: IdMappingEntityType::User, - }; - let mut backend = MockIdentityMappingBackend::default(); - let sot_clone = sot.clone(); - backend - .expect_get_by_public_id() - .withf(|_, pid: &'_ str| pid == "pid") - .returning(move |_, _| Ok(Some(sot_clone.clone()))); - let provider = IdentityMappingProvider { - backend_driver: Arc::new(backend), - }; - - let res: IdMapping = provider - .get_by_public_id(&state, "pid") - .await - .unwrap() - .expect("id mapping should be there"); - assert_eq!(res, sot); - } -} diff --git a/crates/keystone/src/k8s_auth/api.rs b/crates/keystone/src/k8s_auth/api.rs index f1d84fe7..b85ee526 100644 --- a/crates/keystone/src/k8s_auth/api.rs +++ b/crates/keystone/src/k8s_auth/api.rs @@ -22,8 +22,6 @@ use utoipa_axum::router::OpenApiRouter; use crate::keystone::ServiceState; pub mod auth; -//mod common; -pub mod error; pub mod instance; pub mod role; pub mod types; diff --git a/crates/keystone/src/k8s_auth/api/auth.rs b/crates/keystone/src/k8s_auth/api/auth.rs index 63a82c1a..5031a098 100644 --- a/crates/keystone/src/k8s_auth/api/auth.rs +++ b/crates/keystone/src/k8s_auth/api/auth.rs @@ -112,7 +112,7 @@ pub async fn post( .map_err(KeystoneApiError::forbidden)?; let mut api_token = TokenResponse { - token: token.build_api_token_v4(&state).await?, + token: crate::api::v4::auth::token::token_impl::build_api_token_v4(&token, &state).await?, }; api_token.validate()?; diff --git a/crates/keystone/src/k8s_auth/api/instance/create.rs b/crates/keystone/src/k8s_auth/api/instance/create.rs index 40563d02..13a291b1 100644 --- a/crates/keystone/src/k8s_auth/api/instance/create.rs +++ b/crates/keystone/src/k8s_auth/api/instance/create.rs @@ -108,7 +108,7 @@ mod tests { name: Some("name".into()), }) }); - provider = provider.k8s_auth(mock); + provider = provider.mock_k8s_auth(mock); let state = get_mocked_state(provider, true, None, None); let mut api = openapi_router() diff --git a/crates/keystone/src/k8s_auth/api/instance/delete.rs b/crates/keystone/src/k8s_auth/api/instance/delete.rs index 79f7b7ad..51209ea6 100644 --- a/crates/keystone/src/k8s_auth/api/instance/delete.rs +++ b/crates/keystone/src/k8s_auth/api/instance/delete.rs @@ -127,7 +127,7 @@ mod tests { .withf(|_, id: &'_ str| id == "bar") .returning(|_, _| Ok(())); - provider = provider.k8s_auth(mock); + provider = provider.mock_k8s_auth(mock); let state = get_mocked_state(provider, true, None, None); let mut api = openapi_router() diff --git a/crates/keystone/src/k8s_auth/api/instance/list.rs b/crates/keystone/src/k8s_auth/api/instance/list.rs index 1b003037..3c413933 100644 --- a/crates/keystone/src/k8s_auth/api/instance/list.rs +++ b/crates/keystone/src/k8s_auth/api/instance/list.rs @@ -130,7 +130,7 @@ mod tests { name: Some("name".into()), }]) }); - provider = provider.k8s_auth(mock); + provider = provider.mock_k8s_auth(mock); let state = get_mocked_state(provider, true, None, None); let mut api = openapi_router() @@ -191,7 +191,7 @@ mod tests { }]) }); - provider = provider.k8s_auth(mock); + provider = provider.mock_k8s_auth(mock); let state = get_mocked_state(provider, true, None, None); let mut api = openapi_router() @@ -265,7 +265,7 @@ mod tests { }]) }); - provider = provider.k8s_auth(mock); + provider = provider.mock_k8s_auth(mock); let state = get_mocked_state(provider, true, None, None); let mut api = openapi_router() @@ -316,7 +316,7 @@ mod tests { }]) }); - provider = provider.k8s_auth(mock); + provider = provider.mock_k8s_auth(mock); let state = get_mocked_state(provider, true, Some(true), None); let mut api = openapi_router() diff --git a/crates/keystone/src/k8s_auth/api/instance/show.rs b/crates/keystone/src/k8s_auth/api/instance/show.rs index a95bc497..2df24933 100644 --- a/crates/keystone/src/k8s_auth/api/instance/show.rs +++ b/crates/keystone/src/k8s_auth/api/instance/show.rs @@ -114,7 +114,7 @@ mod tests { })) }); - provider = provider.k8s_auth(mock); + provider = provider.mock_k8s_auth(mock); let state = get_mocked_state(provider, true, None, None); let mut api = openapi_router() @@ -183,7 +183,7 @@ mod tests { name: Some("name".into()), })) }); - provider = provider.k8s_auth(mock); + provider = provider.mock_k8s_auth(mock); let state = get_mocked_state(provider, false, None, None); let mut api = openapi_router() diff --git a/crates/keystone/src/k8s_auth/api/instance/update.rs b/crates/keystone/src/k8s_auth/api/instance/update.rs index 782a38fb..15495bd6 100644 --- a/crates/keystone/src/k8s_auth/api/instance/update.rs +++ b/crates/keystone/src/k8s_auth/api/instance/update.rs @@ -133,7 +133,7 @@ mod tests { }) }); - provider = provider.k8s_auth(mock); + provider = provider.mock_k8s_auth(mock); let state = get_mocked_state(provider, true, None, None); let mut api = openapi_router() diff --git a/crates/keystone/src/k8s_auth/api/role/create.rs b/crates/keystone/src/k8s_auth/api/role/create.rs index 4a52a7cb..674204bd 100644 --- a/crates/keystone/src/k8s_auth/api/role/create.rs +++ b/crates/keystone/src/k8s_auth/api/role/create.rs @@ -136,7 +136,7 @@ mod tests { }) }); - provider = provider.k8s_auth(mock); + provider = provider.mock_k8s_auth(mock); let state = get_mocked_state(provider, true, None, None); let mut api = openapi_router() diff --git a/crates/keystone/src/k8s_auth/api/role/delete.rs b/crates/keystone/src/k8s_auth/api/role/delete.rs index e848eb49..cffba6a3 100644 --- a/crates/keystone/src/k8s_auth/api/role/delete.rs +++ b/crates/keystone/src/k8s_auth/api/role/delete.rs @@ -177,7 +177,7 @@ mod tests { .withf(|_, id: &'_ str| id == "bar") .returning(|_, _| Ok(())); - provider = provider.k8s_auth(mock); + provider = provider.mock_k8s_auth(mock); let state = get_mocked_state(provider, true, None, None); let mut api = openapi_router() diff --git a/crates/keystone/src/k8s_auth/api/role/list.rs b/crates/keystone/src/k8s_auth/api/role/list.rs index 1e13fbda..07085a7f 100644 --- a/crates/keystone/src/k8s_auth/api/role/list.rs +++ b/crates/keystone/src/k8s_auth/api/role/list.rs @@ -210,7 +210,7 @@ mod tests { }]) }); - provider = provider.k8s_auth(mock); + provider = provider.mock_k8s_auth(mock); let state = get_mocked_state(provider, true, None, None); // Nested style @@ -327,7 +327,7 @@ mod tests { }]) }); - provider = provider.k8s_auth(mock); + provider = provider.mock_k8s_auth(mock); let state = get_mocked_state(provider, true, None, None); let mut api = openapi_router() diff --git a/crates/keystone/src/k8s_auth/api/role/show.rs b/crates/keystone/src/k8s_auth/api/role/show.rs index 0f14a3d8..36cf30b8 100644 --- a/crates/keystone/src/k8s_auth/api/role/show.rs +++ b/crates/keystone/src/k8s_auth/api/role/show.rs @@ -170,7 +170,7 @@ mod tests { })) }); - provider = provider.k8s_auth(mock); + provider = provider.mock_k8s_auth(mock); let state = get_mocked_state(provider, true, None, None); let mut api = openapi_router() diff --git a/crates/keystone/src/k8s_auth/api/role/update.rs b/crates/keystone/src/k8s_auth/api/role/update.rs index a26e28ee..3f102cd8 100644 --- a/crates/keystone/src/k8s_auth/api/role/update.rs +++ b/crates/keystone/src/k8s_auth/api/role/update.rs @@ -187,7 +187,7 @@ mod tests { }) }); - provider = provider.k8s_auth(mock); + provider = provider.mock_k8s_auth(mock); let state = get_mocked_state(provider, true, None, None); let mut api = openapi_router() diff --git a/crates/keystone/src/k8s_auth/api/types/auth.rs b/crates/keystone/src/k8s_auth/api/types/auth.rs index b7278782..455a37d9 100644 --- a/crates/keystone/src/k8s_auth/api/types/auth.rs +++ b/crates/keystone/src/k8s_auth/api/types/auth.rs @@ -16,16 +16,3 @@ use openstack_keystone_api_types::k8s_auth::auth; pub use auth::K8sAuthRequest; -use secrecy::{ExposeSecret, SecretString}; - -use crate::k8s_auth::types; - -impl From<(K8sAuthRequest, String)> for types::K8sAuthRequest { - fn from(value: (K8sAuthRequest, String)) -> Self { - Self { - auth_instance_id: value.1, - jwt: SecretString::from(value.0.jwt.expose_secret()), - role_name: value.0.role_name, - } - } -} diff --git a/crates/keystone/src/k8s_auth/api/types/instance.rs b/crates/keystone/src/k8s_auth/api/types/instance.rs index 270f2fa3..5487d8a8 100644 --- a/crates/keystone/src/k8s_auth/api/types/instance.rs +++ b/crates/keystone/src/k8s_auth/api/types/instance.rs @@ -12,11 +12,6 @@ // // SPDX-License-Identifier: Apache-2.0 //! # Kubernetes auth instance types -use axum::{ - Json, - http::StatusCode, - response::{IntoResponse, Response}, -}; use openstack_keystone_api_types::k8s_auth::instance; @@ -29,71 +24,8 @@ pub use instance::K8sAuthInstanceResponse; pub use instance::K8sAuthInstanceUpdate; pub use instance::K8sAuthInstanceUpdateRequest; -use crate::k8s_auth::types; - use crate::api::common::ResourceIdentifier; -impl From for K8sAuthInstance { - fn from(value: types::K8sAuthInstance) -> Self { - Self { - ca_cert: value.ca_cert, - disable_local_ca_jwt: value.disable_local_ca_jwt, - domain_id: value.domain_id, - enabled: value.enabled, - host: value.host, - id: value.id, - name: value.name, - } - } -} - -impl From for types::K8sAuthInstanceCreate { - fn from(value: K8sAuthInstanceCreateRequest) -> Self { - Self { - ca_cert: value.instance.ca_cert, - disable_local_ca_jwt: value.instance.disable_local_ca_jwt, - domain_id: value.instance.domain_id, - enabled: value.instance.enabled, - host: value.instance.host, - id: None, - name: value.instance.name, - } - } -} - -impl From for types::K8sAuthInstanceUpdate { - fn from(value: K8sAuthInstanceUpdateRequest) -> Self { - Self { - ca_cert: value.instance.ca_cert, - disable_local_ca_jwt: value.instance.disable_local_ca_jwt, - enabled: value.instance.enabled, - host: value.instance.host, - name: value.instance.name, - } - } -} - -impl IntoResponse for types::K8sAuthInstance { - fn into_response(self) -> Response { - ( - StatusCode::OK, - Json(K8sAuthInstanceResponse { - instance: K8sAuthInstance::from(self), - }), - ) - .into_response() - } -} - -impl From for types::K8sAuthInstanceListParameters { - fn from(value: K8sAuthInstanceListParameters) -> Self { - Self { - domain_id: value.domain_id, - name: value.name, - } - } -} - impl ResourceIdentifier for K8sAuthInstance { fn get_id(&self) -> String { self.id.clone() diff --git a/crates/keystone/src/k8s_auth/api/types/role.rs b/crates/keystone/src/k8s_auth/api/types/role.rs index f942b07b..74e124f8 100644 --- a/crates/keystone/src/k8s_auth/api/types/role.rs +++ b/crates/keystone/src/k8s_auth/api/types/role.rs @@ -12,11 +12,6 @@ // // SPDX-License-Identifier: Apache-2.0 //! Federated identity provider types. -use axum::{ - Json, - http::StatusCode, - response::{IntoResponse, Response}, -}; use openstack_keystone_api_types::k8s_auth::role; @@ -31,77 +26,8 @@ pub use role::K8sAuthRoleResponse; pub use role::K8sAuthRoleUpdate; pub use role::K8sAuthRoleUpdateRequest; -use crate::k8s_auth::types; - use crate::api::common::ResourceIdentifier; -impl From for K8sAuthRole { - fn from(value: types::K8sAuthRole) -> Self { - Self { - auth_instance_id: value.auth_instance_id, - bound_audience: value.bound_audience, - bound_service_account_names: value.bound_service_account_names, - bound_service_account_namespaces: value.bound_service_account_namespaces, - domain_id: value.domain_id, - enabled: value.enabled, - id: value.id, - name: value.name, - token_restriction_id: value.token_restriction_id, - } - } -} - -impl From<(K8sAuthRoleCreateRequest, String, String)> for types::K8sAuthRoleCreate { - fn from(value: (K8sAuthRoleCreateRequest, String, String)) -> Self { - Self { - auth_instance_id: value.1, - bound_audience: value.0.role.bound_audience, - bound_service_account_names: value.0.role.bound_service_account_names, - bound_service_account_namespaces: value.0.role.bound_service_account_namespaces, - domain_id: value.2, - enabled: value.0.role.enabled, - id: None, - name: value.0.role.name, - token_restriction_id: value.0.role.token_restriction_id, - } - } -} - -impl From for types::K8sAuthRoleUpdate { - fn from(value: K8sAuthRoleUpdateRequest) -> Self { - Self { - bound_audience: value.role.bound_audience, - bound_service_account_names: value.role.bound_service_account_names, - bound_service_account_namespaces: value.role.bound_service_account_namespaces, - enabled: value.role.enabled, - name: value.role.name, - token_restriction_id: value.role.token_restriction_id, - } - } -} - -impl IntoResponse for types::K8sAuthRole { - fn into_response(self) -> Response { - ( - StatusCode::OK, - Json(K8sAuthRoleResponse { - role: K8sAuthRole::from(self), - }), - ) - .into_response() - } -} - -//impl From for types::K8sAuthRoleListParameters { -// fn from(value: K8sAuthRoleListParameters) -> Self { -// Self { -// auth_configuration_id: value.auth_configuration_id, -// domain_id: value.domain_id, -// name: value.name, -// } -// } -//} - impl ResourceIdentifier for K8sAuthRole { fn get_id(&self) -> String { self.id.clone() diff --git a/crates/keystone/src/k8s_auth/backend.rs b/crates/keystone/src/k8s_auth/backend.rs index addfc746..ba5f7bf7 100644 --- a/crates/keystone/src/k8s_auth/backend.rs +++ b/crates/keystone/src/k8s_auth/backend.rs @@ -12,88 +12,6 @@ // // SPDX-License-Identifier: Apache-2.0 //! # K8s auth: Backends. -use async_trait::async_trait; - -use crate::k8s_auth::{K8sAuthProviderError, types::*}; -use crate::keystone::ServiceState; pub mod sql; - -/// K8s auth Backend trait. -/// -/// Backend driver interface expected by the revocation auth_instance. -#[cfg_attr(test, mockall::automock)] -#[async_trait] -pub trait K8sAuthBackend: Send + Sync { - /// Register new K8s auth auth_instance. - async fn create_auth_instance( - &self, - state: &ServiceState, - auth_instance: K8sAuthInstanceCreate, - ) -> Result; - - /// Register new K8s auth role. - async fn create_auth_role( - &self, - state: &ServiceState, - role: K8sAuthRoleCreate, - ) -> Result; - - /// Delete K8s auth auth_instance. - async fn delete_auth_instance<'a>( - &self, - state: &ServiceState, - id: &'a str, - ) -> Result<(), K8sAuthProviderError>; - - /// Delete K8s auth role. - async fn delete_auth_role<'a>( - &self, - state: &ServiceState, - id: &'a str, - ) -> Result<(), K8sAuthProviderError>; - - /// Register new K8s auth auth_instance. - async fn get_auth_instance<'a>( - &self, - state: &ServiceState, - id: &'a str, - ) -> Result, K8sAuthProviderError>; - - /// Register new K8s auth role. - async fn get_auth_role<'a>( - &self, - state: &ServiceState, - id: &'a str, - ) -> Result, K8sAuthProviderError>; - - /// List K8s auth auth_instances. - async fn list_auth_instances( - &self, - state: &ServiceState, - params: &K8sAuthInstanceListParameters, - ) -> Result, K8sAuthProviderError>; - - /// List K8s auth roles. - async fn list_auth_roles( - &self, - state: &ServiceState, - params: &K8sAuthRoleListParameters, - ) -> Result, K8sAuthProviderError>; - - /// Update K8s auth auth_instance. - async fn update_auth_instance<'a>( - &self, - state: &ServiceState, - id: &'a str, - data: K8sAuthInstanceUpdate, - ) -> Result; - - /// Update K8s auth role. - async fn update_auth_role<'a>( - &self, - state: &ServiceState, - id: &'a str, - data: K8sAuthRoleUpdate, - ) -> Result; -} +pub use openstack_keystone_core::k8s_auth::backend::K8sAuthBackend; diff --git a/crates/keystone/src/k8s_auth/backend/sql/instance.rs b/crates/keystone/src/k8s_auth/backend/sql/instance.rs index 25c54312..2ef7f4a0 100644 --- a/crates/keystone/src/k8s_auth/backend/sql/instance.rs +++ b/crates/keystone/src/k8s_auth/backend/sql/instance.rs @@ -186,7 +186,7 @@ pub(crate) mod tests { id: "id".into(), name: Some("name".into()), }; - let update = sot.into_active_model_update(crate::k8s_auth::K8sAuthInstanceUpdate { + let update = sot.into_active_model_update(K8sAuthInstanceUpdate { ca_cert: Some("new_ca".into()), disable_local_ca_jwt: Some(true), enabled: Some(true), diff --git a/crates/keystone/src/k8s_auth/backend/sql/role.rs b/crates/keystone/src/k8s_auth/backend/sql/role.rs index d862bbf0..958107e3 100644 --- a/crates/keystone/src/k8s_auth/backend/sql/role.rs +++ b/crates/keystone/src/k8s_auth/backend/sql/role.rs @@ -276,7 +276,7 @@ pub(crate) mod tests { name: "name".into(), token_restriction_id: "trid".into(), }; - let update = sot.into_active_model_update(crate::k8s_auth::K8sAuthRoleUpdate { + let update = sot.into_active_model_update(K8sAuthRoleUpdate { bound_audience: Some("new_aud".into()), bound_service_account_names: Some(vec!["c".into()]), bound_service_account_namespaces: Some(vec!["nc".into()]), diff --git a/crates/keystone/src/k8s_auth/mod.rs b/crates/keystone/src/k8s_auth/mod.rs index ae9a3f4e..76545522 100644 --- a/crates/keystone/src/k8s_auth/mod.rs +++ b/crates/keystone/src/k8s_auth/mod.rs @@ -12,262 +12,7 @@ // // SPDX-License-Identifier: Apache-2.0 //! # Kubernetes authentication. - -use std::collections::HashMap; -use std::sync::Arc; - -use async_trait::async_trait; -use reqwest::Client; -use tokio::sync::RwLock; +pub use openstack_keystone_core::k8s_auth::*; pub mod api; -mod auth; pub mod backend; -pub mod error; -#[cfg(test)] -mod mock; -pub mod types; - -use crate::keystone::ServiceState; -use crate::plugin_manager::PluginManager; -use crate::token::types::TokenRestriction; -use crate::{auth::AuthenticatedInfo, config::Config}; -use backend::{K8sAuthBackend, sql::SqlBackend}; -use types::*; - -pub use error::K8sAuthProviderError; -#[cfg(test)] -pub use mock::MockK8sAuthProvider; -pub use types::K8sAuthApi; - -/// K8s Auth provider. -pub struct K8sAuthProvider { - /// Backend driver. - backend_driver: Arc, - - /// Reqwest client. - http_clients: RwLock>>, -} - -impl K8sAuthProvider { - pub fn new( - config: &Config, - plugin_manager: &PluginManager, - ) -> Result { - let backend_driver = if let Some(driver) = - plugin_manager.get_k8s_auth_backend(config.k8s_auth.driver.clone()) - { - driver.clone() - } else { - match config.revoke.driver.as_str() { - "sql" => Arc::new(SqlBackend::default()), - _ => { - return Err(K8sAuthProviderError::UnsupportedDriver( - config.k8s_auth.driver.clone(), - )); - } - } - }; - Ok(Self { - backend_driver, - http_clients: RwLock::new(HashMap::new()), - }) - } -} - -#[async_trait] -impl K8sAuthApi for K8sAuthProvider { - /// Authenticate (exchange) the K8s Service account token. - async fn authenticate_by_k8s_sa_token( - &self, - state: &ServiceState, - req: &K8sAuthRequest, - ) -> Result<(AuthenticatedInfo, TokenRestriction), K8sAuthProviderError> { - self.authenticate(state, req).await - } - - /// Register new K8s auth instance. - #[tracing::instrument(skip(self, state))] - async fn create_auth_instance( - &self, - state: &ServiceState, - instance: K8sAuthInstanceCreate, - ) -> Result { - let mut new = instance; - if new.id.is_none() { - new.id = Some(uuid::Uuid::new_v4().simple().to_string()); - } - self.backend_driver.create_auth_instance(state, new).await - } - - /// Register new K8s auth role. - #[tracing::instrument(skip(self, state))] - async fn create_auth_role( - &self, - state: &ServiceState, - role: K8sAuthRoleCreate, - ) -> Result { - let mut new = role; - if new.id.is_none() { - new.id = Some(uuid::Uuid::new_v4().simple().to_string()); - } - self.backend_driver.create_auth_role(state, new).await - } - - /// Delete K8s auth provider. - #[tracing::instrument(skip(self, state))] - async fn delete_auth_instance<'a>( - &self, - state: &ServiceState, - id: &'a str, - ) -> Result<(), K8sAuthProviderError> { - self.backend_driver.delete_auth_instance(state, id).await - } - - /// Delete K8s auth role. - #[tracing::instrument(skip(self, state))] - async fn delete_auth_role<'a>( - &self, - state: &ServiceState, - id: &'a str, - ) -> Result<(), K8sAuthProviderError> { - self.backend_driver.delete_auth_role(state, id).await - } - - /// Register new K8s auth instance. - #[tracing::instrument(skip(self, state))] - async fn get_auth_instance<'a>( - &self, - state: &ServiceState, - id: &'a str, - ) -> Result, K8sAuthProviderError> { - self.backend_driver.get_auth_instance(state, id).await - } - - /// Register new K8s auth role. - #[tracing::instrument(skip(self, state))] - async fn get_auth_role<'a>( - &self, - state: &ServiceState, - id: &'a str, - ) -> Result, K8sAuthProviderError> { - self.backend_driver.get_auth_role(state, id).await - } - - /// List K8s auth instances. - #[tracing::instrument(skip(self, state))] - async fn list_auth_instances( - &self, - state: &ServiceState, - params: &K8sAuthInstanceListParameters, - ) -> Result, K8sAuthProviderError> { - self.backend_driver.list_auth_instances(state, params).await - } - - /// List K8s auth roles. - #[tracing::instrument(skip(self, state))] - async fn list_auth_roles( - &self, - state: &ServiceState, - params: &K8sAuthRoleListParameters, - ) -> Result, K8sAuthProviderError> { - self.backend_driver.list_auth_roles(state, params).await - } - - /// Update K8s auth instance. - #[tracing::instrument(skip(self, state))] - async fn update_auth_instance<'a>( - &self, - state: &ServiceState, - id: &'a str, - data: K8sAuthInstanceUpdate, - ) -> Result { - self.backend_driver - .update_auth_instance(state, id, data) - .await - } - - /// Update K8s auth role. - #[tracing::instrument(skip(self, state))] - async fn update_auth_role<'a>( - &self, - state: &ServiceState, - id: &'a str, - data: K8sAuthRoleUpdate, - ) -> Result { - self.backend_driver.update_auth_role(state, id, data).await - } -} - -#[cfg(test)] -pub(crate) mod tests { - use std::sync::Arc; - - use super::backend::MockK8sAuthBackend; - use super::*; - use crate::tests::get_state_mock; - - #[tokio::test] - async fn test_create_auth_instance() { - let state = get_state_mock(); - let mut backend = MockK8sAuthBackend::default(); - backend - .expect_create_auth_instance() - .returning(|_, _| Ok(K8sAuthInstance::default())); - let provider = K8sAuthProvider { - backend_driver: Arc::new(backend), - http_clients: RwLock::new(HashMap::new()), - }; - - assert!( - provider - .create_auth_instance( - &state, - K8sAuthInstanceCreate { - ca_cert: Some("ca".into()), - disable_local_ca_jwt: Some(true), - domain_id: "did".into(), - enabled: true, - host: "host".into(), - id: Some("id".into()), - name: Some("name".into()), - } - ) - .await - .is_ok() - ); - } - - #[tokio::test] - async fn test_create_auth_role() { - let state = get_state_mock(); - let mut backend = MockK8sAuthBackend::default(); - backend - .expect_create_auth_role() - .returning(|_, _| Ok(K8sAuthRole::default())); - let provider = K8sAuthProvider { - backend_driver: Arc::new(backend), - http_clients: RwLock::new(HashMap::new()), - }; - - assert!( - provider - .create_auth_role( - &state, - K8sAuthRoleCreate { - auth_instance_id: "cid".into(), - bound_audience: Some("aud".into()), - bound_service_account_names: vec!["a".into(), "b".into()], - bound_service_account_namespaces: vec!["na".into(), "nb".into()], - domain_id: "did".into(), - enabled: true, - id: Some("id".into()), - name: "name".into(), - token_restriction_id: "trid".into(), - } - ) - .await - .is_ok() - ); - } -} diff --git a/crates/keystone/src/keystone.rs b/crates/keystone/src/keystone.rs index 45754411..5bd29e7e 100644 --- a/crates/keystone/src/keystone.rs +++ b/crates/keystone/src/keystone.rs @@ -12,57 +12,4 @@ // // SPDX-License-Identifier: Apache-2.0 //! # Keystone state -use mockall_double::double; -use sea_orm::DatabaseConnection; -use std::sync::Arc; -use tracing::info; - -use crate::config::Config; -use crate::error::KeystoneError; -#[double] -use crate::policy::PolicyEnforcer; -use crate::provider::Provider; - -// Placing ServiceState behind Arc is necessary to address DatabaseConnection -// not implementing Clone. -//#[derive(Clone)] -pub struct Service { - /// Config file. - pub config: Config, - - /// Database connection. - pub db: DatabaseConnection, - - /// Policy factory. - pub policy_enforcer: Arc, - - /// Service/resource Provider. - pub provider: Provider, - - /// Shutdown flag. - pub shutdown: bool, -} - -pub type ServiceState = Arc; - -impl Service { - pub fn new( - cfg: Config, - db: DatabaseConnection, - provider: Provider, - policy_factory: PolicyEnforcer, - ) -> Result { - Ok(Self { - config: cfg.clone(), - provider, - db, - policy_enforcer: Arc::new(policy_factory), - shutdown: false, - }) - } - - pub async fn terminate(&self) -> Result<(), KeystoneError> { - info!("Terminating Keystone"); - Ok(()) - } -} +pub use openstack_keystone_core::keystone::{Service, ServiceState}; diff --git a/crates/keystone/src/plugin_manager.rs b/crates/keystone/src/plugin_manager.rs index bf68e5f1..00377045 100644 --- a/crates/keystone/src/plugin_manager.rs +++ b/crates/keystone/src/plugin_manager.rs @@ -23,22 +23,37 @@ use std::collections::HashMap; use std::sync::Arc; -use crate::application_credential::backend::ApplicationCredentialBackend; -use crate::assignment::backend::AssignmentBackend; -use crate::catalog::backend::CatalogBackend; -use crate::federation::backend::FederationBackend; -use crate::identity::backend::IdentityBackend; -use crate::identity_mapping::backend::IdentityMappingBackend; -use crate::k8s_auth::backend::K8sAuthBackend; -use crate::resource::backend::ResourceBackend; -use crate::revoke::backend::RevokeBackend; -use crate::role::backend::RoleBackend; -use crate::token::backend::TokenRestrictionBackend; -use crate::trust::backend::TrustBackend; +use openstack_keystone_core::application_credential::{ + ApplicationCredentialProviderError, backend::ApplicationCredentialBackend, +}; +use openstack_keystone_core::assignment::backend::AssignmentBackend; +use openstack_keystone_core::assignment::error::AssignmentProviderError; +use openstack_keystone_core::catalog::backend::CatalogBackend; +use openstack_keystone_core::catalog::error::CatalogProviderError; +use openstack_keystone_core::federation::backend::FederationBackend; +use openstack_keystone_core::federation::error::FederationProviderError; +use openstack_keystone_core::identity::backend::IdentityBackend; +use openstack_keystone_core::identity::error::IdentityProviderError; +use openstack_keystone_core::identity_mapping::IdentityMappingProviderError; +use openstack_keystone_core::identity_mapping::backend::IdentityMappingBackend; +use openstack_keystone_core::k8s_auth::K8sAuthProviderError; +use openstack_keystone_core::k8s_auth::backend::K8sAuthBackend; +use openstack_keystone_core::resource::backend::ResourceBackend; +use openstack_keystone_core::resource::error::ResourceProviderError; +use openstack_keystone_core::revoke::RevokeProviderError; +use openstack_keystone_core::revoke::backend::RevokeBackend; +use openstack_keystone_core::role::RoleProviderError; +use openstack_keystone_core::role::backend::RoleBackend; +use openstack_keystone_core::token::TokenProviderError; +use openstack_keystone_core::token::backend::TokenRestrictionBackend; +use openstack_keystone_core::trust::TrustProviderError; +use openstack_keystone_core::trust::backend::TrustBackend; + +pub use openstack_keystone_core::plugin_manager::*; /// Plugin manager allowing to pass custom backend plugins implementing required /// trait during the service start. -#[derive(Clone, Default)] +#[derive(Clone)] pub struct PluginManager { /// Application credentials backend plugin. application_credential_backends: HashMap>, @@ -66,106 +81,248 @@ pub struct PluginManager { trust_backends: HashMap>, } -impl PluginManager { - /// Register identity backend. - pub fn register_identity_backend>( - &mut self, - name: S, - plugin: Arc, - ) { - self.identity_backends - .insert(name.as_ref().to_string(), plugin); - } - +impl PluginManagerApi for PluginManager { /// Get registered application credential backend. #[allow(clippy::borrowed_box)] - pub fn get_application_credential_backend>( + fn get_application_credential_backend>( &self, name: S, - ) -> Option<&Arc> { - self.application_credential_backends.get(name.as_ref()) + ) -> Result<&Arc, ApplicationCredentialProviderError> { + self.application_credential_backends + .get(name.as_ref()) + .ok_or(ApplicationCredentialProviderError::UnsupportedDriver( + name.as_ref().to_string(), + )) } /// Get registered assignment backend. #[allow(clippy::borrowed_box)] - pub fn get_assignment_backend>( + fn get_assignment_backend>( &self, name: S, - ) -> Option<&Arc> { - self.assignment_backends.get(name.as_ref()) + ) -> Result<&Arc, AssignmentProviderError> { + self.assignment_backends.get(name.as_ref()).ok_or( + AssignmentProviderError::UnsupportedDriver(name.as_ref().to_string()), + ) } /// Get registered catalog backend. #[allow(clippy::borrowed_box)] - pub fn get_catalog_backend>(&self, name: S) -> Option<&Arc> { - self.catalog_backends.get(name.as_ref()) + fn get_catalog_backend>( + &self, + name: S, + ) -> Result<&Arc, CatalogProviderError> { + self.catalog_backends + .get(name.as_ref()) + .ok_or(CatalogProviderError::UnsupportedDriver( + name.as_ref().to_string(), + )) } /// Get registered federation backend. #[allow(clippy::borrowed_box)] - pub fn get_federation_backend>( + fn get_federation_backend>( &self, name: S, - ) -> Option<&Arc> { - self.federation_backends.get(name.as_ref()) + ) -> Result<&Arc, FederationProviderError> { + self.federation_backends.get(name.as_ref()).ok_or( + FederationProviderError::UnsupportedDriver(name.as_ref().to_string()), + ) } /// Get registered identity backend. #[allow(clippy::borrowed_box)] - pub fn get_identity_backend>( + fn get_identity_backend>( &self, name: S, - ) -> Option<&Arc> { - self.identity_backends.get(name.as_ref()) + ) -> Result<&Arc, IdentityProviderError> { + self.identity_backends + .get(name.as_ref()) + .ok_or(IdentityProviderError::UnsupportedDriver( + name.as_ref().to_string(), + )) } /// Get registered identity mapping backend. #[allow(clippy::borrowed_box)] - pub fn get_identity_mapping_backend>( + fn get_identity_mapping_backend>( &self, name: S, - ) -> Option<&Arc> { - self.identity_mapping_backends.get(name.as_ref()) + ) -> Result<&Arc, IdentityMappingProviderError> { + self.identity_mapping_backends.get(name.as_ref()).ok_or( + IdentityMappingProviderError::UnsupportedDriver(name.as_ref().to_string()), + ) } /// Get registered k8s auth backend. #[allow(clippy::borrowed_box)] - pub fn get_k8s_auth_backend>(&self, name: S) -> Option<&Arc> { - self.k8s_auth_backends.get(name.as_ref()) + fn get_k8s_auth_backend>( + &self, + name: S, + ) -> Result<&Arc, K8sAuthProviderError> { + self.k8s_auth_backends + .get(name.as_ref()) + .ok_or(K8sAuthProviderError::UnsupportedDriver( + name.as_ref().to_string(), + )) } /// Get registered resource backend. #[allow(clippy::borrowed_box)] - pub fn get_resource_backend>( + fn get_resource_backend>( &self, name: S, - ) -> Option<&Arc> { - self.resource_backends.get(name.as_ref()) + ) -> Result<&Arc, ResourceProviderError> { + self.resource_backends + .get(name.as_ref()) + .ok_or(ResourceProviderError::UnsupportedDriver( + name.as_ref().to_string(), + )) } /// Get registered revoke backend. #[allow(clippy::borrowed_box)] - pub fn get_revoke_backend>(&self, name: S) -> Option<&Arc> { - self.revoke_backends.get(name.as_ref()) + fn get_revoke_backend>( + &self, + name: S, + ) -> Result<&Arc, RevokeProviderError> { + self.revoke_backends + .get(name.as_ref()) + .ok_or(RevokeProviderError::UnsupportedDriver( + name.as_ref().to_string(), + )) } /// Get role resource backend. #[allow(clippy::borrowed_box)] - pub fn get_role_backend>(&self, name: S) -> Option<&Arc> { - self.role_backends.get(name.as_ref()) + fn get_role_backend>( + &self, + name: S, + ) -> Result<&Arc, RoleProviderError> { + self.role_backends + .get(name.as_ref()) + .ok_or(RoleProviderError::UnsupportedDriver( + name.as_ref().to_string(), + )) } /// Get registered token restriction backend. #[allow(clippy::borrowed_box)] - pub fn get_token_restriction_backend>( + fn get_token_restriction_backend>( + &self, + name: S, + ) -> Result<&Arc, TokenProviderError> { + self.token_restriction_backends.get(name.as_ref()).ok_or( + TokenProviderError::UnsupportedTRDriver(name.as_ref().to_string()), + ) + } + + /// Get registered trust backend. + #[allow(clippy::borrowed_box)] + fn get_trust_backend>( &self, name: S, - ) -> Option<&Arc> { - self.token_restriction_backends.get(name.as_ref()) + ) -> Result<&Arc, TrustProviderError> { + self.trust_backends + .get(name.as_ref()) + .ok_or(TrustProviderError::UnsupportedDriver( + name.as_ref().to_string(), + )) + } + + /// Register application credential backend. + fn register_application_credential_backend>( + &mut self, + name: S, + plugin: Arc, + ) { + self.application_credential_backends + .insert(name.as_ref().to_string(), plugin); + } + + /// Register assignment backend. + fn register_assignment_backend>( + &mut self, + name: S, + plugin: Arc, + ) { + self.assignment_backends + .insert(name.as_ref().to_string(), plugin); + } + + /// Register catalog backend. + fn register_catalog_backend>( + &mut self, + name: S, + plugin: Arc, + ) { + self.catalog_backends + .insert(name.as_ref().to_string(), plugin); + } + + /// Register federation backend. + fn register_federation_backend>( + &mut self, + name: S, + plugin: Arc, + ) { + self.federation_backends + .insert(name.as_ref().to_string(), plugin); + } + + /// Register identity backend. + fn register_identity_backend>( + &mut self, + name: S, + plugin: Arc, + ) { + self.identity_backends + .insert(name.as_ref().to_string(), plugin); + } + + /// Register identity mapping backend. + fn register_identity_mapping_backend>( + &mut self, + name: S, + plugin: Arc, + ) { + self.identity_mapping_backends + .insert(name.as_ref().to_string(), plugin); + } + + /// Register k8s_auth backend. + fn register_k8s_auth_backend>( + &mut self, + name: S, + plugin: Arc, + ) { + self.k8s_auth_backends + .insert(name.as_ref().to_string(), plugin); + } + + /// Register resource backend. + fn register_resource_backend>( + &mut self, + name: S, + plugin: Arc, + ) { + self.resource_backends + .insert(name.as_ref().to_string(), plugin); + } + + /// Register revoke backend. + fn register_revoke_backend>(&mut self, name: S, plugin: Arc) { + self.revoke_backends + .insert(name.as_ref().to_string(), plugin); + } + + /// Register role backend. + fn register_role_backend>(&mut self, name: S, plugin: Arc) { + self.role_backends.insert(name.as_ref().to_string(), plugin); } /// Register token restriction backend. - pub fn register_token_restriction_backend>( + fn register_token_restriction_backend>( &mut self, name: S, plugin: Arc, @@ -174,9 +331,77 @@ impl PluginManager { .insert(name.as_ref().to_string(), plugin); } - /// Get registered trust backend. - #[allow(clippy::borrowed_box)] - pub fn get_trust_backend>(&self, name: S) -> Option<&Arc> { - self.trust_backends.get(name.as_ref()) + /// Register trust backend. + fn register_trust_backend>(&mut self, name: S, plugin: Arc) { + self.trust_backends + .insert(name.as_ref().to_string(), plugin); + } +} + +impl Default for PluginManager { + fn default() -> Self { + let mut slf = Self { + application_credential_backends: HashMap::new(), + assignment_backends: HashMap::new(), + catalog_backends: HashMap::new(), + federation_backends: HashMap::new(), + identity_backends: HashMap::new(), + identity_mapping_backends: HashMap::new(), + k8s_auth_backends: HashMap::new(), + resource_backends: HashMap::new(), + revoke_backends: HashMap::new(), + role_backends: HashMap::new(), + token_restriction_backends: HashMap::new(), + trust_backends: HashMap::new(), + }; + slf.register_application_credential_backend( + "sql", + Arc::new(crate::application_credential::backend::SqlBackend::default()), + ); + slf.register_assignment_backend( + "sql", + Arc::new(crate::assignment::backend::SqlBackend::default()), + ); + slf.register_catalog_backend( + "sql", + Arc::new(crate::catalog::backend::sql::SqlBackend::default()), + ); + slf.register_federation_backend( + "sql", + Arc::new(crate::federation::backend::SqlBackend::default()), + ); + slf.register_identity_backend( + "sql", + Arc::new(crate::identity::backend::sql::SqlBackend::default()), + ); + slf.register_identity_mapping_backend( + "sql", + Arc::new(crate::identity_mapping::backend::sql::SqlBackend::default()), + ); + slf.register_k8s_auth_backend( + "sql", + Arc::new(crate::k8s_auth::backend::sql::SqlBackend::default()), + ); + slf.register_resource_backend( + "sql", + Arc::new(crate::resource::backend::sql::SqlBackend::default()), + ); + slf.register_revoke_backend( + "sql", + Arc::new(crate::revoke::backend::sql::SqlBackend::default()), + ); + slf.register_role_backend( + "sql", + Arc::new(crate::role::backend::sql::SqlBackend::default()), + ); + slf.register_token_restriction_backend( + "sql", + Arc::new(crate::token::token_restriction::SqlBackend::default()), + ); + slf.register_trust_backend( + "sql", + Arc::new(crate::trust::backend::sql::SqlBackend::default()), + ); + slf } } diff --git a/crates/keystone/src/policy.rs b/crates/keystone/src/policy.rs index 84ba9364..5abec217 100644 --- a/crates/keystone/src/policy.rs +++ b/crates/keystone/src/policy.rs @@ -11,102 +11,59 @@ // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 -//! # Policy enforcement -//! -//! Policy enforcement in Keystone is delegated to the Open Policy Agent. It can -//! be invoked either with the HTTP request or as a WASM module. use std::sync::Arc; use std::time::SystemTime; -#[cfg(test)] -use mockall::mock; use reqwest::{Client, Url}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; use serde_json::{Value, json}; -use thiserror::Error; -use tracing::{Level, debug, trace}; +use tracing::{debug, trace}; use crate::token::Token; -/// Policy related error. -#[derive(Debug, Error)] -pub enum PolicyError { - /// Module compilation error. - #[error("module compilation task crashed")] - Compilation(#[from] eyre::Report), - - /// Dummy policy enforcer cannot be used. - #[error("dummy (empty) policy enforcer")] - Dummy, - - /// Forbidden error. - #[error("{}", .0.violations.as_ref().map( - |v| v.iter().cloned().map(|x| x.msg) - .reduce(|acc, s| format!("{acc}, {s}")) - .unwrap_or_default() - ).unwrap_or("The request you made requires authentication.".into()))] - Forbidden(PolicyEvaluationResult), - - #[error(transparent)] - IO(#[from] std::io::Error), - - #[error(transparent)] - Join(#[from] tokio::task::JoinError), - - /// Json serializaion error. - #[error(transparent)] - Json(#[from] serde_json::Error), - - /// HTTP client error. - #[error(transparent)] - Reqwest(#[from] reqwest::Error), - - /// Url parsing error. - #[error(transparent)] - UrlParse(#[from] url::ParseError), -} +pub use openstack_keystone_core::policy::*; /// Policy factory. -#[derive(Default)] -pub struct PolicyEnforcer { +pub struct HttpPolicyEnforcer { /// Requests client. - http_client: Option>, + http_client: Arc, /// OPA url address. - base_url: Option, + base_url: Url, } -impl PolicyEnforcer { +impl HttpPolicyEnforcer { #[allow(clippy::needless_update)] #[tracing::instrument(name = "policy.http", err)] - pub async fn http(url: Url) -> Result { + pub async fn new(url: Url) -> Result { let client = Client::builder() .tcp_keepalive(std::time::Duration::from_secs(60)) .gzip(true) .deflate(true) .build()?; Ok(Self { - http_client: Some(Arc::new(client)), - base_url: Some(url.join("/v1/data/")?), + http_client: Arc::new(client), + base_url: url.join("/v1/data/")?, }) } +} - #[tracing::instrument( - name = "policy.enforce", - skip_all, - fields( - entrypoint = policy_name.as_ref(), - input, - result, - duration_ms - ), - err, - level = Level::DEBUG - )] - pub async fn enforce>( +#[async_trait::async_trait] +impl PolicyEnforcer for HttpPolicyEnforcer { + //#[tracing::instrument( + // name = "policy.enforce", + // skip_all, + // fields( + // entrypoint = policy_name.as_ref(), + // input, + // result, + // duration_ms + // ), + // err, + // level = Level::DEBUG + //)] + async fn enforce( &self, - policy_name: P, - credentials: impl Into, + policy_name: &'static str, + credentials: &Token, target: Value, update: Option, ) -> Result { @@ -120,15 +77,9 @@ impl PolicyEnforcer { let span = tracing::Span::current(); trace!("checking policy decision with OPA using http"); - let url = self - .base_url - .as_ref() - .ok_or(PolicyError::Dummy)? - .join(policy_name.as_ref())?; + let url = self.base_url.join(policy_name.as_ref())?; let res: PolicyEvaluationResult = self .http_client - .as_ref() - .ok_or(PolicyError::Dummy)? .post(url) .json(&json!({"input": input})) .send() @@ -147,141 +98,3 @@ impl PolicyEnforcer { Ok(res) } } - -#[cfg(test)] -mock! { - pub PolicyEnforcer { - pub async fn enforce( - &self, - policy_name: &str, - credentials: &Token, - target: Value, - current: Option - ) -> Result; - } -} - -#[derive(Debug, Error)] -#[error("failed to evaluate policy")] -pub enum EvaluationError { - Serialization(#[from] serde_json::Error), - Evaluation(#[from] eyre::Report), -} - -/// OpenPolicyAgent `Credentials` object. -#[derive(Serialize, Debug)] -pub struct Credentials { - pub user_id: String, - pub roles: Vec, - #[serde(default)] - pub project_id: Option, - #[serde(default)] - pub domain_id: Option, - #[serde(default)] - pub system: Option, -} - -impl From<&Token> for Credentials { - fn from(token: &Token) -> Self { - Self { - user_id: token.user_id().clone(), - roles: token - .effective_roles() - .map(|x| { - x.iter() - .filter_map(|role| role.name.clone()) - .collect::>() - }) - .unwrap_or_default(), - project_id: token.project().map(|val| val.id.clone()), - domain_id: token.domain().map(|val| val.id.clone()), - system: None, - } - } -} - -/// A single violation of a policy. -#[derive(Clone, Deserialize, Debug, JsonSchema, Serialize)] -pub struct Violation { - pub msg: String, - pub field: Option, -} - -/// The OpenPolicyAgent response. -#[derive(Deserialize, Debug)] -pub struct OpaResponse { - pub result: PolicyEvaluationResult, -} - -/// The result of a policy evaluation. -#[derive(Clone, Deserialize, Debug, Serialize)] -pub struct PolicyEvaluationResult { - /// Whether the user is allowed to perform the request or not. - pub allow: bool, - /// Whether the user is allowed to see resources of other domains. - #[serde(default)] - pub can_see_other_domain_resources: Option, - /// List of violations. - #[serde(rename = "violation")] - pub violations: Option>, -} - -impl std::fmt::Display for PolicyEvaluationResult { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let mut first = true; - if let Some(violations) = &self.violations { - for violation in violations { - if first { - first = false; - } else { - write!(f, ", ")?; - } - write!(f, "{}", violation.msg)?; - } - } - Ok(()) - } -} - -impl PolicyEvaluationResult { - #[must_use] - pub fn allow(&self) -> bool { - self.allow - } - - /// Returns true if the policy evaluation was successful. - #[must_use] - pub fn valid(&self) -> bool { - self.violations - .as_deref() - .map(|x| x.is_empty()) - .unwrap_or(false) - } - - #[cfg(test)] - pub fn allowed() -> Self { - Self { - allow: true, - can_see_other_domain_resources: None, - violations: None, - } - } - - #[cfg(test)] - pub fn allowed_admin() -> Self { - Self { - allow: true, - can_see_other_domain_resources: Some(true), - violations: None, - } - } - - #[cfg(test)] - pub fn forbidden() -> Self { - Self { - allow: false, - can_see_other_domain_resources: Some(false), - violations: None, - } - } -} diff --git a/crates/keystone/src/provider.rs b/crates/keystone/src/provider.rs index 1fdfbcfe..36ce63dc 100644 --- a/crates/keystone/src/provider.rs +++ b/crates/keystone/src/provider.rs @@ -17,208 +17,4 @@ //! gives an easy interact for passing overall manager down to the individual //! providers that might need to call other providers while also allowing an //! easy injection of mocked providers. -use derive_builder::Builder; -use mockall_double::double; - -use crate::application_credential::ApplicationCredentialApi; -#[double] -use crate::application_credential::ApplicationCredentialProvider; -use crate::assignment::AssignmentApi; -#[double] -use crate::assignment::AssignmentProvider; -use crate::catalog::CatalogApi; -#[double] -use crate::catalog::CatalogProvider; -use crate::config::Config; -use crate::error::KeystoneError; -use crate::federation::FederationApi; -#[double] -use crate::federation::FederationProvider; -use crate::identity::IdentityApi; -#[double] -use crate::identity::IdentityProvider; -use crate::identity_mapping::IdentityMappingApi; -#[double] -use crate::identity_mapping::IdentityMappingProvider; -use crate::k8s_auth::K8sAuthApi; -#[double] -use crate::k8s_auth::K8sAuthProvider; -use crate::plugin_manager::PluginManager; -use crate::resource::ResourceApi; -#[double] -use crate::resource::ResourceProvider; -use crate::revoke::RevokeApi; -#[double] -use crate::revoke::RevokeProvider; -use crate::role::RoleApi; -#[double] -use crate::role::RoleProvider; -use crate::token::TokenApi; -#[double] -use crate::token::TokenProvider; -use crate::trust::TrustApi; -#[double] -use crate::trust::TrustProvider; - -/// Global provider manager. -#[derive(Builder)] -// It is necessary to use the owned pattern since otherwise builder invokes clone which immediately -// confuses mockall used in tests -#[builder(pattern = "owned")] -pub struct Provider { - /// Configuration. - pub config: Config, - /// Application credential provider. - application_credential: ApplicationCredentialProvider, - /// Assignment provider. - assignment: AssignmentProvider, - /// Catalog provider. - catalog: CatalogProvider, - /// Federation provider. - federation: FederationProvider, - /// Identity provider. - identity: IdentityProvider, - /// Identity mapping provider. - identity_mapping: IdentityMappingProvider, - /// K8s auth provider. - k8s_auth: K8sAuthProvider, - /// Resource provider. - resource: ResourceProvider, - /// Revoke provider. - revoke: RevokeProvider, - /// Role provider. - role: RoleProvider, - /// Token provider. - token: TokenProvider, - /// Trust provider. - trust: TrustProvider, -} - -impl Provider { - pub fn new(cfg: Config, plugin_manager: PluginManager) -> Result { - let application_credential_provider = - ApplicationCredentialProvider::new(&cfg, &plugin_manager)?; - let assignment_provider = AssignmentProvider::new(&cfg, &plugin_manager)?; - let catalog_provider = CatalogProvider::new(&cfg, &plugin_manager)?; - let federation_provider = FederationProvider::new(&cfg, &plugin_manager)?; - let identity_provider = IdentityProvider::new(&cfg, &plugin_manager)?; - let identity_mapping_provider = IdentityMappingProvider::new(&cfg, &plugin_manager)?; - let k8s_auth_provider = K8sAuthProvider::new(&cfg, &plugin_manager)?; - let resource_provider = ResourceProvider::new(&cfg, &plugin_manager)?; - let revoke_provider = RevokeProvider::new(&cfg, &plugin_manager)?; - let role_provider = RoleProvider::new(&cfg, &plugin_manager)?; - let token_provider = TokenProvider::new(&cfg, &plugin_manager)?; - let trust_provider = TrustProvider::new(&cfg, &plugin_manager)?; - - Ok(Self { - config: cfg, - application_credential: application_credential_provider, - assignment: assignment_provider, - catalog: catalog_provider, - federation: federation_provider, - identity: identity_provider, - identity_mapping: identity_mapping_provider, - k8s_auth: k8s_auth_provider, - resource: resource_provider, - revoke: revoke_provider, - role: role_provider, - token: token_provider, - trust: trust_provider, - }) - } - - /// Get the application credential provider. - pub fn get_application_credential_provider(&self) -> &impl ApplicationCredentialApi { - &self.application_credential - } - - /// Get the assignment provider. - pub fn get_assignment_provider(&self) -> &impl AssignmentApi { - &self.assignment - } - - /// Get the catalog provider. - pub fn get_catalog_provider(&self) -> &impl CatalogApi { - &self.catalog - } - - /// Get the federation provider. - pub fn get_federation_provider(&self) -> &impl FederationApi { - &self.federation - } - - /// Get the identity provider. - pub fn get_identity_provider(&self) -> &impl IdentityApi { - &self.identity - } - - /// Get the identity mapping provider. - pub fn get_identity_mapping_provider(&self) -> &impl IdentityMappingApi { - &self.identity_mapping - } - - /// Get the resource provider. - pub fn get_k8s_auth_provider(&self) -> &impl K8sAuthApi { - &self.k8s_auth - } - - /// Get the resource provider. - pub fn get_resource_provider(&self) -> &impl ResourceApi { - &self.resource - } - - /// Get the revocation provider. - pub fn get_revoke_provider(&self) -> &impl RevokeApi { - &self.revoke - } - - /// Get the role provider. - pub fn get_role_provider(&self) -> &impl RoleApi { - &self.role - } - - /// Get the token provider. - pub fn get_token_provider(&self) -> &impl TokenApi { - &self.token - } - - /// Get the trust provider. - pub fn get_trust_provider(&self) -> &impl TrustApi { - &self.trust - } -} - -#[cfg(test)] -impl Provider { - pub fn mocked_builder() -> ProviderBuilder { - let config = Config::default(); - let application_credential_mock = - crate::application_credential::MockApplicationCredentialProvider::default(); - let assignment_mock = crate::assignment::MockAssignmentProvider::default(); - let catalog_mock = crate::catalog::MockCatalogProvider::default(); - let identity_mock = crate::identity::MockIdentityProvider::default(); - let identity_mapping_mock = crate::identity_mapping::MockIdentityMappingProvider::default(); - let federation_mock = crate::federation::MockFederationProvider::default(); - let k8s_auth_mock = crate::k8s_auth::MockK8sAuthProvider::default(); - let resource_mock = crate::resource::MockResourceProvider::default(); - let revoke_mock = crate::revoke::MockRevokeProvider::default(); - let role_mock = crate::role::MockRoleProvider::default(); - let token_mock = crate::token::MockTokenProvider::default(); - let trust_mock = crate::trust::MockTrustProvider::default(); - - ProviderBuilder::default() - .config(config.clone()) - .application_credential(application_credential_mock) - .assignment(assignment_mock) - .catalog(catalog_mock) - .identity(identity_mock) - .identity_mapping(identity_mapping_mock) - .federation(federation_mock) - .k8s_auth(k8s_auth_mock) - .resource(resource_mock) - .revoke(revoke_mock) - .role(role_mock) - .token(token_mock) - .trust(trust_mock) - } -} +pub use openstack_keystone_core::provider::*; diff --git a/crates/keystone/src/resource/backend.rs b/crates/keystone/src/resource/backend.rs index 1bb2bbf5..74f4af26 100644 --- a/crates/keystone/src/resource/backend.rs +++ b/crates/keystone/src/resource/backend.rs @@ -12,100 +12,6 @@ // // SPDX-License-Identifier: Apache-2.0 -use async_trait::async_trait; - pub mod sql; -use crate::keystone::ServiceState; -use crate::resource::ResourceProviderError; -use crate::resource::types::*; - -/// Resource driver interface. -#[cfg_attr(test, mockall::automock)] -#[async_trait] -pub trait ResourceBackend: Send + Sync { - /// Get `enabled` field of the domain. - async fn get_domain_enabled<'a>( - &self, - state: &ServiceState, - domain_id: &'a str, - ) -> Result; - - /// Create new domain. - async fn create_domain( - &self, - state: &ServiceState, - domain: DomainCreate, - ) -> Result; - - /// Create new project. - async fn create_project( - &self, - state: &ServiceState, - project: ProjectCreate, - ) -> Result; - - /// Delete domain by the ID - async fn delete_domain<'a>( - &self, - state: &ServiceState, - id: &'a str, - ) -> Result<(), ResourceProviderError>; - - /// Delete project by the ID - async fn delete_project<'a>( - &self, - state: &ServiceState, - id: &'a str, - ) -> Result<(), ResourceProviderError>; - - /// Get single domain by ID - async fn get_domain<'a>( - &self, - state: &ServiceState, - domain_id: &'a str, - ) -> Result, ResourceProviderError>; - - /// Get single domain by Name - async fn get_domain_by_name<'a>( - &self, - state: &ServiceState, - domain_name: &'a str, - ) -> Result, ResourceProviderError>; - - /// Get single project by ID - async fn get_project<'a>( - &self, - state: &ServiceState, - project_id: &'a str, - ) -> Result, ResourceProviderError>; - - /// Get single project by Name and Domain ID - async fn get_project_by_name<'a>( - &self, - state: &ServiceState, - name: &'a str, - domain_id: &'a str, - ) -> Result, ResourceProviderError>; - - /// Get project parents - async fn get_project_parents<'a>( - &self, - state: &ServiceState, - project_id: &'a str, - ) -> Result>, ResourceProviderError>; - - /// List domains. - async fn list_domains( - &self, - state: &ServiceState, - params: &DomainListParameters, - ) -> Result, ResourceProviderError>; - - /// List projects. - async fn list_projects( - &self, - state: &ServiceState, - params: &ProjectListParameters, - ) -> Result, ResourceProviderError>; -} +pub use openstack_keystone_core::resource::backend::ResourceBackend; diff --git a/crates/keystone/src/resource/mod.rs b/crates/keystone/src/resource/mod.rs index 398f1061..cddad3b1 100644 --- a/crates/keystone/src/resource/mod.rs +++ b/crates/keystone/src/resource/mod.rs @@ -30,194 +30,5 @@ //! A container that groups or isolates resources or identity objects. Depending //! on the service operator, a project might map to a customer, account, //! organization, or tenant. -use async_trait::async_trait; -use std::sync::Arc; -use uuid::Uuid; -use validator::Validate; - +pub use openstack_keystone_core::resource::*; pub mod backend; -pub mod error; -#[cfg(test)] -mod mock; -pub mod types; - -use crate::config::Config; -use crate::keystone::ServiceState; -use crate::plugin_manager::PluginManager; -use crate::resource::backend::{ResourceBackend, sql::SqlBackend}; -use crate::resource::error::ResourceProviderError; -use crate::resource::types::*; - -#[cfg(test)] -pub use mock::MockResourceProvider; -pub use types::ResourceApi; - -pub struct ResourceProvider { - backend_driver: Arc, -} - -impl ResourceProvider { - pub fn new( - config: &Config, - plugin_manager: &PluginManager, - ) -> Result { - let backend_driver = if let Some(driver) = - plugin_manager.get_resource_backend(config.resource.driver.clone()) - { - driver.clone() - } else { - match config.resource.driver.as_str() { - "sql" => Arc::new(SqlBackend::default()), - _ => { - return Err(ResourceProviderError::UnsupportedDriver( - config.resource.driver.clone(), - )); - } - } - }; - Ok(Self { backend_driver }) - } -} - -#[async_trait] -impl ResourceApi for ResourceProvider { - /// Check whether the domain is enabled. - async fn get_domain_enabled<'a>( - &self, - state: &ServiceState, - domain_id: &'a str, - ) -> Result { - self.backend_driver - .get_domain_enabled(state, domain_id) - .await - } - - /// Create new domain. - #[tracing::instrument(level = "info", skip(self, state))] - async fn create_domain( - &self, - state: &ServiceState, - domain: DomainCreate, - ) -> Result { - let mut new_domain = domain; - - if new_domain.id.is_none() { - new_domain.id = Some(Uuid::new_v4().simple().to_string()); - } - new_domain.validate()?; - self.backend_driver.create_domain(state, new_domain).await - } - - /// Create new project. - #[tracing::instrument(level = "info", skip(self, state))] - async fn create_project( - &self, - state: &ServiceState, - project: ProjectCreate, - ) -> Result { - let mut new_project = project; - - if new_project.id.is_none() { - new_project.id = Some(Uuid::new_v4().simple().to_string()); - } - new_project.validate()?; - self.backend_driver.create_project(state, new_project).await - } - - /// Delete a domain by the ID. - #[tracing::instrument(level = "info", skip(self, state))] - async fn delete_domain<'a>( - &self, - state: &ServiceState, - id: &'a str, - ) -> Result<(), ResourceProviderError> { - self.backend_driver.delete_domain(state, id).await - } - - /// Delete a project by the ID. - #[tracing::instrument(level = "info", skip(self, state))] - async fn delete_project<'a>( - &self, - state: &ServiceState, - id: &'a str, - ) -> Result<(), ResourceProviderError> { - self.backend_driver.delete_project(state, id).await - } - - /// Get single domain. - #[tracing::instrument(level = "info", skip(self, state))] - async fn get_domain<'a>( - &self, - state: &ServiceState, - domain_id: &'a str, - ) -> Result, ResourceProviderError> { - self.backend_driver.get_domain(state, domain_id).await - } - - /// Get single project. - #[tracing::instrument(level = "info", skip(self, state))] - async fn get_project<'a>( - &self, - state: &ServiceState, - project_id: &'a str, - ) -> Result, ResourceProviderError> { - self.backend_driver.get_project(state, project_id).await - } - - /// Get single project by Name and Domain ID. - #[tracing::instrument(level = "info", skip(self, state))] - async fn get_project_by_name<'a>( - &self, - state: &ServiceState, - name: &'a str, - domain_id: &'a str, - ) -> Result, ResourceProviderError> { - self.backend_driver - .get_project_by_name(state, name, domain_id) - .await - } - - /// Get project parents. - #[tracing::instrument(level = "info", skip(self, state))] - async fn get_project_parents<'a>( - &self, - state: &ServiceState, - project_id: &'a str, - ) -> Result>, ResourceProviderError> { - self.backend_driver - .get_project_parents(state, project_id) - .await - } - - /// Get single domain by its name. - #[tracing::instrument(level = "info", skip(self, state))] - async fn find_domain_by_name<'a>( - &self, - state: &ServiceState, - domain_name: &'a str, - ) -> Result, ResourceProviderError> { - self.backend_driver - .get_domain_by_name(state, domain_name) - .await - } - - /// List domains. - #[tracing::instrument(level = "info", skip(self, state))] - async fn list_domains( - &self, - state: &ServiceState, - params: &DomainListParameters, - ) -> Result, ResourceProviderError> { - self.backend_driver.list_domains(state, params).await - } - - /// List projects. - #[tracing::instrument(level = "info", skip(self, state))] - async fn list_projects( - &self, - state: &ServiceState, - params: &ProjectListParameters, - ) -> Result, ResourceProviderError> { - self.backend_driver.list_projects(state, params).await - } -} diff --git a/crates/keystone/src/revoke/backend.rs b/crates/keystone/src/revoke/backend.rs index e289017b..e4091923 100644 --- a/crates/keystone/src/revoke/backend.rs +++ b/crates/keystone/src/revoke/backend.rs @@ -12,46 +12,6 @@ // // SPDX-License-Identifier: Apache-2.0 //! Token revocation: Backends. -//! Revocation provider Backend trait. -use async_trait::async_trait; - -use crate::keystone::ServiceState; -use crate::revoke::{RevokeProviderError, types::*}; -use crate::token::types::Token; - -pub mod error; pub mod sql; -/// RevokeBackend trait. -/// -/// Backend driver interface expected by the revocation provider. -#[cfg_attr(test, mockall::automock)] -#[async_trait] -pub trait RevokeBackend: Send + Sync { - /// Create revocation event. - async fn create_revocation_event( - &self, - state: &ServiceState, - event: RevocationEventCreate, - ) -> Result; - - /// Check token revocation. - /// - /// Check whether there are existing revocation records that invalidate the - /// token. - async fn is_token_revoked( - &self, - state: &ServiceState, - token: &Token, - ) -> Result; - - /// Revoke the token. - /// - /// Mark the token as revoked to prohibit from being used even while not - /// expired. - async fn revoke_token( - &self, - state: &ServiceState, - token: &Token, - ) -> Result<(), RevokeProviderError>; -} +pub use openstack_keystone_core::revoke::backend::RevokeBackend; diff --git a/crates/keystone/src/revoke/backend/sql.rs b/crates/keystone/src/revoke/backend/sql.rs index dd2273be..4f731b91 100644 --- a/crates/keystone/src/revoke/backend/sql.rs +++ b/crates/keystone/src/revoke/backend/sql.rs @@ -19,7 +19,7 @@ use super::RevokeBackend; use crate::db::entity::revocation_event as db_revocation_event; use crate::keystone::ServiceState; use crate::revoke::RevokeProviderError; -use crate::revoke::backend::error::RevokeDatabaseError; +//use crate::revoke::backend::error::RevokeDatabaseError; use crate::revoke::types::*; use crate::token::types::Token; @@ -30,10 +30,28 @@ mod list; #[derive(Default)] pub struct SqlBackend {} -impl TryFrom for RevocationEvent { - type Error = RevokeDatabaseError; - fn try_from(value: db_revocation_event::Model) -> Result { - Ok(Self { +//impl TryFrom for RevocationEvent { +// type Error = RevokeDatabaseError; +// fn try_from(value: db_revocation_event::Model) -> Result { +// Ok(Self { +// domain_id: value.domain_id, +// project_id: value.project_id, +// user_id: value.user_id, +// role_id: value.role_id, +// trust_id: value.trust_id, +// consumer_id: value.consumer_id, +// access_token_id: value.access_token_id, +// issued_before: value.issued_before.and_utc(), +// expires_at: value.expires_at.map(|expires_at| expires_at.and_utc()), +// revoked_at: value.revoked_at.and_utc(), +// audit_id: value.audit_id, +// audit_chain_id: value.audit_chain_id, +// }) +// } +//} +impl From for RevocationEvent { + fn from(value: db_revocation_event::Model) -> Self { + Self { domain_id: value.domain_id, project_id: value.project_id, user_id: value.user_id, @@ -46,7 +64,7 @@ impl TryFrom for RevocationEvent { revoked_at: value.revoked_at.and_utc(), audit_id: value.audit_id, audit_chain_id: value.audit_chain_id, - }) + } } } @@ -93,6 +111,15 @@ impl RevokeBackend for SqlBackend { } } +impl From for RevokeProviderError { + fn from(source: crate::error::DatabaseError) -> Self { + match source { + cfl @ crate::error::DatabaseError::Conflict { .. } => Self::Conflict(cfl.to_string()), + other => Self::Driver(other.to_string()), + } + } +} + #[cfg(test)] mod tests { use crate::db::entity::revocation_event as db_revocation_event; diff --git a/crates/keystone/src/revoke/backend/sql/create.rs b/crates/keystone/src/revoke/backend/sql/create.rs index 1a2de532..a30d58a5 100644 --- a/crates/keystone/src/revoke/backend/sql/create.rs +++ b/crates/keystone/src/revoke/backend/sql/create.rs @@ -13,13 +13,14 @@ // SPDX-License-Identifier: Apache-2.0 //! Create a token revocation record. +use openstack_keystone_core::revoke::RevokeProviderError; use sea_orm::DatabaseConnection; use sea_orm::entity::*; use super::{RevocationEvent, RevocationEventCreate}; use crate::db::entity::revocation_event as db_revocation_event; use crate::error::DbContextExt; -use crate::revoke::backend::error::RevokeDatabaseError; +//use crate::revoke::backend::error::RevokeDatabaseError; /// Create token revocation record. /// @@ -27,8 +28,8 @@ use crate::revoke::backend::error::RevokeDatabaseError; pub async fn create( db: &DatabaseConnection, revocation: RevocationEventCreate, -) -> Result { - db_revocation_event::ActiveModel { +) -> Result { + Ok(db_revocation_event::ActiveModel { id: NotSet, access_token_id: revocation .access_token_id @@ -84,7 +85,7 @@ pub async fn create( .insert(db) .await .context("creating token revocation event")? - .try_into() + .into()) } #[cfg(test)] diff --git a/crates/keystone/src/revoke/backend/sql/list.rs b/crates/keystone/src/revoke/backend/sql/list.rs index a041e8ec..dc907991 100644 --- a/crates/keystone/src/revoke/backend/sql/list.rs +++ b/crates/keystone/src/revoke/backend/sql/list.rs @@ -21,12 +21,12 @@ use crate::db::entity::{ prelude::RevocationEvent as DbRevocationEvent, revocation_event as db_revocation_event, }; use crate::error::DbContextExt; -use crate::revoke::backend::error::RevokeDatabaseError; +use crate::revoke::RevokeProviderError; use crate::revoke::types::{RevocationEvent, RevocationEventListParameters}; fn build_query_filters( params: &RevocationEventListParameters, -) -> Result, RevokeDatabaseError> { +) -> Result, RevokeProviderError> { tracing::info!("Query parameters: {:?}", params); let mut select = DbRevocationEvent::find(); @@ -125,7 +125,7 @@ fn build_query_filters( pub async fn count( db: &DatabaseConnection, params: &RevocationEventListParameters, -) -> Result { +) -> Result { Ok(build_query_filters(params)? .count(db) .await @@ -139,19 +139,20 @@ pub async fn count( pub async fn list( db: &DatabaseConnection, params: &RevocationEventListParameters, -) -> Result, RevokeDatabaseError> { +) -> Result, RevokeProviderError> { let db_entities: Vec = build_query_filters(params)? .all(db) .await .context("listing revocation events for the token")?; - let results: Result, _> = db_entities + let results: Vec = db_entities .into_iter() - .map(TryInto::::try_into) + //.map(TryInto::::try_into) + .map(Into::into) .collect(); - results + Ok(results) } #[cfg(test)] diff --git a/crates/keystone/src/revoke/mod.rs b/crates/keystone/src/revoke/mod.rs index 4872c897..7a3be53c 100644 --- a/crates/keystone/src/revoke/mod.rs +++ b/crates/keystone/src/revoke/mod.rs @@ -34,120 +34,5 @@ //! //! Additionally the `token.issued_at` is compared to be lower than the //! `issued_before` field of the revocation record. - -use async_trait::async_trait; -use std::sync::Arc; - +pub use openstack_keystone_core::revoke::*; pub mod backend; -pub mod error; -#[cfg(test)] -mod mock; -pub(crate) mod types; - -use crate::config::Config; -use crate::keystone::ServiceState; -use crate::plugin_manager::PluginManager; -use crate::revoke::backend::{RevokeBackend, sql::SqlBackend}; -use crate::token::types::Token; - -pub use error::RevokeProviderError; -#[cfg(test)] -pub use mock::MockRevokeProvider; -pub use types::*; - -/// Revoke provider. -pub struct RevokeProvider { - /// Backend driver. - backend_driver: Arc, -} - -impl RevokeProvider { - pub fn new( - config: &Config, - plugin_manager: &PluginManager, - ) -> Result { - let backend_driver = - if let Some(driver) = plugin_manager.get_revoke_backend(config.revoke.driver.clone()) { - driver.clone() - } else { - match config.revoke.driver.as_str() { - "sql" => Arc::new(SqlBackend::default()), - _ => { - return Err(RevokeProviderError::UnsupportedDriver( - config.revoke.driver.clone(), - )); - } - } - }; - Ok(Self { backend_driver }) - } -} - -#[async_trait] -impl RevokeApi for RevokeProvider { - /// Create revocation event. - #[tracing::instrument(level = "info", skip(self, state))] - async fn create_revocation_event( - &self, - state: &ServiceState, - event: RevocationEventCreate, - ) -> Result { - self.backend_driver - .create_revocation_event(state, event) - .await - } - - /// Check whether the token has been revoked or not. - /// - /// Checks revocation events matching the token parameters and return - /// `false` if their count is more than `0`. - #[tracing::instrument(level = "info", skip(self, state, token))] - async fn is_token_revoked( - &self, - state: &ServiceState, - token: &Token, - ) -> Result { - tracing::info!("Checking for the revocation events"); - self.backend_driver.is_token_revoked(state, token).await - } - - /// Revoke the token. - /// - /// Mark the token as revoked to prohibit from being used even while not - /// expired. - async fn revoke_token( - &self, - state: &ServiceState, - token: &Token, - ) -> Result<(), RevokeProviderError> { - self.backend_driver.revoke_token(state, token).await - } -} - -#[cfg(test)] -mod tests { - use std::sync::Arc; - - use super::backend::MockRevokeBackend; - use super::*; - use crate::tests::get_state_mock; - - #[tokio::test] - async fn test_create_revocation_event() { - let state = get_state_mock(); - let mut backend = MockRevokeBackend::default(); - backend - .expect_create_revocation_event() - .returning(|_, _| Ok(RevocationEvent::default())); - let provider = RevokeProvider { - backend_driver: Arc::new(backend), - }; - - assert!( - provider - .create_revocation_event(&state, RevocationEventCreate::default()) - .await - .is_ok() - ); - } -} diff --git a/crates/keystone/src/role/backend.rs b/crates/keystone/src/role/backend.rs index 672747e6..ab9ca402 100644 --- a/crates/keystone/src/role/backend.rs +++ b/crates/keystone/src/role/backend.rs @@ -12,57 +12,5 @@ // // SPDX-License-Identifier: Apache-2.0 -use async_trait::async_trait; -use std::collections::{BTreeMap, BTreeSet}; - -use crate::keystone::ServiceState; -use crate::role::{RoleProviderError, types::role::*}; - pub mod sql; pub use sql::SqlBackend; - -#[cfg_attr(test, mockall::automock)] -#[async_trait] -pub trait RoleBackend: Send + Sync { - /// Create Role. - async fn create_role( - &self, - state: &ServiceState, - params: RoleCreate, - ) -> Result; - - /// Delete a role by the ID. - async fn delete_role<'a>( - &self, - state: &ServiceState, - id: &'a str, - ) -> Result<(), RoleProviderError>; - - /// Get single role by ID - async fn get_role<'a>( - &self, - state: &ServiceState, - id: &'a str, - ) -> Result, RoleProviderError>; - - /// Expand implied roles. - async fn expand_implied_roles( - &self, - state: &ServiceState, - roles: &mut Vec, - ) -> Result<(), RoleProviderError>; - - /// List role imply rules. - async fn list_imply_rules( - &self, - state: &ServiceState, - resolve: bool, - ) -> Result>, RoleProviderError>; - - /// List Roles. - async fn list_roles( - &self, - state: &ServiceState, - params: &RoleListParameters, - ) -> Result, RoleProviderError>; -} diff --git a/crates/keystone/src/role/backend/sql.rs b/crates/keystone/src/role/backend/sql.rs index ca6e2ad8..c15b1e75 100644 --- a/crates/keystone/src/role/backend/sql.rs +++ b/crates/keystone/src/role/backend/sql.rs @@ -17,8 +17,9 @@ use std::collections::{BTreeMap, BTreeSet, HashSet}; use super::super::types::*; use crate::keystone::ServiceState; -use crate::role::backend::RoleCreate; -use crate::role::{RoleProviderError, backend::RoleBackend}; +use crate::role::RoleProviderError; +use crate::role::types::RoleCreate; +use openstack_keystone_core::role::backend::RoleBackend; pub(crate) mod implied_role; pub(crate) mod role; diff --git a/crates/keystone/src/role/mod.rs b/crates/keystone/src/role/mod.rs index 9ffdd07a..52bce126 100644 --- a/crates/keystone/src/role/mod.rs +++ b/crates/keystone/src/role/mod.rs @@ -31,125 +31,7 @@ //! that includes a list of roles. When a user calls a service, that service //! interprets the user role set, and determines to which operations or //! resources each role grants access. -use async_trait::async_trait; -use std::collections::{BTreeMap, BTreeSet}; -use std::sync::Arc; -use uuid::Uuid; -use validator::Validate; -pub mod backend; -pub mod error; -#[cfg(test)] -mod mock; -pub mod types; - -use crate::config::Config; -use crate::keystone::ServiceState; -use crate::plugin_manager::PluginManager; -use backend::{RoleBackend, SqlBackend}; -pub use error::RoleProviderError; -use types::*; - -#[cfg(test)] -pub use mock::MockRoleProvider; -pub use types::RoleApi; - -pub struct RoleProvider { - backend_driver: Arc, -} - -impl RoleProvider { - pub fn new(config: &Config, plugin_manager: &PluginManager) -> Result { - let backend_driver = - if let Some(driver) = plugin_manager.get_role_backend(config.role.driver.clone()) { - driver.clone() - } else { - match config.role.driver.as_str() { - "sql" => Arc::new(SqlBackend::default()), - other => { - return Err(RoleProviderError::UnsupportedDriver(other.to_string())); - } - } - }; - Ok(Self { backend_driver }) - } -} - -#[async_trait] -impl RoleApi for RoleProvider { - /// Create role. - #[tracing::instrument(level = "info", skip(self, state))] - async fn create_role( - &self, - state: &ServiceState, - params: RoleCreate, - ) -> Result { - params.validate()?; - - let mut new_params = params; +pub use openstack_keystone_core::role::*; - if new_params.id.is_none() { - new_params.id = Some(Uuid::new_v4().simple().to_string()); - } - self.backend_driver.create_role(state, new_params).await - } - - /// Delete a role by the ID. - #[tracing::instrument(level = "info", skip(self, state))] - async fn delete_role<'a>( - &self, - state: &ServiceState, - id: &'a str, - ) -> Result<(), RoleProviderError> { - self.backend_driver.delete_role(state, id).await - } - - /// Get single role. - #[tracing::instrument(level = "info", skip(self, state))] - async fn get_role<'a>( - &self, - state: &ServiceState, - id: &'a str, - ) -> Result, RoleProviderError> { - self.backend_driver.get_role(state, id).await - } - - /// Expand implied roles. - /// - /// Return list of the roles with the imply rules being considered. - #[tracing::instrument(level = "info", skip(self, state))] - async fn expand_implied_roles( - &self, - state: &ServiceState, - roles: &mut Vec, - ) -> Result<(), RoleProviderError> { - // In most of the cases a logic for expanding the roles may be implemented by - // the provider itself, but some backend drivers may have more efficient - // methods. - self.backend_driver - .expand_implied_roles(state, roles) - .await?; - Ok(()) - } - - /// List role imply rules. - #[tracing::instrument(level = "info", skip(self, state))] - async fn list_imply_rules( - &self, - state: &ServiceState, - resolve: bool, - ) -> Result>, RoleProviderError> { - self.backend_driver.list_imply_rules(state, resolve).await - } - - /// List roles. - #[tracing::instrument(level = "info", skip(self, state))] - async fn list_roles( - &self, - state: &ServiceState, - params: &RoleListParameters, - ) -> Result, RoleProviderError> { - params.validate()?; - self.backend_driver.list_roles(state, params).await - } -} +pub mod backend; diff --git a/crates/keystone/src/tests.rs b/crates/keystone/src/tests.rs index 17ee216f..0662b43d 100644 --- a/crates/keystone/src/tests.rs +++ b/crates/keystone/src/tests.rs @@ -12,25 +12,26 @@ // // SPDX-License-Identifier: Apache-2.0 //! # Test related functionality -use sea_orm::DatabaseConnection; -use std::sync::Arc; +//pub use openstack_keystone_core::tests::*; +//use sea_orm::DatabaseConnection; +//use std::sync::Arc; -use crate::config::Config; -use crate::keystone::Service; -use crate::policy::MockPolicyEnforcer; -use crate::provider::Provider; - -pub(crate) mod api; -pub(crate) mod token; - -pub fn get_state_mock() -> Arc { - Arc::new( - Service::new( - Config::default(), - DatabaseConnection::Disconnected, - Provider::mocked_builder().build().unwrap(), - MockPolicyEnforcer::default(), - ) - .unwrap(), - ) -} +//use crate::config::Config; +//use crate::keystone::Service; +//use crate::policy::MockPolicyEnforcer; +//use crate::provider::Provider; +// +//pub(crate) mod api; +//pub(crate) mod token; +// +//pub fn get_state_mock() -> Arc { +// Arc::new( +// Service::new( +// Config::default(), +// DatabaseConnection::Disconnected, +// Provider::mocked_builder().build().unwrap(), +// MockPolicyEnforcer::default(), +// ) +// .unwrap(), +// ) +//} diff --git a/crates/keystone/src/token/mod.rs b/crates/keystone/src/token/mod.rs index 2a0ab774..40d3244a 100644 --- a/crates/keystone/src/token/mod.rs +++ b/crates/keystone/src/token/mod.rs @@ -18,1712 +18,6 @@ //! valid for a finite duration. OpenStack Identity is an integration service //! that does not aspire to be a full-fledged identity store and management //! solution. +pub use openstack_keystone_core::token::*; -use async_trait::async_trait; -use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD}; -use chrono::{DateTime, TimeDelta, Utc}; -use std::collections::HashSet; -use std::sync::Arc; -use tracing::{debug, trace}; -use uuid::Uuid; - -pub mod backend; -pub mod error; -#[cfg(test)] -mod mock; pub mod token_restriction; -pub mod types; - -use crate::auth::{AuthenticatedInfo, AuthenticationError, AuthzInfo}; -use crate::config::{Config, TokenProviderDriver}; -use crate::identity::IdentityApi; -use crate::keystone::ServiceState; -use crate::plugin_manager::PluginManager; -use crate::resource::{ - ResourceApi, - types::{Domain, Project}, -}; -use crate::revoke::RevokeApi; -use crate::{ - application_credential::ApplicationCredentialApi, - assignment::{ - AssignmentApi, - error::AssignmentProviderError, - types::{RoleAssignmentListParameters, RoleAssignmentListParametersBuilder}, - }, - role::{RoleApi, types::RoleRef}, - trust::{TrustApi, types::Trust}, -}; -use backend::{TokenBackend, TokenRestrictionBackend, fernet::FernetTokenProvider}; -pub use error::TokenProviderError; - -pub use crate::token::types::*; -#[cfg(test)] -pub use mock::MockTokenProvider; - -pub struct TokenProvider { - config: Config, - backend_driver: Arc, - tr_backend_driver: Arc, -} - -impl TokenProvider { - pub fn new( - config: &Config, - plugin_manager: &PluginManager, - ) -> Result { - let backend_driver = match config.token.provider { - TokenProviderDriver::Fernet => FernetTokenProvider::new(config.clone()), - }; - let tr_sql_driver: Arc = - Arc::new(token_restriction::SqlBackend::default()); - let tr_driver = plugin_manager - .get_token_restriction_backend(&config.token_restriction.driver) - .unwrap_or(&tr_sql_driver) - //.ok_or(TokenProviderError::UnsupportedDriver("sql".to_string()))? - .clone(); - Ok(Self { - config: config.clone(), - backend_driver: Arc::new(backend_driver), - tr_backend_driver: tr_driver, - }) - } - - fn get_new_token_expiry( - &self, - auth_expiration: &Option>, - ) -> Result, TokenProviderError> { - let default_expiry = Utc::now() - .checked_add_signed(TimeDelta::seconds(self.config.token.expiration as i64)) - .ok_or(TokenProviderError::ExpiryCalculation)?; - Ok(auth_expiration - .map(|x| std::cmp::min(x, default_expiry)) - .unwrap_or(default_expiry)) - } - - /// Create unscoped token. - fn create_unscoped_token( - &self, - authentication_info: &AuthenticatedInfo, - ) -> Result { - Ok(Token::Unscoped( - UnscopedPayloadBuilder::default() - .user_id(authentication_info.user_id.clone()) - .user(authentication_info.user.clone()) - .methods(authentication_info.methods.clone().iter()) - .audit_ids(authentication_info.audit_ids.clone().iter()) - .expires_at(self.get_new_token_expiry(&authentication_info.expires_at)?) - .build()?, - )) - } - - /// Create project scoped token. - fn create_project_scope_token( - &self, - authentication_info: &AuthenticatedInfo, - project: &Project, - ) -> Result { - let token_expiry = self.get_new_token_expiry(&authentication_info.expires_at)?; - if let Some(application_credential) = &authentication_info.application_credential { - // Token for the application credential authentication - Ok(Token::ApplicationCredential( - ApplicationCredentialPayloadBuilder::default() - .application_credential_id(application_credential.id.clone()) - .application_credential(application_credential.clone()) - .user_id(authentication_info.user_id.clone()) - .user(authentication_info.user.clone()) - .methods(authentication_info.methods.clone().iter()) - .audit_ids(authentication_info.audit_ids.clone().iter()) - .expires_at( - application_credential - .expires_at - .map(|ac_expiry| std::cmp::min(token_expiry, ac_expiry)) - .unwrap_or(token_expiry), - ) - .project_id(project.id.clone()) - .project(project.clone()) - .build()?, - )) - } else { - // General project scoped token - Ok(Token::ProjectScope( - ProjectScopePayloadBuilder::default() - .user_id(authentication_info.user_id.clone()) - .user(authentication_info.user.clone()) - .methods(authentication_info.methods.clone().iter()) - .audit_ids(authentication_info.audit_ids.clone().iter()) - .expires_at(token_expiry) - .project_id(project.id.clone()) - .project(project.clone()) - .build()?, - )) - } - } - - /// Create domain scoped token. - fn create_domain_scope_token( - &self, - authentication_info: &AuthenticatedInfo, - domain: &Domain, - ) -> Result { - Ok(Token::DomainScope( - DomainScopePayloadBuilder::default() - .user_id(authentication_info.user_id.clone()) - .user(authentication_info.user.clone()) - .methods(authentication_info.methods.clone().iter()) - .audit_ids(authentication_info.audit_ids.clone().iter()) - .expires_at(self.get_new_token_expiry(&authentication_info.expires_at)?) - .domain_id(domain.id.clone()) - .domain(domain.clone()) - .build()?, - )) - } - - /// Create unscoped token with the identity provider bind. - fn create_federated_unscoped_token( - &self, - authentication_info: &AuthenticatedInfo, - ) -> Result { - if let (Some(idp_id), Some(protocol_id)) = ( - authentication_info.idp_id.clone(), - authentication_info.protocol_id.clone(), - ) { - Ok(Token::FederationUnscoped( - FederationUnscopedPayloadBuilder::default() - .user_id(authentication_info.user_id.clone()) - .user(authentication_info.user.clone()) - .methods(authentication_info.methods.clone().iter()) - .audit_ids(authentication_info.audit_ids.clone().iter()) - .expires_at(self.get_new_token_expiry(&authentication_info.expires_at)?) - .idp_id(idp_id) - .protocol_id(protocol_id) - .group_ids(vec![]) - .build()?, - )) - } else { - Err(TokenProviderError::FederatedPayloadMissingData) - } - } - - /// Create project scoped token with the identity provider bind. - fn create_federated_project_scope_token( - &self, - authentication_info: &AuthenticatedInfo, - project: &Project, - ) -> Result { - if let (Some(idp_id), Some(protocol_id)) = ( - authentication_info.idp_id.clone(), - authentication_info.protocol_id.clone(), - ) { - Ok(Token::FederationProjectScope( - FederationProjectScopePayloadBuilder::default() - .user_id(authentication_info.user_id.clone()) - .user(authentication_info.user.clone()) - .methods(authentication_info.methods.clone().iter()) - .audit_ids(authentication_info.audit_ids.clone().iter()) - .expires_at(self.get_new_token_expiry(&authentication_info.expires_at)?) - .idp_id(idp_id) - .protocol_id(protocol_id) - .group_ids( - authentication_info - .user_groups - .clone() - .iter() - .map(|grp| grp.id.clone()) - .collect::>(), - ) - .project_id(project.id.clone()) - .project(project.clone()) - .build()?, - )) - } else { - Err(TokenProviderError::FederatedPayloadMissingData) - } - } - - /// Create domain scoped token with the identity provider bind. - fn create_federated_domain_scope_token( - &self, - authentication_info: &AuthenticatedInfo, - domain: &Domain, - ) -> Result { - if let (Some(idp_id), Some(protocol_id)) = ( - authentication_info.idp_id.clone(), - authentication_info.protocol_id.clone(), - ) { - Ok(Token::FederationDomainScope( - FederationDomainScopePayloadBuilder::default() - .user_id(authentication_info.user_id.clone()) - .user(authentication_info.user.clone()) - .methods(authentication_info.methods.clone().iter()) - .audit_ids(authentication_info.audit_ids.clone().iter()) - .expires_at(self.get_new_token_expiry(&authentication_info.expires_at)?) - .idp_id(idp_id) - .protocol_id(protocol_id) - .group_ids( - authentication_info - .user_groups - .clone() - .iter() - .map(|grp| grp.id.clone()) - .collect::>(), - ) - .domain_id(domain.id.clone()) - .domain(domain.clone()) - .build()?, - )) - } else { - Err(TokenProviderError::FederatedPayloadMissingData) - } - } - - /// Create token with the specified restrictions. - fn create_restricted_token( - &self, - authentication_info: &AuthenticatedInfo, - authz_info: &AuthzInfo, - restriction: &TokenRestriction, - ) -> Result { - Ok(Token::Restricted( - RestrictedPayloadBuilder::default() - .user_id( - restriction - .user_id - .as_ref() - .unwrap_or(&authentication_info.user_id.clone()), - ) - .user(authentication_info.user.clone()) - .methods(authentication_info.methods.clone().iter()) - .audit_ids(authentication_info.audit_ids.clone().iter()) - .expires_at(self.get_new_token_expiry(&authentication_info.expires_at)?) - .token_restriction_id(restriction.id.clone()) - .project_id( - restriction - .project_id - .as_ref() - .or(match authz_info { - AuthzInfo::Project(project) => Some(&project.id), - _ => None, - }) - .ok_or_else(|| TokenProviderError::RestrictedTokenNotProjectScoped)?, - ) - .allow_renew(restriction.allow_renew) - .allow_rescope(restriction.allow_rescope) - .roles(restriction.roles.clone()) - .build()?, - )) - } - - /// Create system scoped token. - fn create_system_scoped_token( - &self, - authentication_info: &AuthenticatedInfo, - ) -> Result { - Ok(Token::SystemScope( - SystemScopePayloadBuilder::default() - .user_id(authentication_info.user_id.clone()) - .user(authentication_info.user.clone()) - .methods(authentication_info.methods.clone().iter()) - .audit_ids(authentication_info.audit_ids.clone().iter()) - .system_id("system") - .expires_at(self.get_new_token_expiry(&authentication_info.expires_at)?) - .build()?, - )) - } - - /// Create token based on the trust. - fn create_trust_token( - &self, - authentication_info: &AuthenticatedInfo, - trust: &Trust, - ) -> Result { - if let Some(project_id) = &trust.project_id { - Ok(Token::Trust( - TrustPayloadBuilder::default() - .user_id(authentication_info.user_id.clone()) - .user(authentication_info.user.clone()) - .methods(authentication_info.methods.clone().iter()) - .audit_ids(authentication_info.audit_ids.clone().iter()) - .expires_at(self.get_new_token_expiry(&authentication_info.expires_at)?) - .trust_id(trust.id.clone()) - .project_id(project_id.clone()) - .build()?, - )) - } else { - // Trust without project_id is unscoped - Ok(Token::Unscoped( - UnscopedPayloadBuilder::default() - .user_id(authentication_info.user_id.clone()) - .user(authentication_info.user.clone()) - .methods(authentication_info.methods.clone().iter()) - .audit_ids(authentication_info.audit_ids.clone().iter()) - .expires_at(self.get_new_token_expiry(&authentication_info.expires_at)?) - .build()?, - )) - } - } - - /// Expand user information in the token. - async fn expand_user_information( - &self, - state: &ServiceState, - token: &mut Token, - ) -> Result<(), TokenProviderError> { - if token.user().is_none() { - let user = state - .provider - .get_identity_provider() - .get_user(state, token.user_id()) - .await?; - match token { - Token::ApplicationCredential(data) => { - data.user = user; - } - Token::Unscoped(data) => { - data.user = user; - } - Token::ProjectScope(data) => { - data.user = user; - } - Token::DomainScope(data) => { - data.user = user; - } - Token::FederationUnscoped(data) => { - data.user = user; - } - Token::FederationProjectScope(data) => { - data.user = user; - } - Token::FederationDomainScope(data) => { - data.user = user; - } - Token::Restricted(data) => { - data.user = user; - } - Token::SystemScope(data) => { - data.user = user; - } - Token::Trust(data) => { - data.user = if let Some(trust) = &data.trust - && trust.impersonation - { - state - .provider - .get_identity_provider() - .get_user(state, &trust.trustor_user_id) - .await? - } else { - user - }; - } - } - } - Ok(()) - } - - /// Expand the target scope information in the token. - async fn expand_scope_information( - &self, - state: &ServiceState, - token: &mut Token, - ) -> Result<(), TokenProviderError> { - match token { - Token::ProjectScope(data) => { - if data.project.is_none() { - let project = state - .provider - .get_resource_provider() - .get_project(state, &data.project_id) - .await?; - - data.project = project; - } - } - Token::ApplicationCredential(data) => { - if data.application_credential.is_none() { - data.application_credential = Some( - state - .provider - .get_application_credential_provider() - .get_application_credential(state, &data.application_credential_id) - .await? - .ok_or_else(|| { - TokenProviderError::ApplicationCredentialNotFound( - data.application_credential_id.clone(), - ) - })?, - ); - } - if data.project.is_none() { - let project = state - .provider - .get_resource_provider() - .get_project(state, &data.project_id) - .await?; - - data.project = project; - } - } - Token::FederationProjectScope(data) => { - if data.project.is_none() { - let project = state - .provider - .get_resource_provider() - .get_project(state, &data.project_id) - .await?; - - data.project = project; - } - } - Token::DomainScope(data) => { - if data.domain.is_none() { - let domain = state - .provider - .get_resource_provider() - .get_domain(state, &data.domain_id) - .await?; - - data.domain = domain; - } - } - Token::FederationDomainScope(data) => { - if data.domain.is_none() { - let domain = state - .provider - .get_resource_provider() - .get_domain(state, &data.domain_id) - .await?; - - data.domain = domain; - } - } - Token::Restricted(data) => { - if data.project.is_none() { - let project = state - .provider - .get_resource_provider() - .get_project(state, &data.project_id) - .await?; - - data.project = project; - } - } - Token::SystemScope(_data) => {} - Token::Trust(data) => { - if data.trust.is_none() { - data.trust = state - .provider - .get_trust_provider() - .get_trust(state, &data.trust_id) - .await?; - } - if data.project.is_none() { - data.project = state - .provider - .get_resource_provider() - .get_project(state, &data.project_id) - .await?; - } - } - - _ => {} - }; - Ok(()) - } - - /// Populate role assignments in the token that support that information. - async fn _populate_role_assignments( - &self, - state: &ServiceState, - token: &mut Token, - ) -> Result<(), TokenProviderError> { - match token { - Token::ApplicationCredential(data) => { - if data.application_credential.is_none() { - data.application_credential = Some( - state - .provider - .get_application_credential_provider() - .get_application_credential(state, &data.application_credential_id) - .await? - .ok_or_else(|| { - TokenProviderError::ApplicationCredentialNotFound( - data.application_credential_id.clone(), - ) - })?, - ); - } - if let Some(ref mut ac) = data.application_credential { - let user_role_ids: HashSet = state - .provider - .get_assignment_provider() - .list_role_assignments( - state, - &RoleAssignmentListParametersBuilder::default() - .user_id(&data.user_id) - .project_id(&ac.project_id) - .include_names(false) - .effective(true) - .build() - .map_err(AssignmentProviderError::from)?, - ) - .await? - .into_iter() - .map(|x| x.role_id.clone()) - .collect(); - - // Gather all effective roles that the user have remaining should some of the - // AppCred assigned roles be revoked in the meanwhile. - let mut final_roles: Vec = Vec::new(); - for role in ac.roles.iter() { - if user_role_ids.contains(&role.id) { - final_roles.push(role.clone()); - } - } - if final_roles.is_empty() { - return Err(TokenProviderError::ActorHasNoRolesOnTarget); - } - data.roles = Some(final_roles); - }; - } - Token::DomainScope(data) => { - data.roles = Some( - state - .provider - .get_assignment_provider() - .list_role_assignments( - state, - &RoleAssignmentListParametersBuilder::default() - .user_id(&data.user_id) - .domain_id(&data.domain_id) - .include_names(true) - .effective(true) - .build() - .map_err(AssignmentProviderError::from)?, - ) - .await? - .into_iter() - .map(|x| RoleRef { - id: x.role_id.clone(), - name: x.role_name.clone(), - domain_id: None, - }) - .collect(), - ); - if data.roles.as_ref().is_none_or(|roles| roles.is_empty()) { - return Err(TokenProviderError::ActorHasNoRolesOnTarget); - } - } - Token::FederationProjectScope(data) => { - data.roles = Some( - state - .provider - .get_assignment_provider() - .list_role_assignments( - state, - &RoleAssignmentListParametersBuilder::default() - .user_id(&data.user_id) - .project_id(&data.project_id) - .include_names(true) - .effective(true) - .build() - .map_err(AssignmentProviderError::from)?, - ) - .await? - .into_iter() - .map(|x| RoleRef { - id: x.role_id.clone(), - name: x.role_name.clone(), - domain_id: None, - }) - .collect(), - ); - if data.roles.as_ref().is_none_or(|roles| roles.is_empty()) { - return Err(TokenProviderError::ActorHasNoRolesOnTarget); - } - } - Token::FederationDomainScope(data) => { - data.roles = Some( - state - .provider - .get_assignment_provider() - .list_role_assignments( - state, - &RoleAssignmentListParametersBuilder::default() - .user_id(&data.user_id) - .domain_id(&data.domain_id) - .include_names(true) - .effective(true) - .build() - .map_err(AssignmentProviderError::from)?, - ) - .await? - .into_iter() - .map(|x| RoleRef { - id: x.role_id.clone(), - name: x.role_name.clone(), - domain_id: None, - }) - .collect(), - ); - if data.roles.as_ref().is_none_or(|roles| roles.is_empty()) { - return Err(TokenProviderError::ActorHasNoRolesOnTarget); - } - } - Token::ProjectScope(data) => { - data.roles = Some( - state - .provider - .get_assignment_provider() - .list_role_assignments( - state, - &RoleAssignmentListParametersBuilder::default() - .user_id(&data.user_id) - .project_id(&data.project_id) - .include_names(true) - .effective(true) - .build() - .map_err(AssignmentProviderError::from)?, - ) - .await? - .into_iter() - .map(|x| RoleRef { - id: x.role_id.clone(), - name: x.role_name.clone(), - domain_id: None, - }) - .collect(), - ); - if data.roles.as_ref().is_none_or(|roles| roles.is_empty()) { - return Err(TokenProviderError::ActorHasNoRolesOnTarget); - } - } - Token::Restricted(data) => { - if data.roles.is_none() { - self.get_token_restriction(state, &data.token_restriction_id, true) - .await? - .inspect(|restrictions| data.roles = restrictions.roles.clone()) - .ok_or(TokenProviderError::TokenRestrictionNotFound( - data.token_restriction_id.clone(), - ))?; - } - } - Token::SystemScope(data) => { - data.roles = Some( - state - .provider - .get_assignment_provider() - .list_role_assignments( - state, - &RoleAssignmentListParametersBuilder::default() - .user_id(&data.user_id) - .system_id(&data.system_id) - .include_names(true) - .effective(true) - .build() - .map_err(AssignmentProviderError::from)?, - ) - .await? - .into_iter() - .map(|x| RoleRef { - id: x.role_id.clone(), - name: x.role_name.clone(), - domain_id: None, - }) - .collect(), - ); - if data.roles.as_ref().is_none_or(|roles| roles.is_empty()) { - return Err(TokenProviderError::ActorHasNoRolesOnTarget); - } - } - Token::Trust(data) => { - // Resolve role assignments of the trust verifying that the trustor still has - // those roles on the scope. - if let Some(ref mut trust) = data.trust { - let trustor_roles: HashSet = state - .provider - .get_assignment_provider() - .list_role_assignments( - state, - &RoleAssignmentListParameters { - user_id: Some(trust.trustor_user_id.clone()), - project_id: Some(data.project_id.clone()), - effective: Some(true), - ..Default::default() - }, - ) - .await? - .into_iter() - .map(|x| x.role_id.clone()) - .collect(); - if let Some(ref mut trust_roles) = trust.roles { - // `token_model._get_trust_roles`: Verify that the trustor still has all - // roles mentioned in the trust. Return error when at least one role is not - // available anymore. - - // Expand the implied roles - state - .provider - .get_role_provider() - .expand_implied_roles(state, trust_roles) - .await?; - if !trust_roles - .iter() - .all(|role| trustor_roles.contains(&role.id)) - { - debug!( - "Trust roles {:?} are missing for the trustor {:?}", - trust_roles, trustor_roles - ); - return Err(TokenProviderError::ActorHasNoRolesOnTarget); - } - trust_roles.retain_mut(|role| role.domain_id.is_none()); - } - } - } - _ => {} - } - - Ok(()) - } -} - -#[async_trait] -impl TokenApi for TokenProvider { - /// Authenticate by token. - #[tracing::instrument(level = "info", skip(self, state, credential))] - async fn authenticate_by_token<'a>( - &self, - state: &ServiceState, - credential: &'a str, - allow_expired: Option, - window_seconds: Option, - ) -> Result { - // TODO: is the expand really false? - let token = self - .validate_token(state, credential, allow_expired, window_seconds) - .await?; - if let Token::Restricted(restriction) = &token - && !restriction.allow_renew - { - return Err(AuthenticationError::TokenRenewalForbidden)?; - } - let mut auth_info_builder = AuthenticatedInfo::builder(); - auth_info_builder.user_id(token.user_id()); - auth_info_builder.methods(token.methods().clone()); - auth_info_builder.audit_ids(token.audit_ids().clone()); - auth_info_builder.expires_at(*token.expires_at()); - if let Token::Restricted(restriction) = &token { - auth_info_builder.token_restriction_id(restriction.token_restriction_id.clone()); - } - Ok(auth_info_builder - .build() - .map_err(AuthenticationError::from)?) - } - - /// Validate token. - #[tracing::instrument(level = "info", skip(self, state, credential))] - async fn validate_token<'a>( - &self, - state: &ServiceState, - credential: &'a str, - allow_expired: Option, - window_seconds: Option, - ) -> Result { - let mut token = self.backend_driver.decode(credential)?; - let latest_expiration_cutof = Utc::now() - .checked_add_signed(TimeDelta::seconds(window_seconds.unwrap_or(0))) - .unwrap_or(Utc::now()); - if !allow_expired.unwrap_or_default() && *token.expires_at() < latest_expiration_cutof { - trace!( - "Token has expired at {:?} with cutof: {:?}", - token.expires_at(), - latest_expiration_cutof - ); - return Err(TokenProviderError::Expired); - } - - // Expand the token unless `expand = Some(false)` - token = self.expand_token_information(state, &token).await?; - - if state - .provider - .get_revoke_provider() - .is_token_revoked(state, &token) - .await? - { - return Err(TokenProviderError::TokenRevoked); - } - - token.validate_subject(state).await?; - token.validate_scope(state).await?; - - Ok(token) - } - - /// Issue the Keystone token. - #[tracing::instrument(level = "debug", skip(self))] - fn issue_token( - &self, - authentication_info: AuthenticatedInfo, - authz_info: AuthzInfo, - token_restrictions: Option<&TokenRestriction>, - ) -> Result { - // This should be executed already, but let's better repeat it as last line of - // defence. It is also necessary to call this before to stop before we - // start to resolve authz info. - authentication_info.validate()?; - - // TODO: Check whether it is allowed to change the scope of the token if - // AuthenticatedInfo already contains scope it was issued for. - let mut authentication_info = authentication_info; - authentication_info - .audit_ids - .push(URL_SAFE_NO_PAD.encode(Uuid::new_v4().as_bytes())); - if let Some(token_restrictions) = &token_restrictions { - self.create_restricted_token(&authentication_info, &authz_info, token_restrictions) - } else if authentication_info.idp_id.is_some() && authentication_info.protocol_id.is_some() - { - match &authz_info { - AuthzInfo::Domain(domain) => { - self.create_federated_domain_scope_token(&authentication_info, domain) - } - AuthzInfo::Project(project) => { - self.create_federated_project_scope_token(&authentication_info, project) - } - AuthzInfo::Trust(_trust) => Err(TokenProviderError::Conflict { - message: "cannot create trust token with an identity provider in scope".into(), - context: "issuing token".into(), - }), - AuthzInfo::System => Err(TokenProviderError::Conflict { - message: "cannot create system scope token with an identity provider in scope" - .into(), - context: "issuing token".into(), - }), - AuthzInfo::Unscoped => self.create_federated_unscoped_token(&authentication_info), - } - } else { - match &authz_info { - AuthzInfo::Domain(domain) => { - self.create_domain_scope_token(&authentication_info, domain) - } - AuthzInfo::Project(project) => { - self.create_project_scope_token(&authentication_info, project) - } - AuthzInfo::Trust(trust) => self.create_trust_token(&authentication_info, trust), - AuthzInfo::System => self.create_system_scoped_token(&authentication_info), - AuthzInfo::Unscoped => self.create_unscoped_token(&authentication_info), - } - } - } - - /// Encode the token into a `String` representation. - /// - /// Encode the [`Token`] into the `String` to be used as a http header. - fn encode_token(&self, token: &Token) -> Result { - self.backend_driver.encode(token) - } - - /// Populate role assignments in the token that support that information. - async fn populate_role_assignments( - &self, - state: &ServiceState, - token: &mut Token, - ) -> Result<(), TokenProviderError> { - self._populate_role_assignments(state, token).await - } - - /// Expand the token information. - /// - /// Query and expand information about the user, scope and the role - /// assignments into the token. - async fn expand_token_information( - &self, - state: &ServiceState, - token: &Token, - ) -> Result { - let mut new_token = token.clone(); - self.expand_user_information(state, &mut new_token).await?; - self.expand_scope_information(state, &mut new_token).await?; - self.populate_role_assignments(state, &mut new_token) - .await?; - Ok(new_token) - } - - /// Get the token restriction by the ID. - async fn get_token_restriction<'a>( - &self, - state: &ServiceState, - id: &'a str, - expand_roles: bool, - ) -> Result, TokenProviderError> { - self.tr_backend_driver - .get_token_restriction(state, id, expand_roles) - .await - } - - /// Create new token restriction. - async fn create_token_restriction<'a>( - &self, - state: &ServiceState, - restriction: TokenRestrictionCreate, - ) -> Result { - let mut restriction = restriction; - if restriction.id.is_empty() { - restriction.id = Uuid::new_v4().simple().to_string(); - } - self.tr_backend_driver - .create_token_restriction(state, restriction) - .await - } - - /// List token restrictions. - async fn list_token_restrictions<'a>( - &self, - state: &ServiceState, - params: &TokenRestrictionListParameters, - ) -> Result, TokenProviderError> { - self.tr_backend_driver - .list_token_restrictions(state, params) - .await - } - - /// Update existing token restriction. - async fn update_token_restriction<'a>( - &self, - state: &ServiceState, - id: &'a str, - restriction: TokenRestrictionUpdate, - ) -> Result { - self.tr_backend_driver - .update_token_restriction(state, id, restriction) - .await - } - - /// Delete token restriction by the ID. - async fn delete_token_restriction<'a>( - &self, - state: &ServiceState, - id: &'a str, - ) -> Result<(), TokenProviderError> { - self.tr_backend_driver - .delete_token_restriction(state, id) - .await - } -} - -#[cfg(test)] -mod tests { - use chrono::Utc; - use eyre::{Result, eyre}; - use sea_orm::DatabaseConnection; - use std::fs::File; - use std::io::Write; - use std::sync::Arc; - use tempfile::tempdir; - use tracing_test::traced_test; - use uuid::Uuid; - - use super::*; - use crate::application_credential::{ - MockApplicationCredentialProvider, types::ApplicationCredential, - }; - use crate::assignment::{ - MockAssignmentProvider, - types::{Assignment, AssignmentType, RoleAssignmentListParameters}, - }; - use crate::auth::AuthenticatedInfoBuilder; - use crate::config::Config; - use crate::identity::{MockIdentityProvider, types::UserResponseBuilder}; - use crate::keystone::Service; - use crate::plugin_manager::PluginManager; - use crate::provider::Provider; - use crate::resource::{MockResourceProvider, types::*}; - use crate::revoke::MockRevokeProvider; - use crate::trust::types::*; - - pub(super) fn setup_config() -> Config { - let keys_dir = tempdir().unwrap(); - // write fernet key used to generate tokens in python - let file_path = keys_dir.path().join("0"); - let mut tmp_file = File::create(file_path).unwrap(); - write!(tmp_file, "BFTs1CIVIBLTP4GOrQ26VETrJ7Zwz1O4wbEcCQ966eM=").unwrap(); - - let builder = config::Config::builder() - .set_override( - "auth.methods", - "password,token,openid,application_credential", - ) - .unwrap() - .set_override("database.connection", "dummy") - .unwrap(); - let mut config: Config = Config::try_from(builder).expect("can build a valid config"); - config.fernet_tokens.key_repository = keys_dir.keep(); - config - } - - /// Generate test token to use for validation testing. - fn generate_token(validity: Option) -> Result { - Ok(Token::ProjectScope(ProjectScopePayload { - methods: vec!["password".into()], - user_id: Uuid::new_v4().simple().to_string(), - project_id: Uuid::new_v4().simple().to_string(), - audit_ids: vec!["Zm9vCg".into()], - expires_at: Utc::now() - .checked_add_signed(validity.unwrap_or_default()) - .ok_or(eyre!("timedelta apply failed"))?, - ..Default::default() - })) - } - - fn get_provider(config: &Config) -> TokenProvider { - let mut pm = PluginManager::default(); - pm.register_token_restriction_backend( - "sql", - Arc::new(backend::MockTokenRestrictionBackend::default()), - ); - TokenProvider::new(config, &pm).unwrap() - } - - #[tokio::test] - async fn test_populate_role_assignments() { - let token_provider = get_provider(&Config::default()); - let mut assignment_mock = MockAssignmentProvider::default(); - assignment_mock - .expect_list_role_assignments() - .withf(|_, q: &RoleAssignmentListParameters| { - q.project_id == Some("project_id".to_string()) - }) - .returning(|_, q: &RoleAssignmentListParameters| { - Ok(vec![Assignment { - role_id: "rid".into(), - role_name: Some("role_name".into()), - actor_id: q.user_id.clone().unwrap(), - target_id: q.project_id.clone().unwrap(), - r#type: AssignmentType::UserProject, - inherited: false, - implied_via: None, - }]) - }); - assignment_mock - .expect_list_role_assignments() - .withf(|_, q: &RoleAssignmentListParameters| { - q.domain_id == Some("domain_id".to_string()) - }) - .returning(|_, q: &RoleAssignmentListParameters| { - Ok(vec![Assignment { - role_id: "rid".into(), - role_name: Some("role_name".into()), - actor_id: q.user_id.clone().unwrap(), - target_id: q.domain_id.clone().unwrap(), - r#type: AssignmentType::UserProject, - inherited: false, - implied_via: None, - }]) - }); - let provider = Provider::mocked_builder() - .assignment(assignment_mock) - .build() - .unwrap(); - - let state = Arc::new( - Service::new( - Config::default(), - DatabaseConnection::Disconnected, - provider, - crate::policy::MockPolicyEnforcer::new(), - ) - .unwrap(), - ); - - let mut ptoken = Token::ProjectScope(ProjectScopePayload { - user_id: "bar".into(), - project_id: "project_id".into(), - ..Default::default() - }); - token_provider - .populate_role_assignments(&state, &mut ptoken) - .await - .unwrap(); - - if let Token::ProjectScope(data) = ptoken { - assert_eq!( - data.roles.unwrap(), - vec![RoleRef { - id: "rid".into(), - name: Some("role_name".into()), - domain_id: None - }] - ); - } else { - panic!("Not project scope"); - } - - let mut dtoken = Token::DomainScope(DomainScopePayload { - user_id: "bar".into(), - domain_id: "domain_id".into(), - ..Default::default() - }); - token_provider - .populate_role_assignments(&state, &mut dtoken) - .await - .unwrap(); - - if let Token::DomainScope(data) = dtoken { - assert_eq!( - data.roles.unwrap(), - vec![RoleRef { - id: "rid".into(), - name: Some("role_name".into()), - domain_id: None - }] - ); - } else { - panic!("Not domain scope"); - } - - let mut utoken = Token::Unscoped(UnscopedPayload { - user_id: "bar".into(), - ..Default::default() - }); - assert!( - token_provider - .populate_role_assignments(&state, &mut utoken) - .await - .is_ok() - ); - } - - /// Test that a valid token with revocation events fails validation. - #[tokio::test] - #[traced_test] - async fn test_validate_token_revoked() { - let token = generate_token(Some(TimeDelta::hours(1))).unwrap(); - - let config = setup_config(); - let token_provider = get_provider(&config); - let mut revoke_mock = MockRevokeProvider::default(); - //let token_clone = token.clone(); - revoke_mock - .expect_is_token_revoked() - // TODO: in roundtrip the precision of expiry is reduced and issued_at is different - //.withf(move |_, t: &Token| { - // *t == token_clone - //}) - .returning(|_, _| Ok(true)); - - let mut identity_mock = MockIdentityProvider::default(); - let token_clone = token.clone(); - identity_mock - .expect_get_user() - .withf(move |_, id: &'_ str| id == token_clone.user_id()) - .returning(|_, id: &'_ str| { - Ok(Some( - UserResponseBuilder::default() - .domain_id("user_domain_id") - .enabled(true) - .name("name") - .id(id) - .build() - .unwrap(), - )) - }); - let mut resource_mock = MockResourceProvider::default(); - let token_clone2 = token.clone(); - resource_mock - .expect_get_project() - .withf(move |_, id: &'_ str| id == token_clone2.project_id().unwrap()) - .returning(|_, id: &'_ str| { - Ok(Some(Project { - id: id.to_string(), - name: "project".to_string(), - ..Default::default() - })) - }); - - let mut assignment_mock = MockAssignmentProvider::default(); - let token_clone3 = token.clone(); - assignment_mock - .expect_list_role_assignments() - .withf(move |_, q: &RoleAssignmentListParameters| { - q.project_id == token_clone3.project_id().cloned() - }) - .returning(|_, q: &RoleAssignmentListParameters| { - Ok(vec![Assignment { - role_id: "rid".into(), - role_name: Some("role_name".into()), - actor_id: q.user_id.clone().unwrap(), - target_id: q.project_id.clone().unwrap(), - r#type: AssignmentType::UserProject, - inherited: false, - implied_via: None, - }]) - }); - let provider = Provider::mocked_builder() - .assignment(assignment_mock) - .identity(identity_mock) - .revoke(revoke_mock) - .resource(resource_mock) - .build() - .unwrap(); - let state = Arc::new( - Service::new( - config, - DatabaseConnection::Disconnected, - provider, - crate::policy::MockPolicyEnforcer::new(), - ) - .unwrap(), - ); - - let credential = token_provider.encode_token(&token).unwrap(); - match token_provider - .validate_token(&state, &credential, Some(false), None) - .await - { - Err(TokenProviderError::TokenRevoked) => {} - _ => { - panic!("token must be revoked") - } - } - } - - #[tokio::test] - async fn test_populate_role_assignments_application_credential() { - let token_provider = get_provider(&Config::default()); - let mut assignment_mock = MockAssignmentProvider::default(); - assignment_mock - .expect_list_role_assignments() - .withf(|_, q: &RoleAssignmentListParameters| { - q.project_id == Some("project_id".to_string()) - && q.user_id == Some("bar".to_string()) - }) - .returning(|_, q: &RoleAssignmentListParameters| { - Ok(vec![Assignment { - role_id: "role_1".into(), - role_name: Some("role_name".into()), - actor_id: q.user_id.clone().unwrap(), - target_id: q.project_id.clone().unwrap(), - r#type: AssignmentType::UserProject, - inherited: false, - implied_via: None, - }]) - }); - assignment_mock - .expect_list_role_assignments() - .withf(|_, q: &RoleAssignmentListParameters| { - q.domain_id == Some("domain_id".to_string()) - }) - .returning(|_, q: &RoleAssignmentListParameters| { - Ok(vec![Assignment { - role_id: "rid".into(), - role_name: Some("role_name".into()), - actor_id: q.user_id.clone().unwrap(), - target_id: q.domain_id.clone().unwrap(), - r#type: AssignmentType::UserProject, - inherited: false, - implied_via: None, - }]) - }); - let mut ac_mock = MockApplicationCredentialProvider::default(); - ac_mock - .expect_get_application_credential() - .withf(|_, id: &'_ str| id == "app_cred_id") - .returning(|_, id: &'_ str| { - Ok(Some(ApplicationCredential { - access_rules: None, - description: None, - expires_at: None, - id: id.into(), - name: "foo".into(), - project_id: "project_id".into(), - roles: vec![ - RoleRef { - id: "role_1".into(), - name: Some("role_name_1".into()), - domain_id: None, - }, - RoleRef { - id: "role_2".into(), - name: Some("role_name_2".into()), - domain_id: None, - }, - ], - unrestricted: false, - user_id: "bar".into(), - })) - }); - ac_mock - .expect_get_application_credential() - .withf(|_, id: &'_ str| id == "app_cred_bad_roles") - .returning(|_, id: &'_ str| { - Ok(Some(ApplicationCredential { - access_rules: None, - description: None, - expires_at: None, - id: id.into(), - name: "foo".into(), - project_id: "project_id".into(), - roles: vec![ - RoleRef { - id: "-role_1".into(), - name: Some("-role_name_1".into()), - domain_id: None, - }, - RoleRef { - id: "-role_2".into(), - name: Some("-role_name_2".into()), - domain_id: None, - }, - ], - unrestricted: false, - user_id: "bar".into(), - })) - }); - ac_mock - .expect_get_application_credential() - .withf(|_, id: &'_ str| id == "missing") - .returning(|_, _| Ok(None)); - let provider = Provider::mocked_builder() - .application_credential(ac_mock) - .assignment(assignment_mock) - .build() - .unwrap(); - - let state = Arc::new( - Service::new( - Config::default(), - DatabaseConnection::Disconnected, - provider, - crate::policy::MockPolicyEnforcer::new(), - ) - .unwrap(), - ); - - let mut token = Token::ApplicationCredential(ApplicationCredentialPayload { - user_id: "bar".into(), - project_id: "project_id".into(), - application_credential_id: "app_cred_id".into(), - ..Default::default() - }); - token_provider - .populate_role_assignments(&state, &mut token) - .await - .unwrap(); - - if let Token::ApplicationCredential(..) = &token { - assert_eq!( - token.effective_roles().unwrap(), - &vec![RoleRef { - id: "role_1".into(), - name: Some("role_name_1".into()), - domain_id: None, - }], - "only still active role assignment is returned" - ); - } else { - panic!("Not application credential scope"); - } - - // Try populating role assignments for not existing appcred - if let Err(TokenProviderError::ApplicationCredentialNotFound(id)) = token_provider - .populate_role_assignments( - &state, - &mut Token::ApplicationCredential(ApplicationCredentialPayload { - user_id: "bar".into(), - project_id: "project_id".into(), - application_credential_id: "missing".into(), - ..Default::default() - }), - ) - .await - { - assert_eq!(id, "missing"); - } else { - panic!("role expansion for missing application credential should fail"); - } - - // No roles remain after subtracting current user roles - if let Err(TokenProviderError::ActorHasNoRolesOnTarget) = token_provider - .populate_role_assignments( - &state, - &mut Token::ApplicationCredential(ApplicationCredentialPayload { - user_id: "bar".into(), - project_id: "project_id".into(), - application_credential_id: "app_cred_bad_roles".into(), - ..Default::default() - }), - ) - .await - { - } else { - panic!( - "role expansion for application credential with roles the user does not have anymore should fail" - ); - } - } - - #[tokio::test] - async fn test_create_unscoped_token() { - let token_provider = get_provider(&Config::default()); - let now = Utc::now(); - let token = token_provider - .create_unscoped_token( - &AuthenticatedInfoBuilder::default() - .user_id("uid") - .expires_at(now) - .build() - .unwrap(), - ) - .unwrap(); - assert_eq!(*token.expires_at(), now); - assert_eq!(*token.user_id(), "uid"); - let token = token_provider - .create_unscoped_token( - &AuthenticatedInfoBuilder::default() - .user_id("uid") - .build() - .unwrap(), - ) - .unwrap(); - assert!(now < *token.expires_at()); - assert_eq!(*token.user_id(), "uid"); - assert!(token.project_id().is_none()); - } - - #[tokio::test] - async fn test_create_project_scope_token() { - let token_provider = get_provider(&Config::default()); - let now = Utc::now(); - let token = token_provider - .create_project_scope_token( - &AuthenticatedInfoBuilder::default() - .user_id("uid") - .expires_at(now) - .build() - .unwrap(), - &ProjectBuilder::default() - .id("pid") - .domain_id("did") - .name("pname") - .enabled(true) - .build() - .unwrap(), - ) - .unwrap(); - assert_eq!(*token.expires_at(), now); - assert_eq!(*token.user_id(), "uid"); - let token = token_provider - .create_project_scope_token( - &AuthenticatedInfoBuilder::default() - .user_id("uid") - .build() - .unwrap(), - &ProjectBuilder::default() - .id("pid") - .domain_id("did") - .name("pname") - .enabled(true) - .build() - .unwrap(), - ) - .unwrap(); - assert!(now < *token.expires_at()); - assert_eq!(*token.user_id(), "uid"); - assert_eq!(*token.project_id().unwrap(), "pid"); - } - - #[tokio::test] - async fn test_create_domain_scope_token() { - let token_provider = get_provider(&Config::default()); - let now = Utc::now(); - let token = token_provider - .create_domain_scope_token( - &AuthenticatedInfoBuilder::default() - .user_id("uid") - .expires_at(now) - .build() - .unwrap(), - &DomainBuilder::default() - .id("did") - .name("pname") - .enabled(true) - .build() - .unwrap(), - ) - .unwrap(); - assert_eq!(*token.expires_at(), now); - assert_eq!(*token.user_id(), "uid"); - let token = token_provider - .create_domain_scope_token( - &AuthenticatedInfoBuilder::default() - .user_id("uid") - .build() - .unwrap(), - &DomainBuilder::default() - .id("did") - .name("pname") - .enabled(true) - .build() - .unwrap(), - ) - .unwrap(); - assert!(now < *token.expires_at()); - assert_eq!(*token.user_id(), "uid"); - assert_eq!(token.domain().unwrap().id, "did"); - } - - #[tokio::test] - async fn test_create_system_token() { - let token_provider = get_provider(&Config::default()); - let now = Utc::now(); - let token = token_provider - .create_system_scoped_token( - &AuthenticatedInfoBuilder::default() - .user_id("uid") - .expires_at(now) - .build() - .unwrap(), - ) - .unwrap(); - assert_eq!(*token.expires_at(), now); - assert_eq!(*token.user_id(), "uid"); - let token = token_provider - .create_system_scoped_token( - &AuthenticatedInfoBuilder::default() - .user_id("uid") - .build() - .unwrap(), - ) - .unwrap(); - assert!(now < *token.expires_at()); - assert_eq!(*token.user_id(), "uid"); - if let Token::SystemScope(data) = token { - assert_eq!(data.system_id, "system"); - } else { - panic!("wrong token type"); - } - } - - #[tokio::test] - async fn test_create_trust_token() { - let token_provider = get_provider(&Config::default()); - let now = Utc::now(); - let token = token_provider - .create_trust_token( - &AuthenticatedInfoBuilder::default() - .user_id("uid") - .expires_at(now) - .build() - .unwrap(), - &TrustBuilder::default() - .id("tid") - .impersonation(false) - .trustor_user_id("trustor_uid") - .trustee_user_id("trustor_uid") - .build() - .unwrap(), - ) - .unwrap(); - assert_eq!(*token.expires_at(), now); - assert_eq!(*token.user_id(), "uid"); - let token = token_provider - .create_trust_token( - &AuthenticatedInfoBuilder::default() - .user_id("uid") - .build() - .unwrap(), - &TrustBuilder::default() - .id("tid") - .impersonation(false) - .trustor_user_id("trustor_uid") - .trustee_user_id("trustor_uid") - .project_id("pid") - .build() - .unwrap(), - ) - .unwrap(); - assert!(now < *token.expires_at()); - assert_eq!(*token.user_id(), "uid"); - if let Token::Trust(data) = token { - assert_eq!(data.trust_id, "tid"); - } else { - panic!("wrong token type"); - } - - // unscoped - let token = token_provider - .create_trust_token( - &AuthenticatedInfoBuilder::default() - .user_id("uid") - .build() - .unwrap(), - &TrustBuilder::default() - .id("tid") - .impersonation(false) - .trustor_user_id("trustor_uid") - .trustee_user_id("trustor_uid") - .build() - .unwrap(), - ) - .unwrap(); - assert!(now < *token.expires_at()); - assert_eq!(*token.user_id(), "uid"); - if let Token::Unscoped(_data) = token { - } else { - panic!("wrong token type"); - } - } - - #[tokio::test] - async fn test_create_restricted_token() { - let token_provider = get_provider(&Config::default()); - let now = Utc::now(); - let token = token_provider - .create_restricted_token( - &AuthenticatedInfoBuilder::default() - .user_id("uid") - .expires_at(now) - .build() - .unwrap(), - &AuthzInfo::System, - &TokenRestrictionBuilder::default() - .id("rid") - .domain_id("did") - .project_id("pid") - .allow_renew(true) - .allow_rescope(true) - .role_ids([]) - .build() - .unwrap(), - ) - .unwrap(); - assert_eq!(*token.expires_at(), now); - assert_eq!(*token.user_id(), "uid"); - let token = token_provider - .create_restricted_token( - &AuthenticatedInfoBuilder::default() - .user_id("uid") - .build() - .unwrap(), - &AuthzInfo::System, - &TokenRestrictionBuilder::default() - .id("rid") - .domain_id("did") - .project_id("pid") - .allow_renew(true) - .allow_rescope(true) - .role_ids([]) - .build() - .unwrap(), - ) - .unwrap(); - assert!(now < *token.expires_at()); - assert_eq!(*token.user_id(), "uid"); - if let Token::Restricted(data) = token { - assert_eq!(data.token_restriction_id, "rid"); - } else { - panic!("wrong token type"); - } - } -} diff --git a/crates/keystone/src/token/token_restriction/sql.rs b/crates/keystone/src/token/token_restriction/sql.rs index 93e74159..e865f059 100644 --- a/crates/keystone/src/token/token_restriction/sql.rs +++ b/crates/keystone/src/token/token_restriction/sql.rs @@ -27,12 +27,6 @@ mod get; mod list; mod update; -//use create::create; -//use delete::delete; -//use get::get; -//use list::list; -//use update::update; - #[derive(Default)] pub struct SqlBackend {} @@ -101,13 +95,26 @@ impl From for TokenRestriction { } } -impl - From<( - token_restriction::Model, - Vec, - )> for TokenRestriction -{ - fn from( +pub trait FromModelWithRoleAssociation { + fn from_model_with_ra( + value: ( + token_restriction::Model, + Vec, + ), + ) -> Self; + //fn from_model_with_ra_and_role( + // value: ( + // token_restriction::Model, + // Vec<( + // token_restriction_role_association::Model, + // Option, + // )>, + // ), + //) -> Self; +} + +impl FromModelWithRoleAssociation for TokenRestriction { + fn from_model_with_ra( value: ( token_restriction::Model, Vec, @@ -119,27 +126,26 @@ impl } } -impl - From<( - token_restriction::Model, - Vec<( +pub trait FromModelWithRoleAssociationAndRoles { + fn from_model_with_ra_and_roles( + tr_model: token_restriction::Model, + roles: Vec<( + token_restriction_role_association::Model, + Option, + )>, + ) -> Self; +} + +impl FromModelWithRoleAssociationAndRoles for TokenRestriction { + fn from_model_with_ra_and_roles( + tr_model: token_restriction::Model, + roles: Vec<( token_restriction_role_association::Model, Option, )>, - )> for TokenRestriction -{ - fn from( - value: ( - token_restriction::Model, - Vec<( - token_restriction_role_association::Model, - Option, - )>, - ), ) -> Self { - let mut restriction: TokenRestriction = value.0.into(); - let roles: Vec = value - .1 + let mut restriction: TokenRestriction = tr_model.into(); + let roles: Vec = roles .into_iter() .filter_map(|(_a, r)| r) .map(|role| crate::role::types::RoleRef { @@ -154,6 +160,18 @@ impl } } +impl From for TokenProviderError { + fn from(source: crate::error::DatabaseError) -> Self { + match source { + cfl @ crate::error::DatabaseError::Conflict { .. } => Self::Conflict { + message: cfl.to_string(), + context: String::new(), + }, + other => Self::Driver(other.to_string()), + } + } +} + #[cfg(test)] mod tests { use crate::db::entity::token_restriction; diff --git a/crates/keystone/src/token/token_restriction/sql/get.rs b/crates/keystone/src/token/token_restriction/sql/get.rs index e63b130c..bc58ca8f 100644 --- a/crates/keystone/src/token/token_restriction/sql/get.rs +++ b/crates/keystone/src/token/token_restriction/sql/get.rs @@ -23,6 +23,9 @@ use crate::db::entity::prelude::{ use crate::db::entity::token_restriction_role_association; use crate::error::DbContextExt; use crate::token::error::TokenProviderError; +use crate::token::token_restriction::sql::{ + FromModelWithRoleAssociation, FromModelWithRoleAssociationAndRoles, +}; use crate::token::types::TokenRestriction; /// Get existing token restriction by the ID. @@ -47,7 +50,7 @@ pub async fn get>( .all(db) .await .context("reading token restriction roles")?; - Some((entry, roles).into()) + Some(TokenRestriction::from_model_with_ra_and_roles(entry, roles)) } else { let roles = DbTokenRestrictionRoleAssociation::find() .filter( @@ -57,7 +60,7 @@ pub async fn get>( .all(db) .await .context("reading token restriction roles")?; - Some((entry, roles).into()) + Some(TokenRestriction::from_model_with_ra((entry, roles))) } } else { None diff --git a/crates/keystone/src/token/token_restriction/sql/list.rs b/crates/keystone/src/token/token_restriction/sql/list.rs index 5b87f491..2167407e 100644 --- a/crates/keystone/src/token/token_restriction/sql/list.rs +++ b/crates/keystone/src/token/token_restriction/sql/list.rs @@ -25,6 +25,7 @@ use crate::db::entity::token_restriction; use crate::db::entity::token_restriction_role_association; use crate::error::DbContextExt; use crate::token::error::TokenProviderError; +use crate::token::token_restriction::sql::FromModelWithRoleAssociation; use crate::token::types::{TokenRestriction, TokenRestrictionListParameters}; /// List existing token restrictions. @@ -51,7 +52,10 @@ pub async fn list( .await .context("listing token restrictions")?; - Ok(db_restrictions.into_iter().map(Into::into).collect()) + Ok(db_restrictions + .into_iter() + .map(TokenRestriction::from_model_with_ra) + .collect()) } #[cfg(test)] diff --git a/crates/keystone/src/trust/api.rs b/crates/keystone/src/trust/api.rs index aaa7e745..91711239 100644 --- a/crates/keystone/src/trust/api.rs +++ b/crates/keystone/src/trust/api.rs @@ -13,5 +13,4 @@ // SPDX-License-Identifier: Apache-2.0 //! # Trust API -pub mod error; pub mod types; diff --git a/crates/keystone/src/trust/api/types/trust.rs b/crates/keystone/src/trust/api/types/trust.rs index 12efcf0f..3f69794d 100644 --- a/crates/keystone/src/trust/api/types/trust.rs +++ b/crates/keystone/src/trust/api/types/trust.rs @@ -11,25 +11,4 @@ // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 -use crate::trust::types::Trust; - pub use openstack_keystone_api_types::trust::*; - -impl From<&Trust> for TokenTrustRepr { - fn from(value: &Trust) -> Self { - Self { - expires_at: value.expires_at, - id: value.id.clone(), - impersonation: value.impersonation, - remaining_uses: value.remaining_uses, - redelegated_trust_id: value.redelegated_trust_id.clone(), - redelegation_count: value.redelegation_count, - trustor_user: TokenTrustUser { - id: value.trustor_user_id.clone(), - }, - trustee_user: TokenTrustUser { - id: value.trustee_user_id.clone(), - }, - } - } -} diff --git a/crates/keystone/src/trust/backend.rs b/crates/keystone/src/trust/backend.rs index fb5edea8..55673a92 100644 --- a/crates/keystone/src/trust/backend.rs +++ b/crates/keystone/src/trust/backend.rs @@ -12,39 +12,7 @@ // // SPDX-License-Identifier: Apache-2.0 //! Trust provider Backend trait. -use async_trait::async_trait; - -use crate::keystone::ServiceState; -use crate::trust::{TrustProviderError, types::*}; pub mod sql; pub use sql::SqlBackend; - -/// TrustBackend trait. -/// -/// Backend driver interface expected by the trust provider. -#[cfg_attr(test, mockall::automock)] -#[async_trait] -pub trait TrustBackend: Send + Sync { - /// Get trust by ID. - async fn get_trust<'a>( - &self, - state: &ServiceState, - id: &'a str, - ) -> Result, TrustProviderError>; - - /// Resolve trust chain by the trust ID. - async fn get_trust_delegation_chain<'a>( - &self, - state: &ServiceState, - id: &'a str, - ) -> Result>, TrustProviderError>; - - /// List trusts. - async fn list_trusts( - &self, - state: &ServiceState, - params: &TrustListParameters, - ) -> Result, TrustProviderError>; -} diff --git a/crates/keystone/src/trust/backend/error.rs b/crates/keystone/src/trust/backend/error.rs deleted file mode 100644 index a4ba6e7f..00000000 --- a/crates/keystone/src/trust/backend/error.rs +++ /dev/null @@ -1,60 +0,0 @@ -// 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 - -use thiserror::Error; - -use crate::assignment::backend::error::AssignmentDatabaseError; -use crate::error::{BuilderError, DatabaseError}; -use crate::role::backend::error::RoleDatabaseError; - -/// Database backend error for the database driver. -#[derive(Error, Debug)] -pub enum TrustDatabaseError { - /// Assignment database error. - #[error(transparent)] - AssignmentDatabase(#[from] AssignmentDatabaseError), - - /// Database error. - #[error(transparent)] - Database { - #[from] - source: DatabaseError, - }, - - /// DateTime parsing error. - #[error("error parsing int column as datetime: {expires_at}")] - ExpirationDateTimeParse { id: String, expires_at: i64 }, - - /// Role database error. - #[error(transparent)] - RoleDatabase(#[from] RoleDatabaseError), - - /// The trust has not been found. - #[error("{0}")] - TrustNotFound(String), - - #[error(transparent)] - Serde { - #[from] - source: serde_json::Error, - }, - - /// Structures builder error. - #[error(transparent)] - StructBuilder { - /// The source of the error. - #[from] - source: BuilderError, - }, -} diff --git a/crates/keystone/src/trust/backend/sql.rs b/crates/keystone/src/trust/backend/sql.rs index 655fc3ec..525b1858 100644 --- a/crates/keystone/src/trust/backend/sql.rs +++ b/crates/keystone/src/trust/backend/sql.rs @@ -15,7 +15,8 @@ use async_trait::async_trait; -use super::TrustBackend; +use openstack_keystone_core::trust::backend::TrustBackend; + use crate::keystone::ServiceState; use crate::trust::{TrustProviderError, types::*}; diff --git a/crates/keystone/src/trust/mod.rs b/crates/keystone/src/trust/mod.rs index 4f6265bd..42542818 100644 --- a/crates/keystone/src/trust/mod.rs +++ b/crates/keystone/src/trust/mod.rs @@ -50,830 +50,7 @@ //! //! Trusts can also be chained, meaning, a trust can be created by using a trust //! scoped token. - -use std::collections::{HashMap, HashSet}; -use std::hash::RandomState; -use std::sync::Arc; - -use async_trait::async_trait; -use chrono::{DateTime, Utc}; -use tracing::debug; +pub use openstack_keystone_core::trust::*; pub mod api; pub mod backend; -pub mod error; -#[cfg(test)] -mod mock; -pub mod types; - -use crate::keystone::ServiceState; -use crate::plugin_manager::PluginManager; -use crate::role::types::Role; -use crate::{config::Config, role::RoleApi}; -use backend::{SqlBackend, TrustBackend}; - -pub use error::TrustProviderError; -#[cfg(test)] -pub use mock::MockTrustProvider; -pub use types::*; - -/// Trust provider. -pub struct TrustProvider { - /// Backend driver. - backend_driver: Arc, -} - -impl TrustProvider { - pub fn new( - config: &Config, - plugin_manager: &PluginManager, - ) -> Result { - let backend_driver = - if let Some(driver) = plugin_manager.get_trust_backend(config.trust.driver.clone()) { - driver.clone() - } else { - match config.trust.driver.as_str() { - "sql" => Arc::new(SqlBackend::default()), - _ => { - return Err(TrustProviderError::UnsupportedDriver( - config.trust.driver.clone(), - )); - } - } - }; - Ok(Self { backend_driver }) - } -} - -#[async_trait] -impl TrustApi for TrustProvider { - /// Get trust by ID. - #[tracing::instrument(level = "debug", skip(self, state))] - async fn get_trust<'a>( - &self, - state: &ServiceState, - id: &'a str, - ) -> Result, TrustProviderError> { - if let Some(mut trust) = self.backend_driver.get_trust(state, id).await? { - let all_roles: HashMap = HashMap::from_iter( - state - .provider - .get_role_provider() - .list_roles( - state, - &crate::role::types::RoleListParameters { - domain_id: Some(None), - ..Default::default() - }, - ) - .await? - .iter() - .map(|role| (role.id.clone(), role.to_owned())), - ); - if let Some(ref mut roles) = trust.roles { - for role in roles.iter_mut() { - if let Some(erole) = all_roles.get(&role.id) { - role.domain_id = erole.domain_id.clone(); - role.name = Some(erole.name.clone()); - } - } - // Drop all roles for which name is not set (it is a signal that the processing - // above has not found the role matching the parameters. - roles.retain_mut(|role| role.name.is_some()); - } - return Ok(Some(trust)); - } - Ok(None) - } - - /// Resolve trust delegation chain by the trust ID. - #[tracing::instrument(level = "debug", skip(self, state))] - async fn get_trust_delegation_chain<'a>( - &self, - state: &ServiceState, - id: &'a str, - ) -> Result>, TrustProviderError> { - self.backend_driver - .get_trust_delegation_chain(state, id) - .await - } - - /// List trusts. - #[tracing::instrument(level = "debug", skip(self, state))] - async fn list_trusts( - &self, - state: &ServiceState, - params: &TrustListParameters, - ) -> Result, TrustProviderError> { - let mut trusts = self.backend_driver.list_trusts(state, params).await?; - - let all_roles: HashMap = HashMap::from_iter( - state - .provider - .get_role_provider() - .list_roles( - state, - &crate::role::types::RoleListParameters { - domain_id: Some(None), - ..Default::default() - }, - ) - .await? - .iter() - .map(|role| (role.id.clone(), role.to_owned())), - ); - for trust in trusts.iter_mut() { - if let Some(ref mut roles) = trust.roles { - for role in roles.iter_mut() { - if let Some(erole) = all_roles.get(&role.id) { - role.domain_id = erole.domain_id.clone(); - role.name = Some(erole.name.clone()); - } - } - // Drop all roles for which name is not set (it is a signal that the processing - // above has not found the role matching the parameters. - roles.retain_mut(|role| role.name.is_some()); - } - } - - Ok(trusts) - } - - /// Validate trust delegation chain. - /// - /// - redelegation deepness cannot exceed the global limit. - /// - redelegated trusts must not specify use limit. - /// - validate redelegated trust expiration is not later than of the - /// original. - /// - redelegated trust must not add new roles. - #[tracing::instrument(level = "debug", skip(self, state))] - async fn validate_trust_delegation_chain( - &self, - state: &ServiceState, - trust: &Trust, - ) -> Result { - if trust.redelegated_trust_id.is_some() - && let Some(chain) = self.get_trust_delegation_chain(state, &trust.id).await? - { - if chain.len() > state.config.trust.max_redelegation_count { - return Err(TrustProviderError::RedelegationDeepnessExceed { - length: chain.len(), - max_depth: state.config.trust.max_redelegation_count, - }); - } - let mut parent_trust: Option = None; - let mut parent_expiration: Option> = None; - for delegation in chain.iter().rev() { - // None of the trusts can specify the redelegation_count > delegation_count of - // the top level trust - if let Some(current_redelegation_count) = delegation.redelegation_count - && current_redelegation_count > state.config.trust.max_redelegation_count as u32 - { - return Err(TrustProviderError::RedelegationDeepnessExceed { - length: current_redelegation_count as usize, - max_depth: state.config.trust.max_redelegation_count, - }); - } - if delegation.remaining_uses.is_some() { - return Err(TrustProviderError::RemainingUsesMustBeUnset); - } - // Check that the parent trust is not expiring earlier than the redelegated - if let Some(trust_expiry) = delegation.expires_at { - if let Some(parent_expiry) = parent_trust - .as_ref() - .and_then(|x| x.expires_at) - .or(parent_expiration) - { - if trust_expiry > parent_expiry { - return Err(TrustProviderError::ExpirationImpossible); - } - // reset the parent_expiration to the one of the current delegation. - parent_expiration = Some(trust_expiry); - } - // Ensure we set the parent_expiration with the first met value. - if parent_expiration.is_none() { - parent_expiration = Some(trust_expiry); - } - } - // Check that the redelegation is not adding new roles - if let Some(parent_trust) = &parent_trust - && !HashSet::::from_iter( - delegation - .roles - .as_deref() - .unwrap_or_default() - .iter() - .map(|role| role.id.clone()), - ) - .is_subset(&HashSet::from_iter( - parent_trust - .roles - .as_deref() - .unwrap_or_default() - .iter() - .map(|role| role.id.clone()), - )) - { - debug!( - "Trust roles {:?} are missing for the trustor {:?}", - trust.roles, parent_trust.roles, - ); - return Err(TrustProviderError::RedelegatedRolesNotAvailable); - } - // Check the impersonation - if delegation.impersonation && !parent_trust.is_some_and(|x| x.impersonation) { - return Err(TrustProviderError::RedelegatedImpersonationNotAllowed); - } - parent_trust = Some(delegation.clone()); - } - } - - Ok(true) - } -} - -#[cfg(test)] -mod tests { - use chrono::{DateTime, Utc}; - use sea_orm::DatabaseConnection; - use std::sync::Arc; - - use super::backend::MockTrustBackend; - use super::*; - use crate::config::Config; - use crate::keystone::Service; - use crate::policy::MockPolicyEnforcer; - use crate::provider::{Provider, ProviderBuilder}; - use crate::role::{MockRoleProvider, types::*}; - - fn get_state_mock(provider_builder: ProviderBuilder) -> Arc { - Arc::new( - Service::new( - Config::default(), - DatabaseConnection::Disconnected, - provider_builder.build().unwrap(), - MockPolicyEnforcer::default(), - ) - .unwrap(), - ) - } - - #[tokio::test] - async fn test_get_trust() { - let mut role_mock = MockRoleProvider::default(); - role_mock - .expect_list_roles() - .withf(|_, qp: &RoleListParameters| { - RoleListParameters { - domain_id: Some(None), - ..Default::default() - } == *qp - }) - .returning(|_, _| Ok(Vec::new())); - let provider_builder = Provider::mocked_builder().role(role_mock); - let state = get_state_mock(provider_builder); - - let mut backend = MockTrustBackend::new(); - backend - .expect_get_trust() - .withf(|_, id: &'_ str| id == "fake_trust") - .returning(|_, _| { - Ok(Some(Trust { - id: "fake_trust".into(), - ..Default::default() - })) - }); - - let trust_provider = TrustProvider { - backend_driver: Arc::new(backend), - }; - - let trust: Trust = trust_provider - .get_trust(&state, "fake_trust") - .await - .unwrap() - .expect("trust found"); - assert_eq!(trust.id, "fake_trust"); - } - - #[tokio::test] - async fn test_get_trust_delegation_chain() { - let mut role_mock = MockRoleProvider::default(); - role_mock - .expect_list_roles() - .withf(|_, qp: &RoleListParameters| { - RoleListParameters { - domain_id: Some(None), - ..Default::default() - } == *qp - }) - .returning(|_, _| Ok(Vec::new())); - let provider_builder = Provider::mocked_builder().role(role_mock); - let state = get_state_mock(provider_builder); - - let mut backend = MockTrustBackend::new(); - backend - .expect_get_trust_delegation_chain() - .withf(|_, id: &'_ str| id == "fake_trust") - .returning(|_, _| { - Ok(Some(vec![ - Trust { - id: "redelegated_trust".into(), - redelegated_trust_id: Some("trust_id".into()), - ..Default::default() - }, - Trust { - id: "trust_id".into(), - ..Default::default() - }, - ])) - }); - - let trust_provider = TrustProvider { - backend_driver: Arc::new(backend), - }; - - let chain = trust_provider - .get_trust_delegation_chain(&state, "fake_trust") - .await - .unwrap() - .expect("chain fetched"); - assert_eq!(chain.len(), 2); - } - - #[tokio::test] - async fn test_validate_trust_delegation_chain_not_redelegated() { - let mut role_mock = MockRoleProvider::default(); - role_mock - .expect_list_roles() - .withf(|_, qp: &RoleListParameters| { - RoleListParameters { - domain_id: Some(None), - ..Default::default() - } == *qp - }) - .returning(|_, _| Ok(Vec::new())); - let provider_builder = Provider::mocked_builder().role(role_mock); - let state = get_state_mock(provider_builder); - - let mut backend = MockTrustBackend::new(); - backend - .expect_get_trust() - .withf(|_, id: &'_ str| id == "fake_trust") - .returning(|_, _| { - Ok(Some(Trust { - id: "fake_trust".into(), - ..Default::default() - })) - }); - - let trust_provider = TrustProvider { - backend_driver: Arc::new(backend), - }; - let trust = trust_provider - .get_trust(&state, "fake_trust") - .await - .unwrap() - .expect("trust found"); - trust_provider - .validate_trust_delegation_chain(&state, &trust) - .await - .unwrap(); - } - - #[tokio::test] - async fn test_validate_trust_delegation_chain() { - let mut role_mock = MockRoleProvider::default(); - role_mock - .expect_list_roles() - .withf(|_, qp: &RoleListParameters| { - RoleListParameters { - domain_id: Some(None), - ..Default::default() - } == *qp - }) - .returning(|_, _| Ok(Vec::new())); - let provider_builder = Provider::mocked_builder().role(role_mock); - let state = get_state_mock(provider_builder); - let mut backend = MockTrustBackend::new(); - backend - .expect_get_trust() - .withf(|_, id: &'_ str| id == "redelegated_trust") - .returning(|_, _| { - Ok(Some(Trust { - id: "redelegated_trust".into(), - redelegated_trust_id: Some("trust_id".into()), - ..Default::default() - })) - }); - backend - .expect_get_trust_delegation_chain() - .withf(|_, id: &'_ str| id == "redelegated_trust") - .returning(|_, _| { - Ok(Some(vec![ - Trust { - id: "redelegated_trust".into(), - redelegated_trust_id: Some("trust_id".into()), - ..Default::default() - }, - Trust { - id: "trust_id".into(), - ..Default::default() - }, - ])) - }); - - let trust_provider = TrustProvider { - backend_driver: Arc::new(backend), - }; - let trust = trust_provider - .get_trust(&state, "redelegated_trust") - .await - .unwrap() - .expect("trust found"); - trust_provider - .validate_trust_delegation_chain(&state, &trust) - .await - .unwrap(); - } - - #[tokio::test] - async fn test_validate_trust_delegation_chain_expiration() { - let mut role_mock = MockRoleProvider::default(); - role_mock - .expect_list_roles() - .withf(|_, qp: &RoleListParameters| { - RoleListParameters { - domain_id: Some(None), - ..Default::default() - } == *qp - }) - .returning(|_, _| Ok(Vec::new())); - let provider_builder = Provider::mocked_builder().role(role_mock); - let state = get_state_mock(provider_builder); - let mut backend = MockTrustBackend::new(); - backend - .expect_get_trust() - .withf(|_, id: &'_ str| id == "redelegated_trust2") - .returning(|_, _| { - Ok(Some(Trust { - id: "redelegated_trust2".into(), - redelegated_trust_id: Some("redelegated_trust1".into()), - ..Default::default() - })) - }); - backend - .expect_get_trust_delegation_chain() - .withf(|_, id: &'_ str| id == "redelegated_trust2") - .returning(|_, _| { - Ok(Some(vec![ - Trust { - id: "redelegated_trust2".into(), - redelegated_trust_id: Some("redelegated_trust1".into()), - expires_at: Some(DateTime::::MAX_UTC), - ..Default::default() - }, - Trust { - id: "redelegated_trust1".into(), - redelegated_trust_id: Some("trust_id".into()), - ..Default::default() - }, - Trust { - id: "trust_id".into(), - expires_at: Some(Utc::now()), - ..Default::default() - }, - ])) - }); - - let trust_provider = TrustProvider { - backend_driver: Arc::new(backend), - }; - let trust = trust_provider - .get_trust(&state, "redelegated_trust2") - .await - .unwrap() - .expect("trust found"); - if let Err(TrustProviderError::ExpirationImpossible) = trust_provider - .validate_trust_delegation_chain(&state, &trust) - .await - { - } else { - panic!("redelegated trust cannot expire later than the parent"); - }; - } - - #[tokio::test] - async fn test_validate_trust_delegation_chain_no_new_roles() { - let mut role_mock = MockRoleProvider::default(); - role_mock - .expect_list_roles() - .withf(|_, qp: &RoleListParameters| { - RoleListParameters { - domain_id: Some(None), - ..Default::default() - } == *qp - }) - .returning(|_, _| Ok(Vec::new())); - let provider_builder = Provider::mocked_builder().role(role_mock); - let state = get_state_mock(provider_builder); - let mut backend = MockTrustBackend::new(); - backend - .expect_get_trust() - .withf(|_, id: &'_ str| id == "redelegated_trust") - .returning(|_, _| { - Ok(Some(Trust { - id: "redelegated_trust".into(), - redelegated_trust_id: Some("trust_id".into()), - ..Default::default() - })) - }); - backend - .expect_get_trust_delegation_chain() - .withf(|_, id: &'_ str| id == "redelegated_trust") - .returning(|_, _| { - Ok(Some(vec![ - Trust { - id: "redelegated_trust".into(), - redelegated_trust_id: Some("trust_id".into()), - roles: Some(vec![ - RoleRef { - id: "rid1".into(), - name: None, - domain_id: None, - }, - RoleRef { - id: "rid2".into(), - name: None, - domain_id: None, - }, - ]), - ..Default::default() - }, - Trust { - id: "trust_id".into(), - roles: Some(vec![RoleRef { - id: "rid1".into(), - name: None, - domain_id: None, - }]), - ..Default::default() - }, - ])) - }); - - let trust_provider = TrustProvider { - backend_driver: Arc::new(backend), - }; - let trust = trust_provider - .get_trust(&state, "redelegated_trust") - .await - .unwrap() - .expect("trust found"); - - if let Err(TrustProviderError::RedelegatedRolesNotAvailable) = trust_provider - .validate_trust_delegation_chain(&state, &trust) - .await - { - } else { - panic!("adding new roles on redelegation should be disallowed"); - }; - } - - #[tokio::test] - async fn test_validate_trust_delegation_chain_impersonation() { - let mut role_mock = MockRoleProvider::default(); - role_mock - .expect_list_roles() - .withf(|_, qp: &RoleListParameters| { - RoleListParameters { - domain_id: Some(None), - ..Default::default() - } == *qp - }) - .returning(|_, _| Ok(Vec::new())); - let provider_builder = Provider::mocked_builder().role(role_mock); - let state = get_state_mock(provider_builder); - let mut backend = MockTrustBackend::new(); - backend - .expect_get_trust() - .withf(|_, id: &'_ str| id == "redelegated_trust2") - .returning(|_, _| { - Ok(Some(Trust { - id: "redelegated_trust2".into(), - redelegated_trust_id: Some("redelegated_trust1".into()), - ..Default::default() - })) - }); - backend - .expect_get_trust_delegation_chain() - .withf(|_, id: &'_ str| id == "redelegated_trust2") - .returning(|_, _| { - Ok(Some(vec![ - Trust { - id: "redelegated_trust2".into(), - redelegated_trust_id: Some("redelegated_trust1".into()), - ..Default::default() - }, - Trust { - id: "redelegated_trust1".into(), - redelegated_trust_id: Some("trust_id".into()), - impersonation: true, - ..Default::default() - }, - Trust { - id: "trust_id".into(), - impersonation: false, - ..Default::default() - }, - ])) - }); - - let trust_provider = TrustProvider { - backend_driver: Arc::new(backend), - }; - let trust = trust_provider - .get_trust(&state, "redelegated_trust2") - .await - .unwrap() - .expect("trust found"); - match trust_provider - .validate_trust_delegation_chain(&state, &trust) - .await - { - Err(TrustProviderError::RedelegatedImpersonationNotAllowed) => {} - other => { - panic!( - "redelegated trust impersonation cannot be enabled, {:?}", - other - ); - } - } - } - - #[tokio::test] - async fn test_validate_trust_delegation_chain_deepness() { - let mut role_mock = MockRoleProvider::default(); - role_mock - .expect_list_roles() - .withf(|_, qp: &RoleListParameters| { - RoleListParameters { - domain_id: Some(None), - ..Default::default() - } == *qp - }) - .returning(|_, _| Ok(Vec::new())); - let provider_builder = Provider::mocked_builder().role(role_mock); - let state = get_state_mock(provider_builder); - let mut backend = MockTrustBackend::new(); - backend - .expect_get_trust() - .withf(|_, id: &'_ str| id == "redelegated_trust2") - .returning(|_, _| { - Ok(Some(Trust { - id: "redelegated_trust2".into(), - redelegated_trust_id: Some("redelegated_trust1".into()), - ..Default::default() - })) - }); - backend - .expect_get_trust() - .withf(|_, id: &'_ str| id == "redelegated_trust_long") - .returning(|_, _| { - Ok(Some(Trust { - id: "redelegated_trust_long".into(), - redelegated_trust_id: Some("redelegated_trust2".into()), - ..Default::default() - })) - }); - backend - .expect_get_trust_delegation_chain() - .withf(|_, id: &'_ str| id == "redelegated_trust2") - .returning(|_, _| { - Ok(Some(vec![ - Trust { - id: "redelegated_trust2".into(), - redelegated_trust_id: Some("redelegated_trust1".into()), - redelegation_count: Some(4), - ..Default::default() - }, - Trust { - id: "redelegated_trust1".into(), - redelegated_trust_id: Some("trust_id".into()), - ..Default::default() - }, - Trust { - id: "trust_id".into(), - ..Default::default() - }, - ])) - }); - - backend - .expect_get_trust_delegation_chain() - .withf(|_, id: &'_ str| id == "redelegated_trust_long") - .returning(|_, _| { - Ok(Some(vec![ - Trust { - id: "redelegated_trust_long".into(), - redelegated_trust_id: Some("redelegated_trust2".into()), - ..Default::default() - }, - Trust { - id: "redelegated_trust2".into(), - redelegated_trust_id: Some("redelegated_trust1".into()), - ..Default::default() - }, - Trust { - id: "redelegated_trust1".into(), - redelegated_trust_id: Some("trust_id".into()), - ..Default::default() - }, - Trust { - id: "trust_id".into(), - ..Default::default() - }, - ])) - }); - - let trust_provider = TrustProvider { - backend_driver: Arc::new(backend), - }; - let trust = trust_provider - .get_trust(&state, "redelegated_trust2") - .await - .unwrap() - .expect("trust found"); - match trust_provider - .validate_trust_delegation_chain(&state, &trust) - .await - { - Err(TrustProviderError::RedelegationDeepnessExceed { .. }) => {} - other => { - panic!( - "redelegated trust redelegation_count exceeds limit, but {:?}", - other - ); - } - } - - let trust = trust_provider - .get_trust(&state, "redelegated_trust_long") - .await - .unwrap() - .expect("trust found"); - match trust_provider - .validate_trust_delegation_chain(&state, &trust) - .await - { - Err(TrustProviderError::RedelegationDeepnessExceed { .. }) => {} - other => { - panic!("trust redelegation chain exceeds limit, but {:?}", other); - } - } - } - - #[tokio::test] - async fn test_list_trusts() { - let mut role_mock = MockRoleProvider::default(); - role_mock - .expect_list_roles() - .withf(|_, qp: &RoleListParameters| { - RoleListParameters { - domain_id: Some(None), - ..Default::default() - } == *qp - }) - .returning(|_, _| Ok(Vec::new())); - let provider_builder = Provider::mocked_builder().role(role_mock); - let state = get_state_mock(provider_builder); - - let mut backend = MockTrustBackend::new(); - backend - .expect_list_trusts() - .withf(|_, params: &TrustListParameters| *params == TrustListParameters::default()) - .returning(|_, _| { - Ok(vec![ - Trust { - id: "redelegated_trust".into(), - redelegated_trust_id: Some("trust_id".into()), - ..Default::default() - }, - Trust { - id: "trust_id".into(), - ..Default::default() - }, - ]) - }); - - let trust_provider = TrustProvider { - backend_driver: Arc::new(backend), - }; - - let list = trust_provider - .list_trusts(&state, &TrustListParameters::default()) - .await - .unwrap(); - assert_eq!(list.len(), 2); - } -} diff --git a/crates/keystone/src/webauthn/api/auth/finish.rs b/crates/keystone/src/webauthn/api/auth/finish.rs index f7c74dba..216cf85b 100644 --- a/crates/keystone/src/webauthn/api/auth/finish.rs +++ b/crates/keystone/src/webauthn/api/auth/finish.rs @@ -69,7 +69,7 @@ pub async fn finish( match state .extension .webauthn - .finish_passkey_authentication(&req.try_into()?, &s) + .finish_passkey_authentication(&req.try_into().map_err(WebauthnError::from)?, &s) { Ok(auth_result) => { // As per https://www.w3.org/TR/webauthn-3/#sctn-verifying-assertion 21: @@ -159,7 +159,8 @@ pub async fn finish( )?; let api_token = TokenResponse { - token: token.build_api_token_v4(&state.core).await?, + token: crate::api::v4::auth::token::token_impl::build_api_token_v4(&token, &state.core) + .await?, }; Ok(( StatusCode::OK, diff --git a/crates/keystone/src/webauthn/api/register/finish.rs b/crates/keystone/src/webauthn/api/register/finish.rs index bf398e33..0f3ff13c 100644 --- a/crates/keystone/src/webauthn/api/register/finish.rs +++ b/crates/keystone/src/webauthn/api/register/finish.rs @@ -27,7 +27,7 @@ use crate::api::KeystoneApiError; use crate::api::auth::Auth; use crate::identity::IdentityApi; use crate::webauthn::{ - WebauthnApi, + WebauthnApi, WebauthnError, api::types::{CombinedExtensionState, register::*}, types::{CredentialType, WebauthnCredential}, }; @@ -94,7 +94,7 @@ pub(super) async fn finish( let passkey = match state .extension .webauthn - .finish_passkey_registration(&req.try_into()?, &s) + .finish_passkey_registration(&req.try_into().map_err(WebauthnError::from)?, &s) { Ok(sk) => { let cred = WebauthnCredential { diff --git a/crates/keystone/src/webauthn/api/register/start.rs b/crates/keystone/src/webauthn/api/register/start.rs index 86ce0c2f..3359028c 100644 --- a/crates/keystone/src/webauthn/api/register/start.rs +++ b/crates/keystone/src/webauthn/api/register/start.rs @@ -25,7 +25,7 @@ use crate::api::KeystoneApiError; use crate::api::auth::Auth; use crate::identity::IdentityApi; use crate::webauthn::{ - WebauthnApi, + WebauthnApi, WebauthnError, api::types::{CombinedExtensionState, register::*}, }; @@ -104,7 +104,7 @@ pub(super) async fn start( .provider .save_user_webauthn_credential_registration_state(&state.core, &user_id, reg_state) .await?; - Json(UserPasskeyRegistrationStartResponse::try_from(ccr)?) + Json(UserPasskeyRegistrationStartResponse::try_from(ccr).map_err(WebauthnError::from)?) } Err(e) => { debug!("challenge_register -> {:?}", e); diff --git a/crates/keystone/src/webauthn/api/types.rs b/crates/keystone/src/webauthn/api/types.rs index ba1dc0a1..d6d9c306 100644 --- a/crates/keystone/src/webauthn/api/types.rs +++ b/crates/keystone/src/webauthn/api/types.rs @@ -63,16 +63,16 @@ impl From for KeystoneApiError { } } -impl From for KeystoneApiError { - fn from(value: openstack_keystone_api_types::webauthn::error::WebauthnError) -> Self { - match value { - other => Self::InternalError(other.to_string()), - } - } -} +//impl From for KeystoneApiError { +// fn from(value: openstack_keystone_api_types::webauthn::error::WebauthnError) -> Self { +// match value { +// other => Self::InternalError(other.to_string()), +// } +// } +//} -impl From for KeystoneApiError { - fn from(value: uuid::Error) -> Self { - Self::InternalError(value.to_string()) - } -} +//impl From for KeystoneApiError { +// fn from(value: uuid::Error) -> Self { +// Self::InternalError(value.to_string()) +// } +//} diff --git a/crates/keystone/src/webauthn/driver/state/get.rs b/crates/keystone/src/webauthn/driver/state/get.rs index 26e9cd87..48fef519 100644 --- a/crates/keystone/src/webauthn/driver/state/get.rs +++ b/crates/keystone/src/webauthn/driver/state/get.rs @@ -19,7 +19,7 @@ use webauthn_rs::prelude::{PasskeyAuthentication, PasskeyRegistration}; use crate::db::entity::{prelude::WebauthnState as DbPasskeyState, webauthn_state}; use crate::error::DbContextExt; -use crate::webauthn::error::WebauthnError; +use crate::webauthn::WebauthnError; pub async fn get_register>( db: &DatabaseConnection, diff --git a/crates/keystone/src/webauthn/error.rs b/crates/keystone/src/webauthn/error.rs index cdefd52a..cca79523 100644 --- a/crates/keystone/src/webauthn/error.rs +++ b/crates/keystone/src/webauthn/error.rs @@ -15,6 +15,7 @@ use thiserror::Error; use crate::error::DatabaseError; +use crate::error::KeystoneError; /// WebAuthN extension error. #[derive(Error, Debug)] @@ -87,3 +88,11 @@ pub enum WebauthnError { source: webauthn_rs::prelude::WebauthnError, }, } + +impl From for KeystoneError { + fn from(value: WebauthnError) -> Self { + Self::Provider { + source: Box::new(value), + } + } +} diff --git a/tests/integration/Cargo.toml b/tests/integration/Cargo.toml index 0496b954..1c92fb80 100644 --- a/tests/integration/Cargo.toml +++ b/tests/integration/Cargo.toml @@ -13,7 +13,8 @@ dist = false chrono.workspace = true eyre.workspace = true itertools.workspace = true -openstack-keystone = { path = "../../crates/keystone" } +openstack-keystone-core = { version = "0.1", path = "../../crates/core", features = ["mock"] } +openstack-keystone = { version = "0.1", path = "../../crates/keystone" } sea-orm = { workspace = true, features = ["sqlx-sqlite"] } secrecy = { workspace = true, features = ["serde"] } serde.workspace = true diff --git a/tests/integration/src/application_credential.rs b/tests/integration/src/application_credential.rs index 333340f9..304ba6be 100644 --- a/tests/integration/src/application_credential.rs +++ b/tests/integration/src/application_credential.rs @@ -24,9 +24,9 @@ use openstack_keystone::db::entity::prelude::*; use openstack_keystone::db::entity::project; use openstack_keystone::keystone::Service; use openstack_keystone::plugin_manager::PluginManager; -use openstack_keystone::policy::PolicyEnforcer; use openstack_keystone::provider::Provider; use openstack_keystone::role::types as role_types; +use openstack_keystone_core::policy::MockPolicy; mod create; mod get; @@ -72,8 +72,13 @@ async fn get_state() -> Result, Report> { let cfg: Config = Config::default(); let plugin_manager = PluginManager::default(); - let provider = Provider::new(cfg.clone(), plugin_manager)?; - let state = Arc::new(Service::new(cfg, db, provider, PolicyEnforcer::default())?); + let provider = Provider::new(cfg.clone(), &plugin_manager)?; + let state = Arc::new(Service::new( + cfg, + db, + provider, + Arc::new(MockPolicy::default()), + )?); create_role(&state, "role_a").await?; create_role(&state, "role_b").await?; diff --git a/tests/integration/src/assignment/grant.rs b/tests/integration/src/assignment/grant.rs index 72d491e0..2e77e80f 100644 --- a/tests/integration/src/assignment/grant.rs +++ b/tests/integration/src/assignment/grant.rs @@ -25,8 +25,8 @@ use openstack_keystone::config::Config; use openstack_keystone::db::entity::{prelude::*, project}; use openstack_keystone::keystone::Service; use openstack_keystone::plugin_manager::PluginManager; -use openstack_keystone::policy::PolicyEnforcer; use openstack_keystone::provider::Provider; +use openstack_keystone_core::policy::MockPolicy; use crate::common::{bootstrap, get_isolated_database}; @@ -87,10 +87,15 @@ async fn get_state() -> Result<(Arc, TempDir), Report> { fernet_utils.initialize_key_repository()?; let plugin_manager = PluginManager::default(); - let provider = Provider::new(cfg.clone(), plugin_manager)?; + let provider = Provider::new(cfg.clone(), &plugin_manager)?; Ok(( - Arc::new(Service::new(cfg, db, provider, PolicyEnforcer::default())?), + Arc::new(Service::new( + cfg, + db, + provider, + Arc::new(MockPolicy::default()), + )?), tmp_fernet_repo, )) } diff --git a/tests/integration/src/common.rs b/tests/integration/src/common.rs index c0a01798..9cde1f38 100644 --- a/tests/integration/src/common.rs +++ b/tests/integration/src/common.rs @@ -32,9 +32,9 @@ use openstack_keystone::db::entity::{local_user, project, user}; use openstack_keystone::identity::{IdentityApi, types::*}; use openstack_keystone::keystone::Service; use openstack_keystone::plugin_manager::PluginManager; -use openstack_keystone::policy::PolicyEnforcer; use openstack_keystone::provider::Provider; use openstack_keystone::role::{RoleApi, types::RoleCreate}; +use openstack_keystone_core::policy::MockPolicy; /// Create table with the related types and indexes (when known) async fn create_table(conn: &C, schema: &Schema, entity: E) -> Result<()> @@ -245,9 +245,14 @@ pub async fn get_state() -> Result<(Arc, TempDir)> { fernet_utils.initialize_key_repository()?; let plugin_manager = PluginManager::default(); - let provider = Provider::new(cfg.clone(), plugin_manager)?; + let provider = Provider::new(cfg.clone(), &plugin_manager)?; - let state = Arc::new(Service::new(cfg, db, provider, PolicyEnforcer::default())?); + let state = Arc::new(Service::new( + cfg, + db, + provider, + Arc::new(MockPolicy::default()), + )?); Ok((state, tmp_fernet_repo)) } diff --git a/tests/integration/src/identity/service_account.rs b/tests/integration/src/identity/service_account.rs index 9cdb3bf5..1eedbf5d 100644 --- a/tests/integration/src/identity/service_account.rs +++ b/tests/integration/src/identity/service_account.rs @@ -20,8 +20,8 @@ use openstack_keystone::config::Config; use openstack_keystone::db::entity::project; use openstack_keystone::keystone::Service; use openstack_keystone::plugin_manager::PluginManager; -use openstack_keystone::policy::PolicyEnforcer; use openstack_keystone::provider::Provider; +use openstack_keystone_core::policy::MockPolicy; use crate::common::{bootstrap, get_isolated_database}; @@ -54,11 +54,11 @@ async fn get_state() -> Result, Report> { let cfg: Config = Config::default(); let plugin_manager = PluginManager::default(); - let provider = Provider::new(cfg.clone(), plugin_manager)?; + let provider = Provider::new(cfg.clone(), &plugin_manager)?; Ok(Arc::new(Service::new( cfg, db, provider, - PolicyEnforcer::default(), + Arc::new(MockPolicy::default()), )?)) } diff --git a/tests/integration/src/identity/user.rs b/tests/integration/src/identity/user.rs index 9fea2ff5..17916a3a 100644 --- a/tests/integration/src/identity/user.rs +++ b/tests/integration/src/identity/user.rs @@ -20,8 +20,8 @@ use openstack_keystone::config::Config; use openstack_keystone::db::entity::project; use openstack_keystone::keystone::Service; use openstack_keystone::plugin_manager::PluginManager; -use openstack_keystone::policy::PolicyEnforcer; use openstack_keystone::provider::Provider; +use openstack_keystone_core::policy::MockPolicy; use crate::common::{bootstrap, get_isolated_database}; @@ -56,11 +56,11 @@ async fn get_state() -> Result, Report> { cfg.federation.default_authorization_ttl = 20; let plugin_manager = PluginManager::default(); - let provider = Provider::new(cfg.clone(), plugin_manager)?; + let provider = Provider::new(cfg.clone(), &plugin_manager)?; Ok(Arc::new(Service::new( cfg, db, provider, - PolicyEnforcer::default(), + Arc::new(MockPolicy::default()), )?)) } diff --git a/tests/integration/src/identity/user_group.rs b/tests/integration/src/identity/user_group.rs index 02014472..65ef8607 100644 --- a/tests/integration/src/identity/user_group.rs +++ b/tests/integration/src/identity/user_group.rs @@ -25,8 +25,8 @@ use openstack_keystone::identity::IdentityApi; use openstack_keystone::identity::types::*; use openstack_keystone::keystone::{Service, ServiceState}; use openstack_keystone::plugin_manager::PluginManager; -use openstack_keystone::policy::PolicyEnforcer; use openstack_keystone::provider::Provider; +use openstack_keystone_core::policy::MockPolicy; use crate::common::{bootstrap, get_isolated_database}; @@ -69,12 +69,12 @@ async fn get_state() -> Result, Report> { cfg.federation.default_authorization_ttl = 20; let plugin_manager = PluginManager::default(); - let provider = Provider::new(cfg.clone(), plugin_manager)?; + let provider = Provider::new(cfg.clone(), &plugin_manager)?; Ok(Arc::new(Service::new( cfg, db, provider, - PolicyEnforcer::default(), + Arc::new(MockPolicy::default()), )?)) } diff --git a/tests/integration/src/k8s_auth.rs b/tests/integration/src/k8s_auth.rs index ba6bad34..73feb88c 100644 --- a/tests/integration/src/k8s_auth.rs +++ b/tests/integration/src/k8s_auth.rs @@ -21,8 +21,8 @@ use openstack_keystone::config::Config; use openstack_keystone::db::entity::{prelude::Project, project}; use openstack_keystone::keystone::Service; use openstack_keystone::plugin_manager::PluginManager; -use openstack_keystone::policy::PolicyEnforcer; use openstack_keystone::provider::Provider; +use openstack_keystone_core::policy::MockPolicy; use crate::common::{bootstrap, get_isolated_database}; @@ -49,11 +49,11 @@ async fn get_state() -> Result, Report> { let cfg: Config = Config::default(); let plugin_manager = PluginManager::default(); - let provider = Provider::new(cfg.clone(), plugin_manager)?; + let provider = Provider::new(cfg.clone(), &plugin_manager)?; Ok(Arc::new(Service::new( cfg, db, provider, - PolicyEnforcer::default(), + Arc::new(MockPolicy::default()), )?)) } diff --git a/tests/integration/src/token/validate.rs b/tests/integration/src/token/validate.rs index acbabd58..7801c72d 100644 --- a/tests/integration/src/token/validate.rs +++ b/tests/integration/src/token/validate.rs @@ -25,8 +25,8 @@ use openstack_keystone::db::entity::project; use openstack_keystone::identity::{IdentityApi, types::*}; use openstack_keystone::keystone::Service; use openstack_keystone::plugin_manager::PluginManager; -use openstack_keystone::policy::PolicyEnforcer; use openstack_keystone::provider::Provider; +use openstack_keystone_core::policy::MockPolicy; mod application_credential; mod trust; @@ -80,9 +80,14 @@ async fn get_state() -> Result<(Arc, TempDir), Report> { fernet_utils.initialize_key_repository()?; let plugin_manager = PluginManager::default(); - let provider = Provider::new(cfg.clone(), plugin_manager)?; + let provider = Provider::new(cfg.clone(), &plugin_manager)?; - let state = Arc::new(Service::new(cfg, db, provider, PolicyEnforcer::default())?); + let state = Arc::new(Service::new( + cfg, + db, + provider, + Arc::new(MockPolicy::default()), + )?); Ok((state, tmp_fernet_repo)) } diff --git a/tools/Dockerfile.functest b/tools/Dockerfile.functest index b446c25b..395311ac 100644 --- a/tools/Dockerfile.functest +++ b/tools/Dockerfile.functest @@ -14,8 +14,9 @@ RUN USER=root cargo new keystone # We want dependencies cached, so copy those first. COPY Cargo.toml Cargo.lock /usr/src/keystone/ -COPY crates/keystone/Cargo.toml /usr/src/keystone/crates/keystone/ COPY crates/api-types/Cargo.toml /usr/src/keystone/crates/api-types/ +COPY crates/core/Cargo.toml /usr/src/keystone/crates/core/ +COPY crates/keystone/Cargo.toml /usr/src/keystone/crates/keystone/ COPY crates/storage/Cargo.toml /usr/src/keystone/crates/storage/ COPY tests/federation/Cargo.toml /usr/src/keystone/tests/federation/ COPY tests/integration/Cargo.toml /usr/src/keystone/tests/integration/ @@ -24,12 +25,11 @@ COPY tests/loadtest/Cargo.toml /usr/src/keystone/tests/loadtest/ RUN mkdir -p keystone/crates/keystone/src/bin && touch keystone/crates/keystone/src/lib.rs &&\ cp keystone/src/main.rs keystone/crates/keystone/src/bin/keystone.rs &&\ cp keystone/src/main.rs keystone/crates/keystone/src/bin/keystone_db.rs &&\ - mkdir keystone/tests/loadtest/src &&\ + mkdir -p keystone/tests/loadtest/src &&\ cp keystone/src/main.rs keystone/tests/loadtest/src/main.rs &&\ - mkdir keystone/crates/api-types/src &&\ - touch keystone/crates/api-types/src/lib.rs &&\ - mkdir keystone/crates/storage/src &&\ - touch keystone/crates/storage/src/lib.rs + mkdir -p keystone/crates/api-types/src && touch keystone/crates/api-types/src/lib.rs &&\ + mkdir -p keystone/crates/core/src && touch keystone/crates/core/src/lib.rs &&\ + mkdir -p keystone/crates/storage/src && touch keystone/crates/storage/src/lib.rs # Set the working directory WORKDIR /usr/src/keystone