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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 24 additions & 2 deletions src/api/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ use serde_json::json;
use thiserror::Error;

use crate::identity::error::IdentityProviderError;
use crate::resource::error::ResourceProviderError;

/// Keystone API operation errors
#[derive(Debug, Error)]
Expand Down Expand Up @@ -52,6 +53,12 @@ pub enum KeystoneApiError {
source: crate::api::v3::auth::token::types::TokenBuilderError,
},

#[error("error building token user data: {}", source)]
TokenUserBuilder {
#[from]
source: crate::api::v3::auth::token::types::UserBuilderError,
},

#[error("internal server error")]
InternalError(String),

Expand All @@ -60,6 +67,12 @@ pub enum KeystoneApiError {
#[from]
source: IdentityProviderError,
},

#[error(transparent)]
ResourceError {
#[from]
source: ResourceProviderError,
},
}

impl IntoResponse for KeystoneApiError {
Expand All @@ -84,12 +97,12 @@ impl IntoResponse for KeystoneApiError {
Json(json!({"error": {"code": StatusCode::INTERNAL_SERVER_ERROR.as_u16(), "message": self.to_string()}})),
).into_response()
}
KeystoneApiError::IdentityError { .. } => {
KeystoneApiError::IdentityError { .. } | KeystoneApiError::ResourceError { .. } => {
(StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"error": {"code": StatusCode::INTERNAL_SERVER_ERROR.as_u16(), "message": self.to_string()}})),
).into_response()
}
KeystoneApiError::SubjectTokenMissing | KeystoneApiError::InvalidHeader | KeystoneApiError::InvalidToken | KeystoneApiError::TokenBuilder{..} => {
KeystoneApiError::SubjectTokenMissing | KeystoneApiError::InvalidHeader | KeystoneApiError::InvalidToken | KeystoneApiError::TokenBuilder{..} | KeystoneApiError::TokenUserBuilder {..}=> {
(StatusCode::BAD_REQUEST,
Json(json!({"error": {"code": StatusCode::BAD_REQUEST.as_u16(), "message": self.to_string()}})),
).into_response()
Expand All @@ -112,4 +125,13 @@ impl KeystoneApiError {
_ => Self::IdentityError { source },
}
}
pub fn resource(source: ResourceProviderError) -> Self {
match source {
ResourceProviderError::DomainNotFound(x) => Self::NotFound {
resource: "domain".into(),
identifier: x,
},
_ => Self::ResourceError { source },
}
}
}
61 changes: 56 additions & 5 deletions src/api/v3/auth/token/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ use crate::api::auth::Auth;
use crate::api::error::KeystoneApiError;
use crate::identity::IdentityApi;
use crate::keystone::ServiceState;
use crate::resource::ResourceApi;
use crate::token::TokenApi;
use types::{TokenBuilder, TokenResponse, User};
use types::{TokenBuilder, TokenResponse, UserBuilder};

pub mod types;

Expand Down Expand Up @@ -75,8 +76,23 @@ async fn validate(
identifier: token.user_id().clone(),
})?;

let user_response: User = user.into();
response.user(user_response);
let user_domain = state
.provider
.get_resource_provider()
.get_domain(&state.db, user.domain_id.clone())
.await
.map_err(KeystoneApiError::resource)?
.ok_or_else(|| KeystoneApiError::NotFound {
resource: "domain".into(),
identifier: user.domain_id.clone(),
})?;

let mut user_response: UserBuilder = UserBuilder::default();
user_response.id(user.id);
user_response.name(user.name);
user_response.password_expires_at(user.password_expires_at);
user_response.domain(user_domain);
response.user(user_response.build()?);

