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
5 changes: 3 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
[package]
name = "openstack_keystone"
description = "OpenStack Keystone service"
version = "0.1.0"
edition = "2024"
license = "Apache-2.0"
Expand All @@ -18,7 +19,7 @@ harness = false
[dependencies]
async-trait = { version = "^0.1" }
axum = { version = "^0.8", features = ["macros"] }
base64 = "0.22.1"
base64 = { version = "^0.22" }
bcrypt = { version = "0.17", features = ["alloc"] }
bytes = { version = "^1.10" }
chrono = { version = "^0.4" }
Expand Down Expand Up @@ -52,7 +53,7 @@ webauthn-rs = { version = "^0.5", features = ["danger-allow-state-serialisation"
criterion = { version = "^0.5", features = ["async_tokio"] }
http-body-util = "^0.1"
mockall = { version = "^0.13" }
sea-orm = { version = "*", features = ["mock"]}
sea-orm = { version = "^1.1", features = ["mock"]}
tempfile = { version = "^3.19" }
tracing-test = { version = "^0.2" }

Expand Down
6 changes: 5 additions & 1 deletion migration/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
pub use sea_orm_migration::prelude::*;

mod m20250301_000001_passkey;
mod m20250414_000001_idp;

pub struct Migrator;

#[async_trait::async_trait]
impl MigratorTrait for Migrator {
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
vec![Box::new(m20250301_000001_passkey::Migration)]
vec![
Box::new(m20250301_000001_passkey::Migration),
Box::new(m20250414_000001_idp::Migration),
]
}
}
2 changes: 1 addition & 1 deletion migration/src/m20250301_000001_passkey.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,5 +89,5 @@ enum WebauthnState {
UserId,
State,
CreatedAt,
Type
Type,
}
95 changes: 95 additions & 0 deletions migration/src/m20250414_000001_idp.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
use openstack_keystone::db::entity::prelude::Project;
use openstack_keystone::db::entity::project;
use sea_orm_migration::{prelude::*, schema::*};

#[derive(DeriveMigrationName)]
pub struct Migration;

#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(FederatedIdentityProvider::Table)
.if_not_exists()
.col(string_len(FederatedIdentityProvider::Id, 64).primary_key())
.col(string_len(FederatedIdentityProvider::Name, 255))
.col(string_len_null(FederatedIdentityProvider::DomainId, 64))
.col(string_len_null(
FederatedIdentityProvider::OidcDiscoveryUrl,
255,
))
.col(string_len_null(
FederatedIdentityProvider::OidcClientId,
255,
))
.col(string_len_null(
FederatedIdentityProvider::OidcClientSecret,
255,
))
.col(string_len_null(
FederatedIdentityProvider::OidcResponseMode,
64,
))
.col(string_len_null(
FederatedIdentityProvider::OidcResponseTypes,
255,
))
.col(text_null(FederatedIdentityProvider::JwtValidationPubkeys))
.col(string_len_null(FederatedIdentityProvider::BoundIssuer, 255))
.col(json_null(FederatedIdentityProvider::ProviderConfig))
.foreign_key(
ForeignKey::create()
.name("fk-user-passkey-credential")
.from(
FederatedIdentityProvider::Table,
FederatedIdentityProvider::DomainId,
)
.to(Project, project::Column::Id)
.on_delete(ForeignKeyAction::Cascade),
)
.index(
Index::create()
.unique()
.name("idx-idp-name-domain")
.col(FederatedIdentityProvider::DomainId)
.col(FederatedIdentityProvider::Name),
)
.to_owned(),
)
.await?;

Ok(())
}

async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(
Table::drop()
.table(FederatedIdentityProvider::Table)
.to_owned(),
)
.await?;

Ok(())
}
}

#[derive(DeriveIden)]
enum FederatedIdentityProvider {
Table,
Id,
DomainId,
Name,
OidcDiscoveryUrl,
OidcClientId,
OidcClientSecret,
OidcResponseMode,
OidcResponseTypes,
BoundIssuer,
JwtValidationPubkeys,
//JwksUrl,
//JwksCaPem,
ProviderConfig,
}
30 changes: 11 additions & 19 deletions src/api/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,20 +55,14 @@ mod tests {

use super::*;

use crate::assignment::MockAssignmentProvider;
use crate::catalog::MockCatalogProvider;
use crate::config::Config;
use crate::identity::MockIdentityProvider;

use crate::keystone::Service;
use crate::provider::ProviderBuilder;
use crate::provider::Provider;
use crate::resource::{MockResourceProvider, types::Domain};
use crate::token::MockTokenProvider;

#[tokio::test]
async fn test_get_domain() {
let db = DatabaseConnection::Disconnected;
let config = Config::default();

let mut resource_mock = MockResourceProvider::default();
resource_mock
.expect_get_domain()
Expand All @@ -90,21 +84,19 @@ mod tests {
..Default::default()
}))
});
let identity_mock = MockIdentityProvider::default();
let token_mock = MockTokenProvider::default();
let assignment_mock = MockAssignmentProvider::default();
let catalog_mock = MockCatalogProvider::default();
let provider = ProviderBuilder::default()
.config(config.clone())
.assignment(assignment_mock)
.catalog(catalog_mock)
.identity(identity_mock)
let provider = Provider::mocked_builder()
.resource(resource_mock)
.token(token_mock)
.build()
.unwrap();

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

