From 20176d0e655023551cf00e72323a85cdf34887fa Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Wed, 18 Feb 2026 19:50:21 +0100 Subject: [PATCH] feat: Implement k8s auth provider Introduce a provider for managing the k8s auth resources (configs and roles). Closes: #566 --- .../src/assignment/types/provider_api.rs | 4 +- crates/keystone/src/config.rs | 6 + crates/keystone/src/config/k8s_auth.rs | 32 ++ crates/keystone/src/db/entity.rs | 2 + .../keystone/src/db/entity/kubernetes_auth.rs | 62 ++++ .../src/db/entity/kubernetes_auth_role.rs | 89 ++++++ crates/keystone/src/db/entity/prelude.rs | 2 + .../src/db_migration/m20260217_164934_k8.rs | 144 +++++++++ crates/keystone/src/db_migration/mod.rs | 2 + crates/keystone/src/error.rs | 27 +- crates/keystone/src/k8s_auth/backend.rs | 100 +++++++ crates/keystone/src/k8s_auth/backend/error.rs | 45 +++ crates/keystone/src/k8s_auth/backend/sql.rs | 157 ++++++++++ .../src/k8s_auth/backend/sql/k8s_auth.rs | 190 ++++++++++++ .../k8s_auth/backend/sql/k8s_auth/create.rs | 81 +++++ .../k8s_auth/backend/sql/k8s_auth/delete.rs | 62 ++++ .../src/k8s_auth/backend/sql/k8s_auth/get.rs | 63 ++++ .../src/k8s_auth/backend/sql/k8s_auth/list.rs | 131 ++++++++ .../k8s_auth/backend/sql/k8s_auth/update.rs | 112 +++++++ .../src/k8s_auth/backend/sql/k8s_auth_role.rs | 238 +++++++++++++++ .../backend/sql/k8s_auth_role/create.rs | 87 ++++++ .../backend/sql/k8s_auth_role/delete.rs | 62 ++++ .../k8s_auth/backend/sql/k8s_auth_role/get.rs | 63 ++++ .../backend/sql/k8s_auth_role/list.rs | 146 +++++++++ .../backend/sql/k8s_auth_role/update.rs | 113 +++++++ crates/keystone/src/k8s_auth/error.rs | 69 +++++ crates/keystone/src/k8s_auth/mock.rs | 106 +++++++ crates/keystone/src/k8s_auth/mod.rs | 274 +++++++++++++++++ crates/keystone/src/k8s_auth/types.rs | 22 ++ .../keystone/src/k8s_auth/types/k8s_auth.rs | 108 +++++++ .../src/k8s_auth/types/k8s_auth_role.rs | 132 +++++++++ .../src/k8s_auth/types/provider_api.rs | 95 ++++++ crates/keystone/src/lib.rs | 1 + crates/keystone/src/plugin_manager.rs | 9 + crates/keystone/src/provider.rs | 14 + .../src/token/token_restriction/create.rs | 4 +- doc/src/SUMMARY.md | 1 + doc/src/adr/0015-kubernetes-auth.md | 144 +++++++++ doc/src/federation/intro.md | 16 +- doc/src/passkey.md | 18 +- tests/integration/src/common.rs | 72 ++++- tests/integration/src/integration.rs | 5 + tests/integration/src/k8s_auth.rs | 59 ++++ tests/integration/src/k8s_auth/config.rs | 51 ++++ .../integration/src/k8s_auth/config/create.rs | 52 ++++ .../integration/src/k8s_auth/config/delete.rs | 49 +++ tests/integration/src/k8s_auth/config/get.rs | 70 +++++ tests/integration/src/k8s_auth/config/list.rs | 152 ++++++++++ .../integration/src/k8s_auth/config/update.rs | 57 ++++ tests/integration/src/k8s_auth/role.rs | 51 ++++ tests/integration/src/k8s_auth/role/create.rs | 94 ++++++ tests/integration/src/k8s_auth/role/delete.rs | 87 ++++++ tests/integration/src/k8s_auth/role/get.rs | 108 +++++++ tests/integration/src/k8s_auth/role/list.rs | 279 ++++++++++++++++++ tests/integration/src/k8s_auth/role/update.rs | 96 ++++++ tests/integration/src/macros.rs | 29 ++ tests/integration/src/role/create.rs | 1 - tests/integration/src/token.rs | 1 + .../src/token/token_restriction.rs | 45 +++ 59 files changed, 4359 insertions(+), 32 deletions(-) create mode 100644 crates/keystone/src/config/k8s_auth.rs create mode 100644 crates/keystone/src/db/entity/kubernetes_auth.rs create mode 100644 crates/keystone/src/db/entity/kubernetes_auth_role.rs create mode 100644 crates/keystone/src/db_migration/m20260217_164934_k8.rs create mode 100644 crates/keystone/src/k8s_auth/backend.rs create mode 100644 crates/keystone/src/k8s_auth/backend/error.rs create mode 100644 crates/keystone/src/k8s_auth/backend/sql.rs create mode 100644 crates/keystone/src/k8s_auth/backend/sql/k8s_auth.rs create mode 100644 crates/keystone/src/k8s_auth/backend/sql/k8s_auth/create.rs create mode 100644 crates/keystone/src/k8s_auth/backend/sql/k8s_auth/delete.rs create mode 100644 crates/keystone/src/k8s_auth/backend/sql/k8s_auth/get.rs create mode 100644 crates/keystone/src/k8s_auth/backend/sql/k8s_auth/list.rs create mode 100644 crates/keystone/src/k8s_auth/backend/sql/k8s_auth/update.rs create mode 100644 crates/keystone/src/k8s_auth/backend/sql/k8s_auth_role.rs create mode 100644 crates/keystone/src/k8s_auth/backend/sql/k8s_auth_role/create.rs create mode 100644 crates/keystone/src/k8s_auth/backend/sql/k8s_auth_role/delete.rs create mode 100644 crates/keystone/src/k8s_auth/backend/sql/k8s_auth_role/get.rs create mode 100644 crates/keystone/src/k8s_auth/backend/sql/k8s_auth_role/list.rs create mode 100644 crates/keystone/src/k8s_auth/backend/sql/k8s_auth_role/update.rs create mode 100644 crates/keystone/src/k8s_auth/error.rs create mode 100644 crates/keystone/src/k8s_auth/mock.rs create mode 100644 crates/keystone/src/k8s_auth/mod.rs create mode 100644 crates/keystone/src/k8s_auth/types.rs create mode 100644 crates/keystone/src/k8s_auth/types/k8s_auth.rs create mode 100644 crates/keystone/src/k8s_auth/types/k8s_auth_role.rs create mode 100644 crates/keystone/src/k8s_auth/types/provider_api.rs create mode 100644 doc/src/adr/0015-kubernetes-auth.md create mode 100644 tests/integration/src/k8s_auth.rs create mode 100644 tests/integration/src/k8s_auth/config.rs create mode 100644 tests/integration/src/k8s_auth/config/create.rs create mode 100644 tests/integration/src/k8s_auth/config/delete.rs create mode 100644 tests/integration/src/k8s_auth/config/get.rs create mode 100644 tests/integration/src/k8s_auth/config/list.rs create mode 100644 tests/integration/src/k8s_auth/config/update.rs create mode 100644 tests/integration/src/k8s_auth/role.rs create mode 100644 tests/integration/src/k8s_auth/role/create.rs create mode 100644 tests/integration/src/k8s_auth/role/delete.rs create mode 100644 tests/integration/src/k8s_auth/role/get.rs create mode 100644 tests/integration/src/k8s_auth/role/list.rs create mode 100644 tests/integration/src/k8s_auth/role/update.rs create mode 100644 tests/integration/src/macros.rs create mode 100644 tests/integration/src/token/token_restriction.rs diff --git a/crates/keystone/src/assignment/types/provider_api.rs b/crates/keystone/src/assignment/types/provider_api.rs index 5aa77d78..c38d062d 100644 --- a/crates/keystone/src/assignment/types/provider_api.rs +++ b/crates/keystone/src/assignment/types/provider_api.rs @@ -18,8 +18,8 @@ use super::assignment::*; use crate::assignment::AssignmentProviderError; use crate::keystone::ServiceState; -/// The trait covering `[Role](crate::role::Role)` assignments between `actors` -/// and `objects`. +/// The trait covering [`Role`](crate::role::types::Role) assignments between +/// `actors` and `objects`. #[async_trait] pub trait AssignmentApi: Send + Sync { /// Create assignment grant. diff --git a/crates/keystone/src/config.rs b/crates/keystone/src/config.rs index d461c27e..89bfad52 100644 --- a/crates/keystone/src/config.rs +++ b/crates/keystone/src/config.rs @@ -31,6 +31,7 @@ mod federation; mod fernet_token; mod identity; mod identity_mapping; +mod k8s_auth; mod policy; mod resource; mod revoke; @@ -51,6 +52,7 @@ 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; @@ -111,6 +113,10 @@ pub struct Config { #[serde(default)] pub identity_mapping: IdentityMappingProvider, + /// K8s Auth provider configuration. + #[serde(default)] + pub k8s_auth: K8sAuthProvider, + /// Resource provider configuration. #[serde(default)] pub resource: ResourceProvider, diff --git a/crates/keystone/src/config/k8s_auth.rs b/crates/keystone/src/config/k8s_auth.rs new file mode 100644 index 00000000..3b682d71 --- /dev/null +++ b/crates/keystone/src/config/k8s_auth.rs @@ -0,0 +1,32 @@ +// 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::Deserialize; + +use crate::config::common::default_sql_driver; + +/// K8s auth provider. +#[derive(Debug, Deserialize, Clone)] +pub struct K8sAuthProvider { + /// K8s auth provider backend. + #[serde(default = "default_sql_driver")] + pub driver: String, +} + +impl Default for K8sAuthProvider { + fn default() -> Self { + Self { + driver: default_sql_driver(), + } + } +} diff --git a/crates/keystone/src/db/entity.rs b/crates/keystone/src/db/entity.rs index 11bbd457..22e8a9ce 100644 --- a/crates/keystone/src/db/entity.rs +++ b/crates/keystone/src/db/entity.rs @@ -39,6 +39,8 @@ pub mod id_mapping; pub mod identity_provider; pub mod idp_remote_ids; pub mod implied_role; +pub mod kubernetes_auth; +pub mod kubernetes_auth_role; pub mod limit; pub mod local_user; pub mod mapping; diff --git a/crates/keystone/src/db/entity/kubernetes_auth.rs b/crates/keystone/src/db/entity/kubernetes_auth.rs new file mode 100644 index 00000000..3a34f604 --- /dev/null +++ b/crates/keystone/src/db/entity/kubernetes_auth.rs @@ -0,0 +1,62 @@ +// 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 +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19 + +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm(table_name = "kubernetes_auth")] +pub struct Model { + #[sea_orm(column_type = "Text", nullable)] + pub ca_cert: Option, + + pub domain_id: String, + + pub enabled: bool, + + pub host: String, + + #[sea_orm(primary_key, auto_increment = false)] + pub id: String, + + pub name: Option, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm(has_many = "super::kubernetes_auth_role::Entity")] + KubernetesAuthRole, + #[sea_orm( + belongs_to = "super::project::Entity", + from = "Column::DomainId", + to = "super::project::Column::Id", + on_update = "NoAction", + on_delete = "Cascade" + )] + Project, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::KubernetesAuthRole.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Project.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/keystone/src/db/entity/kubernetes_auth_role.rs b/crates/keystone/src/db/entity/kubernetes_auth_role.rs new file mode 100644 index 00000000..62420f7c --- /dev/null +++ b/crates/keystone/src/db/entity/kubernetes_auth_role.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 +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19 + +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm(table_name = "kubernetes_auth_role")] +pub struct Model { + pub auth_configuration_id: String, + + pub bound_audience: Option, + + #[sea_orm(column_type = "Text")] + pub bound_service_account_names: String, + + #[sea_orm(column_type = "Text")] + pub bound_service_account_namespaces: String, + + pub domain_id: String, + + #[sea_orm(primary_key, auto_increment = false)] + pub id: String, + + pub enabled: bool, + + pub name: String, + + pub token_restriction_id: String, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::kubernetes_auth::Entity", + from = "Column::AuthConfigurationId", + to = "super::kubernetes_auth::Column::Id", + on_update = "NoAction", + on_delete = "Cascade" + )] + KubernetesAuth, + #[sea_orm( + belongs_to = "super::project::Entity", + from = "Column::DomainId", + to = "super::project::Column::Id", + on_update = "NoAction", + on_delete = "Cascade" + )] + Project, + #[sea_orm( + belongs_to = "super::token_restriction::Entity", + from = "Column::TokenRestrictionId", + to = "super::token_restriction::Column::Id", + on_update = "NoAction", + on_delete = "Cascade" + )] + TokenRestriction, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::KubernetesAuth.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Project.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::TokenRestriction.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/keystone/src/db/entity/prelude.rs b/crates/keystone/src/db/entity/prelude.rs index 42dcf44f..ded7ae25 100644 --- a/crates/keystone/src/db/entity/prelude.rs +++ b/crates/keystone/src/db/entity/prelude.rs @@ -38,6 +38,8 @@ pub use super::id_mapping::Entity as IdMapping; pub use super::identity_provider::Entity as IdentityProvider; pub use super::idp_remote_ids::Entity as IdpRemoteIds; pub use super::implied_role::Entity as ImpliedRole; +pub use super::kubernetes_auth::Entity as KubernetesAuth; +pub use super::kubernetes_auth_role::Entity as KubernetesAuthRole; pub use super::limit::Entity as Limit; pub use super::local_user::Entity as LocalUser; pub use super::mapping::Entity as Mapping; diff --git a/crates/keystone/src/db_migration/m20260217_164934_k8.rs b/crates/keystone/src/db_migration/m20260217_164934_k8.rs new file mode 100644 index 00000000..9d022dbc --- /dev/null +++ b/crates/keystone/src/db_migration/m20260217_164934_k8.rs @@ -0,0 +1,144 @@ +// 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 sea_orm_migration::{prelude::*, schema::*}; + +use crate::db::entity::prelude::{Project, TokenRestriction}; +use crate::db::entity::{project, token_restriction}; + +#[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(KubernetesAuth::Table) + .if_not_exists() + .col(string_len(KubernetesAuth::Id, 64).primary_key()) + .col(string_len(KubernetesAuth::DomainId, 64)) + .col(string_len_null(KubernetesAuth::Name, 255)) + .col(string_len(KubernetesAuth::Host, 128)) + .col(boolean(KubernetesAuth::Enabled)) + .col(text_null(KubernetesAuth::CaCert)) + .foreign_key( + ForeignKey::create() + .name("fk-k8auth-domain") + .from(KubernetesAuth::Table, KubernetesAuth::DomainId) + .to(Project, project::Column::Id) + .on_delete(ForeignKeyAction::Cascade), + ) + .index( + Index::create() + .unique() + .nulls_not_distinct() + .name("idx-k8auth-domain-name") + .col(KubernetesAuth::DomainId) + .col(KubernetesAuth::Name), + ) + .to_owned(), + ) + .await?; + + manager + .create_table( + Table::create() + .table(KubernetesAuthRole::Table) + .if_not_exists() + .col(string_len(KubernetesAuthRole::Id, 64).primary_key()) + .col(string_len(KubernetesAuthRole::DomainId, 64)) + .col(string_len(KubernetesAuthRole::AuthConfigurationId, 64)) + .col(string_len(KubernetesAuthRole::Name, 255)) + .col(boolean(KubernetesAuthRole::Enabled)) + .col(text(KubernetesAuthRole::BoundServiceAccountNames)) + .col(text(KubernetesAuthRole::BoundServiceAccountNamespaces)) + .col(string_len_null(KubernetesAuthRole::BoundAudience, 128)) + .col(string_len(KubernetesAuthRole::TokenRestrictionId, 64)) + .foreign_key( + ForeignKey::create() + .name("fk-k8role-domain") + .from(KubernetesAuthRole::Table, KubernetesAuthRole::DomainId) + .to(Project, project::Column::Id) + .on_delete(ForeignKeyAction::Cascade), + ) + .foreign_key( + ForeignKey::create() + .name("fk-k8role-k8") + .from( + KubernetesAuthRole::Table, + KubernetesAuthRole::AuthConfigurationId, + ) + .to(KubernetesAuth::Table, KubernetesAuth::Id) + .on_delete(ForeignKeyAction::Cascade), + ) + .foreign_key( + ForeignKey::create() + .name("fk-k8role-token-restriction") + .from( + KubernetesAuthRole::Table, + KubernetesAuthRole::TokenRestrictionId, + ) + .to(TokenRestriction, token_restriction::Column::Id) + .on_delete(ForeignKeyAction::Cascade), + ) + .index( + Index::create() + .unique() + .nulls_not_distinct() + .name("idx-k8role-domain-name") + .col(KubernetesAuth::DomainId) + .col(KubernetesAuth::Name), + ) + .to_owned(), + ) + .await?; + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(KubernetesAuthRole::Table).to_owned()) + .await?; + manager + .drop_table(Table::drop().table(KubernetesAuth::Table).to_owned()) + .await?; + Ok(()) + } +} + +#[derive(DeriveIden)] +enum KubernetesAuth { + Table, + Id, + DomainId, + Name, + Enabled, + Host, + CaCert, +} + +#[derive(DeriveIden)] +enum KubernetesAuthRole { + Table, + Id, + DomainId, + AuthConfigurationId, + Name, + Enabled, + BoundServiceAccountNames, + BoundServiceAccountNamespaces, + BoundAudience, + TokenRestrictionId, +} diff --git a/crates/keystone/src/db_migration/mod.rs b/crates/keystone/src/db_migration/mod.rs index cbd991eb..d2d936fd 100644 --- a/crates/keystone/src/db_migration/mod.rs +++ b/crates/keystone/src/db_migration/mod.rs @@ -17,6 +17,7 @@ pub use sea_orm_migration::prelude::*; mod m20250301_000001_passkey; mod m20250414_000001_idp; mod m20251005_131042_token_restriction; +mod m20260217_164934_k8; pub struct Migrator; @@ -27,6 +28,7 @@ impl MigratorTrait for Migrator { Box::new(m20250301_000001_passkey::Migration), Box::new(m20250414_000001_idp::Migration), Box::new(m20251005_131042_token_restriction::Migration), + Box::new(m20260217_164934_k8::Migration), ] } } diff --git a/crates/keystone/src/error.rs b/crates/keystone/src/error.rs index b4020dde..980deff5 100644 --- a/crates/keystone/src/error.rs +++ b/crates/keystone/src/error.rs @@ -16,15 +16,16 @@ //! Diverse errors that can occur during the Keystone processing (not the API). use thiserror::Error; -use crate::application_credential::error::*; -use crate::assignment::error::*; -use crate::catalog::error::*; -use crate::federation::error::*; -use crate::identity::error::*; -use crate::identity_mapping::error::*; -use crate::policy::*; -use crate::resource::error::*; -use crate::revoke::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::IdentityMappingError; +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::TrustError; @@ -97,6 +98,14 @@ pub enum KeystoneError { source: serde_json::Error, }, + /// K8s auth provider. + #[error(transparent)] + K8sAuthProvider { + /// The source of the error. + #[from] + source: K8sAuthProviderError, + }, + /// Policy engine. #[error(transparent)] Policy { diff --git a/crates/keystone/src/k8s_auth/backend.rs b/crates/keystone/src/k8s_auth/backend.rs new file mode 100644 index 00000000..505ea83f --- /dev/null +++ b/crates/keystone/src/k8s_auth/backend.rs @@ -0,0 +1,100 @@ +// 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; + +pub mod error; +pub mod sql; + +/// K8s auth Backend trait. +/// +/// Backend driver interface expected by the revocation provider. +#[cfg_attr(test, mockall::automock)] +#[async_trait] +pub trait K8sAuthBackend: Send + Sync { + /// Register new K8s auth. + async fn create_k8s_auth_configuration( + &self, + state: &ServiceState, + config: K8sAuthConfigurationCreate, + ) -> Result; + + /// Register new K8s auth role. + async fn create_k8s_auth_role( + &self, + state: &ServiceState, + role: K8sAuthRoleCreate, + ) -> Result; + + /// Delete K8s auth. + async fn delete_k8s_auth_configuration<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result<(), K8sAuthProviderError>; + + /// Delete K8s auth role. + async fn delete_k8s_auth_role<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result<(), K8sAuthProviderError>; + + /// Register new K8s auth. + async fn get_k8s_auth_configuration<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result, K8sAuthProviderError>; + + /// Register new K8s auth role. + async fn get_k8s_auth_role<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result, K8sAuthProviderError>; + + /// List K8s auth configurations. + async fn list_k8s_auth_configurations( + &self, + state: &ServiceState, + params: &K8sAuthConfigurationListParameters, + ) -> Result, K8sAuthProviderError>; + + /// List K8s auth roles. + async fn list_k8s_auth_roles( + &self, + state: &ServiceState, + params: &K8sAuthRoleListParameters, + ) -> Result, K8sAuthProviderError>; + + /// Update K8s auth. + async fn update_k8s_auth_configuration<'a>( + &self, + state: &ServiceState, + id: &'a str, + data: K8sAuthConfigurationUpdate, + ) -> Result; + + /// Update K8s auth role. + async fn update_k8s_auth_role<'a>( + &self, + state: &ServiceState, + id: &'a str, + data: K8sAuthRoleUpdate, + ) -> Result; +} diff --git a/crates/keystone/src/k8s_auth/backend/error.rs b/crates/keystone/src/k8s_auth/backend/error.rs new file mode 100644 index 00000000..10f5522c --- /dev/null +++ b/crates/keystone/src/k8s_auth/backend/error.rs @@ -0,0 +1,45 @@ +// 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 database backend error. + +use thiserror::Error; + +use crate::error::{BuilderError, DatabaseError}; + +/// K8s Auth provider database error. +#[derive(Error, Debug)] +pub enum K8sAuthDatabaseError { + /// Database error. + #[error(transparent)] + Database { + #[from] + source: DatabaseError, + }, + + /// K8s auth configuration not found. + #[error("k8s configuration {0} not found")] + ConfigurationNotFound(String), + + /// K8s auth role not found. + #[error("k8s role {0} not found")] + RoleNotFound(String), + + /// Structures builder error. + #[error(transparent)] + StructBuilder { + /// The source of the error. + #[from] + source: BuilderError, + }, +} diff --git a/crates/keystone/src/k8s_auth/backend/sql.rs b/crates/keystone/src/k8s_auth/backend/sql.rs new file mode 100644 index 00000000..8fd497cd --- /dev/null +++ b/crates/keystone/src/k8s_auth/backend/sql.rs @@ -0,0 +1,157 @@ +// 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 +//! Revoke provider: database backend. + +use async_trait::async_trait; + +use super::K8sAuthBackend; +use crate::k8s_auth::error::K8sAuthProviderError; +use crate::k8s_auth::types::*; +use crate::keystone::ServiceState; + +mod k8s_auth; +mod k8s_auth_role; + +/// Sql Database K8s auth backend. +#[derive(Default)] +pub struct SqlBackend {} + +#[async_trait] +impl K8sAuthBackend for SqlBackend { + /// Register new K8s auth. + #[tracing::instrument(level = "debug", skip(self, state))] + async fn create_k8s_auth_configuration( + &self, + state: &ServiceState, + config: K8sAuthConfigurationCreate, + ) -> Result { + Ok(k8s_auth::create(&state.db, config).await?) + } + + /// Register new K8s auth role. + #[tracing::instrument(level = "debug", skip(self, state))] + async fn create_k8s_auth_role( + &self, + state: &ServiceState, + role: K8sAuthRoleCreate, + ) -> Result { + Ok(k8s_auth_role::create(&state.db, role).await?) + } + + /// Delete K8s auth. + #[tracing::instrument(level = "debug", skip(self, state))] + async fn delete_k8s_auth_configuration<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result<(), K8sAuthProviderError> { + Ok(k8s_auth::delete(&state.db, id).await?) + } + + /// Delete K8s auth role. + #[tracing::instrument(level = "debug", skip(self, state))] + async fn delete_k8s_auth_role<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result<(), K8sAuthProviderError> { + Ok(k8s_auth_role::delete(&state.db, id).await?) + } + + /// Register new K8s auth. + #[tracing::instrument(level = "debug", skip(self, state))] + async fn get_k8s_auth_configuration<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result, K8sAuthProviderError> { + Ok(k8s_auth::get(&state.db, id).await?) + } + + /// Register new K8s auth role. + #[tracing::instrument(level = "debug", skip(self, state))] + async fn get_k8s_auth_role<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result, K8sAuthProviderError> { + Ok(k8s_auth_role::get(&state.db, id).await?) + } + + /// List K8s auth configurations. + #[tracing::instrument(level = "debug", skip(self, state))] + async fn list_k8s_auth_configurations( + &self, + state: &ServiceState, + params: &K8sAuthConfigurationListParameters, + ) -> Result, K8sAuthProviderError> { + Ok(k8s_auth::list(&state.db, params).await?) + } + + /// List K8s auth roles. + #[tracing::instrument(level = "debug", skip(self, state))] + async fn list_k8s_auth_roles( + &self, + state: &ServiceState, + params: &K8sAuthRoleListParameters, + ) -> Result, K8sAuthProviderError> { + Ok(k8s_auth_role::list(&state.db, params).await?) + } + + /// Update K8s auth. + #[tracing::instrument(level = "debug", skip(self, state))] + async fn update_k8s_auth_configuration<'a>( + &self, + state: &ServiceState, + id: &'a str, + data: K8sAuthConfigurationUpdate, + ) -> Result { + Ok(k8s_auth::update(&state.db, id, data).await?) + } + + /// Update K8s auth role. + #[tracing::instrument(level = "debug", skip(self, state))] + async fn update_k8s_auth_role<'a>( + &self, + state: &ServiceState, + id: &'a str, + data: K8sAuthRoleUpdate, + ) -> Result { + Ok(k8s_auth_role::update(&state.db, id, data).await?) + } +} + +#[cfg(test)] +mod tests { + // use crate::db::entity::revocation_event as db_revocation_event; + // use chrono::NaiveDateTime; + // + // pub(super) fn get_mock() -> db_revocation_event::Model { + // db_revocation_event::Model { + // id: 1i32, + // domain_id: Some("did".into()), + // project_id: Some("pid".into()), + // user_id: Some("uid".into()), + // role_id: Some("rid".into()), + // trust_id: Some("trust_id".into()), + // consumer_id: Some("consumer_id".into()), + // access_token_id: Some("access_token_id".into()), + // issued_before: NaiveDateTime::default(), + // expires_at: Some(NaiveDateTime::default()), + // revoked_at: NaiveDateTime::default(), + // audit_id: Some("audit_id".into()), + // audit_chain_id: Some("audit_chain_id".into()), + // } + // } +} diff --git a/crates/keystone/src/k8s_auth/backend/sql/k8s_auth.rs b/crates/keystone/src/k8s_auth/backend/sql/k8s_auth.rs new file mode 100644 index 00000000..6de9102d --- /dev/null +++ b/crates/keystone/src/k8s_auth/backend/sql/k8s_auth.rs @@ -0,0 +1,190 @@ +// 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 sea_orm::entity::*; + +use crate::db::entity::kubernetes_auth as db_k8s_auth; +use crate::k8s_auth::types::*; + +mod create; +mod delete; +mod get; +mod list; +mod update; + +pub use create::create; +pub use delete::delete; +pub use get::get; +pub use list::list; +pub use update::update; + +impl From for K8sAuthConfiguration { + fn from(value: db_k8s_auth::Model) -> Self { + Self { + ca_cert: value.ca_cert.into(), + domain_id: value.domain_id.into(), + enabled: value.enabled, + host: value.host.into(), + id: value.id.into(), + name: value.name.into(), + } + } +} + +impl From<&db_k8s_auth::Model> for K8sAuthConfiguration { + fn from(value: &db_k8s_auth::Model) -> Self { + Self::from(value.clone()) + } +} + +impl From for db_k8s_auth::ActiveModel { + fn from(value: K8sAuthConfigurationCreate) -> Self { + Self { + ca_cert: value.ca_cert.map(Set).unwrap_or(NotSet).into(), + domain_id: Set(value.domain_id), + enabled: Set(value.enabled), + host: Set(value.host), + id: value + .id + .map(Set) + .unwrap_or(Set(uuid::Uuid::new_v4().simple().to_string())), + name: value.name.map(Set).unwrap_or(NotSet).into(), + } + } +} + +impl db_k8s_auth::Model { + /// Build an [`kubernetes_auth::ActiveModel`] for the update operation using + /// the [`K8sAuthConfigurationUpdate`]. + fn to_active_model_update( + self, + update: K8sAuthConfigurationUpdate, + ) -> db_k8s_auth::ActiveModel { + let mut new: db_k8s_auth::ActiveModel = self.into(); + if let Some(val) = &update.ca_cert { + new.ca_cert = Set(Some(val.into())); + } + if let Some(val) = update.enabled { + new.enabled = Set(val.into()); + } + if let Some(val) = &update.host { + new.host = Set(val.into()); + } + if let Some(val) = &update.name { + new.name = Set(Some(val.into())); + } + + new + } +} + +#[cfg(test)] +pub(crate) mod tests { + use sea_orm::entity::*; + + use crate::db::entity::kubernetes_auth; + use crate::k8s_auth::types::*; + + pub fn get_k8s_auth_config_mock>(id: S) -> kubernetes_auth::Model { + kubernetes_auth::Model { + ca_cert: Some("key".into()), + domain_id: "did".into(), + enabled: true, + host: "host.local".into(), + id: id.into(), + name: Some("name".into()), + } + } + + #[test] + fn test_db_to_provider_model() { + assert_eq!( + K8sAuthConfiguration { + ca_cert: Some("ca".into()), + domain_id: "did".into(), + enabled: true, + host: "host".into(), + id: "id".into(), + name: Some("name".into()), + }, + K8sAuthConfiguration::from(kubernetes_auth::Model { + ca_cert: Some("ca".into()), + domain_id: "did".into(), + enabled: true, + host: "host".into(), + id: "id".into(), + name: Some("name".into()), + }) + ); + } + + #[test] + fn test_create_to_db_model() { + assert_eq!( + kubernetes_auth::ActiveModel { + ca_cert: Set(Some("ca".into())), + domain_id: Set("did".into()), + enabled: Set(true), + host: Set("host".into()), + id: Set("id".into()), + name: Set(Some("name".into())), + }, + kubernetes_auth::ActiveModel::from(K8sAuthConfigurationCreate { + ca_cert: Some("ca".into()), + domain_id: "did".into(), + enabled: true, + host: "host".into(), + id: Some("id".into()), + name: Some("name".into()), + }) + ); + assert!( + !kubernetes_auth::ActiveModel::from(K8sAuthConfigurationCreate { + ca_cert: Some("ca".into()), + domain_id: "did".into(), + enabled: true, + host: "host".into(), + id: None, + name: Some("name".into()), + }) + .id + .unwrap() + .is_empty() + ); + } + + #[test] + fn test_model_to_active_model_update() { + let sot = kubernetes_auth::Model { + ca_cert: Some("ca".into()), + domain_id: "did".into(), + enabled: true, + host: "host".into(), + id: "id".into(), + name: Some("name".into()), + }; + let update = sot.to_active_model_update(crate::k8s_auth::K8sAuthConfigurationUpdate { + ca_cert: Some("new_ca".into()), + enabled: Some(true), + host: Some("new_host".into()), + name: Some("new_name".into()), + }); + assert_eq!(Set(Some("new_ca".into())), update.ca_cert); + assert_eq!(Unchanged("did".into()), update.domain_id); + assert_eq!(Unchanged("id".into()), update.id); + assert_eq!(Set(true), update.enabled); + assert_eq!(Set("new_host".into()), update.host); + assert_eq!(Set(Some("new_name".into())), update.name); + } +} diff --git a/crates/keystone/src/k8s_auth/backend/sql/k8s_auth/create.rs b/crates/keystone/src/k8s_auth/backend/sql/k8s_auth/create.rs new file mode 100644 index 00000000..23ee64ab --- /dev/null +++ b/crates/keystone/src/k8s_auth/backend/sql/k8s_auth/create.rs @@ -0,0 +1,81 @@ +// 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 +//! Create K8s auth configuration + +use sea_orm::DatabaseConnection; +use sea_orm::entity::*; + +use crate::db::entity::kubernetes_auth; +use crate::error::DbContextExt; +use crate::k8s_auth::{ + backend::error::K8sAuthDatabaseError, + types::{K8sAuthConfiguration, K8sAuthConfigurationCreate}, +}; + +/// Create new k8s auth configuration. +pub async fn create( + db: &DatabaseConnection, + data: K8sAuthConfigurationCreate, +) -> Result { + Ok(kubernetes_auth::ActiveModel::from(data) + .insert(db) + .await + .context("creating k8s auth configuration")? + .into()) +} + +#[cfg(test)] +mod tests { + use sea_orm::{DatabaseBackend, MockDatabase, Transaction}; + + use super::super::tests::get_k8s_auth_config_mock; + use super::*; + + #[tokio::test] + async fn test_create() { + // Create MockDatabase with mock query results + let db = MockDatabase::new(DatabaseBackend::Postgres) + .append_query_results([vec![get_k8s_auth_config_mock("1")]]) + .into_connection(); + + let cid = uuid::Uuid::new_v4().simple().to_string(); + let req = K8sAuthConfigurationCreate { + ca_cert: Some("ca_cert".into()), + domain_id: "did".into(), + enabled: true, + host: "host".into(), + id: Some(cid.clone()), + name: Some("name".into()), + }; + + create(&db, req).await.unwrap(); + + // Checking transaction log + assert_eq!( + db.into_transaction_log(), + [Transaction::from_sql_and_values( + DatabaseBackend::Postgres, + r#"INSERT INTO "kubernetes_auth" ("ca_cert", "domain_id", "enabled", "host", "id", "name") VALUES ($1, $2, $3, $4, $5, $6) RETURNING "ca_cert", "domain_id", "enabled", "host", "id", "name""#, + [ + "ca_cert".into(), + "did".into(), + true.into(), + "host".into(), + cid.into(), + "name".into() + ] + ),] + ); + } +} diff --git a/crates/keystone/src/k8s_auth/backend/sql/k8s_auth/delete.rs b/crates/keystone/src/k8s_auth/backend/sql/k8s_auth/delete.rs new file mode 100644 index 00000000..2ee352bc --- /dev/null +++ b/crates/keystone/src/k8s_auth/backend/sql/k8s_auth/delete.rs @@ -0,0 +1,62 @@ +// 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 +//! Delete k8s auth configuration + +use sea_orm::DatabaseConnection; +use sea_orm::entity::*; + +use crate::db::entity::prelude::KubernetesAuth; +use crate::error::DbContextExt; +use crate::k8s_auth::backend::error::K8sAuthDatabaseError; + +/// Delete existing K8s auth configuration. +pub async fn delete>( + db: &DatabaseConnection, + id: S, +) -> Result<(), K8sAuthDatabaseError> { + KubernetesAuth::delete_by_id(id.as_ref()) + .exec(db) + .await + .context("deleting k8s auth configuration")?; + Ok(()) +} + +#[cfg(test)] +mod tests { + use sea_orm::{DatabaseBackend, MockDatabase, MockExecResult, Transaction}; + + use super::*; + + #[tokio::test] + async fn test_delete() { + // Create MockDatabase with mock query results + let db = MockDatabase::new(DatabaseBackend::Postgres) + .append_exec_results([MockExecResult { + rows_affected: 1, + ..Default::default() + }]) + .into_connection(); + + delete(&db, "id").await.unwrap(); + // Checking transaction log + assert_eq!( + db.into_transaction_log(), + [Transaction::from_sql_and_values( + DatabaseBackend::Postgres, + r#"DELETE FROM "kubernetes_auth" WHERE "kubernetes_auth"."id" = $1"#, + ["id".into()] + ),] + ); + } +} diff --git a/crates/keystone/src/k8s_auth/backend/sql/k8s_auth/get.rs b/crates/keystone/src/k8s_auth/backend/sql/k8s_auth/get.rs new file mode 100644 index 00000000..16b36613 --- /dev/null +++ b/crates/keystone/src/k8s_auth/backend/sql/k8s_auth/get.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 +//! Get existing k8s auth configuration. +use sea_orm::DatabaseConnection; +use sea_orm::entity::*; + +use crate::db::entity::prelude::KubernetesAuth; +use crate::error::DbContextExt; +use crate::k8s_auth::{backend::error::K8sAuthDatabaseError, types::K8sAuthConfiguration}; + +/// Get existing k8s auth configuration by the ID. +pub async fn get>( + db: &DatabaseConnection, + id: S, +) -> Result, K8sAuthDatabaseError> { + Ok(KubernetesAuth::find_by_id(id.as_ref()) + .one(db) + .await + .context("reading kubernetes auth configuration record")? + .map(Into::into)) +} + +#[cfg(test)] +mod tests { + use sea_orm::{DatabaseBackend, MockDatabase, Transaction}; + + use super::super::tests::get_k8s_auth_config_mock; + use super::*; + + #[tokio::test] + async fn test_get() { + // Create MockDatabase with mock query results + let db = MockDatabase::new(DatabaseBackend::Postgres) + .append_query_results([vec![get_k8s_auth_config_mock("id")]]) + .into_connection(); + + assert_eq!( + get(&db, "id").await.unwrap(), + Some(get_k8s_auth_config_mock("id").try_into().unwrap()) + ); + + // Checking transaction log + assert_eq!( + db.into_transaction_log(), + [Transaction::from_sql_and_values( + DatabaseBackend::Postgres, + r#"SELECT "kubernetes_auth"."ca_cert", "kubernetes_auth"."domain_id", "kubernetes_auth"."enabled", "kubernetes_auth"."host", "kubernetes_auth"."id", "kubernetes_auth"."name" FROM "kubernetes_auth" WHERE "kubernetes_auth"."id" = $1 LIMIT $2"#, + ["id".into(), 1u64.into()] + ),] + ); + } +} diff --git a/crates/keystone/src/k8s_auth/backend/sql/k8s_auth/list.rs b/crates/keystone/src/k8s_auth/backend/sql/k8s_auth/list.rs new file mode 100644 index 00000000..c95c33de --- /dev/null +++ b/crates/keystone/src/k8s_auth/backend/sql/k8s_auth/list.rs @@ -0,0 +1,131 @@ +// 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 +//! List K8s auth configurations + +use sea_orm::DatabaseConnection; +use sea_orm::entity::*; +use sea_orm::query::*; +use sea_orm::{Cursor, SelectModel}; + +use crate::db::entity::kubernetes_auth; +use crate::db::entity::prelude::KubernetesAuth; +use crate::error::DbContextExt; +use crate::k8s_auth::{ + backend::error::K8sAuthDatabaseError, + types::{K8sAuthConfiguration, K8sAuthConfigurationListParameters}, +}; + +/// Prepare the query for listing k8s auth configurations. +fn get_list_query( + params: &K8sAuthConfigurationListParameters, +) -> Result>, K8sAuthDatabaseError> { + let mut select = KubernetesAuth::find(); + if let Some(val) = ¶ms.domain_id { + select = select.filter(kubernetes_auth::Column::DomainId.eq(val)); + } + if let Some(val) = ¶ms.name { + select = select.filter(kubernetes_auth::Column::Name.eq(val)); + } + + Ok(select.cursor_by(kubernetes_auth::Column::Id)) +} + +/// List K8s auth configurations. +pub async fn list( + db: &DatabaseConnection, + params: &K8sAuthConfigurationListParameters, +) -> Result, K8sAuthDatabaseError> { + Ok(get_list_query(params)? + .all(db) + .await + .context("listing k8s auth configurations")? + .into_iter() + .map(Into::into) + .collect()) +} + +#[cfg(test)] +mod tests { + use sea_orm::{DatabaseBackend, MockDatabase, QueryOrder, Transaction, sea_query::*}; + + use super::super::tests::get_k8s_auth_config_mock; + use super::*; + + #[tokio::test] + async fn test_query_all() { + assert_eq!( + r#"SELECT "kubernetes_auth"."ca_cert", "kubernetes_auth"."domain_id", "kubernetes_auth"."enabled", "kubernetes_auth"."host", "kubernetes_auth"."id", "kubernetes_auth"."name" FROM "kubernetes_auth""#, + QueryOrder::query( + &mut get_list_query(&K8sAuthConfigurationListParameters::default()).unwrap() + ) + .to_string(PostgresQueryBuilder) + ); + } + + #[tokio::test] + async fn test_query_name() { + assert!( + QueryOrder::query( + &mut get_list_query(&K8sAuthConfigurationListParameters { + name: Some("name".into()), + ..Default::default() + }) + .unwrap() + ) + .to_string(PostgresQueryBuilder) + .contains("\"kubernetes_auth\".\"name\" = 'name'") + ); + } + + #[tokio::test] + async fn test_query_domain_id() { + let query = QueryOrder::query( + &mut get_list_query(&K8sAuthConfigurationListParameters { + domain_id: Some("d1".into()), + ..Default::default() + }) + .unwrap(), + ) + .to_string(PostgresQueryBuilder); + assert!(query.contains("\"kubernetes_auth\".\"domain_id\" = 'd1'")); + } + + #[tokio::test] + async fn test_list_no_params() { + // Create MockDatabase with mock query results + let db = MockDatabase::new(DatabaseBackend::Postgres) + .append_query_results([vec![ + get_k8s_auth_config_mock("id1"), + get_k8s_auth_config_mock("id2"), + ]]) + .into_connection(); + + let res = list(&db, &K8sAuthConfigurationListParameters::default()) + .await + .unwrap(); + + assert_eq!(2, res.len()); + assert!(res.contains(&K8sAuthConfiguration::from(get_k8s_auth_config_mock("id1")))); + + // Checking transaction log + assert_eq!( + db.into_transaction_log(), + [Transaction::from_sql_and_values( + DatabaseBackend::Postgres, + r#"SELECT "kubernetes_auth"."ca_cert", "kubernetes_auth"."domain_id", "kubernetes_auth"."enabled", "kubernetes_auth"."host", "kubernetes_auth"."id", "kubernetes_auth"."name" FROM "kubernetes_auth" ORDER BY "kubernetes_auth"."id" ASC"#, + [] + ),] + ); + } +} diff --git a/crates/keystone/src/k8s_auth/backend/sql/k8s_auth/update.rs b/crates/keystone/src/k8s_auth/backend/sql/k8s_auth/update.rs new file mode 100644 index 00000000..ad30576f --- /dev/null +++ b/crates/keystone/src/k8s_auth/backend/sql/k8s_auth/update.rs @@ -0,0 +1,112 @@ +// 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 +//! Update the existing K8s auth configuration + +use sea_orm::DatabaseConnection; +use sea_orm::TransactionTrait; +use sea_orm::entity::*; + +use crate::db::entity::prelude::KubernetesAuth; +use crate::error::DbContextExt; +use crate::k8s_auth::{ + backend::error::K8sAuthDatabaseError, + types::{K8sAuthConfiguration, K8sAuthConfigurationUpdate}, +}; + +/// Update existing k8s auth configuration by the ID. +/// +/// Perform search and update of the k8s auth configuration in an isolated +/// transaction. +pub async fn update>( + db: &DatabaseConnection, + id: S, + data: K8sAuthConfigurationUpdate, +) -> Result { + // Start transaction to prevent TOCTOU + let txn = db + .begin() + .await + .context("starting transaction for updating k8s auth configuration")?; + let res = if let Some(current) = KubernetesAuth::find_by_id(id.as_ref()) + .one(&txn) + .await + .context("searching for the existing k8s auth configuration for update")? + { + Ok(current + .to_active_model_update(data) + .update(&txn) + .await + .context("updating k8s auth configuration")? + .into()) + } else { + Err(K8sAuthDatabaseError::ConfigurationNotFound( + id.as_ref().to_string(), + )) + }; + txn.commit() + .await + .context("committing the k8s auth configuration update transaction")?; + res +} + +#[cfg(test)] +mod tests { + use sea_orm::{DatabaseBackend, MockDatabase, Statement, Transaction}; + + use super::super::tests::get_k8s_auth_config_mock; + use super::*; + + #[tokio::test] + async fn test_update() { + // Create MockDatabase with mock query results + let db = MockDatabase::new(DatabaseBackend::Postgres) + .append_query_results([vec![get_k8s_auth_config_mock("id1")]]) + .append_query_results([vec![get_k8s_auth_config_mock("id1")]]) + .into_connection(); + + let req = K8sAuthConfigurationUpdate { + ca_cert: Some("new_ca".into()), + enabled: Some(true), + host: Some("new_host".into()), + name: Some("new_name".into()), + }; + + update(&db, "id1", req).await.unwrap(); + + // Checking transaction log + assert_eq!( + db.into_transaction_log(), + [Transaction::many(vec![ + Statement::from_string(DatabaseBackend::Postgres, r#"BEGIN"#,), + Statement::from_sql_and_values( + DatabaseBackend::Postgres, + r#"SELECT "kubernetes_auth"."ca_cert", "kubernetes_auth"."domain_id", "kubernetes_auth"."enabled", "kubernetes_auth"."host", "kubernetes_auth"."id", "kubernetes_auth"."name" FROM "kubernetes_auth" WHERE "kubernetes_auth"."id" = $1 LIMIT $2"#, + ["id1".into(), 1u64.into()] + ), + Statement::from_sql_and_values( + DatabaseBackend::Postgres, + r#"UPDATE "kubernetes_auth" SET "ca_cert" = $1, "enabled" = $2, "host" = $3, "name" = $4 WHERE "kubernetes_auth"."id" = $5 RETURNING "ca_cert", "domain_id", "enabled", "host", "id", "name""#, + [ + "new_ca".into(), + true.into(), + "new_host".into(), + "new_name".into(), + "id1".into(), + ] + ), + Statement::from_string(DatabaseBackend::Postgres, r#"COMMIT"#,) + ])] + ); + } +} diff --git a/crates/keystone/src/k8s_auth/backend/sql/k8s_auth_role.rs b/crates/keystone/src/k8s_auth/backend/sql/k8s_auth_role.rs new file mode 100644 index 00000000..1d3c23e5 --- /dev/null +++ b/crates/keystone/src/k8s_auth/backend/sql/k8s_auth_role.rs @@ -0,0 +1,238 @@ +// 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 sea_orm::entity::*; + +use crate::db::entity::kubernetes_auth_role; +use crate::k8s_auth::types::*; + +mod create; +mod delete; +mod get; +mod list; +mod update; + +pub use create::create; +pub use delete::delete; +pub use get::get; +pub use list::list; +pub use update::update; + +impl From for K8sAuthRole { + fn from(value: kubernetes_auth_role::Model) -> Self { + Self { + auth_configuration_id: value.auth_configuration_id.into(), + bound_audience: value.bound_audience.into(), + bound_service_account_names: value + .bound_service_account_names + .split(',') + .map(String::from) + .collect(), + bound_service_account_namespaces: value + .bound_service_account_namespaces + .split(',') + .map(String::from) + .collect(), + domain_id: value.domain_id.into(), + enabled: value.enabled, + id: value.id.into(), + name: value.name.into(), + token_restriction_id: value.token_restriction_id.into(), + } + } +} + +impl From<&kubernetes_auth_role::Model> for K8sAuthRole { + fn from(value: &kubernetes_auth_role::Model) -> Self { + Self::from(value.clone()) + } +} + +impl From for kubernetes_auth_role::ActiveModel { + fn from(value: K8sAuthRoleCreate) -> Self { + Self { + auth_configuration_id: Set(value.auth_configuration_id.into()), + bound_audience: value.bound_audience.map(Set).unwrap_or(NotSet).into(), + bound_service_account_names: Set(value.bound_service_account_names.join(",").into()), + bound_service_account_namespaces: Set(value + .bound_service_account_namespaces + .join(",") + .into()), + domain_id: Set(value.domain_id), + enabled: Set(value.enabled), + id: value + .id + .map(Set) + .unwrap_or(Set(uuid::Uuid::new_v4().simple().to_string())), + name: Set(value.name.into()), + token_restriction_id: Set(value.token_restriction_id.into()), + } + } +} + +impl kubernetes_auth_role::Model { + /// Build an [`kubernetes_auth_role::ActiveModel`] for the update operation + /// using the [`K8sAuthRoleUpdate`]. + fn to_active_model_update( + self, + update: K8sAuthRoleUpdate, + ) -> kubernetes_auth_role::ActiveModel { + let mut new: kubernetes_auth_role::ActiveModel = self.into(); + if let Some(val) = &update.bound_audience { + new.bound_audience = Set(Some(val.into())); + } + if let Some(val) = update.bound_service_account_names { + new.bound_service_account_names = Set(val.join(",").into()); + } + if let Some(val) = update.bound_service_account_namespaces { + new.bound_service_account_namespaces = Set(val.join(",").into()); + } + if let Some(val) = update.enabled { + new.enabled = Set(val.into()); + } + if let Some(val) = &update.name { + new.name = Set(val.into()); + } + if let Some(val) = &update.token_restriction_id { + new.token_restriction_id = Set(val.into()); + } + + new + } +} + +#[cfg(test)] +pub(crate) mod tests { + use sea_orm::entity::*; + + use crate::db::entity::kubernetes_auth_role; + use crate::k8s_auth::types::*; + + pub fn get_k8s_auth_role_mock>(id: S) -> kubernetes_auth_role::Model { + kubernetes_auth_role::Model { + auth_configuration_id: "cid".into(), + bound_audience: Some("aud".into()), + bound_service_account_names: "a,b".into(), + bound_service_account_namespaces: "na,nb".into(), + domain_id: "did".into(), + enabled: true, + id: id.into(), + name: "name".into(), + token_restriction_id: "trid".into(), + } + } + + #[test] + fn test_db_to_provider_model() { + assert_eq!( + K8sAuthRole { + auth_configuration_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: "id".into(), + name: "name".into(), + token_restriction_id: "trid".into(), + }, + K8sAuthRole::from(kubernetes_auth_role::Model { + auth_configuration_id: "cid".into(), + bound_audience: Some("aud".into()), + bound_service_account_names: "a,b".into(), + bound_service_account_namespaces: "na,nb".into(), + domain_id: "did".into(), + enabled: true, + id: "id".into(), + name: "name".into(), + token_restriction_id: "trid".into(), + }) + ); + } + + #[test] + fn test_create_to_db_model() { + assert_eq!( + kubernetes_auth_role::ActiveModel { + auth_configuration_id: Set("cid".into()), + bound_audience: Set(Some("aud".into())), + bound_service_account_names: Set("a,b".into()), + bound_service_account_namespaces: Set("na,nb".into()), + domain_id: Set("did".into()), + enabled: Set(true), + id: Set("id".into()), + name: Set("name".into()), + token_restriction_id: Set("trid".into()), + }, + kubernetes_auth_role::ActiveModel::from(K8sAuthRoleCreate { + auth_configuration_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(), + },) + ); + assert!( + !kubernetes_auth_role::ActiveModel::from(K8sAuthRoleCreate { + auth_configuration_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: None, + name: "name".into(), + token_restriction_id: "trid".into(), + }) + .id + .unwrap() + .is_empty() + ); + } + + #[test] + fn test_model_to_active_model_update() { + let sot = kubernetes_auth_role::Model { + auth_configuration_id: "cid".into(), + bound_audience: Some("aud".into()), + bound_service_account_names: "a,b".into(), + bound_service_account_namespaces: "na,nb".into(), + domain_id: "did".into(), + enabled: true, + id: "id".into(), + name: "name".into(), + token_restriction_id: "trid".into(), + }; + let update = sot.to_active_model_update(crate::k8s_auth::K8sAuthRoleUpdate { + bound_audience: Some("new_aud".into()), + bound_service_account_names: Some(vec!["c".into()]), + bound_service_account_namespaces: Some(vec!["nc".into()]), + enabled: Some(true), + name: Some("new_name".into()), + token_restriction_id: Some("new_trid".into()), + }); + assert_eq!(Set(Some("new_aud".into())), update.bound_audience); + assert_eq!(Set("c".into()), update.bound_service_account_names); + assert_eq!(Set("nc".into()), update.bound_service_account_namespaces); + assert_eq!(Unchanged("did".into()), update.domain_id); + assert_eq!(Unchanged("id".into()), update.id); + assert_eq!(Set(true), update.enabled); + assert_eq!(Set("new_name".into()), update.name); + assert_eq!(Set("new_trid".into()), update.token_restriction_id); + } +} diff --git a/crates/keystone/src/k8s_auth/backend/sql/k8s_auth_role/create.rs b/crates/keystone/src/k8s_auth/backend/sql/k8s_auth_role/create.rs new file mode 100644 index 00000000..ac06d322 --- /dev/null +++ b/crates/keystone/src/k8s_auth/backend/sql/k8s_auth_role/create.rs @@ -0,0 +1,87 @@ +// 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 +//! Create K8s auth configuration + +use sea_orm::DatabaseConnection; +use sea_orm::entity::*; + +use crate::db::entity::kubernetes_auth_role; +use crate::error::DbContextExt; +use crate::k8s_auth::{ + backend::error::K8sAuthDatabaseError, + types::{K8sAuthRole, K8sAuthRoleCreate}, +}; + +/// Create new k8s auth role. +pub async fn create( + db: &DatabaseConnection, + data: K8sAuthRoleCreate, +) -> Result { + Ok(kubernetes_auth_role::ActiveModel::from(data) + .insert(db) + .await + .context("creating k8s auth role")? + .into()) +} + +#[cfg(test)] +mod tests { + use sea_orm::{DatabaseBackend, MockDatabase, Transaction}; + + use super::super::tests::get_k8s_auth_role_mock; + use super::*; + + #[tokio::test] + async fn test_create() { + // Create MockDatabase with mock query results + let db = MockDatabase::new(DatabaseBackend::Postgres) + .append_query_results([vec![get_k8s_auth_role_mock("1")]]) + .into_connection(); + + let cid = uuid::Uuid::new_v4().simple().to_string(); + let req = K8sAuthRoleCreate { + auth_configuration_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(cid.clone()), + name: "name".into(), + token_restriction_id: "trid".into(), + }; + + create(&db, req).await.unwrap(); + + // Checking transaction log + assert_eq!( + db.into_transaction_log(), + [Transaction::from_sql_and_values( + DatabaseBackend::Postgres, + r#"INSERT INTO "kubernetes_auth_role" ("auth_configuration_id", "bound_audience", "bound_service_account_names", "bound_service_account_namespaces", "domain_id", "id", "enabled", "name", "token_restriction_id") VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING "auth_configuration_id", "bound_audience", "bound_service_account_names", "bound_service_account_namespaces", "domain_id", "id", "enabled", "name", "token_restriction_id""#, + [ + "cid".into(), + "aud".into(), + "a,b".into(), + "na,nb".into(), + "did".into(), + cid.into(), + true.into(), + "name".into(), + "trid".into(), + ] + ),] + ); + } +} diff --git a/crates/keystone/src/k8s_auth/backend/sql/k8s_auth_role/delete.rs b/crates/keystone/src/k8s_auth/backend/sql/k8s_auth_role/delete.rs new file mode 100644 index 00000000..1ca5caf2 --- /dev/null +++ b/crates/keystone/src/k8s_auth/backend/sql/k8s_auth_role/delete.rs @@ -0,0 +1,62 @@ +// 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 +//! Delete k8s auth configuration + +use sea_orm::DatabaseConnection; +use sea_orm::entity::*; + +use crate::db::entity::prelude::KubernetesAuthRole; +use crate::error::DbContextExt; +use crate::k8s_auth::backend::error::K8sAuthDatabaseError; + +/// Delete existing K8s auth configuration. +pub async fn delete>( + db: &DatabaseConnection, + id: S, +) -> Result<(), K8sAuthDatabaseError> { + KubernetesAuthRole::delete_by_id(id.as_ref()) + .exec(db) + .await + .context("deleting k8s auth role")?; + Ok(()) +} + +#[cfg(test)] +mod tests { + use sea_orm::{DatabaseBackend, MockDatabase, MockExecResult, Transaction}; + + use super::*; + + #[tokio::test] + async fn test_delete() { + // Create MockDatabase with mock query results + let db = MockDatabase::new(DatabaseBackend::Postgres) + .append_exec_results([MockExecResult { + rows_affected: 1, + ..Default::default() + }]) + .into_connection(); + + delete(&db, "id").await.unwrap(); + // Checking transaction log + assert_eq!( + db.into_transaction_log(), + [Transaction::from_sql_and_values( + DatabaseBackend::Postgres, + r#"DELETE FROM "kubernetes_auth_role" WHERE "kubernetes_auth_role"."id" = $1"#, + ["id".into()] + ),] + ); + } +} diff --git a/crates/keystone/src/k8s_auth/backend/sql/k8s_auth_role/get.rs b/crates/keystone/src/k8s_auth/backend/sql/k8s_auth_role/get.rs new file mode 100644 index 00000000..512857df --- /dev/null +++ b/crates/keystone/src/k8s_auth/backend/sql/k8s_auth_role/get.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 +//! Get existing k8s auth configuration. +use sea_orm::DatabaseConnection; +use sea_orm::entity::*; + +use crate::db::entity::prelude::KubernetesAuthRole; +use crate::error::DbContextExt; +use crate::k8s_auth::{backend::error::K8sAuthDatabaseError, types::K8sAuthRole}; + +/// Get existing k8s auth configuration by the ID. +pub async fn get>( + db: &DatabaseConnection, + id: S, +) -> Result, K8sAuthDatabaseError> { + Ok(KubernetesAuthRole::find_by_id(id.as_ref()) + .one(db) + .await + .context("reading kubernetes auth configuration record")? + .map(Into::into)) +} + +#[cfg(test)] +mod tests { + use sea_orm::{DatabaseBackend, MockDatabase, Transaction}; + + use super::super::tests::get_k8s_auth_role_mock; + use super::*; + + #[tokio::test] + async fn test_get() { + // Create MockDatabase with mock query results + let db = MockDatabase::new(DatabaseBackend::Postgres) + .append_query_results([vec![get_k8s_auth_role_mock("id")]]) + .into_connection(); + + assert_eq!( + get(&db, "id").await.unwrap(), + Some(get_k8s_auth_role_mock("id").try_into().unwrap()) + ); + + // Checking transaction log + assert_eq!( + db.into_transaction_log(), + [Transaction::from_sql_and_values( + DatabaseBackend::Postgres, + r#"SELECT "kubernetes_auth_role"."auth_configuration_id", "kubernetes_auth_role"."bound_audience", "kubernetes_auth_role"."bound_service_account_names", "kubernetes_auth_role"."bound_service_account_namespaces", "kubernetes_auth_role"."domain_id", "kubernetes_auth_role"."id", "kubernetes_auth_role"."enabled", "kubernetes_auth_role"."name", "kubernetes_auth_role"."token_restriction_id" FROM "kubernetes_auth_role" WHERE "kubernetes_auth_role"."id" = $1 LIMIT $2"#, + ["id".into(), 1u64.into()] + ),] + ); + } +} diff --git a/crates/keystone/src/k8s_auth/backend/sql/k8s_auth_role/list.rs b/crates/keystone/src/k8s_auth/backend/sql/k8s_auth_role/list.rs new file mode 100644 index 00000000..f67541f8 --- /dev/null +++ b/crates/keystone/src/k8s_auth/backend/sql/k8s_auth_role/list.rs @@ -0,0 +1,146 @@ +// 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 +//! List K8s auth configurations + +use sea_orm::DatabaseConnection; +use sea_orm::entity::*; +use sea_orm::query::*; +use sea_orm::{Cursor, SelectModel}; + +use crate::db::entity::kubernetes_auth_role; +use crate::db::entity::prelude::KubernetesAuthRole; +use crate::error::DbContextExt; +use crate::k8s_auth::{ + backend::error::K8sAuthDatabaseError, + types::{K8sAuthRole, K8sAuthRoleListParameters}, +}; + +/// Prepare the query for listing k8s auth roles. +fn get_list_query( + params: &K8sAuthRoleListParameters, +) -> Result>, K8sAuthDatabaseError> { + let mut select = KubernetesAuthRole::find(); + if let Some(val) = ¶ms.auth_configuration_id { + select = select.filter(kubernetes_auth_role::Column::AuthConfigurationId.eq(val)); + } + if let Some(val) = ¶ms.domain_id { + select = select.filter(kubernetes_auth_role::Column::DomainId.eq(val)); + } + if let Some(val) = ¶ms.name { + select = select.filter(kubernetes_auth_role::Column::Name.eq(val)); + } + + Ok(select.cursor_by(kubernetes_auth_role::Column::Id)) +} + +/// List K8s auth roles. +pub async fn list( + db: &DatabaseConnection, + params: &K8sAuthRoleListParameters, +) -> Result, K8sAuthDatabaseError> { + Ok(get_list_query(params)? + .all(db) + .await + .context("listing k8s auth roles")? + .into_iter() + .map(Into::into) + .collect()) +} + +#[cfg(test)] +mod tests { + use sea_orm::{DatabaseBackend, MockDatabase, QueryOrder, Transaction, sea_query::*}; + + use super::super::tests::get_k8s_auth_role_mock; + use super::*; + + #[tokio::test] + async fn test_query_all() { + assert_eq!( + r#"SELECT "kubernetes_auth_role"."auth_configuration_id", "kubernetes_auth_role"."bound_audience", "kubernetes_auth_role"."bound_service_account_names", "kubernetes_auth_role"."bound_service_account_namespaces", "kubernetes_auth_role"."domain_id", "kubernetes_auth_role"."id", "kubernetes_auth_role"."enabled", "kubernetes_auth_role"."name", "kubernetes_auth_role"."token_restriction_id" FROM "kubernetes_auth_role""#, + QueryOrder::query(&mut get_list_query(&K8sAuthRoleListParameters::default()).unwrap()) + .to_string(PostgresQueryBuilder) + ); + } + + #[tokio::test] + async fn test_query_name() { + assert!( + QueryOrder::query( + &mut get_list_query(&K8sAuthRoleListParameters { + name: Some("name".into()), + ..Default::default() + }) + .unwrap() + ) + .to_string(PostgresQueryBuilder) + .contains("\"kubernetes_auth_role\".\"name\" = 'name'") + ); + } + + #[tokio::test] + async fn test_query_domain_id() { + let query = QueryOrder::query( + &mut get_list_query(&K8sAuthRoleListParameters { + domain_id: Some("d1".into()), + ..Default::default() + }) + .unwrap(), + ) + .to_string(PostgresQueryBuilder); + assert!(query.contains("\"kubernetes_auth_role\".\"domain_id\" = 'd1'")); + } + + #[tokio::test] + async fn test_query_configuration_id() { + let query = QueryOrder::query( + &mut get_list_query(&K8sAuthRoleListParameters { + auth_configuration_id: Some("cid".into()), + ..Default::default() + }) + .unwrap(), + ) + .to_string(PostgresQueryBuilder); + assert!(query.contains("\"kubernetes_auth_role\".\"auth_configuration_id\" = 'cid'")); + } + + #[tokio::test] + async fn test_list_no_params() { + // Create MockDatabase with mock query results + let db = MockDatabase::new(DatabaseBackend::Postgres) + .append_query_results([vec![ + get_k8s_auth_role_mock("id1"), + get_k8s_auth_role_mock("id2"), + ]]) + .into_connection(); + + let res = list(&db, &K8sAuthRoleListParameters::default()) + .await + .unwrap(); + + assert_eq!(2, res.len()); + assert!(res.contains(&K8sAuthRole::from(get_k8s_auth_role_mock("id1")))); + assert!(res.contains(&K8sAuthRole::from(get_k8s_auth_role_mock("id2")))); + + // Checking transaction log + assert_eq!( + db.into_transaction_log(), + [Transaction::from_sql_and_values( + DatabaseBackend::Postgres, + r#"SELECT "kubernetes_auth_role"."auth_configuration_id", "kubernetes_auth_role"."bound_audience", "kubernetes_auth_role"."bound_service_account_names", "kubernetes_auth_role"."bound_service_account_namespaces", "kubernetes_auth_role"."domain_id", "kubernetes_auth_role"."id", "kubernetes_auth_role"."enabled", "kubernetes_auth_role"."name", "kubernetes_auth_role"."token_restriction_id" FROM "kubernetes_auth_role" ORDER BY "kubernetes_auth_role"."id" ASC"#, + [] + ),] + ); + } +} diff --git a/crates/keystone/src/k8s_auth/backend/sql/k8s_auth_role/update.rs b/crates/keystone/src/k8s_auth/backend/sql/k8s_auth_role/update.rs new file mode 100644 index 00000000..4c5a3e11 --- /dev/null +++ b/crates/keystone/src/k8s_auth/backend/sql/k8s_auth_role/update.rs @@ -0,0 +1,113 @@ +// 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 +//! Update the existing K8s auth configuration + +use sea_orm::DatabaseConnection; +use sea_orm::TransactionTrait; +use sea_orm::entity::*; + +use crate::db::entity::prelude::KubernetesAuthRole; +use crate::error::DbContextExt; +use crate::k8s_auth::{ + backend::error::K8sAuthDatabaseError, + types::{K8sAuthRole, K8sAuthRoleUpdate}, +}; + +/// Update existing k8s auth role by the ID. +/// +/// Perform search and update of the k8s auth role in an isolated transaction. +pub async fn update>( + db: &DatabaseConnection, + id: S, + data: K8sAuthRoleUpdate, +) -> Result { + // Start transaction to prevent TOCTOU + let txn = db + .begin() + .await + .context("starting transaction for updating k8s auth role")?; + let res = if let Some(current) = KubernetesAuthRole::find_by_id(id.as_ref()) + .one(&txn) + .await + .context("searching for the existing k8s auth role")? + { + Ok(current + .to_active_model_update(data) + .update(&txn) + .await + .context("updating k8s auth role")? + .into()) + } else { + Err(K8sAuthDatabaseError::RoleNotFound(id.as_ref().to_string())) + }; + txn.commit() + .await + .context("committing the k8s auth role update transaction")?; + res +} + +#[cfg(test)] +mod tests { + use sea_orm::{DatabaseBackend, MockDatabase, Statement, Transaction}; + + use super::super::tests::get_k8s_auth_role_mock; + use super::*; + + #[tokio::test] + async fn test_update() { + // Create MockDatabase with mock query results + let db = MockDatabase::new(DatabaseBackend::Postgres) + .append_query_results([vec![get_k8s_auth_role_mock("id1")]]) + .append_query_results([vec![get_k8s_auth_role_mock("id1")]]) + .into_connection(); + + let req = K8sAuthRoleUpdate { + bound_audience: Some("new_aud".into()), + bound_service_account_names: Some(vec!["c".into()]), + bound_service_account_namespaces: Some(vec!["nc".into()]), + enabled: Some(true), + name: Some("new_name".into()), + token_restriction_id: Some("new_trid".into()), + }; + + update(&db, "id1", req).await.unwrap(); + + // Checking transaction log + assert_eq!( + db.into_transaction_log(), + [Transaction::many(vec![ + Statement::from_string(DatabaseBackend::Postgres, r#"BEGIN"#,), + Statement::from_sql_and_values( + DatabaseBackend::Postgres, + r#"SELECT "kubernetes_auth_role"."auth_configuration_id", "kubernetes_auth_role"."bound_audience", "kubernetes_auth_role"."bound_service_account_names", "kubernetes_auth_role"."bound_service_account_namespaces", "kubernetes_auth_role"."domain_id", "kubernetes_auth_role"."id", "kubernetes_auth_role"."enabled", "kubernetes_auth_role"."name", "kubernetes_auth_role"."token_restriction_id" FROM "kubernetes_auth_role" WHERE "kubernetes_auth_role"."id" = $1 LIMIT $2"#, + ["id1".into(), 1u64.into()] + ), + Statement::from_sql_and_values( + DatabaseBackend::Postgres, + r#"UPDATE "kubernetes_auth_role" SET "bound_audience" = $1, "bound_service_account_names" = $2, "bound_service_account_namespaces" = $3, "enabled" = $4, "name" = $5, "token_restriction_id" = $6 WHERE "kubernetes_auth_role"."id" = $7 RETURNING "auth_configuration_id", "bound_audience", "bound_service_account_names", "bound_service_account_namespaces", "domain_id", "id", "enabled", "name", "token_restriction_id""#, + [ + "new_aud".into(), + "c".into(), + "nc".into(), + true.into(), + "new_name".into(), + "new_trid".into(), + "id1".into(), + ] + ), + Statement::from_string(DatabaseBackend::Postgres, r#"COMMIT"#,) + ])] + ); + } +} diff --git a/crates/keystone/src/k8s_auth/error.rs b/crates/keystone/src/k8s_auth/error.rs new file mode 100644 index 00000000..3f1a39b7 --- /dev/null +++ b/crates/keystone/src/k8s_auth/error.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 +//! # K8s Auth error + +use thiserror::Error; + +use crate::error::DatabaseError; +use crate::k8s_auth::backend::error::K8sAuthDatabaseError; + +/// K8s auth provider error. +#[derive(Error, Debug)] +pub enum K8sAuthProviderError { + /// SQL backend error. + #[error(transparent)] + Backend { + /// The source of the error. + source: K8sAuthDatabaseError, + }, + + /// K8s auth configuration not found. + #[error("k8s configuration {0} not found")] + ConfigurationNotFound(String), + + /// Conflict. + #[error("conflict: {0}")] + Conflict(String), + + /// Database error. + #[error(transparent)] + Database(#[from] DatabaseError), + + /// K8s auth role not found. + #[error("k8s role {0} not found")] + RoleNotFound(String), + + /// Unsupported driver. + #[error("unsupported driver {0}")] + UnsupportedDriver(String), +} + +impl From for K8sAuthProviderError { + fn from(source: K8sAuthDatabaseError) -> Self { + match source { + K8sAuthDatabaseError::Database { source } => match source { + cfl @ crate::error::DatabaseError::Conflict { .. } => { + Self::Conflict(cfl.to_string()) + } + other => Self::Backend { + source: K8sAuthDatabaseError::Database { source: other }, + }, + }, + K8sAuthDatabaseError::ConfigurationNotFound(val) => Self::ConfigurationNotFound(val), + K8sAuthDatabaseError::RoleNotFound(val) => Self::RoleNotFound(val), + + _ => Self::Backend { source }, + } + } +} diff --git a/crates/keystone/src/k8s_auth/mock.rs b/crates/keystone/src/k8s_auth/mock.rs new file mode 100644 index 00000000..5e42960f --- /dev/null +++ b/crates/keystone/src/k8s_auth/mock.rs @@ -0,0 +1,106 @@ +// 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 - internal mocking tools. +use async_trait::async_trait; +#[cfg(test)] +use mockall::mock; + +use crate::config::Config; +use crate::k8s_auth::{K8sAuthApi, K8sAuthProviderError, types::*}; +use crate::plugin_manager::PluginManager; + +use crate::keystone::ServiceState; + +#[cfg(test)] +mock! { + pub K8sAuthProvider { + pub fn new(cfg: &Config, plugin_manager: &PluginManager) -> Result; + } + + #[async_trait] + impl K8sAuthApi for K8sAuthProvider { + + /// Register new K8s auth. + async fn create_k8s_auth_configuration( + &self, + state: &ServiceState, + config: K8sAuthConfigurationCreate, + ) -> Result; + + /// Register new K8s auth role. + async fn create_k8s_auth_role( + &self, + state: &ServiceState, + role: K8sAuthRoleCreate, + ) -> Result; + + /// Delete K8s auth. + async fn delete_k8s_auth_configuration<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result<(), K8sAuthProviderError>; + + /// Delete K8s auth role. + async fn delete_k8s_auth_role<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result<(), K8sAuthProviderError>; + + /// Register new K8s auth. + async fn get_k8s_auth_configuration<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result, K8sAuthProviderError>; + + /// Register new K8s auth role. + async fn get_k8s_auth_role<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result, K8sAuthProviderError>; + + /// List K8s auth configurations. + async fn list_k8s_auth_configurations( + &self, + state: &ServiceState, + params: &K8sAuthConfigurationListParameters, + ) -> Result, K8sAuthProviderError>; + + /// List K8s auth roles. + async fn list_k8s_auth_roles( + &self, + state: &ServiceState, + params: &K8sAuthRoleListParameters, + ) -> Result, K8sAuthProviderError>; + + /// Update K8s auth. + async fn update_k8s_auth_configuration<'a>( + &self, + state: &ServiceState, + id: &'a str, + data: K8sAuthConfigurationUpdate, + ) -> Result; + + /// Update K8s auth role. + async fn update_k8s_auth_role<'a>( + &self, + state: &ServiceState, + id: &'a str, + data: K8sAuthRoleUpdate, + ) -> Result; + } +} diff --git a/crates/keystone/src/k8s_auth/mod.rs b/crates/keystone/src/k8s_auth/mod.rs new file mode 100644 index 00000000..aa3a1ace --- /dev/null +++ b/crates/keystone/src/k8s_auth/mod.rs @@ -0,0 +1,274 @@ +// 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; +use std::sync::Arc; + +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::{K8sAuthBackend, sql::SqlBackend}; +// +pub use error::K8sAuthProviderError; +#[cfg(test)] +pub use mock::MockK8sAuthProvider; +pub use types::K8sAuthApi; +use types::*; +// +/// K8s Auth provider. +pub struct K8sAuthProvider { + /// Backend driver. + backend_driver: Arc, +} + +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 }) + } +} + +#[async_trait] +impl K8sAuthApi for K8sAuthProvider { + /// Register new K8s auth. + #[tracing::instrument(skip(self, state))] + async fn create_k8s_auth_configuration( + &self, + state: &ServiceState, + config: K8sAuthConfigurationCreate, + ) -> Result { + let mut new = config; + if new.id.is_none() { + new.id = Some(uuid::Uuid::new_v4().simple().to_string()); + } + self.backend_driver + .create_k8s_auth_configuration(state, new) + .await + } + + /// Register new K8s auth role. + #[tracing::instrument(skip(self, state))] + async fn create_k8s_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_k8s_auth_role(state, new).await + } + + /// Delete K8s auth. + #[tracing::instrument(skip(self, state))] + async fn delete_k8s_auth_configuration<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result<(), K8sAuthProviderError> { + self.backend_driver + .delete_k8s_auth_configuration(state, id) + .await + } + + /// Delete K8s auth role. + #[tracing::instrument(skip(self, state))] + async fn delete_k8s_auth_role<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result<(), K8sAuthProviderError> { + self.backend_driver.delete_k8s_auth_role(state, id).await + } + + /// Register new K8s auth. + #[tracing::instrument(skip(self, state))] + async fn get_k8s_auth_configuration<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result, K8sAuthProviderError> { + self.backend_driver + .get_k8s_auth_configuration(state, id) + .await + } + + /// Register new K8s auth role. + #[tracing::instrument(skip(self, state))] + async fn get_k8s_auth_role<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result, K8sAuthProviderError> { + self.backend_driver.get_k8s_auth_role(state, id).await + } + + /// List K8s auth configurations. + #[tracing::instrument(skip(self, state))] + async fn list_k8s_auth_configurations( + &self, + state: &ServiceState, + params: &K8sAuthConfigurationListParameters, + ) -> Result, K8sAuthProviderError> { + self.backend_driver + .list_k8s_auth_configurations(state, params) + .await + } + + /// List K8s auth roles. + #[tracing::instrument(skip(self, state))] + async fn list_k8s_auth_roles( + &self, + state: &ServiceState, + params: &K8sAuthRoleListParameters, + ) -> Result, K8sAuthProviderError> { + self.backend_driver.list_k8s_auth_roles(state, params).await + } + + /// Update K8s auth. + #[tracing::instrument(skip(self, state))] + async fn update_k8s_auth_configuration<'a>( + &self, + state: &ServiceState, + id: &'a str, + data: K8sAuthConfigurationUpdate, + ) -> Result { + self.backend_driver + .update_k8s_auth_configuration(state, id, data) + .await + } + + /// Update K8s auth role. + #[tracing::instrument(skip(self, state))] + async fn update_k8s_auth_role<'a>( + &self, + state: &ServiceState, + id: &'a str, + data: K8sAuthRoleUpdate, + ) -> Result { + self.backend_driver + .update_k8s_auth_role(state, id, data) + .await + } +} +// +#[cfg(test)] +mod tests { + use sea_orm::DatabaseConnection; + use std::sync::Arc; + + use super::backend::MockK8sAuthBackend; + use super::*; + use crate::config::Config; + use crate::keystone::Service; + use crate::policy::MockPolicyFactory; + use crate::provider::Provider; + + fn get_state_mock() -> Arc { + Arc::new( + Service::new( + Config::default(), + DatabaseConnection::Disconnected, + Provider::mocked_builder().build().unwrap(), + MockPolicyFactory::default(), + ) + .unwrap(), + ) + } + + #[tokio::test] + async fn test_create_k8s_auth_config() { + let state = get_state_mock(); + let mut backend = MockK8sAuthBackend::default(); + backend + .expect_create_k8s_auth_configuration() + .returning(|_, _| Ok(K8sAuthConfiguration::default())); + let provider = K8sAuthProvider { + backend_driver: Arc::new(backend), + }; + + assert!( + provider + .create_k8s_auth_configuration( + &state, + K8sAuthConfigurationCreate { + ca_cert: Some("ca".into()), + 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_k8s_auth_role() { + let state = get_state_mock(); + let mut backend = MockK8sAuthBackend::default(); + backend + .expect_create_k8s_auth_role() + .returning(|_, _| Ok(K8sAuthRole::default())); + let provider = K8sAuthProvider { + backend_driver: Arc::new(backend), + }; + + assert!( + provider + .create_k8s_auth_role( + &state, + K8sAuthRoleCreate { + auth_configuration_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/keystone/src/k8s_auth/types.rs new file mode 100644 index 00000000..8daa6c05 --- /dev/null +++ b/crates/keystone/src/k8s_auth/types.rs @@ -0,0 +1,22 @@ +// 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 definitions. + +mod k8s_auth; +mod k8s_auth_role; +mod provider_api; + +pub use k8s_auth::*; +pub use k8s_auth_role::*; +pub use provider_api::K8sAuthApi; diff --git a/crates/keystone/src/k8s_auth/types/k8s_auth.rs b/crates/keystone/src/k8s_auth/types/k8s_auth.rs new file mode 100644 index 00000000..f7c0b1a4 --- /dev/null +++ b/crates/keystone/src/k8s_auth/types/k8s_auth.rs @@ -0,0 +1,108 @@ +// 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 configuration types. + +use derive_builder::Builder; +use serde::{Deserialize, Serialize}; + +use crate::error::BuilderError; + +/// K8s authentication configuration. +#[derive(Builder, Clone, Debug, Default, Deserialize, Serialize, PartialEq)] +#[builder(build_fn(error = "BuilderError"))] +#[builder(setter(strip_option, into))] +pub struct K8sAuthConfiguration { + /// PEM encoded CA cert for use by the TLS client used to talk with the + /// Kubernetes API. NOTE: Every line must end with a newline: \n If not set, + /// the local CA cert will be used if running in a Kubernetes pod. + #[builder(default)] + pub ca_cert: Option, + + /// Domain ID owning the K8s auth configuration. + pub domain_id: String, + + pub enabled: bool, + + /// Host must be a host string, a host:port pair, or a URL to the base of + /// the Kubernetes API server. + pub host: String, + + /// K8s auth configuration ID. + pub id: String, + + /// K8s auth name. + #[builder(default)] + pub name: Option, +} + +/// New K8s authentication configuration. +#[derive(Builder, Clone, Debug, Default, Deserialize, Serialize, PartialEq)] +#[builder(build_fn(error = "BuilderError"))] +#[builder(setter(strip_option, into))] +pub struct K8sAuthConfigurationCreate { + /// PEM encoded CA cert for use by the TLS client used to talk with the + /// Kubernetes API. NOTE: Every line must end with a newline: \n If not set, + /// the local CA cert will be used if running in a Kubernetes pod. + pub ca_cert: Option, + + /// Domain ID owning the K8s auth configuration. + pub domain_id: String, + + pub enabled: bool, + + /// Host must be a host string, a host:port pair, or a URL to the base of + /// the Kubernetes API server. + pub host: String, + + /// Optional ID for the configuration + pub id: Option, + + /// K8s auth name. + #[builder(default)] + pub name: Option, +} + +/// Update K8s authentication configuration. +#[derive(Builder, Clone, Debug, Default, Deserialize, Serialize, PartialEq)] +#[builder(build_fn(error = "BuilderError"))] +#[builder(setter(strip_option, into))] +pub struct K8sAuthConfigurationUpdate { + /// PEM encoded CA cert for use by the TLS client used to talk with the + /// Kubernetes API. NOTE: Every line must end with a newline: \n If not set, + /// the local CA cert will be used if running in a Kubernetes pod. + #[builder(default)] + pub ca_cert: Option, + + #[builder(default)] + pub enabled: Option, + + /// Host must be a host string, a host:port pair, or a URL to the base of + /// the Kubernetes API server. + #[builder(default)] + pub host: Option, + + /// K8s auth name. + #[builder(default)] + pub name: Option, +} + +/// K8s Auth configuration list parameters. +#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +#[builder(build_fn(error = "BuilderError"))] +pub struct K8sAuthConfigurationListParameters { + /// Domain id. + pub domain_id: Option, + /// Name. + pub name: Option, +} diff --git a/crates/keystone/src/k8s_auth/types/k8s_auth_role.rs b/crates/keystone/src/k8s_auth/types/k8s_auth_role.rs new file mode 100644 index 00000000..9ab13a23 --- /dev/null +++ b/crates/keystone/src/k8s_auth/types/k8s_auth_role.rs @@ -0,0 +1,132 @@ +// 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 types. + +use derive_builder::Builder; +use serde::{Deserialize, Serialize}; + +use crate::error::BuilderError; + +/// K8s authentication role. +#[derive(Builder, Clone, Debug, Default, Deserialize, Serialize, PartialEq)] +#[builder(build_fn(error = "BuilderError"))] +#[builder(setter(strip_option, into))] +pub struct K8sAuthRole { + /// ID of the K8s auth configuration this role belongs to. + pub auth_configuration_id: String, + + /// Optional Audience claim to verify in the JWT. + #[builder(default)] + pub bound_audience: Option, + + /// List of service account names able to access this role. + pub bound_service_account_names: Vec, + + /// List of namespaces allowed to access this role. + pub bound_service_account_namespaces: Vec, + + /// Domain ID owning the K8s auth role configuration. It must always match + /// the `domain_id` of the referred configuration. + pub domain_id: String, + + pub enabled: bool, + + pub id: String, + + /// K8s auth role name. + #[builder(default)] + pub name: String, + + /// A token restriction ID that is used to bind the K8s token to the + /// Keystone Identity and Authorization mapping. + pub token_restriction_id: String, +} + +/// New K8s authentication role. +#[derive(Builder, Clone, Debug, Default, Deserialize, Serialize, PartialEq)] +#[builder(build_fn(error = "BuilderError"))] +#[builder(setter(strip_option, into))] +pub struct K8sAuthRoleCreate { + /// ID of the K8s auth configuration this role belongs to. + pub auth_configuration_id: String, + + /// Optional Audience claim to verify in the JWT. + #[builder(default)] + pub bound_audience: Option, + + /// List of service account names able to access this role. + pub bound_service_account_names: Vec, + + /// List of namespaces allowed to access this role. + pub bound_service_account_namespaces: Vec, + + /// Domain ID owning the K8s auth role configuration. It must always match + /// the `domain_id` of the referred configuration. + pub domain_id: String, + + pub enabled: bool, + + /// Optional ID. + #[builder(default)] + pub id: Option, + + /// K8s auth role name. + pub name: String, + + /// A token restriction ID that is used to bind the K8s token to the + /// Keystone Identity and Authorization mapping. + pub token_restriction_id: String, +} + +/// Update K8s authentication role. +#[derive(Builder, Clone, Debug, Default, Deserialize, Serialize, PartialEq)] +#[builder(build_fn(error = "BuilderError"))] +#[builder(setter(strip_option, into))] +pub struct K8sAuthRoleUpdate { + /// Optional Audience claim to verify in the JWT. + #[builder(default)] + pub bound_audience: Option, + + /// List of service account names able to access this role. + #[builder(default)] + pub bound_service_account_names: Option>, + + /// List of namespaces allowed to access this role. + #[builder(default)] + pub bound_service_account_namespaces: Option>, + + #[builder(default)] + pub enabled: Option, + + /// K8s auth role name. + #[builder(default)] + pub name: Option, + + /// A token restriction ID that is used to bind the K8s token to the + /// Keystone Identity and Authorization mapping. + #[builder(default)] + pub token_restriction_id: Option, +} + +/// K8s Auth role list parameters. +#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +#[builder(build_fn(error = "BuilderError"))] +pub struct K8sAuthRoleListParameters { + /// K8s auth configuration id. + pub auth_configuration_id: Option, + /// Domain id. + pub domain_id: Option, + /// Name. + pub name: Option, +} diff --git a/crates/keystone/src/k8s_auth/types/provider_api.rs b/crates/keystone/src/k8s_auth/types/provider_api.rs new file mode 100644 index 00000000..44e5f13f --- /dev/null +++ b/crates/keystone/src/k8s_auth/types/provider_api.rs @@ -0,0 +1,95 @@ +// 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 interface. + +use async_trait::async_trait; + +use crate::k8s_auth::{K8sAuthProviderError, types::*}; +use crate::keystone::ServiceState; + +/// The trait for managing the K8s_auth functionality. +#[async_trait] +pub trait K8sAuthApi: Send + Sync { + /// Register new K8s auth. + async fn create_k8s_auth_configuration( + &self, + state: &ServiceState, + config: K8sAuthConfigurationCreate, + ) -> Result; + + /// Register new K8s auth role. + async fn create_k8s_auth_role( + &self, + state: &ServiceState, + role: K8sAuthRoleCreate, + ) -> Result; + + /// Delete K8s auth. + async fn delete_k8s_auth_configuration<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result<(), K8sAuthProviderError>; + + /// Delete K8s auth role. + async fn delete_k8s_auth_role<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result<(), K8sAuthProviderError>; + + /// Register new K8s auth. + async fn get_k8s_auth_configuration<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result, K8sAuthProviderError>; + + /// Register new K8s auth role. + async fn get_k8s_auth_role<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result, K8sAuthProviderError>; + + /// List K8s auth configurations. + async fn list_k8s_auth_configurations( + &self, + state: &ServiceState, + params: &K8sAuthConfigurationListParameters, + ) -> Result, K8sAuthProviderError>; + + /// List K8s auth roles. + async fn list_k8s_auth_roles( + &self, + state: &ServiceState, + params: &K8sAuthRoleListParameters, + ) -> Result, K8sAuthProviderError>; + + /// Update K8s auth. + async fn update_k8s_auth_configuration<'a>( + &self, + state: &ServiceState, + id: &'a str, + data: K8sAuthConfigurationUpdate, + ) -> Result; + + /// Update K8s auth role. + async fn update_k8s_auth_role<'a>( + &self, + state: &ServiceState, + id: &'a str, + data: K8sAuthRoleUpdate, + ) -> Result; +} diff --git a/crates/keystone/src/lib.rs b/crates/keystone/src/lib.rs index 4ebc23df..a63dfea2 100644 --- a/crates/keystone/src/lib.rs +++ b/crates/keystone/src/lib.rs @@ -85,6 +85,7 @@ 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; diff --git a/crates/keystone/src/plugin_manager.rs b/crates/keystone/src/plugin_manager.rs index f9e70d32..85d8abf9 100644 --- a/crates/keystone/src/plugin_manager.rs +++ b/crates/keystone/src/plugin_manager.rs @@ -29,6 +29,7 @@ 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; @@ -50,6 +51,8 @@ pub struct PluginManager { identity_backends: HashMap>, /// Identity mapping backend plugins. identity_mapping_backends: HashMap>, + /// K8s auth backend plugins. + k8s_auth_backends: HashMap>, /// Resource backend plugins. resource_backends: HashMap>, /// Revoke backend plugins. @@ -122,6 +125,12 @@ impl PluginManager { self.identity_mapping_backends.get(name.as_ref()) } + /// 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()) + } + /// Get registered resource backend. #[allow(clippy::borrowed_box)] pub fn get_resource_backend>( diff --git a/crates/keystone/src/provider.rs b/crates/keystone/src/provider.rs index a97db6aa..c3dd1524 100644 --- a/crates/keystone/src/provider.rs +++ b/crates/keystone/src/provider.rs @@ -40,6 +40,9 @@ 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] @@ -77,6 +80,8 @@ pub struct Provider { identity: IdentityProvider, /// Identity mapping provider. identity_mapping: IdentityMappingProvider, + /// K8s auth provider. + k8s_auth: K8sAuthProvider, /// Resource provider. resource: ResourceProvider, /// Revoke provider. @@ -98,6 +103,7 @@ impl Provider { 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)?; @@ -112,6 +118,7 @@ impl 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, @@ -150,6 +157,11 @@ impl Provider { &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 @@ -187,6 +199,7 @@ impl Provider { 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(); @@ -201,6 +214,7 @@ impl Provider { .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) diff --git a/crates/keystone/src/token/token_restriction/create.rs b/crates/keystone/src/token/token_restriction/create.rs index 4ba77f8a..e96c8cd1 100644 --- a/crates/keystone/src/token/token_restriction/create.rs +++ b/crates/keystone/src/token/token_restriction/create.rs @@ -49,7 +49,7 @@ pub async fn create( let txn = db.begin().await.context("starting the transaction")?; let db_entry: token_restriction::Model = entry - .insert(db) + .insert(&txn) .await .context("creating token restriction")?; @@ -62,7 +62,7 @@ pub async fn create( } }), ) - .exec(db) + .exec(&txn) .await .context("persisting token restriction role association")?; } diff --git a/doc/src/SUMMARY.md b/doc/src/SUMMARY.md index e7f9f317..e20cce35 100644 --- a/doc/src/SUMMARY.md +++ b/doc/src/SUMMARY.md @@ -24,6 +24,7 @@ - [PCI-DSS: Account Password Expiration](adr/0012-pci-dss-account-password-expiry.md) - [Federation OIDC: Expiring Group Membership](adr/0013-federation-oidc-expiring-group-membership.md) - [Application Credentials](adr/0014-application-credentials.md) + - [Kubernetes Auth](adr/0015-kubernetes-auth.md) - [Policy enforcement](policy.md) - [Fernet token]() - [Token payloads]() diff --git a/doc/src/adr/0015-kubernetes-auth.md b/doc/src/adr/0015-kubernetes-auth.md new file mode 100644 index 00000000..4b18059a --- /dev/null +++ b/doc/src/adr/0015-kubernetes-auth.md @@ -0,0 +1,144 @@ +# 15. Kubernetes Authentication Mechanism for Keystone + +Date: 2026-02-17 + +## Status + +Accepted + +## Context + +Currently, Keystone supports various authentication mechanisms (Password, Token, +TOTP, External, etc.). As OpenStack increasingly runs alongside or underneath +Kubernetes workloads, there is a need for "machine authentication" where a +Kubernetes Pod can exchange its **Service Account Token (JWT)** for a Keystone +token without managing long-lived secrets like passwords or API keys. + +This implementation follows the logic used by OpenBao: + +1. **Trust Establishment:** Keystone is configured to trust a Kubernetes API + server's JWT issuer. +2. **Role Mapping:** A Kubernetes Service Account (and namespace) is mapped to a + specific Keystone Project/Role. +3. **Validation:** Keystone validates the incoming JWT against the Kubernetes + TokenReview API. + +## Decision + +We will implement a new `kubernetes` auth method in Keystone. This requires +persistent storage to manage multiple Kubernetes clusters (backends) and the +mapping of Kubernetes identities to OpenStack identities. + +### 1. Data Model Changes + +Two new tables will be introduced to the Keystone schema. + +#### Table: `kubernetes_auth` + +This table stores the configuration for connecting to and validating tokens from +external Kubernetes clusters. + +| Column | Type | Description | +| -------------------- | ----------- | --------------------------------------------------------------------- | +| `id` | String(64) | Primary Key (UUID). | +| `domain_id` | String(64) | Domain ID (UUID). | +| `enabled` | Boolean | Enabled flag. | +| `name` | String(255) | Unique name for this K8s backend configuration. | +| `host` | String(255) | The URL of the Kubernetes API server (e.g., `https://10.0.0.1:6443`). | +| `token_reviewer_jwt` | Text | A long-lived JWT used by Keystone to access the K8s TokenReview API. | +| `ca_cert` | Text | PEM encoded CA cert for the K8s API (optional for self-signed). | + +#### Table: `kubernetes_auth_role` + +This table maps Kubernetes-specific attributes (Namespace/ServiceAccount) to +Keystone-specific token restriction (User/Project/Roles). + +| Column | Type | Description | +| ---------------------------------- | ----------- | ----------------------------------------------------- | +| `id` | String(64) | Primary Key (UUID). | +| `kubernetes_id` | String(64) | Foreign Key to `kubernetes_auth.id`. | +| `enabled` | Boolean | Enabled flag. | +| `token_restriction_id` | String(64) | Foreign Key to `token_restriction.id`. | +| `bound_service_account_names` | Text | List of allowed SAs (comma-separated or JSON). | +| `bound_service_account_namespaces` | Text | List of allowed Namespaces (comma-separated or JSON). | +| `bound_audience` | String(128) | Optional Audience claim to verify in the JWT. | + +Token Restrictions represent here a finite mapping of the `user_id` (which +should point to the service account user and MUST be set), the target project +(based on the `project_id`) and the corresponding roles granted on this scope. +As such it is not required to grant the user roles on the project directly and +instead only specify them in the token restriction mapping. + +--- + +### 2. Required API + +#### Administrative API (CRUD for Configuration) + +Admin-only endpoints to manage the trust relationship. + +- **POST** `/v4/k8s_auth/`: Register a new Kubernetes cluster. +- **GET/PATCH/DELETE** `/v4/k8s_auth/{cluster_id}`: Manage cluster config. +- **POST** `/v4/k8s_auth/{cluster_id}/roles/role`: Create a mapping between a + K8s SA/Namespace and a Keystone Project. +- **GET/PATCH/DELETE** `/v4/k8s_auth/{cluster_id}/roles/{role_name}`: Manage + role mappings. +- **POST** `/v4/k8s_auth/{cluster_id}/auth`: Exchange K8s SA token for Keystone + token. + +#### Authentication API (The "Login" Flow) + +The new authentication endpoint is exposed under +`/v4/k8s_auth/{cluster_id}/auth` and expects a json payload with a **POST** +method. + +**Request Payload:** + +```json +{ + "k8s_role": "web-servers-role", + "jwt": "" +} +``` + +--- + +### 3. Authentication Workflow + +1. **Lookup:** Keystone receives the request, identifies the `role`. It fetches + the associated `kubernetes_auth` config. +2. **Verification:** Keystone calls the Kubernetes API (`host`) at the + `/apis/authentication.k8s.io/v1/tokenreviews` endpoint using the + `token_reviewer_jwt` or the user specified `jwt` when `token_reviewer_jwt` is + unset. In the later case it is required that the service account has the + `system:auth-delegator` ClusterRole. It can be granted with + +``` +kubectl create clusterrolebinding client-auth-delegator \ + --clusterrole=system:auth-delegator \ + --group=group1 \ + --serviceaccount=default:svcaccount1 ... +``` + +3. **Validation:** + +- K8s returns the status of the JWT. +- Keystone verifies that the `kubernetes.io/serviceaccount/service-account.name` + and `namespace` claims match the `bound_service_account_names` and + `namespaces` in the `kubernetes_auth_role` table. + +4. **Token Issuance:** If valid, Keystone issues a scoped token for the + `token_restriction_id` defined in the role mapping. + +## Consequences + +- **Pros:** + - Enables seamless "Secretless" authentication for workloads running on + Kubernetes. + - Matches industry standards set by OpenBao/Vault. + - Supports multi-tenancy by allowing multiple Kubernetes clusters to connect + to one Keystone. + +- **Cons:** + - Keystone must have network line-of-sight to the Kubernetes API server. + - Adds complexity to the identity backend. diff --git a/doc/src/federation/intro.md b/doc/src/federation/intro.md index cb29e6b0..88974847 100644 --- a/doc/src/federation/intro.md +++ b/doc/src/federation/intro.md @@ -56,22 +56,22 @@ A series of brand new API endpoints have been added to the Keystone API. Following tables are added: -- federated_identity_provider +- `federated_identity_provider` ```rust -{{#rustdoc_include ../../../src/db/entity/federated_identity_provider.rs:15:30}} +{{#rustdoc_include ../../../crates/keystone/src/db/entity/federated_identity_provider.rs:15:30}} ``` -- federated_mapping +- `federated_mapping` ```rust -{{#include ../../../src/db/entity/federated_mapping.rs:15:32}} +{{#include ../../../crates/keystone/src/db/entity/federated_mapping.rs:15:32}} ``` -- federated_auth_state +- `federated_auth_state` ```rust -{{#include ../../../src/db/entity/federated_auth_state.rs:8:16}} +{{#include ../../../crates/keystone/src/db/entity/federated_auth_state.rs:8:16}} ``` ## Compatibility notes @@ -79,11 +79,11 @@ Following tables are added: Since the federation is implemented very differently to how it was done before it certain compatibility steps are implemented: -- Identity provider is "mirrored" into the existing identity_provider with the +- Identity provider is "mirrored" into the existing `identity_provider` with the subset of attributes - For every identity provider "oidc" and "jwt" protocol entries in the - federation_protocol table is created pointing to the "\<\\>" mapping. + `federation_protocol` table is created pointing to the "\<\\>" mapping. ## Testing diff --git a/doc/src/passkey.md b/doc/src/passkey.md index f6f69802..3365aa05 100644 --- a/doc/src/passkey.md +++ b/doc/src/passkey.md @@ -31,29 +31,29 @@ sequenceDiagram Few dedicated API resources are added controlling the necessary aspects: -- /users/{user_id}/passkeys/register_start (initialize registering of the +- `/users/{user_id}/passkeys/register_start` (initialize registering of the security device of the user) -- /users/{user_id}/passkeys/register_finish (complete the security key +- `/users/{user_id}/passkeys/register_finish` (complete the security key registration) -- /users/{user_id}/passkeys/login_start (initialize login of the security device +- `/users/{user_id}/passkeys/login_start` (initialize login of the security device of the user) -- /users/{user_id}/passkeys/login_finish (complete the security key login) +- `/users/{user_id}/passkeys/login_finish` (complete the security key login) ## DB changes Following DB tables are added: -- webauthn_credential +- `webauthn_credential` ```rust -{{#include ../../src/db/entity/webauthn_credential.rs:9:17}} +{{#include ../../crates/keystone/src/db/entity/webauthn_credential.rs:9:17}} ``` -- webauthn_state +- `webauthn_state` -```rust -{{#include ../../src/db/entity/webauthn_state.rs:9:12}} +````rust +{{#include ../../crates/keystone/src/db/entity/webauthn_state.rs:9:12}} ``` diff --git a/tests/integration/src/common.rs b/tests/integration/src/common.rs index 2d1e077e..4329ea84 100644 --- a/tests/integration/src/common.rs +++ b/tests/integration/src/common.rs @@ -13,12 +13,16 @@ // SPDX-License-Identifier: Apache-2.0 // +use std::future::Future; +use std::ops::Deref; +use std::pin::Pin; +use std::sync::Arc; + use eyre::{Result, WrapErr}; use sea_orm::{ ConnectOptions, ConnectionTrait, Database, DatabaseConnection, DbConn, EntityTrait, entity::*, schema::Schema, sea_query::*, }; -use std::sync::Arc; use uuid::Uuid; use openstack_keystone::db::entity::prelude::*; @@ -129,6 +133,11 @@ pub async fn setup_schema(db: &DbConn) -> Result<()> { create_table(db, &schema, Trust).await?; create_table(db, &schema, TrustRole).await?; + create_table(db, &schema, TokenRestriction).await?; + create_table(db, &schema, TokenRestrictionRoleAssociation).await?; + create_table(db, &schema, KubernetesAuth).await?; + create_table(db, &schema, KubernetesAuthRole).await?; + Ok(()) } @@ -215,6 +224,67 @@ pub async fn get_isolated_database() -> Result { Ok(db) } +/// Trait to allow State to delete various resource types T +pub trait ResourceDeleter: Send + Sync + 'static { + fn delete(&self, resource: T) -> Pin + Send + '_>>; +} + +pub struct AsyncResourceGuard +where + T: Clone + Send + Sync + 'static, + S: ResourceDeleter + Clone + Send + Sync + 'static, +{ + pub resource: T, + pub state: S, +} + +impl AsyncResourceGuard +where + T: Clone + Send + Sync + 'static, + S: ResourceDeleter + Clone + Send + Sync + 'static, +{ + pub fn new(resource: T, state: S) -> Self { + Self { resource, state } + } + + /// Use this at the end of a test if you want to WAIT for cleanup + /// instead of letting it happen in the background. + #[allow(unused)] + pub async fn cleanup(self) { + let state = self.state.clone(); + let res = self.resource.clone(); + state.delete(res).await; + std::mem::forget(self); + } +} + +impl Drop for AsyncResourceGuard +where + T: Clone + Send + Sync + 'static, + S: ResourceDeleter + Clone + Send + Sync + 'static, +{ + fn drop(&mut self) { + let state = self.state.clone(); + let res = self.resource.clone(); + + // Safety net for test panics + tokio::spawn(async move { + state.delete(res).await; + }); + } +} + +impl Deref for AsyncResourceGuard +where + T: Clone + Send + Sync + 'static, + S: ResourceDeleter + Clone + Send + Sync + 'static, +{ + type Target = T; + fn deref(&self) -> &Self::Target { + &self.resource + } +} + pub async fn create_user>( state: &Arc, user_id: Option, diff --git a/tests/integration/src/integration.rs b/tests/integration/src/integration.rs index 89be6220..0f4e41b5 100644 --- a/tests/integration/src/integration.rs +++ b/tests/integration/src/integration.rs @@ -14,9 +14,14 @@ //! # Integration tests //! //! Test the functionality on the provider level (not through the API). + mod application_credential; mod assignment; mod common; mod identity; +mod k8s_auth; mod role; mod token; + +#[macro_use] +mod macros; diff --git a/tests/integration/src/k8s_auth.rs b/tests/integration/src/k8s_auth.rs new file mode 100644 index 00000000..295a63ce --- /dev/null +++ b/tests/integration/src/k8s_auth.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 std::sync::Arc; + +use eyre::Report; +use sea_orm::entity::*; + +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::PolicyFactory; +use openstack_keystone::provider::Provider; + +use crate::common::{bootstrap, get_isolated_database}; + +mod config; +mod role; + +async fn get_state() -> Result, Report> { + let db = get_isolated_database().await?; + + bootstrap(&db).await?; + Project::insert_many([project::ActiveModel { + is_domain: Set(true), + id: Set("domain_a".into()), + name: Set("domain_a".into()), + extra: NotSet, + description: NotSet, + enabled: Set(Some(true)), + domain_id: Set("<>".into()), + parent_id: NotSet, + }]) + .exec(&db) + .await?; + + let cfg: Config = Config::default(); + + let plugin_manager = PluginManager::default(); + let provider = Provider::new(cfg.clone(), plugin_manager)?; + Ok(Arc::new(Service::new( + cfg, + db, + provider, + PolicyFactory::default(), + )?)) +} diff --git a/tests/integration/src/k8s_auth/config.rs b/tests/integration/src/k8s_auth/config.rs new file mode 100644 index 00000000..fc221b57 --- /dev/null +++ b/tests/integration/src/k8s_auth/config.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 std::pin::Pin; +use std::sync::Arc; + +use eyre::Result; + +use crate::common::*; +use crate::impl_deleter; + +use openstack_keystone::k8s_auth::{K8sAuthApi, types::*}; +use openstack_keystone::keystone::Service; +use openstack_keystone::keystone::ServiceState; + +mod create; +mod delete; +mod get; +mod list; +mod update; + +impl_deleter!( + Service, + K8sAuthConfiguration, + get_k8s_auth_provider, + delete_k8s_auth_configuration +); + +pub async fn create_k8s_auth_configuration( + state: &ServiceState, + data: K8sAuthConfigurationCreate, +) -> Result> { + let res = state + .provider + .get_k8s_auth_provider() + .create_k8s_auth_configuration(state, data) + .await + .unwrap(); + Ok(AsyncResourceGuard::new(res, state.clone())) +} diff --git a/tests/integration/src/k8s_auth/config/create.rs b/tests/integration/src/k8s_auth/config/create.rs new file mode 100644 index 00000000..16c1488d --- /dev/null +++ b/tests/integration/src/k8s_auth/config/create.rs @@ -0,0 +1,52 @@ +// 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 k8s auth config. + +use eyre::Result; +use tracing_test::traced_test; + +use openstack_keystone::k8s_auth::{K8sAuthApi, types::*}; + +use super::super::get_state; + +#[traced_test] +#[tokio::test] +async fn test_create() -> Result<()> { + let state = get_state().await?; + let sot = K8sAuthConfigurationCreate { + ca_cert: Some("ca".into()), + domain_id: "domain_a".into(), + enabled: true, + host: "host".into(), + id: Some(uuid::Uuid::new_v4().simple().to_string()), + name: Some(uuid::Uuid::new_v4().to_string()), + }; + let res = state + .provider + .get_k8s_auth_provider() + .create_k8s_auth_configuration(&state, sot.clone()) + .await?; + assert_eq!(sot.name, res.name); + assert_eq!(sot.id.unwrap(), res.id); + assert_eq!(sot.ca_cert, res.ca_cert); + assert_eq!(sot.enabled, res.enabled); + assert_eq!(sot.host, res.host); + + state + .provider + .get_k8s_auth_provider() + .delete_k8s_auth_configuration(&state, &res.id) + .await?; + Ok(()) +} diff --git a/tests/integration/src/k8s_auth/config/delete.rs b/tests/integration/src/k8s_auth/config/delete.rs new file mode 100644 index 00000000..2662ca4a --- /dev/null +++ b/tests/integration/src/k8s_auth/config/delete.rs @@ -0,0 +1,49 @@ +// 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 k8s auth config. + +use eyre::Result; +use tracing_test::traced_test; + +use openstack_keystone::k8s_auth::{K8sAuthApi, types::*}; + +use super::super::get_state; + +#[traced_test] +#[tokio::test] +async fn test_delete() -> Result<()> { + let state = get_state().await?; + let res = state + .provider + .get_k8s_auth_provider() + .create_k8s_auth_configuration( + &state, + K8sAuthConfigurationCreate { + ca_cert: Some("ca".into()), + domain_id: "domain_a".into(), + enabled: true, + host: "host".into(), + id: Some(uuid::Uuid::new_v4().simple().to_string()), + name: Some(uuid::Uuid::new_v4().to_string()), + }, + ) + .await?; + + let res = state + .provider + .get_k8s_auth_provider() + .delete_k8s_auth_configuration(&state, &res.id) + .await?; + Ok(()) +} diff --git a/tests/integration/src/k8s_auth/config/get.rs b/tests/integration/src/k8s_auth/config/get.rs new file mode 100644 index 00000000..a21bceef --- /dev/null +++ b/tests/integration/src/k8s_auth/config/get.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 +//! Test k8s auth config. + +use eyre::Result; +use tracing_test::traced_test; + +use openstack_keystone::k8s_auth::{K8sAuthApi, types::*}; + +use super::super::get_state; +use super::create_k8s_auth_configuration; + +#[traced_test] +#[tokio::test] +async fn test_get_config() -> Result<()> { + let state = get_state().await?; + + let k8s_conf = create_k8s_auth_configuration( + &state, + K8sAuthConfigurationCreate { + ca_cert: Some("ca".into()), + domain_id: "domain_a".into(), + enabled: true, + host: "host".into(), + id: None, + name: Some(uuid::Uuid::new_v4().to_string()), + }, + ) + .await?; + + let res = state + .provider + .get_k8s_auth_provider() + .get_k8s_auth_configuration(&state, &k8s_conf.id) + .await? + .expect("config should be there"); + assert_eq!(res.id, k8s_conf.id); + assert_eq!(res.name, k8s_conf.name); + assert_eq!(res.ca_cert, k8s_conf.ca_cert); + assert_eq!(res.host, k8s_conf.host); + assert_eq!(res.enabled, k8s_conf.enabled); + Ok(()) +} + +#[traced_test] +#[tokio::test] +async fn test_get_config_missing() -> Result<()> { + let state = get_state().await?; + + assert!( + state + .provider + .get_k8s_auth_provider() + .get_k8s_auth_configuration(&state, &uuid::Uuid::new_v4().to_string()) + .await? + .is_none() + ); + Ok(()) +} diff --git a/tests/integration/src/k8s_auth/config/list.rs b/tests/integration/src/k8s_auth/config/list.rs new file mode 100644 index 00000000..023dc612 --- /dev/null +++ b/tests/integration/src/k8s_auth/config/list.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 +//! Test k8s auth config. + +use eyre::Result; +use tracing_test::traced_test; + +use openstack_keystone::k8s_auth::{K8sAuthApi, types::*}; + +use super::super::get_state; +use super::create_k8s_auth_configuration; + +#[traced_test] +#[tokio::test] +async fn test_list() -> Result<()> { + let state = get_state().await?; + + let k8s_conf = create_k8s_auth_configuration( + &state, + K8sAuthConfigurationCreate { + ca_cert: Some("ca".into()), + domain_id: "domain_a".into(), + enabled: true, + host: "host".into(), + id: None, + name: Some(uuid::Uuid::new_v4().to_string()), + }, + ) + .await?; + let k8s_conf2 = create_k8s_auth_configuration( + &state, + K8sAuthConfigurationCreate { + ca_cert: Some("ca".into()), + domain_id: "domain_a".into(), + enabled: true, + host: "host".into(), + id: None, + name: Some(uuid::Uuid::new_v4().to_string()), + }, + ) + .await?; + + let res = state + .provider + .get_k8s_auth_provider() + .list_k8s_auth_configurations(&state, &K8sAuthConfigurationListParameters::default()) + .await?; + assert!(res.contains(&k8s_conf)); + assert!(res.contains(&k8s_conf2)); + Ok(()) +} + +#[traced_test] +#[tokio::test] +async fn test_list_name() -> Result<()> { + let state = get_state().await?; + + let k8s_conf = create_k8s_auth_configuration( + &state, + K8sAuthConfigurationCreate { + ca_cert: Some("ca".into()), + domain_id: "domain_a".into(), + enabled: true, + host: "host".into(), + id: None, + name: Some(uuid::Uuid::new_v4().to_string()), + }, + ) + .await?; + let k8s_conf2 = create_k8s_auth_configuration( + &state, + K8sAuthConfigurationCreate { + ca_cert: Some("ca".into()), + domain_id: "domain_a".into(), + enabled: true, + host: "host".into(), + id: None, + name: Some(uuid::Uuid::new_v4().to_string()), + }, + ) + .await?; + let res = state + .provider + .get_k8s_auth_provider() + .list_k8s_auth_configurations( + &state, + &K8sAuthConfigurationListParameters { + name: k8s_conf.name.clone(), + ..Default::default() + }, + ) + .await?; + assert!(res.contains(&k8s_conf)); + assert!(!res.contains(&k8s_conf2)); + Ok(()) +} + +#[traced_test] +#[tokio::test] +async fn test_list_domain() -> Result<()> { + let state = get_state().await?; + + let k8s_conf = create_k8s_auth_configuration( + &state, + K8sAuthConfigurationCreate { + ca_cert: Some("ca".into()), + domain_id: "domain_a".into(), + enabled: true, + host: "host".into(), + id: None, + name: Some(uuid::Uuid::new_v4().to_string()), + }, + ) + .await?; + let k8s_conf2 = create_k8s_auth_configuration( + &state, + K8sAuthConfigurationCreate { + ca_cert: Some("ca".into()), + domain_id: "domain_a".into(), + enabled: true, + host: "host".into(), + id: None, + name: Some(uuid::Uuid::new_v4().to_string()), + }, + ) + .await?; + let res = state + .provider + .get_k8s_auth_provider() + .list_k8s_auth_configurations( + &state, + &K8sAuthConfigurationListParameters { + domain_id: Some("domain_a".into()), + ..Default::default() + }, + ) + .await?; + assert!(res.contains(&k8s_conf)); + assert!(res.contains(&k8s_conf2)); + Ok(()) +} diff --git a/tests/integration/src/k8s_auth/config/update.rs b/tests/integration/src/k8s_auth/config/update.rs new file mode 100644 index 00000000..5d727332 --- /dev/null +++ b/tests/integration/src/k8s_auth/config/update.rs @@ -0,0 +1,57 @@ +// 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 k8s auth config. + +use eyre::Result; +use tracing_test::traced_test; + +use openstack_keystone::k8s_auth::{K8sAuthApi, types::*}; + +use super::super::get_state; +use super::create_k8s_auth_configuration; + +#[traced_test] +#[tokio::test] +async fn test_update() -> Result<()> { + let state = get_state().await?; + + let sot = K8sAuthConfigurationCreate { + ca_cert: Some("ca".into()), + domain_id: "domain_a".into(), + enabled: false, + host: "host".into(), + id: None, + name: Some(uuid::Uuid::new_v4().to_string()), + }; + let k8s_conf = create_k8s_auth_configuration(&state, sot.clone()).await?; + + let req = K8sAuthConfigurationUpdate { + ca_cert: Some("new_ca".into()), + enabled: Some(true), + host: Some("new_host".into()), + name: Some("new_name".into()), + }; + let res = state + .provider + .get_k8s_auth_provider() + .update_k8s_auth_configuration(&state, &k8s_conf.id, req) + .await?; + assert_eq!(k8s_conf.id, res.id); + assert_eq!(Some("new_name".into()), res.name); + assert_eq!(Some("new_ca".into()), res.ca_cert); + assert_eq!("new_host", res.host); + assert!(res.enabled); + + Ok(()) +} diff --git a/tests/integration/src/k8s_auth/role.rs b/tests/integration/src/k8s_auth/role.rs new file mode 100644 index 00000000..0e311c39 --- /dev/null +++ b/tests/integration/src/k8s_auth/role.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 std::pin::Pin; +use std::sync::Arc; + +use eyre::Result; + +use crate::common::*; +use crate::impl_deleter; + +use openstack_keystone::k8s_auth::{K8sAuthApi, types::*}; +use openstack_keystone::keystone::Service; +use openstack_keystone::keystone::ServiceState; + +mod create; +mod delete; +mod get; +mod list; +mod update; + +impl_deleter!( + Service, + K8sAuthRole, + get_k8s_auth_provider, + delete_k8s_auth_role +); + +pub async fn create_k8s_auth_role( + state: &ServiceState, + data: K8sAuthRoleCreate, +) -> Result> { + let res = state + .provider + .get_k8s_auth_provider() + .create_k8s_auth_role(state, data) + .await + .unwrap(); + Ok(AsyncResourceGuard::new(res, state.clone())) +} diff --git a/tests/integration/src/k8s_auth/role/create.rs b/tests/integration/src/k8s_auth/role/create.rs new file mode 100644 index 00000000..13a7badb --- /dev/null +++ b/tests/integration/src/k8s_auth/role/create.rs @@ -0,0 +1,94 @@ +// 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 k8s auth config. + +use eyre::Result; +use tracing_test::traced_test; + +use openstack_keystone::k8s_auth::{K8sAuthApi, types::*}; + +use super::super::config::create_k8s_auth_configuration; +use super::super::get_state; +use crate::token::token_restriction::create_token_restriction; + +#[traced_test] +#[tokio::test] +async fn test_create() -> Result<()> { + let state = get_state().await?; + + let k8s_conf = create_k8s_auth_configuration( + &state, + K8sAuthConfigurationCreate { + ca_cert: Some("ca".into()), + domain_id: "domain_a".into(), + enabled: true, + host: "host".into(), + id: None, + name: Some(uuid::Uuid::new_v4().to_string()), + }, + ) + .await?; + let tr = create_token_restriction( + &state, + openstack_keystone::token::TokenRestrictionCreate { + allow_rescope: false, + allow_renew: false, + id: String::new(), + domain_id: "domain_a".into(), + project_id: None, + role_ids: Vec::new(), + user_id: None, + }, + ) + .await?; + let sot = K8sAuthRoleCreate { + auth_configuration_id: k8s_conf.id.clone(), + 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: "domain_a".into(), + enabled: true, + id: None, + name: uuid::Uuid::new_v4().to_string(), + token_restriction_id: tr.id.clone(), + }; + let res = state + .provider + .get_k8s_auth_provider() + .create_k8s_auth_role(&state, sot.clone()) + .await?; + assert_eq!(sot.auth_configuration_id, res.auth_configuration_id); + assert_eq!(sot.bound_audience, res.bound_audience); + assert_eq!( + sot.bound_service_account_names, + res.bound_service_account_names + ); + assert_eq!( + sot.bound_service_account_namespaces, + res.bound_service_account_namespaces + ); + assert_eq!(sot.domain_id, res.domain_id); + assert_eq!(sot.enabled, res.enabled); + assert_eq!(sot.name, res.name); + assert_eq!(sot.token_restriction_id, res.token_restriction_id); + + state + .provider + .get_k8s_auth_provider() + .delete_k8s_auth_role(&state, &res.id) + .await?; + Ok(()) +} + +// TODO: token restriction must be validated to contain user_id, project_id and the roles. diff --git a/tests/integration/src/k8s_auth/role/delete.rs b/tests/integration/src/k8s_auth/role/delete.rs new file mode 100644 index 00000000..44f1bb2a --- /dev/null +++ b/tests/integration/src/k8s_auth/role/delete.rs @@ -0,0 +1,87 @@ +// 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 k8s auth config. + +use eyre::Result; +use tracing_test::traced_test; + +use openstack_keystone::k8s_auth::{K8sAuthApi, types::*}; + +use super::super::config::create_k8s_auth_configuration; +use super::super::get_state; +use crate::token::token_restriction::create_token_restriction; + +#[traced_test] +#[tokio::test] +async fn test_delete() -> Result<()> { + let state = get_state().await?; + + let k8s_conf = create_k8s_auth_configuration( + &state, + K8sAuthConfigurationCreate { + ca_cert: Some("ca".into()), + domain_id: "domain_a".into(), + enabled: true, + host: "host".into(), + id: None, + name: Some(uuid::Uuid::new_v4().to_string()), + }, + ) + .await?; + let tr = create_token_restriction( + &state, + openstack_keystone::token::TokenRestrictionCreate { + allow_rescope: false, + allow_renew: false, + id: String::new(), + domain_id: "domain_a".into(), + project_id: None, + role_ids: Vec::new(), + user_id: None, + }, + ) + .await?; + let sot = K8sAuthRoleCreate { + auth_configuration_id: k8s_conf.id.clone(), + 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: "domain_a".into(), + enabled: true, + id: None, + name: uuid::Uuid::new_v4().to_string(), + token_restriction_id: tr.id.clone(), + }; + let res = state + .provider + .get_k8s_auth_provider() + .create_k8s_auth_role(&state, sot.clone()) + .await?; + + state + .provider + .get_k8s_auth_provider() + .delete_k8s_auth_role(&state, &res.id) + .await?; + + assert!( + state + .provider + .get_k8s_auth_provider() + .get_k8s_auth_role(&state, &uuid::Uuid::new_v4().to_string()) + .await? + .is_none() + ); + Ok(()) +} diff --git a/tests/integration/src/k8s_auth/role/get.rs b/tests/integration/src/k8s_auth/role/get.rs new file mode 100644 index 00000000..975387f4 --- /dev/null +++ b/tests/integration/src/k8s_auth/role/get.rs @@ -0,0 +1,108 @@ +// 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 k8s auth config. + +use eyre::Result; +use tracing_test::traced_test; + +use openstack_keystone::k8s_auth::{K8sAuthApi, types::*}; + +use super::super::config::create_k8s_auth_configuration; +use super::super::get_state; +use super::super::role::create_k8s_auth_role; +use crate::token::token_restriction::create_token_restriction; + +#[traced_test] +#[tokio::test] +async fn test_get() -> Result<()> { + let state = get_state().await?; + + let k8s_conf = create_k8s_auth_configuration( + &state, + K8sAuthConfigurationCreate { + ca_cert: Some("ca".into()), + domain_id: "domain_a".into(), + enabled: true, + host: "host".into(), + id: None, + name: Some(uuid::Uuid::new_v4().to_string()), + }, + ) + .await?; + let tr = create_token_restriction( + &state, + openstack_keystone::token::TokenRestrictionCreate { + allow_rescope: false, + allow_renew: false, + id: String::new(), + domain_id: "domain_a".into(), + project_id: None, + role_ids: Vec::new(), + user_id: None, + }, + ) + .await?; + let sot = K8sAuthRoleCreate { + auth_configuration_id: k8s_conf.id.clone(), + 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: "domain_a".into(), + enabled: true, + id: None, + name: uuid::Uuid::new_v4().to_string(), + token_restriction_id: tr.id.clone(), + }; + let k8s_role = create_k8s_auth_role(&state, sot.clone()).await?; + + let res = state + .provider + .get_k8s_auth_provider() + .get_k8s_auth_role(&state, &k8s_role.id) + .await? + .expect("role should be present"); + + assert_eq!(sot.auth_configuration_id, res.auth_configuration_id); + assert_eq!(sot.bound_audience, res.bound_audience); + assert_eq!( + sot.bound_service_account_names, + res.bound_service_account_names + ); + assert_eq!( + sot.bound_service_account_namespaces, + res.bound_service_account_namespaces + ); + assert_eq!(sot.domain_id, res.domain_id); + assert_eq!(sot.enabled, res.enabled); + assert_eq!(k8s_role.id, res.id); + assert_eq!(sot.name, res.name); + assert_eq!(sot.token_restriction_id, res.token_restriction_id); + + Ok(()) +} + +#[traced_test] +#[tokio::test] +async fn test_get_missing() -> Result<()> { + let state = get_state().await?; + let res = state + .provider + .get_k8s_auth_provider() + .get_k8s_auth_role(&state, &uuid::Uuid::new_v4().to_string()) + .await?; + + assert!(res.is_none()); + + Ok(()) +} diff --git a/tests/integration/src/k8s_auth/role/list.rs b/tests/integration/src/k8s_auth/role/list.rs new file mode 100644 index 00000000..8488946d --- /dev/null +++ b/tests/integration/src/k8s_auth/role/list.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 +//! Test k8s auth config. + +use eyre::Result; +use tracing_test::traced_test; + +use openstack_keystone::k8s_auth::{K8sAuthApi, types::*}; + +use super::super::config::create_k8s_auth_configuration; +use super::super::get_state; +use super::super::role::create_k8s_auth_role; +use crate::token::token_restriction::create_token_restriction; + +#[traced_test] +#[tokio::test] +async fn test_list() -> Result<()> { + let state = get_state().await?; + + let k8s_conf = create_k8s_auth_configuration( + &state, + K8sAuthConfigurationCreate { + ca_cert: Some("ca".into()), + domain_id: "domain_a".into(), + enabled: true, + host: "host".into(), + id: None, + name: Some(uuid::Uuid::new_v4().to_string()), + }, + ) + .await?; + let tr = create_token_restriction( + &state, + openstack_keystone::token::TokenRestrictionCreate { + allow_rescope: false, + allow_renew: false, + id: String::new(), + domain_id: "domain_a".into(), + project_id: None, + role_ids: Vec::new(), + user_id: None, + }, + ) + .await?; + let k8s_role = create_k8s_auth_role( + &state, + K8sAuthRoleCreate { + auth_configuration_id: k8s_conf.id.clone(), + 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: "domain_a".into(), + enabled: true, + id: None, + name: uuid::Uuid::new_v4().to_string(), + token_restriction_id: tr.id.clone(), + }, + ) + .await?; + let k8s_role2 = create_k8s_auth_role( + &state, + K8sAuthRoleCreate { + auth_configuration_id: k8s_conf.id.clone(), + 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: "domain_a".into(), + enabled: true, + id: None, + name: uuid::Uuid::new_v4().to_string(), + token_restriction_id: tr.id.clone(), + }, + ) + .await?; + + let res = state + .provider + .get_k8s_auth_provider() + .list_k8s_auth_roles(&state, &K8sAuthRoleListParameters::default()) + .await?; + + assert!(res.contains(&k8s_role)); + assert!(res.contains(&k8s_role2)); + + Ok(()) +} + +#[tokio::test] +async fn test_list_name() -> Result<()> { + let state = get_state().await?; + + let k8s_conf = create_k8s_auth_configuration( + &state, + K8sAuthConfigurationCreate { + ca_cert: Some("ca".into()), + domain_id: "domain_a".into(), + enabled: true, + host: "host".into(), + id: None, + name: Some(uuid::Uuid::new_v4().to_string()), + }, + ) + .await?; + let tr = create_token_restriction( + &state, + openstack_keystone::token::TokenRestrictionCreate { + allow_rescope: false, + allow_renew: false, + id: String::new(), + domain_id: "domain_a".into(), + project_id: None, + role_ids: Vec::new(), + user_id: None, + }, + ) + .await?; + let k8s_role = create_k8s_auth_role( + &state, + K8sAuthRoleCreate { + auth_configuration_id: k8s_conf.id.clone(), + 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: "domain_a".into(), + enabled: true, + id: None, + name: uuid::Uuid::new_v4().to_string(), + token_restriction_id: tr.id.clone(), + }, + ) + .await?; + let k8s_role2 = create_k8s_auth_role( + &state, + K8sAuthRoleCreate { + auth_configuration_id: k8s_conf.id.clone(), + 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: "domain_a".into(), + enabled: true, + id: None, + name: uuid::Uuid::new_v4().to_string(), + token_restriction_id: tr.id.clone(), + }, + ) + .await?; + + let res = state + .provider + .get_k8s_auth_provider() + .list_k8s_auth_roles( + &state, + &K8sAuthRoleListParameters { + name: Some(k8s_role.name.clone()), + ..Default::default() + }, + ) + .await?; + + assert!(res.contains(&k8s_role)); + assert!(!res.contains(&k8s_role2)); + + Ok(()) +} + +#[tokio::test] +async fn test_list_config() -> Result<()> { + let state = get_state().await?; + + let k8s_conf = create_k8s_auth_configuration( + &state, + K8sAuthConfigurationCreate { + ca_cert: Some("ca".into()), + domain_id: "domain_a".into(), + enabled: true, + host: "host".into(), + id: None, + name: Some(uuid::Uuid::new_v4().to_string()), + }, + ) + .await?; + let k8s_conf2 = create_k8s_auth_configuration( + &state, + K8sAuthConfigurationCreate { + ca_cert: Some("ca".into()), + domain_id: "domain_a".into(), + enabled: true, + host: "host".into(), + id: None, + name: Some(uuid::Uuid::new_v4().to_string()), + }, + ) + .await?; + let tr = create_token_restriction( + &state, + openstack_keystone::token::TokenRestrictionCreate { + allow_rescope: false, + allow_renew: false, + id: String::new(), + domain_id: "domain_a".into(), + project_id: None, + role_ids: Vec::new(), + user_id: None, + }, + ) + .await?; + let k8s_role = create_k8s_auth_role( + &state, + K8sAuthRoleCreate { + auth_configuration_id: k8s_conf.id.clone(), + 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: "domain_a".into(), + enabled: true, + id: None, + name: uuid::Uuid::new_v4().to_string(), + token_restriction_id: tr.id.clone(), + }, + ) + .await?; + let k8s_role2 = create_k8s_auth_role( + &state, + K8sAuthRoleCreate { + auth_configuration_id: k8s_conf2.id.clone(), + 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: "domain_a".into(), + enabled: true, + id: None, + name: uuid::Uuid::new_v4().to_string(), + token_restriction_id: tr.id.clone(), + }, + ) + .await?; + + let res = state + .provider + .get_k8s_auth_provider() + .list_k8s_auth_roles( + &state, + &K8sAuthRoleListParameters { + auth_configuration_id: Some(k8s_role.auth_configuration_id.clone()), + ..Default::default() + }, + ) + .await?; + + assert!(res.contains(&k8s_role)); + assert!(!res.contains(&k8s_role2)); + + let res = state + .provider + .get_k8s_auth_provider() + .list_k8s_auth_roles( + &state, + &K8sAuthRoleListParameters { + auth_configuration_id: Some(k8s_role2.auth_configuration_id.clone()), + ..Default::default() + }, + ) + .await?; + + assert!(!res.contains(&k8s_role)); + assert!(res.contains(&k8s_role2)); + Ok(()) +} diff --git a/tests/integration/src/k8s_auth/role/update.rs b/tests/integration/src/k8s_auth/role/update.rs new file mode 100644 index 00000000..ddd1ef7a --- /dev/null +++ b/tests/integration/src/k8s_auth/role/update.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 +//! Test k8s auth config. + +use eyre::Result; +use tracing_test::traced_test; + +use openstack_keystone::k8s_auth::{K8sAuthApi, types::*}; + +use super::super::config::create_k8s_auth_configuration; +use super::super::get_state; +use super::super::role::create_k8s_auth_role; +use crate::token::token_restriction::create_token_restriction; + +#[traced_test] +#[tokio::test] +async fn test_update() -> Result<()> { + let state = get_state().await?; + + let k8s_conf = create_k8s_auth_configuration( + &state, + K8sAuthConfigurationCreate { + ca_cert: Some("ca".into()), + domain_id: "domain_a".into(), + enabled: true, + host: "host".into(), + id: None, + name: Some(uuid::Uuid::new_v4().to_string()), + }, + ) + .await?; + let tr = create_token_restriction( + &state, + openstack_keystone::token::TokenRestrictionCreate { + allow_rescope: false, + allow_renew: false, + id: String::new(), + domain_id: "domain_a".into(), + project_id: None, + role_ids: Vec::new(), + user_id: None, + }, + ) + .await?; + let k8s_role = create_k8s_auth_role( + &state, + K8sAuthRoleCreate { + auth_configuration_id: k8s_conf.id.clone(), + 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: "domain_a".into(), + enabled: false, + id: None, + name: uuid::Uuid::new_v4().to_string(), + token_restriction_id: tr.id.clone(), + }, + ) + .await?; + + let req = K8sAuthRoleUpdate { + bound_audience: Some("new_aud".into()), + bound_service_account_names: Some(vec!["c".into()]), + bound_service_account_namespaces: Some(vec!["nc".into()]), + enabled: Some(true), + name: Some("new_name".into()), + token_restriction_id: Some(tr.id.clone()), + }; + + let res = state + .provider + .get_k8s_auth_provider() + .update_k8s_auth_role(&state, &k8s_role.id, req) + .await?; + + assert_eq!(k8s_role.id, res.id); + assert_eq!(Some("new_aud"), res.bound_audience.as_deref()); + assert_eq!(vec!["c".to_string()], res.bound_service_account_names); + assert_eq!(vec!["nc".to_string()], res.bound_service_account_namespaces); + assert!(res.enabled); + assert_eq!("new_name", res.name); + assert_eq!(tr.id, res.token_restriction_id); + + Ok(()) +} diff --git a/tests/integration/src/macros.rs b/tests/integration/src/macros.rs new file mode 100644 index 00000000..361f9f10 --- /dev/null +++ b/tests/integration/src/macros.rs @@ -0,0 +1,29 @@ +// 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 + +#[macro_export] +macro_rules! impl_deleter { + ($state:ty, $resource:ty, $provider_getter:ident, $method:ident) => { + impl ResourceDeleter<$resource> for Arc<$state> { + fn delete(&self, resource: $resource) -> Pin + Send + '_>> { + Box::pin(async move { + self.provider + .$provider_getter() + .$method(self, &resource.id) + .await; + }) + } + } + }; +} diff --git a/tests/integration/src/role/create.rs b/tests/integration/src/role/create.rs index 86a790b2..6ddb8769 100644 --- a/tests/integration/src/role/create.rs +++ b/tests/integration/src/role/create.rs @@ -16,7 +16,6 @@ use eyre::Result; use uuid::Uuid; -use openstack_keystone::keystone::ServiceState; use openstack_keystone::role::{RoleApi, types::*}; use super::get_state; diff --git a/tests/integration/src/token.rs b/tests/integration/src/token.rs index 8a2da101..713533e9 100644 --- a/tests/integration/src/token.rs +++ b/tests/integration/src/token.rs @@ -11,4 +11,5 @@ // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 +pub mod token_restriction; mod validate; diff --git a/tests/integration/src/token/token_restriction.rs b/tests/integration/src/token/token_restriction.rs new file mode 100644 index 00000000..3dafa0c9 --- /dev/null +++ b/tests/integration/src/token/token_restriction.rs @@ -0,0 +1,45 @@ +// 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::pin::Pin; +use std::sync::Arc; + +use eyre::Result; + +use crate::common::*; +use crate::impl_deleter; + +use openstack_keystone::keystone::Service; +use openstack_keystone::keystone::ServiceState; +use openstack_keystone::token::{TokenApi, types::*}; + +impl_deleter!( + Service, + TokenRestriction, + get_token_provider, + delete_token_restriction +); + +pub async fn create_token_restriction( + state: &ServiceState, + data: TokenRestrictionCreate, +) -> Result> { + let res = state + .provider + .get_token_provider() + .create_token_restriction(state, data) + .await + .unwrap(); + Ok(AsyncResourceGuard::new(res, state.clone())) +}