Ok(TokenResponse {
token: response.build()?,
Expand All @@ -91,28 +107,63 @@ mod tests {
};
use http_body_util::BodyExt; // for `collect`
use sea_orm::DatabaseConnection;
use std::sync::Arc;
use tower::ServiceExt; // for `call`, `oneshot`, and `ready`
use tower_http::trace::TraceLayer;

use super::openapi_router;
use crate::api::v3::auth::token::types::TokenResponse;
use crate::config::Config;
use crate::identity::{MockIdentityProvider, types::User};
use crate::tests::api::{get_mocked_state, get_mocked_state_unauthed};
use crate::keystone::Service;
use crate::provider::ProviderBuilder;
use crate::resource::{MockResourceProvider, types::Domain};
use crate::tests::api::get_mocked_state_unauthed;
use crate::token::{MockTokenProvider, Token, UnscopedToken};

#[tokio::test]
async fn test_get() {
let db = DatabaseConnection::Disconnected;
let config = Config::default();
let mut identity_mock = MockIdentityProvider::default();
identity_mock
.expect_get_user()
.withf(|_: &DatabaseConnection, id: &String| *id == "bar")
.returning(|_, _| {
Ok(Some(User {
id: "bar".into(),
domain_id: "domain_id".into(),
..Default::default()
}))
});

let state = get_mocked_state(identity_mock);
let mut resource_mock = MockResourceProvider::default();
resource_mock
.expect_get_domain()
.withf(|_: &DatabaseConnection, id: &String| *id == "domain_id")
.returning(|_, _| {
Ok(Some(Domain {
id: "domain_id".into(),
..Default::default()
}))
});
let mut token_mock = MockTokenProvider::default();
token_mock.expect_validate_token().returning(|_, _| {
Ok(Token::Unscoped(UnscopedToken {
user_id: "bar".into(),
..Default::default()
}))
});

let provider = ProviderBuilder::default()
.config(config.clone())
.identity(identity_mock)
.resource(resource_mock)
.token(token_mock)
.build()
.unwrap();

let state = Arc::new(Service::new(config, db, provider).unwrap());

let mut api = openapi_router()
.layer(TraceLayer::new_for_http())
Expand Down
38 changes: 29 additions & 9 deletions src/api/v3/auth/token/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ use derive_builder::Builder;
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;

use crate::identity::types as provider_types;
use crate::resource::types as resource_provider_types;

/// Authorization token
#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)]
Expand Down Expand Up @@ -78,30 +78,50 @@ impl IntoResponse for TokenResponse {
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)]
pub struct Project {
/// Project ID
id: String,
pub id: String,
/// Project Name
name: String,
pub name: String,
}

/// User information
#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)]
#[builder(setter(into))]
pub struct User {
/// User ID
id: String,
pub id: String,
/// User Name
name: String,
pub name: String,
/// User domain
pub domain: Domain,
/// User password expiry date
#[serde(skip_serializing_if = "Option::is_none")]
password_expires_at: Option<DateTime<Utc>>,
pub password_expires_at: Option<DateTime<Utc>>,
}

impl From<provider_types::User> for User {
fn from(value: provider_types::User) -> Self {
/// Domain information
#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)]
#[builder(setter(into))]
pub struct Domain {
/// Domain ID
pub id: String,
/// Domain Name
pub name: String,
}

//impl From<identity_provider_types::User> for User {
// fn from(value: identity_provider_types::User) -> Self {
// Self {
// id: value.id.clone(),
// name: value.name.clone(),
// }
// }
//}

impl From<resource_provider_types::Domain> for Domain {
fn from(value: resource_provider_types::Domain) -> Self {
Self {
id: value.id.clone(),
name: value.name.clone(),
password_expires_at: value.password_expires_at.clone(),
}
}
}
12 changes: 11 additions & 1 deletion src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ pub struct Config {
#[serde(default)]
pub identity: IdentitySection,

/// Resource provider related configuration
#[serde(default)]
pub resource: ResourceSection,

/// Security compliance
#[serde(default)]
pub security_compliance: SecurityComplianceSection,
Expand Down Expand Up @@ -105,6 +109,11 @@ pub struct IdentitySection {
pub password_hash_rounds: Option<usize>,
}

#[derive(Debug, Default, Deserialize, Clone)]
pub struct ResourceSection {
pub driver: String,
}

#[derive(Debug, Default, Deserialize, Clone)]
pub enum PasswordHashingAlgo {
#[default]
Expand Down Expand Up @@ -152,7 +161,8 @@ impl Config {
builder = builder
.set_default("identity.max_password_length", "4096")?
.set_default("fernet_tokens.key_repository", "/etc/keystone/fernet-keys/")?
.set_default("fernet_tokens.max_active_keys", "3")?;
.set_default("fernet_tokens.max_active_keys", "3")?
.set_default("resource.driver", "sql")?;
if std::path::Path::new(&path).is_file() {
builder = builder.add_source(File::from(path).format(FileFormat::Ini));
}
Expand Down
7 changes: 7 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use thiserror::Error;

use crate::identity::error::*;
use crate::resource::error::*;
use crate::token::TokenProviderError;

#[derive(Debug, Error)]
Expand All @@ -25,6 +26,12 @@ pub enum KeystoneError {
source: IdentityProviderError,
},

#[error(transparent)]
ResourceError {
#[from]
source: ResourceProviderError,
},

#[error(transparent)]
TokenProvider {
#[from]
Expand Down
11 changes: 11 additions & 0 deletions src/plugin_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@
use std::collections::HashMap;

use crate::identity::types::IdentityBackend;
use crate::resource::types::ResourceBackend;

/// Plugin manager allowing to pass custom backend plugins implementing required trait during the
/// service start
#[derive(Clone, Debug, Default)]
pub struct PluginManager {
/// Identity backend plugins
identity_backends: HashMap<String, Box<dyn IdentityBackend>>,
resource_backends: HashMap<String, Box<dyn ResourceBackend>>,
}

impl PluginManager {
Expand All @@ -43,4 +45,13 @@ impl PluginManager {
) -> Option<&Box<dyn IdentityBackend>> {
self.identity_backends.get(name.as_ref())
}

/// Get registered resource backend
#[allow(clippy::borrowed_box)]
pub fn get_resource_backend<S: AsRef<str>>(
&self,
name: S,
) -> Option<&Box<dyn ResourceBackend>> {
self.resource_backends.get(name.as_ref())
}
}
10 changes: 10 additions & 0 deletions src/provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ use crate::identity::IdentityApi;
#[double]
use crate::identity::IdentityProvider;
use crate::plugin_manager::PluginManager;
use crate::resource::ResourceApi;
#[double]
use crate::resource::ResourceProvider;
use crate::token::TokenApi;
#[double]
use crate::token::TokenProvider;
Expand All @@ -36,17 +39,20 @@ use crate::token::TokenProvider;
pub struct Provider {
pub config: Config,
identity: IdentityProvider,
resource: ResourceProvider,
token: TokenProvider,
}

impl Provider {
pub fn new(cfg: Config, plugin_manager: PluginManager) -> Result<Self, KeystoneError> {
let identity_provider = IdentityProvider::new(&cfg, &plugin_manager)?;
let resource_provider = ResourceProvider::new(&cfg, &plugin_manager)?;
let token_provider = TokenProvider::new(&cfg)?;

Ok(Self {
config: cfg,
identity: identity_provider,
resource: resource_provider,
token: token_provider,
})
}
Expand All @@ -55,6 +61,10 @@ impl Provider {
&self.identity
}

pub fn get_resource_provider(&self) -> &impl ResourceApi {
&self.resource
}

pub fn get_token_provider(&self) -> &impl TokenApi {
&self.token
}
Expand Down
16 changes: 16 additions & 0 deletions src/resource/backends.rs
Original file line number Diff line number Diff line change
@@ -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 error;
pub mod sql;
Loading