assert_eq!(
"domain_id",
Expand Down
18 changes: 17 additions & 1 deletion src/api/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ use tracing::error;

use crate::assignment::error::AssignmentProviderError;
use crate::catalog::error::CatalogProviderError;
use crate::federation::error::FederationProviderError;
use crate::identity::error::IdentityProviderError;
use crate::resource::error::ResourceProviderError;
use crate::token::error::TokenProviderError;
Expand Down Expand Up @@ -72,6 +73,12 @@ pub enum KeystoneApiError {
source: CatalogProviderError,
},

#[error(transparent)]
Federation {
#[from]
source: FederationProviderError,
},

#[error(transparent)]
IdentityError {
#[from]
Expand Down Expand Up @@ -137,7 +144,7 @@ impl IntoResponse for KeystoneApiError {
Json(json!({"error": {"code": StatusCode::UNAUTHORIZED.as_u16(), "message": self.to_string()}})),
).into_response()
}
KeystoneApiError::InternalError(_) | KeystoneApiError::IdentityError { .. } | KeystoneApiError::ResourceError { .. } | KeystoneApiError::AssignmentError { .. } | KeystoneApiError::TokenError{..} => {
KeystoneApiError::InternalError(_) | KeystoneApiError::IdentityError { .. } | KeystoneApiError::ResourceError { .. } | KeystoneApiError::AssignmentError { .. } | KeystoneApiError::TokenError{..} | KeystoneApiError::Federation {..} => {
(StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"error": {"code": StatusCode::INTERNAL_SERVER_ERROR.as_u16(), "message": self.to_string()}})),
).into_response()
Expand All @@ -162,6 +169,15 @@ impl KeystoneApiError {
_ => Self::AssignmentError { source },
}
}
pub fn federation(source: FederationProviderError) -> Self {
match source {
FederationProviderError::IdentityProviderNotFound(x) => Self::NotFound {
resource: "identity provider".into(),
identifier: x,
},
_ => Self::Federation { source },
}
}
pub fn identity(source: IdentityProviderError) -> Self {
match source {
IdentityProviderError::UserNotFound(x) => Self::NotFound {
Expand Down
65 changes: 30 additions & 35 deletions src/api/v3/auth/token/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -183,24 +183,21 @@ mod tests {
MockAssignmentProvider,
types::{Assignment, AssignmentType, Role as ProviderRole, RoleAssignmentListParameters},
};
use crate::catalog::MockCatalogProvider;

use crate::config::Config;
use crate::identity::{MockIdentityProvider, types::UserResponse};
use crate::keystone::Service;
use crate::provider::ProviderBuilder;
use crate::provider::Provider;
use crate::resource::{
MockResourceProvider,
types::{Domain, Project},
};
use crate::token::{
DomainScopeToken, MockTokenProvider, ProjectScopeToken, Token as ProviderToken,
UnscopedToken,
DomainScopeToken, ProjectScopeToken, Token as ProviderToken, UnscopedToken,
};

#[tokio::test]
async fn test_from_unscoped() {
let db = DatabaseConnection::Disconnected;
let config = Config::default();
let mut identity_mock = MockIdentityProvider::default();
identity_mock
.expect_get_user()
Expand All @@ -223,20 +220,20 @@ mod tests {
..Default::default()
}))
});
let token_mock = MockTokenProvider::default();
let assignment_mock = MockAssignmentProvider::default();
let catalog_mock = MockCatalogProvider::default();
let provider = ProviderBuilder::default()
.config(config.clone())
.assignment(assignment_mock)
.catalog(catalog_mock)
let provider = Provider::mocked_builder()
.identity(identity_mock)
.resource(resource_mock)
.token(token_mock)
.build()
.unwrap();

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

let api_token = Token::from_provider_token(
&state,
Expand All @@ -255,8 +252,6 @@ mod tests {

#[tokio::test]
async fn test_from_domain_scoped() {
let db = DatabaseConnection::Disconnected;
let config = Config::default();
let mut identity_mock = MockIdentityProvider::default();
identity_mock
.expect_get_user()
Expand All @@ -278,20 +273,20 @@ mod tests {
..Default::default()
}))
});
let token_mock = MockTokenProvider::default();
let assignment_mock = MockAssignmentProvider::default();
let catalog_mock = MockCatalogProvider::default();
let provider = ProviderBuilder::default()
.config(config.clone())
.assignment(assignment_mock)
.catalog(catalog_mock)
let provider = Provider::mocked_builder()
.identity(identity_mock)
.resource(resource_mock)
.token(token_mock)
.build()
.unwrap();

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

let api_token = Token::from_provider_token(
&state,
Expand All @@ -315,8 +310,6 @@ mod tests {

#[tokio::test]
async fn test_from_project_scoped() {
let db = DatabaseConnection::Disconnected;
let config = Config::default();
let mut identity_mock = MockIdentityProvider::default();
identity_mock
.expect_get_user()
Expand Down Expand Up @@ -347,9 +340,7 @@ mod tests {
..Default::default()
}))
});
let token_mock = MockTokenProvider::default();
let mut assignment_mock = MockAssignmentProvider::default();
let catalog_mock = MockCatalogProvider::default();
assignment_mock.expect_list_role_assignments().returning(
|_, _, q: &RoleAssignmentListParameters| {
Ok(vec![Assignment {
Expand All @@ -362,17 +353,21 @@ mod tests {
}])
},
);
let provider = ProviderBuilder::default()
.config(config.clone())
let provider = Provider::mocked_builder()
.assignment(assignment_mock)
.catalog(catalog_mock)
.identity(identity_mock)
.resource(resource_mock)
.token(token_mock)
.build()
.unwrap();

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

let api_token = Token::from_provider_token(
&state,
Expand Down
Loading
Loading