diff --git a/src/federation/backends/sql/auth_state.rs b/src/federation/backends/sql/auth_state.rs index 429452c1..1085fc3b 100644 --- a/src/federation/backends/sql/auth_state.rs +++ b/src/federation/backends/sql/auth_state.rs @@ -12,83 +12,17 @@ // // SPDX-License-Identifier: Apache-2.0 -use chrono::Utc; -use sea_orm::DatabaseConnection; -use sea_orm::entity::*; -use sea_orm::query::*; - -use crate::config::Config; -use crate::db::entity::{ - federated_auth_state as db_federated_auth_state, - prelude::FederatedAuthState as DbFederatedAuthState, -}; +use crate::db::entity::federated_auth_state as db_federated_auth_state; use crate::federation::backends::error::FederationDatabaseError; use crate::federation::types::*; -pub async fn get>( - _conf: &Config, - db: &DatabaseConnection, - state: I, -) -> Result, FederationDatabaseError> { - let select = DbFederatedAuthState::find_by_id(state.as_ref()); - - let entry: Option = select.one(db).await?; - entry.map(TryInto::try_into).transpose() -} - -pub async fn create( - _conf: &Config, - db: &DatabaseConnection, - rec: AuthState, -) -> Result { - let scope: Option = if let Some(scope) = rec.scope { - Some(serde_json::to_value(&scope)?) - } else { - None - }; - let entry = db_federated_auth_state::ActiveModel { - state: Set(rec.state.clone()), - idp_id: Set(rec.idp_id.clone()), - mapping_id: Set(rec.mapping_id.clone()), - nonce: Set(rec.nonce.clone()), - redirect_uri: Set(rec.redirect_uri.clone()), - pkce_verifier: Set(rec.pkce_verifier.clone()), - expires_at: Set(rec.expires_at.naive_utc()), - requested_scope: scope.map(Set).unwrap_or(NotSet).into(), - }; +mod create; +mod delete; +mod get; - let db_entry: db_federated_auth_state::Model = entry.insert(db).await?; - - db_entry.try_into() -} - -pub async fn delete>( - _conf: &Config, - db: &DatabaseConnection, - id: S, -) -> Result<(), FederationDatabaseError> { - let res = DbFederatedAuthState::delete_by_id(id.as_ref()) - .exec(db) - .await?; - if res.rows_affected == 1 { - Ok(()) - } else { - Err(FederationDatabaseError::AuthStateNotFound( - id.as_ref().to_string(), - )) - } -} - -pub async fn delete_expired( - _conf: &Config, - db: &DatabaseConnection, -) -> Result<(), FederationDatabaseError> { - DbFederatedAuthState::delete_many() - .filter(db_federated_auth_state::Column::ExpiresAt.lt(Utc::now())) - .exec(db) - .await?; - Ok(()) -} +pub use create::create; +pub use delete::{delete, delete_expired}; +pub use get::get; impl TryFrom for AuthState { type Error = FederationDatabaseError; @@ -109,246 +43,21 @@ impl TryFrom for AuthState { } } -//#[cfg(test)] -//mod tests { -// use sea_orm::{DatabaseBackend, MockDatabase, MockExecResult, Transaction}; -// use serde_json::json; -// -// use crate::config::Config; -// use crate::db::entity::federated_mapping; -// -// use super::*; -// -// fn get_mapping_mock>(id: S) -> federated_mapping::Model { -// federated_mapping::Model { -// id: id.as_ref().into(), -// name: "name".into(), -// domain_id: Some("did".into()), -// idp_id: "idp".into(), -// user_claim: "sub".into(), -// ..Default::default() -// } -// } -// -// #[tokio::test] -// async fn test_get() { -// let db = MockDatabase::new(DatabaseBackend::Postgres) -// .append_query_results([vec![get_mapping_mock("1")]]) -// .into_connection(); -// let config = Config::default(); -// assert_eq!( -// get(&config, &db, "1").await.unwrap().unwrap(), -// Mapping { -// id: "1".into(), -// name: "name".into(), -// domain_id: Some("did".into()), -// idp_id: "idp".into(), -// user_claim: "sub".into(), -// ..Default::default() -// } -// ); -// -// assert_eq!( -// db.into_transaction_log(), -// [Transaction::from_sql_and_values( -// DatabaseBackend::Postgres, -// r#"SELECT "federated_mapping"."id", "federated_mapping"."name", "federated_mapping"."idp_id", "federated_mapping"."domain_id", "federated_mapping"."allowed_redirect_uris", "federated_mapping"."user_claim", "federated_mapping"."user_claim_json_pointer", "federated_mapping"."groups_claim", "federated_mapping"."bound_audiences", "federated_mapping"."bound_subject", "federated_mapping"."bound_claims", "federated_mapping"."oidc_scopes", "federated_mapping"."claim_mappings", "federated_mapping"."token_user_id", "federated_mapping"."token_role_ids", "federated_mapping"."token_project_id" FROM "federated_mapping" WHERE "federated_mapping"."id" = $1 LIMIT $2"#, -// ["1".into(), 1u64.into()] -// ),] -// ); -// } -// -// #[tokio::test] -// async fn test_list() { -// let db = MockDatabase::new(DatabaseBackend::Postgres) -// .append_query_results([vec![get_mapping_mock("1")]]) -// .append_query_results([vec![get_mapping_mock("1")]]) -// .into_connection(); -// let config = Config::default(); -// assert!( -// list(&config, &db, &MappingListParameters::default()) -// .await -// .is_ok() -// ); -// assert_eq!( -// list( -// &config, -// &db, -// &MappingListParameters { -// name: Some("mapping_name".into()), -// domain_id: Some("did".into()), -// idp_id: Some("idp".into()) -// } -// ) -// .await -// .unwrap(), -// vec![Mapping { -// id: "1".into(), -// name: "name".into(), -// domain_id: Some("did".into()), -// idp_id: "idp".into(), -// user_claim: "sub".into(), -// ..Default::default() -// }] -// ); -// -// assert_eq!( -// db.into_transaction_log(), -// [ -// Transaction::from_sql_and_values( -// DatabaseBackend::Postgres, -// r#"SELECT "federated_mapping"."id", "federated_mapping"."name", "federated_mapping"."idp_id", "federated_mapping"."domain_id", "federated_mapping"."allowed_redirect_uris", "federated_mapping"."user_claim", "federated_mapping"."user_claim_json_pointer", "federated_mapping"."groups_claim", "federated_mapping"."bound_audiences", "federated_mapping"."bound_subject", "federated_mapping"."bound_claims", "federated_mapping"."oidc_scopes", "federated_mapping"."claim_mappings", "federated_mapping"."token_user_id", "federated_mapping"."token_role_ids", "federated_mapping"."token_project_id" FROM "federated_mapping""#, -// [] -// ), -// Transaction::from_sql_and_values( -// DatabaseBackend::Postgres, -// r#"SELECT "federated_mapping"."id", "federated_mapping"."name", "federated_mapping"."idp_id", "federated_mapping"."domain_id", "federated_mapping"."allowed_redirect_uris", "federated_mapping"."user_claim", "federated_mapping"."user_claim_json_pointer", "federated_mapping"."groups_claim", "federated_mapping"."bound_audiences", "federated_mapping"."bound_subject", "federated_mapping"."bound_claims", "federated_mapping"."oidc_scopes", "federated_mapping"."claim_mappings", "federated_mapping"."token_user_id", "federated_mapping"."token_role_ids", "federated_mapping"."token_project_id" FROM "federated_mapping" WHERE "federated_mapping"."name" = $1 AND "federated_mapping"."domain_id" = $2 AND "federated_mapping"."idp_id" = $3"#, -// ["mapping_name".into(), "did".into(), "idp".into()] -// ), -// ] -// ); -// } -// -// #[tokio::test] -// async fn test_create() { -// let db = MockDatabase::new(DatabaseBackend::Postgres) -// .append_query_results([vec![get_mapping_mock("1")]]) -// .into_connection(); -// let config = Config::default(); -// -// let req = Mapping { -// id: "1".into(), -// name: "mapping".into(), -// domain_id: Some("foo_domain".into()), -// idp_id: "idp".into(), -// allowed_redirect_uris: Some(vec!["url".into()]), -// user_claim: "sub".into(), -// user_claim_json_pointer: Some(".".into()), -// groups_claim: Some("groups".into()), -// bound_audiences: Some(vec!["a1".into(), "a2".into()]), -// bound_subject: Some("subject".into()), -// bound_claims: Some(json!({"department": "foo"})), -// claim_mappings: Some(json!({"foo": "bar"})), -// oidc_scopes: Some(vec!["oidc".into(), "oauth".into()]), -// token_user_id: Some("uid".into()), -// token_role_ids: Some(vec!["r1".into(), "r2".into()]), -// token_project_id: Some("pid".into()), -// }; -// -// assert_eq!( -// create(&config, &db, req).await.unwrap(), -// get_mapping_mock("1").try_into().unwrap() -// ); -// assert_eq!( -// db.into_transaction_log(), -// [Transaction::from_sql_and_values( -// DatabaseBackend::Postgres, -// r#"INSERT INTO "federated_mapping" ("id", "name", "idp_id", "domain_id", "allowed_redirect_uris", "user_claim", "user_claim_json_pointer", "groups_claim", "bound_audiences", "bound_subject", "bound_claims", "oidc_scopes", "claim_mappings", "token_user_id", "token_role_ids", "token_project_id") VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16) RETURNING "id", "name", "idp_id", "domain_id", "allowed_redirect_uris", "user_claim", "user_claim_json_pointer", "groups_claim", "bound_audiences", "bound_subject", "bound_claims", "oidc_scopes", "claim_mappings", "token_user_id", "token_role_ids", "token_project_id""#, -// [ -// "1".into(), -// "mapping".into(), -// "idp".into(), -// "foo_domain".into(), -// "url".into(), -// "sub".into(), -// ".".into(), -// "groups".into(), -// "a1,a2".into(), -// "subject".into(), -// json!({"department": "foo"}).into(), -// "oidc,oauth".into(), -// json!({"foo": "bar"}).into(), -// "uid".into(), -// "r1,r2".into(), -// "pid".into(), -// ] -// ),] -// ); -// } -// -// #[tokio::test] -// async fn test_update() { -// let db = MockDatabase::new(DatabaseBackend::Postgres) -// .append_query_results([vec![get_mapping_mock("1")], vec![get_mapping_mock("1")]]) -// .append_exec_results([MockExecResult { -// rows_affected: 1, -// ..Default::default() -// }]) -// .into_connection(); -// let config = Config::default(); -// -// let req = MappingUpdate { -// name: Some("name".into()), -// idp_id: Some("idp".into()), -// allowed_redirect_uris: Some(Some(vec!["url".into()])), -// user_claim: Some("sub".into()), -// user_claim_json_pointer: Some(Some(".".into())), -// groups_claim: Some(Some("groups".into())), -// bound_audiences: Some(Some(vec!["a1".into(), "a2".into()])), -// bound_subject: Some(Some("subject".into())), -// bound_claims: Some(json!({"department": "foo"})), -// claim_mappings: Some(json!({"foo": "bar"})), -// oidc_scopes: Some(Some(vec!["oidc".into(), "oauth".into()])), -// token_user_id: Some(Some("uid".into())), -// token_role_ids: Some(Some(vec!["r1".into(), "r2".into()])), -// token_project_id: Some(Some("pid".into())), -// }; -// -// assert_eq!( -// update(&config, &db, "1", req).await.unwrap(), -// get_mapping_mock("1").try_into().unwrap() -// ); -// assert_eq!( -// db.into_transaction_log(), -// [ -// Transaction::from_sql_and_values( -// DatabaseBackend::Postgres, -// r#"SELECT "federated_mapping"."id", "federated_mapping"."name", "federated_mapping"."idp_id", "federated_mapping"."domain_id", "federated_mapping"."allowed_redirect_uris", "federated_mapping"."user_claim", "federated_mapping"."user_claim_json_pointer", "federated_mapping"."groups_claim", "federated_mapping"."bound_audiences", "federated_mapping"."bound_subject", "federated_mapping"."bound_claims", "federated_mapping"."oidc_scopes", "federated_mapping"."claim_mappings", "federated_mapping"."token_user_id", "federated_mapping"."token_role_ids", "federated_mapping"."token_project_id" FROM "federated_mapping" WHERE "federated_mapping"."id" = $1 LIMIT $2"#, -// ["1".into(), 1u64.into()] -// ), -// Transaction::from_sql_and_values( -// DatabaseBackend::Postgres, -// r#"UPDATE "federated_mapping" SET "name" = $1, "idp_id" = $2, "allowed_redirect_uris" = $3, "user_claim" = $4, "user_claim_json_pointer" = $5, "groups_claim" = $6, "bound_audiences" = $7, "bound_subject" = $8, "bound_claims" = $9, "oidc_scopes" = $10, "claim_mappings" = $11, "token_user_id" = $12, "token_role_ids" = $13, "token_project_id" = $14 WHERE "federated_mapping"."id" = $15 RETURNING "id", "name", "idp_id", "domain_id", "allowed_redirect_uris", "user_claim", "user_claim_json_pointer", "groups_claim", "bound_audiences", "bound_subject", "bound_claims", "oidc_scopes", "claim_mappings", "token_user_id", "token_role_ids", "token_project_id""#, -// [ -// "name".into(), -// "idp".into(), -// "url".into(), -// "sub".into(), -// ".".into(), -// "groups".into(), -// "a1,a2".into(), -// "subject".into(), -// json!({"department": "foo"}).into(), -// "oidc,oauth".into(), -// json!({"foo": "bar"}).into(), -// "uid".into(), -// "r1,r2".into(), -// "pid".into(), -// "1".into() -// ] -// ), -// ] -// ); -// } -// -// #[tokio::test] -// async fn test_delete() { -// let db = MockDatabase::new(DatabaseBackend::Postgres) -// .append_exec_results([MockExecResult { -// rows_affected: 1, -// ..Default::default() -// }]) -// .into_connection(); -// let config = Config::default(); -// -// delete(&config, &db, "id").await.unwrap(); -// assert_eq!( -// db.into_transaction_log(), -// [Transaction::from_sql_and_values( -// DatabaseBackend::Postgres, -// r#"DELETE FROM "federated_mapping" WHERE "federated_mapping"."id" = $1"#, -// ["id".into()] -// ),] -// ); -// } -//} +#[cfg(test)] +mod tests { + use crate::db::entity::federated_auth_state as db_federated_auth_state; + use chrono::NaiveDateTime; + + pub(super) fn get_auth_state_mock>(state: S) -> db_federated_auth_state::Model { + db_federated_auth_state::Model { + idp_id: "idp".into(), + mapping_id: "mapping".into(), + state: state.as_ref().into(), + nonce: "nonce".into(), + redirect_uri: "redirect_uri".into(), + pkce_verifier: "pkce_verifier".into(), + expires_at: NaiveDateTime::default(), + requested_scope: None, + } + } +} diff --git a/src/federation/backends/sql/auth_state/create.rs b/src/federation/backends/sql/auth_state/create.rs new file mode 100644 index 00000000..d7fb8913 --- /dev/null +++ b/src/federation/backends/sql/auth_state/create.rs @@ -0,0 +1,98 @@ +// 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::DatabaseConnection; +use sea_orm::entity::*; + +use crate::config::Config; +use crate::db::entity::federated_auth_state as db_federated_auth_state; +use crate::federation::backends::error::FederationDatabaseError; +use crate::federation::types::*; + +pub async fn create( + _conf: &Config, + db: &DatabaseConnection, + rec: AuthState, +) -> Result { + let scope: Option = if let Some(scope) = rec.scope { + Some(serde_json::to_value(&scope)?) + } else { + None + }; + let entry = db_federated_auth_state::ActiveModel { + state: Set(rec.state.clone()), + idp_id: Set(rec.idp_id.clone()), + mapping_id: Set(rec.mapping_id.clone()), + nonce: Set(rec.nonce.clone()), + redirect_uri: Set(rec.redirect_uri.clone()), + pkce_verifier: Set(rec.pkce_verifier.clone()), + expires_at: Set(rec.expires_at.naive_utc()), + requested_scope: scope.map(Set).unwrap_or(NotSet).into(), + }; + + let db_entry: db_federated_auth_state::Model = entry.insert(db).await?; + + db_entry.try_into() +} + +#[cfg(test)] +mod tests { + use chrono::{DateTime, NaiveDateTime, Utc}; + use sea_orm::{DatabaseBackend, MockDatabase, Transaction}; + + use crate::config::Config; + + use super::super::tests::get_auth_state_mock; + use super::*; + + #[tokio::test] + async fn test_create() { + let db = MockDatabase::new(DatabaseBackend::Postgres) + .append_query_results([vec![get_auth_state_mock("state")]]) + .into_connection(); + let config = Config::default(); + + let req = AuthState { + idp_id: "idp".into(), + mapping_id: "mapping".into(), + state: "state".into(), + nonce: "nonce".into(), + redirect_uri: "redirect_uri".into(), + pkce_verifier: "pkce_verifier".into(), + expires_at: DateTime::::default(), + scope: None, + }; + + assert_eq!( + create(&config, &db, req).await.unwrap(), + get_auth_state_mock("state").try_into().unwrap() + ); + assert_eq!( + db.into_transaction_log(), + [Transaction::from_sql_and_values( + DatabaseBackend::Postgres, + r#"INSERT INTO "federated_auth_state" ("idp_id", "mapping_id", "state", "nonce", "redirect_uri", "pkce_verifier", "expires_at") VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING "idp_id", "mapping_id", "state", "nonce", "redirect_uri", "pkce_verifier", "expires_at", "requested_scope""#, + [ + "idp".into(), + "mapping".into(), + "state".into(), + "nonce".into(), + "redirect_uri".into(), + "pkce_verifier".into(), + NaiveDateTime::default().into(), + ] + ),] + ); + } +} diff --git a/src/federation/backends/sql/auth_state/delete.rs b/src/federation/backends/sql/auth_state/delete.rs new file mode 100644 index 00000000..91afabff --- /dev/null +++ b/src/federation/backends/sql/auth_state/delete.rs @@ -0,0 +1,107 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +use chrono::Utc; +use sea_orm::DatabaseConnection; +use sea_orm::entity::*; +use sea_orm::query::*; + +use crate::config::Config; +use crate::db::entity::{ + federated_auth_state as db_federated_auth_state, + prelude::FederatedAuthState as DbFederatedAuthState, +}; +use crate::federation::backends::error::FederationDatabaseError; + +pub async fn delete>( + _conf: &Config, + db: &DatabaseConnection, + id: S, +) -> Result<(), FederationDatabaseError> { + let res = DbFederatedAuthState::delete_by_id(id.as_ref()) + .exec(db) + .await?; + if res.rows_affected == 1 { + Ok(()) + } else { + Err(FederationDatabaseError::AuthStateNotFound( + id.as_ref().to_string(), + )) + } +} + +pub async fn delete_expired( + _conf: &Config, + db: &DatabaseConnection, +) -> Result<(), FederationDatabaseError> { + DbFederatedAuthState::delete_many() + .filter(db_federated_auth_state::Column::ExpiresAt.lt(Utc::now())) + .exec(db) + .await?; + Ok(()) +} + +#[cfg(test)] +mod tests { + use chrono::NaiveDateTime; + use sea_orm::{DatabaseBackend, MockDatabase, MockExecResult, Transaction}; + + use crate::config::Config; + + use super::*; + + #[tokio::test] + async fn test_delete() { + let db = MockDatabase::new(DatabaseBackend::Postgres) + .append_exec_results([MockExecResult { + rows_affected: 1, + ..Default::default() + }]) + .into_connection(); + let config = Config::default(); + + delete(&config, &db, "state").await.unwrap(); + assert_eq!( + db.into_transaction_log(), + [Transaction::from_sql_and_values( + DatabaseBackend::Postgres, + r#"DELETE FROM "federated_auth_state" WHERE "federated_auth_state"."state" = $1"#, + ["state".into()] + ),] + ); + } + + #[tokio::test] + async fn test_delete_expired() { + let db = MockDatabase::new(DatabaseBackend::Postgres) + .append_exec_results([MockExecResult { + rows_affected: 1, + ..Default::default() + }]) + .into_connection(); + let config = Config::default(); + + delete_expired(&config, &db).await.unwrap(); + for (l,r) in db.into_transaction_log().iter().zip([Transaction::from_sql_and_values( + DatabaseBackend::Postgres, + r#"DELETE FROM "federated_auth_state" WHERE "federated_auth_state"."expires_at" < $1"#, + [NaiveDateTime::default().into()] + ),]) { + assert_eq!( + l.statements().iter().map(|x| x.sql.clone()).collect::>(), + r.statements().iter().map(|x| x.sql.clone()).collect::>() + ); + } + } +} diff --git a/src/federation/backends/sql/auth_state/get.rs b/src/federation/backends/sql/auth_state/get.rs new file mode 100644 index 00000000..51aaaa96 --- /dev/null +++ b/src/federation/backends/sql/auth_state/get.rs @@ -0,0 +1,76 @@ +// 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::DatabaseConnection; +use sea_orm::entity::*; + +use crate::config::Config; +use crate::db::entity::{ + federated_auth_state as db_federated_auth_state, + prelude::FederatedAuthState as DbFederatedAuthState, +}; +use crate::federation::backends::error::FederationDatabaseError; +use crate::federation::types::*; + +pub async fn get>( + _conf: &Config, + db: &DatabaseConnection, + state: I, +) -> Result, FederationDatabaseError> { + let select = DbFederatedAuthState::find_by_id(state.as_ref()); + + let entry: Option = select.one(db).await?; + entry.map(TryInto::try_into).transpose() +} + +#[cfg(test)] +mod tests { + use chrono::{DateTime, Utc}; + use sea_orm::{DatabaseBackend, MockDatabase, Transaction}; + + use crate::config::Config; + + use super::super::tests::get_auth_state_mock; + use super::*; + + #[tokio::test] + async fn test_get() { + let db = MockDatabase::new(DatabaseBackend::Postgres) + .append_query_results([vec![get_auth_state_mock("state")]]) + .into_connection(); + let config = Config::default(); + assert_eq!( + get(&config, &db, "state").await.unwrap().unwrap(), + AuthState { + idp_id: "idp".into(), + mapping_id: "mapping".into(), + state: "state".into(), + nonce: "nonce".into(), + redirect_uri: "redirect_uri".into(), + pkce_verifier: "pkce_verifier".into(), + expires_at: DateTime::::default(), + scope: None, + } + ); + + assert_eq!( + db.into_transaction_log(), + [Transaction::from_sql_and_values( + DatabaseBackend::Postgres, + r#"SELECT "federated_auth_state"."idp_id", "federated_auth_state"."mapping_id", "federated_auth_state"."state", "federated_auth_state"."nonce", "federated_auth_state"."redirect_uri", "federated_auth_state"."pkce_verifier", "federated_auth_state"."expires_at", "federated_auth_state"."requested_scope" FROM "federated_auth_state" WHERE "federated_auth_state"."state" = $1 LIMIT $2"#, + ["state".into(), 1u64.into()] + ),] + ); + } +} diff --git a/src/federation/backends/sql/identity_provider.rs b/src/federation/backends/sql/identity_provider.rs index 738fa8ff..0013f6a6 100644 --- a/src/federation/backends/sql/identity_provider.rs +++ b/src/federation/backends/sql/identity_provider.rs @@ -12,221 +12,21 @@ // // SPDX-License-Identifier: Apache-2.0 -use sea_orm::DatabaseConnection; -use sea_orm::entity::*; -use sea_orm::query::*; - -use crate::config::Config; -use crate::db::entity::{ - federated_identity_provider as db_federated_identity_provider, - federation_protocol as db_old_federation_protocol, - identity_provider as db_old_identity_provider, - prelude::{ - FederatedIdentityProvider as DbFederatedIdentityProvider, - IdentityProvider as DbIdentityProvider, - }, -}; +use crate::db::entity::federated_identity_provider as db_federated_identity_provider; use crate::federation::backends::error::FederationDatabaseError; use crate::federation::types::*; -pub async fn get>( - _conf: &Config, - db: &DatabaseConnection, - id: I, -) -> Result, FederationDatabaseError> { - let select = DbFederatedIdentityProvider::find_by_id(id.as_ref()); - - let entry: Option = select.one(db).await?; - entry.map(TryInto::try_into).transpose() -} - -pub async fn list( - _conf: &Config, - db: &DatabaseConnection, - params: &IdentityProviderListParameters, -) -> Result, FederationDatabaseError> { - let mut select = DbFederatedIdentityProvider::find(); - - if let Some(val) = ¶ms.name { - select = select.filter(db_federated_identity_provider::Column::Name.eq(val)); - } - - if let Some(val) = ¶ms.domain_id { - select = select.filter(db_federated_identity_provider::Column::DomainId.eq(val)); - } +mod create; +mod delete; +mod get; +mod list; +mod update; - let db_entities: Vec = select.all(db).await?; - let results: Result, _> = db_entities - .into_iter() - .map(TryInto::::try_into) - .collect(); - - results -} - -pub async fn create( - _conf: &Config, - db: &DatabaseConnection, - idp: IdentityProvider, -) -> Result { - let entry = db_federated_identity_provider::ActiveModel { - id: Set(idp.id.clone()), - domain_id: Set(idp.domain_id.clone()), - name: Set(idp.name.clone()), - oidc_discovery_url: idp - .oidc_discovery_url - .clone() - .map(Set) - .unwrap_or(NotSet) - .into(), - oidc_client_id: idp.oidc_client_id.clone().map(Set).unwrap_or(NotSet).into(), - oidc_client_secret: idp - .oidc_client_secret - .clone() - .map(Set) - .unwrap_or(NotSet) - .into(), - oidc_response_mode: idp - .oidc_response_mode - .clone() - .map(Set) - .unwrap_or(NotSet) - .into(), - oidc_response_types: idp - .oidc_response_types - .clone() - .map(|x| Set(x.join(","))) - .unwrap_or(NotSet) - .into(), - jwks_url: idp.jwks_url.clone().map(Set).unwrap_or(NotSet).into(), - jwt_validation_pubkeys: idp - .jwt_validation_pubkeys - .clone() - .map(|x| Set(x.join(","))) - .unwrap_or(NotSet) - .into(), - bound_issuer: idp.bound_issuer.clone().map(Set).unwrap_or(NotSet).into(), - default_mapping_name: idp - .default_mapping_name - .clone() - .map(Set) - .unwrap_or(NotSet) - .into(), - provider_config: idp - .provider_config - .clone() - .map(|x| Set(Some(x))) - .unwrap_or(NotSet), - }; - - let db_entry: db_federated_identity_provider::Model = entry.insert(db).await?; - - // For compatibility reasons add entry for the IDP old-style as well as the protocol to keep - // constraints working - db_old_identity_provider::ActiveModel { - id: Set(idp.id.clone()), - enabled: Set(false), - description: Set(Some(idp.name.clone())), - domain_id: Set(idp.domain_id.clone().unwrap_or("<>".into())), - authorization_ttl: NotSet, - } - .insert(db) - .await?; - - db_old_federation_protocol::ActiveModel { - id: Set("oidc".into()), - idp_id: Set(idp.id.clone()), - mapping_id: Set("<>".into()), - remote_id_attribute: NotSet, - } - .insert(db) - .await?; - - db_old_federation_protocol::ActiveModel { - id: Set("jwt".into()), - idp_id: Set(idp.id.clone()), - mapping_id: Set("<>".into()), - remote_id_attribute: NotSet, - } - .insert(db) - .await?; - - db_entry.try_into() -} - -pub async fn update>( - _conf: &Config, - db: &DatabaseConnection, - id: S, - idp: IdentityProviderUpdate, -) -> Result { - if let Some(current) = DbFederatedIdentityProvider::find_by_id(id.as_ref()) - .one(db) - .await? - { - let mut entry: db_federated_identity_provider::ActiveModel = current.into(); - if let Some(val) = idp.name { - entry.name = Set(val.to_owned()); - } - if let Some(val) = idp.oidc_discovery_url { - entry.oidc_discovery_url = Set(val.to_owned()); - } - if let Some(val) = idp.oidc_client_id { - entry.oidc_client_id = Set(val.to_owned()); - } - if let Some(val) = idp.oidc_client_secret { - entry.oidc_client_secret = Set(val.to_owned()); - } - if let Some(val) = idp.oidc_response_mode { - entry.oidc_response_mode = Set(val.to_owned()); - } - if let Some(val) = idp.oidc_response_types { - entry.oidc_response_types = Set(val.clone().map(|x| x.join(","))); - } - if let Some(val) = idp.jwks_url { - entry.jwks_url = Set(val.to_owned()); - } - if let Some(val) = idp.jwt_validation_pubkeys { - entry.jwt_validation_pubkeys = Set(val.clone().map(|x| x.join(","))); - } - if let Some(val) = idp.bound_issuer { - entry.bound_issuer = Set(val.to_owned()); - } - if let Some(val) = idp.provider_config { - entry.provider_config = Set(val.to_owned()); - } - if let Some(val) = idp.default_mapping_name { - entry.default_mapping_name = Set(val.to_owned()); - } - - let db_entry: db_federated_identity_provider::Model = entry.update(db).await?; - db_entry.try_into() - } else { - Err(FederationDatabaseError::IdentityProviderNotFound( - id.as_ref().to_string(), - )) - } -} - -pub async fn delete>( - _conf: &Config, - db: &DatabaseConnection, - id: S, -) -> Result<(), FederationDatabaseError> { - let res = DbFederatedIdentityProvider::delete_by_id(id.as_ref()) - .exec(db) - .await?; - if res.rows_affected == 1 { - DbIdentityProvider::delete_by_id(id.as_ref()) - .exec(db) - .await?; - Ok(()) - } else { - Err(FederationDatabaseError::IdentityProviderNotFound( - id.as_ref().to_string(), - )) - } -} +pub use create::create; +pub use delete::delete; +pub use get::get; +pub use list::list; +pub use update::update; impl TryFrom for IdentityProvider { type Error = FederationDatabaseError; @@ -278,15 +78,10 @@ impl TryFrom for IdentityProvider { #[cfg(test)] mod tests { - use sea_orm::{DatabaseBackend, MockDatabase, MockExecResult, Transaction}; - use serde_json::json; - use crate::config::Config; use crate::db::entity::{federated_identity_provider, federation_protocol, identity_provider}; - use super::*; - - fn get_idp_mock>(id: S) -> federated_identity_provider::Model { + pub(super) fn get_idp_mock>(id: S) -> federated_identity_provider::Model { federated_identity_provider::Model { id: id.as_ref().into(), name: "name".into(), @@ -295,7 +90,7 @@ mod tests { } } - fn get_old_idp_mock>(id: S) -> identity_provider::Model { + pub(super) fn get_old_idp_mock>(id: S) -> identity_provider::Model { identity_provider::Model { id: id.as_ref().into(), enabled: true, @@ -305,7 +100,7 @@ mod tests { } } - fn get_old_proto_mock>(id: S) -> federation_protocol::Model { + pub(super) fn get_old_proto_mock>(id: S) -> federation_protocol::Model { federation_protocol::Model { id: "oidc".into(), idp_id: id.as_ref().into(), @@ -316,250 +111,4 @@ mod tests { #[test] fn test_from_db_model() {} - - #[tokio::test] - async fn test_get() { - let db = MockDatabase::new(DatabaseBackend::Postgres) - .append_query_results([vec![get_idp_mock("1")]]) - .into_connection(); - let config = Config::default(); - assert_eq!( - get(&config, &db, "1").await.unwrap().unwrap(), - IdentityProvider { - id: "1".into(), - name: "name".into(), - domain_id: Some("did".into()), - ..Default::default() - } - ); - - // Checking transaction log - assert_eq!( - db.into_transaction_log(), - [Transaction::from_sql_and_values( - DatabaseBackend::Postgres, - r#"SELECT "federated_identity_provider"."id", "federated_identity_provider"."name", "federated_identity_provider"."domain_id", "federated_identity_provider"."oidc_discovery_url", "federated_identity_provider"."oidc_client_id", "federated_identity_provider"."oidc_client_secret", "federated_identity_provider"."oidc_response_mode", "federated_identity_provider"."oidc_response_types", "federated_identity_provider"."jwks_url", "federated_identity_provider"."jwt_validation_pubkeys", "federated_identity_provider"."bound_issuer", "federated_identity_provider"."default_mapping_name", "federated_identity_provider"."provider_config" FROM "federated_identity_provider" WHERE "federated_identity_provider"."id" = $1 LIMIT $2"#, - ["1".into(), 1u64.into()] - ),] - ); - } - - #[tokio::test] - async fn test_list() { - let db = MockDatabase::new(DatabaseBackend::Postgres) - .append_query_results([vec![get_idp_mock("1")]]) - .append_query_results([vec![get_idp_mock("1")]]) - .into_connection(); - let config = Config::default(); - assert!( - list(&config, &db, &IdentityProviderListParameters::default()) - .await - .is_ok() - ); - assert_eq!( - list( - &config, - &db, - &IdentityProviderListParameters { - name: Some("idp_name".into()), - domain_id: Some("did".into()), - } - ) - .await - .unwrap(), - vec![IdentityProvider { - id: "1".into(), - name: "name".into(), - domain_id: Some("did".into()), - ..Default::default() - }] - ); - - // Checking transaction log - assert_eq!( - db.into_transaction_log(), - [ - Transaction::from_sql_and_values( - DatabaseBackend::Postgres, - r#"SELECT "federated_identity_provider"."id", "federated_identity_provider"."name", "federated_identity_provider"."domain_id", "federated_identity_provider"."oidc_discovery_url", "federated_identity_provider"."oidc_client_id", "federated_identity_provider"."oidc_client_secret", "federated_identity_provider"."oidc_response_mode", "federated_identity_provider"."oidc_response_types", "federated_identity_provider"."jwks_url", "federated_identity_provider"."jwt_validation_pubkeys", "federated_identity_provider"."bound_issuer", "federated_identity_provider"."default_mapping_name", "federated_identity_provider"."provider_config" FROM "federated_identity_provider""#, - [] - ), - Transaction::from_sql_and_values( - DatabaseBackend::Postgres, - r#"SELECT "federated_identity_provider"."id", "federated_identity_provider"."name", "federated_identity_provider"."domain_id", "federated_identity_provider"."oidc_discovery_url", "federated_identity_provider"."oidc_client_id", "federated_identity_provider"."oidc_client_secret", "federated_identity_provider"."oidc_response_mode", "federated_identity_provider"."oidc_response_types", "federated_identity_provider"."jwks_url", "federated_identity_provider"."jwt_validation_pubkeys", "federated_identity_provider"."bound_issuer", "federated_identity_provider"."default_mapping_name", "federated_identity_provider"."provider_config" FROM "federated_identity_provider" WHERE "federated_identity_provider"."name" = $1 AND "federated_identity_provider"."domain_id" = $2"#, - ["idp_name".into(), "did".into()] - ), - ] - ); - } - - #[tokio::test] - async fn test_create() { - // Create MockDatabase with mock query results - let db = MockDatabase::new(DatabaseBackend::Postgres) - .append_query_results([vec![get_idp_mock("1")]]) - .append_query_results([vec![get_old_idp_mock("1")]]) - .append_query_results([vec![get_old_proto_mock("1")]]) - .append_query_results([vec![get_old_proto_mock("2")]]) - .into_connection(); - let config = Config::default(); - - let req = IdentityProvider { - id: "1".into(), - name: "idp".into(), - domain_id: Some("foo_domain".into()), - oidc_discovery_url: Some("url".into()), - oidc_client_id: Some("oidccid".into()), - oidc_client_secret: Some("oidccs".into()), - oidc_response_mode: Some("oidcrm".into()), - oidc_response_types: Some(vec!["t1".into(), "t2".into()]), - jwks_url: Some("http://jwks".into()), - jwt_validation_pubkeys: Some(vec!["jt1".into(), "jt2".into()]), - bound_issuer: Some("bi".into()), - default_mapping_name: Some("dummy".into()), - provider_config: Some(json!({"foo": "bar"})), - }; - - assert_eq!( - create(&config, &db, req).await.unwrap(), - get_idp_mock("1").try_into().unwrap() - ); - // Checking transaction log - assert_eq!( - db.into_transaction_log(), - [ - Transaction::from_sql_and_values( - DatabaseBackend::Postgres, - r#"INSERT INTO "federated_identity_provider" ("id", "name", "domain_id", "oidc_discovery_url", "oidc_client_id", "oidc_client_secret", "oidc_response_mode", "oidc_response_types", "jwks_url", "jwt_validation_pubkeys", "bound_issuer", "default_mapping_name", "provider_config") VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) RETURNING "id", "name", "domain_id", "oidc_discovery_url", "oidc_client_id", "oidc_client_secret", "oidc_response_mode", "oidc_response_types", "jwks_url", "jwt_validation_pubkeys", "bound_issuer", "default_mapping_name", "provider_config""#, - [ - "1".into(), - "idp".into(), - "foo_domain".into(), - "url".into(), - "oidccid".into(), - "oidccs".into(), - "oidcrm".into(), - "t1,t2".into(), - "http://jwks".into(), - "jt1,jt2".into(), - "bi".into(), - "dummy".into(), - json!({"foo": "bar"}).into() - ] - ), - Transaction::from_sql_and_values( - DatabaseBackend::Postgres, - r#"INSERT INTO "identity_provider" ("id", "enabled", "description", "domain_id") VALUES ($1, $2, $3, $4) RETURNING "id", "enabled", "description", "domain_id", "authorization_ttl""#, - ["1".into(), false.into(), "idp".into(), "foo_domain".into(),] - ), - Transaction::from_sql_and_values( - DatabaseBackend::Postgres, - r#"INSERT INTO "federation_protocol" ("id", "idp_id", "mapping_id") VALUES ($1, $2, $3) RETURNING "id", "idp_id", "mapping_id", "remote_id_attribute""#, - ["oidc".into(), "1".into(), "<>".into()] - ), - Transaction::from_sql_and_values( - DatabaseBackend::Postgres, - r#"INSERT INTO "federation_protocol" ("id", "idp_id", "mapping_id") VALUES ($1, $2, $3) RETURNING "id", "idp_id", "mapping_id", "remote_id_attribute""#, - ["jwt".into(), "1".into(), "<>".into()] - ), - ] - ); - } - - #[tokio::test] - async fn test_update() { - // Create MockDatabase with mock query results - let db = MockDatabase::new(DatabaseBackend::Postgres) - .append_query_results([vec![get_idp_mock("1")], vec![get_idp_mock("1")]]) - .append_exec_results([MockExecResult { - rows_affected: 1, - ..Default::default() - }]) - .into_connection(); - let config = Config::default(); - - let req = IdentityProviderUpdate { - name: Some("idp".into()), - oidc_discovery_url: Some(Some("url".into())), - oidc_client_id: Some(Some("oidccid".into())), - oidc_client_secret: Some(Some("oidccs".into())), - oidc_response_mode: Some(Some("oidcrm".into())), - oidc_response_types: Some(Some(vec!["t1".into(), "t2".into()])), - jwks_url: Some(Some("http://jwks".into())), - jwt_validation_pubkeys: Some(Some(vec!["jt1".into(), "jt2".into()])), - bound_issuer: Some(Some("bi".into())), - default_mapping_name: Some(Some("dummy".into())), - provider_config: Some(Some(json!({"foo": "bar"}))), - }; - - assert_eq!( - update(&config, &db, "1", req).await.unwrap(), - get_idp_mock("1").try_into().unwrap() - ); - // Checking transaction log - assert_eq!( - db.into_transaction_log(), - [ - Transaction::from_sql_and_values( - DatabaseBackend::Postgres, - r#"SELECT "federated_identity_provider"."id", "federated_identity_provider"."name", "federated_identity_provider"."domain_id", "federated_identity_provider"."oidc_discovery_url", "federated_identity_provider"."oidc_client_id", "federated_identity_provider"."oidc_client_secret", "federated_identity_provider"."oidc_response_mode", "federated_identity_provider"."oidc_response_types", "federated_identity_provider"."jwks_url", "federated_identity_provider"."jwt_validation_pubkeys", "federated_identity_provider"."bound_issuer", "federated_identity_provider"."default_mapping_name", "federated_identity_provider"."provider_config" FROM "federated_identity_provider" WHERE "federated_identity_provider"."id" = $1 LIMIT $2"#, - ["1".into(), 1u64.into()] - ), - Transaction::from_sql_and_values( - DatabaseBackend::Postgres, - r#"UPDATE "federated_identity_provider" SET "name" = $1, "oidc_discovery_url" = $2, "oidc_client_id" = $3, "oidc_client_secret" = $4, "oidc_response_mode" = $5, "oidc_response_types" = $6, "jwks_url" = $7, "jwt_validation_pubkeys" = $8, "bound_issuer" = $9, "default_mapping_name" = $10, "provider_config" = $11 WHERE "federated_identity_provider"."id" = $12 RETURNING "id", "name", "domain_id", "oidc_discovery_url", "oidc_client_id", "oidc_client_secret", "oidc_response_mode", "oidc_response_types", "jwks_url", "jwt_validation_pubkeys", "bound_issuer", "default_mapping_name", "provider_config""#, - [ - "idp".into(), - "url".into(), - "oidccid".into(), - "oidccs".into(), - "oidcrm".into(), - "t1,t2".into(), - "http://jwks".into(), - "jt1,jt2".into(), - "bi".into(), - "dummy".into(), - json!({"foo": "bar"}).into(), - "1".into(), - ] - ), - ] - ); - } - - #[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() - }, - MockExecResult { - rows_affected: 1, - ..Default::default() - }, - ]) - .into_connection(); - let config = Config::default(); - - delete(&config, &db, "id").await.unwrap(); - // Checking transaction log - assert_eq!( - db.into_transaction_log(), - [ - Transaction::from_sql_and_values( - DatabaseBackend::Postgres, - r#"DELETE FROM "federated_identity_provider" WHERE "federated_identity_provider"."id" = $1"#, - ["id".into()] - ), - Transaction::from_sql_and_values( - DatabaseBackend::Postgres, - r#"DELETE FROM "identity_provider" WHERE "identity_provider"."id" = $1"#, - ["id".into()] - ), - ] - ); - } } diff --git a/src/federation/backends/sql/identity_provider/create.rs b/src/federation/backends/sql/identity_provider/create.rs new file mode 100644 index 00000000..990a1040 --- /dev/null +++ b/src/federation/backends/sql/identity_provider/create.rs @@ -0,0 +1,199 @@ +// 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::DatabaseConnection; +use sea_orm::entity::*; + +use crate::config::Config; +use crate::db::entity::{ + federated_identity_provider as db_federated_identity_provider, + federation_protocol as db_old_federation_protocol, + identity_provider as db_old_identity_provider, +}; +use crate::federation::backends::error::FederationDatabaseError; +use crate::federation::types::*; + +pub async fn create( + _conf: &Config, + db: &DatabaseConnection, + idp: IdentityProvider, +) -> Result { + let entry = db_federated_identity_provider::ActiveModel { + id: Set(idp.id.clone()), + domain_id: Set(idp.domain_id.clone()), + name: Set(idp.name.clone()), + oidc_discovery_url: idp + .oidc_discovery_url + .clone() + .map(Set) + .unwrap_or(NotSet) + .into(), + oidc_client_id: idp.oidc_client_id.clone().map(Set).unwrap_or(NotSet).into(), + oidc_client_secret: idp + .oidc_client_secret + .clone() + .map(Set) + .unwrap_or(NotSet) + .into(), + oidc_response_mode: idp + .oidc_response_mode + .clone() + .map(Set) + .unwrap_or(NotSet) + .into(), + oidc_response_types: idp + .oidc_response_types + .clone() + .map(|x| Set(x.join(","))) + .unwrap_or(NotSet) + .into(), + jwks_url: idp.jwks_url.clone().map(Set).unwrap_or(NotSet).into(), + jwt_validation_pubkeys: idp + .jwt_validation_pubkeys + .clone() + .map(|x| Set(x.join(","))) + .unwrap_or(NotSet) + .into(), + bound_issuer: idp.bound_issuer.clone().map(Set).unwrap_or(NotSet).into(), + default_mapping_name: idp + .default_mapping_name + .clone() + .map(Set) + .unwrap_or(NotSet) + .into(), + provider_config: idp + .provider_config + .clone() + .map(|x| Set(Some(x))) + .unwrap_or(NotSet), + }; + + let db_entry: db_federated_identity_provider::Model = entry.insert(db).await?; + + // For compatibility reasons add entry for the IDP old-style as well as the protocol to keep + // constraints working + db_old_identity_provider::ActiveModel { + id: Set(idp.id.clone()), + enabled: Set(false), + description: Set(Some(idp.name.clone())), + domain_id: Set(idp.domain_id.clone().unwrap_or("<>".into())), + authorization_ttl: NotSet, + } + .insert(db) + .await?; + + db_old_federation_protocol::ActiveModel { + id: Set("oidc".into()), + idp_id: Set(idp.id.clone()), + mapping_id: Set("<>".into()), + remote_id_attribute: NotSet, + } + .insert(db) + .await?; + + db_old_federation_protocol::ActiveModel { + id: Set("jwt".into()), + idp_id: Set(idp.id.clone()), + mapping_id: Set("<>".into()), + remote_id_attribute: NotSet, + } + .insert(db) + .await?; + + db_entry.try_into() +} + +#[cfg(test)] +mod tests { + use sea_orm::{DatabaseBackend, MockDatabase, Transaction}; + use serde_json::json; + + use crate::config::Config; + + use super::super::tests::{get_idp_mock, get_old_idp_mock, get_old_proto_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_idp_mock("1")]]) + .append_query_results([vec![get_old_idp_mock("1")]]) + .append_query_results([vec![get_old_proto_mock("1")]]) + .append_query_results([vec![get_old_proto_mock("2")]]) + .into_connection(); + let config = Config::default(); + + let req = IdentityProvider { + id: "1".into(), + name: "idp".into(), + domain_id: Some("foo_domain".into()), + oidc_discovery_url: Some("url".into()), + oidc_client_id: Some("oidccid".into()), + oidc_client_secret: Some("oidccs".into()), + oidc_response_mode: Some("oidcrm".into()), + oidc_response_types: Some(vec!["t1".into(), "t2".into()]), + jwks_url: Some("http://jwks".into()), + jwt_validation_pubkeys: Some(vec!["jt1".into(), "jt2".into()]), + bound_issuer: Some("bi".into()), + default_mapping_name: Some("dummy".into()), + provider_config: Some(json!({"foo": "bar"})), + }; + + assert_eq!( + create(&config, &db, req).await.unwrap(), + get_idp_mock("1").try_into().unwrap() + ); + // Checking transaction log + assert_eq!( + db.into_transaction_log(), + [ + Transaction::from_sql_and_values( + DatabaseBackend::Postgres, + r#"INSERT INTO "federated_identity_provider" ("id", "name", "domain_id", "oidc_discovery_url", "oidc_client_id", "oidc_client_secret", "oidc_response_mode", "oidc_response_types", "jwks_url", "jwt_validation_pubkeys", "bound_issuer", "default_mapping_name", "provider_config") VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) RETURNING "id", "name", "domain_id", "oidc_discovery_url", "oidc_client_id", "oidc_client_secret", "oidc_response_mode", "oidc_response_types", "jwks_url", "jwt_validation_pubkeys", "bound_issuer", "default_mapping_name", "provider_config""#, + [ + "1".into(), + "idp".into(), + "foo_domain".into(), + "url".into(), + "oidccid".into(), + "oidccs".into(), + "oidcrm".into(), + "t1,t2".into(), + "http://jwks".into(), + "jt1,jt2".into(), + "bi".into(), + "dummy".into(), + json!({"foo": "bar"}).into() + ] + ), + Transaction::from_sql_and_values( + DatabaseBackend::Postgres, + r#"INSERT INTO "identity_provider" ("id", "enabled", "description", "domain_id") VALUES ($1, $2, $3, $4) RETURNING "id", "enabled", "description", "domain_id", "authorization_ttl""#, + ["1".into(), false.into(), "idp".into(), "foo_domain".into(),] + ), + Transaction::from_sql_and_values( + DatabaseBackend::Postgres, + r#"INSERT INTO "federation_protocol" ("id", "idp_id", "mapping_id") VALUES ($1, $2, $3) RETURNING "id", "idp_id", "mapping_id", "remote_id_attribute""#, + ["oidc".into(), "1".into(), "<>".into()] + ), + Transaction::from_sql_and_values( + DatabaseBackend::Postgres, + r#"INSERT INTO "federation_protocol" ("id", "idp_id", "mapping_id") VALUES ($1, $2, $3) RETURNING "id", "idp_id", "mapping_id", "remote_id_attribute""#, + ["jwt".into(), "1".into(), "<>".into()] + ), + ] + ); + } +} diff --git a/src/federation/backends/sql/identity_provider/delete.rs b/src/federation/backends/sql/identity_provider/delete.rs new file mode 100644 index 00000000..16f9950a --- /dev/null +++ b/src/federation/backends/sql/identity_provider/delete.rs @@ -0,0 +1,88 @@ +// 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::DatabaseConnection; +use sea_orm::entity::*; + +use crate::config::Config; +use crate::db::entity::prelude::{ + FederatedIdentityProvider as DbFederatedIdentityProvider, + IdentityProvider as DbIdentityProvider, +}; +use crate::federation::backends::error::FederationDatabaseError; + +pub async fn delete>( + _conf: &Config, + db: &DatabaseConnection, + id: S, +) -> Result<(), FederationDatabaseError> { + let res = DbFederatedIdentityProvider::delete_by_id(id.as_ref()) + .exec(db) + .await?; + if res.rows_affected == 1 { + DbIdentityProvider::delete_by_id(id.as_ref()) + .exec(db) + .await?; + Ok(()) + } else { + Err(FederationDatabaseError::IdentityProviderNotFound( + id.as_ref().to_string(), + )) + } +} + +#[cfg(test)] +mod tests { + use sea_orm::{DatabaseBackend, MockDatabase, MockExecResult, Transaction}; + + use crate::config::Config; + + 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() + }, + MockExecResult { + rows_affected: 1, + ..Default::default() + }, + ]) + .into_connection(); + let config = Config::default(); + + delete(&config, &db, "id").await.unwrap(); + // Checking transaction log + assert_eq!( + db.into_transaction_log(), + [ + Transaction::from_sql_and_values( + DatabaseBackend::Postgres, + r#"DELETE FROM "federated_identity_provider" WHERE "federated_identity_provider"."id" = $1"#, + ["id".into()] + ), + Transaction::from_sql_and_values( + DatabaseBackend::Postgres, + r#"DELETE FROM "identity_provider" WHERE "identity_provider"."id" = $1"#, + ["id".into()] + ), + ] + ); + } +} diff --git a/src/federation/backends/sql/identity_provider/get.rs b/src/federation/backends/sql/identity_provider/get.rs new file mode 100644 index 00000000..61dcf67c --- /dev/null +++ b/src/federation/backends/sql/identity_provider/get.rs @@ -0,0 +1,71 @@ +// 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::DatabaseConnection; +use sea_orm::entity::*; + +use crate::config::Config; +use crate::db::entity::{ + federated_identity_provider as db_federated_identity_provider, + prelude::FederatedIdentityProvider as DbFederatedIdentityProvider, +}; +use crate::federation::backends::error::FederationDatabaseError; +use crate::federation::types::*; + +pub async fn get>( + _conf: &Config, + db: &DatabaseConnection, + id: I, +) -> Result, FederationDatabaseError> { + let select = DbFederatedIdentityProvider::find_by_id(id.as_ref()); + + let entry: Option = select.one(db).await?; + entry.map(TryInto::try_into).transpose() +} +#[cfg(test)] +mod tests { + use sea_orm::{DatabaseBackend, MockDatabase, Transaction}; + + use crate::config::Config; + + use super::super::tests::get_idp_mock; + use super::*; + + #[tokio::test] + async fn test_get() { + let db = MockDatabase::new(DatabaseBackend::Postgres) + .append_query_results([vec![get_idp_mock("1")]]) + .into_connection(); + let config = Config::default(); + assert_eq!( + get(&config, &db, "1").await.unwrap().unwrap(), + IdentityProvider { + id: "1".into(), + name: "name".into(), + domain_id: Some("did".into()), + ..Default::default() + } + ); + + // Checking transaction log + assert_eq!( + db.into_transaction_log(), + [Transaction::from_sql_and_values( + DatabaseBackend::Postgres, + r#"SELECT "federated_identity_provider"."id", "federated_identity_provider"."name", "federated_identity_provider"."domain_id", "federated_identity_provider"."oidc_discovery_url", "federated_identity_provider"."oidc_client_id", "federated_identity_provider"."oidc_client_secret", "federated_identity_provider"."oidc_response_mode", "federated_identity_provider"."oidc_response_types", "federated_identity_provider"."jwks_url", "federated_identity_provider"."jwt_validation_pubkeys", "federated_identity_provider"."bound_issuer", "federated_identity_provider"."default_mapping_name", "federated_identity_provider"."provider_config" FROM "federated_identity_provider" WHERE "federated_identity_provider"."id" = $1 LIMIT $2"#, + ["1".into(), 1u64.into()] + ),] + ); + } +} diff --git a/src/federation/backends/sql/identity_provider/list.rs b/src/federation/backends/sql/identity_provider/list.rs new file mode 100644 index 00000000..6fddd637 --- /dev/null +++ b/src/federation/backends/sql/identity_provider/list.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 + +use sea_orm::DatabaseConnection; +use sea_orm::entity::*; +use sea_orm::query::*; + +use crate::config::Config; +use crate::db::entity::{ + federated_identity_provider as db_federated_identity_provider, + prelude::FederatedIdentityProvider as DbFederatedIdentityProvider, +}; +use crate::federation::backends::error::FederationDatabaseError; +use crate::federation::types::*; + +pub async fn list( + _conf: &Config, + db: &DatabaseConnection, + params: &IdentityProviderListParameters, +) -> Result, FederationDatabaseError> { + let mut select = DbFederatedIdentityProvider::find(); + + if let Some(val) = ¶ms.name { + select = select.filter(db_federated_identity_provider::Column::Name.eq(val)); + } + + if let Some(val) = ¶ms.domain_id { + select = select.filter(db_federated_identity_provider::Column::DomainId.eq(val)); + } + + let db_entities: Vec = select.all(db).await?; + let results: Result, _> = db_entities + .into_iter() + .map(TryInto::::try_into) + .collect(); + + results +} + +#[cfg(test)] +mod tests { + use sea_orm::{DatabaseBackend, MockDatabase, Transaction}; + + use crate::config::Config; + + use super::super::tests::get_idp_mock; + use super::*; + + #[tokio::test] + async fn test_list() { + let db = MockDatabase::new(DatabaseBackend::Postgres) + .append_query_results([vec![get_idp_mock("1")]]) + .append_query_results([vec![get_idp_mock("1")]]) + .into_connection(); + let config = Config::default(); + assert!( + list(&config, &db, &IdentityProviderListParameters::default()) + .await + .is_ok() + ); + assert_eq!( + list( + &config, + &db, + &IdentityProviderListParameters { + name: Some("idp_name".into()), + domain_id: Some("did".into()), + } + ) + .await + .unwrap(), + vec![IdentityProvider { + id: "1".into(), + name: "name".into(), + domain_id: Some("did".into()), + ..Default::default() + }] + ); + + // Checking transaction log + assert_eq!( + db.into_transaction_log(), + [ + Transaction::from_sql_and_values( + DatabaseBackend::Postgres, + r#"SELECT "federated_identity_provider"."id", "federated_identity_provider"."name", "federated_identity_provider"."domain_id", "federated_identity_provider"."oidc_discovery_url", "federated_identity_provider"."oidc_client_id", "federated_identity_provider"."oidc_client_secret", "federated_identity_provider"."oidc_response_mode", "federated_identity_provider"."oidc_response_types", "federated_identity_provider"."jwks_url", "federated_identity_provider"."jwt_validation_pubkeys", "federated_identity_provider"."bound_issuer", "federated_identity_provider"."default_mapping_name", "federated_identity_provider"."provider_config" FROM "federated_identity_provider""#, + [] + ), + Transaction::from_sql_and_values( + DatabaseBackend::Postgres, + r#"SELECT "federated_identity_provider"."id", "federated_identity_provider"."name", "federated_identity_provider"."domain_id", "federated_identity_provider"."oidc_discovery_url", "federated_identity_provider"."oidc_client_id", "federated_identity_provider"."oidc_client_secret", "federated_identity_provider"."oidc_response_mode", "federated_identity_provider"."oidc_response_types", "federated_identity_provider"."jwks_url", "federated_identity_provider"."jwt_validation_pubkeys", "federated_identity_provider"."bound_issuer", "federated_identity_provider"."default_mapping_name", "federated_identity_provider"."provider_config" FROM "federated_identity_provider" WHERE "federated_identity_provider"."name" = $1 AND "federated_identity_provider"."domain_id" = $2"#, + ["idp_name".into(), "did".into()] + ), + ] + ); + } +} diff --git a/src/federation/backends/sql/identity_provider/update.rs b/src/federation/backends/sql/identity_provider/update.rs new file mode 100644 index 00000000..ba2c81d9 --- /dev/null +++ b/src/federation/backends/sql/identity_provider/update.rs @@ -0,0 +1,150 @@ +// 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::DatabaseConnection; +use sea_orm::entity::*; + +use crate::config::Config; +use crate::db::entity::{ + federated_identity_provider as db_federated_identity_provider, + prelude::FederatedIdentityProvider as DbFederatedIdentityProvider, +}; +use crate::federation::backends::error::FederationDatabaseError; +use crate::federation::types::*; + +pub async fn update>( + _conf: &Config, + db: &DatabaseConnection, + id: S, + idp: IdentityProviderUpdate, +) -> Result { + if let Some(current) = DbFederatedIdentityProvider::find_by_id(id.as_ref()) + .one(db) + .await? + { + let mut entry: db_federated_identity_provider::ActiveModel = current.into(); + if let Some(val) = idp.name { + entry.name = Set(val.to_owned()); + } + if let Some(val) = idp.oidc_discovery_url { + entry.oidc_discovery_url = Set(val.to_owned()); + } + if let Some(val) = idp.oidc_client_id { + entry.oidc_client_id = Set(val.to_owned()); + } + if let Some(val) = idp.oidc_client_secret { + entry.oidc_client_secret = Set(val.to_owned()); + } + if let Some(val) = idp.oidc_response_mode { + entry.oidc_response_mode = Set(val.to_owned()); + } + if let Some(val) = idp.oidc_response_types { + entry.oidc_response_types = Set(val.clone().map(|x| x.join(","))); + } + if let Some(val) = idp.jwks_url { + entry.jwks_url = Set(val.to_owned()); + } + if let Some(val) = idp.jwt_validation_pubkeys { + entry.jwt_validation_pubkeys = Set(val.clone().map(|x| x.join(","))); + } + if let Some(val) = idp.bound_issuer { + entry.bound_issuer = Set(val.to_owned()); + } + if let Some(val) = idp.provider_config { + entry.provider_config = Set(val.to_owned()); + } + if let Some(val) = idp.default_mapping_name { + entry.default_mapping_name = Set(val.to_owned()); + } + + let db_entry: db_federated_identity_provider::Model = entry.update(db).await?; + db_entry.try_into() + } else { + Err(FederationDatabaseError::IdentityProviderNotFound( + id.as_ref().to_string(), + )) + } +} + +#[cfg(test)] +mod tests { + use sea_orm::{DatabaseBackend, MockDatabase, MockExecResult, Transaction}; + use serde_json::json; + + use crate::config::Config; + + use super::super::tests::get_idp_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_idp_mock("1")], vec![get_idp_mock("1")]]) + .append_exec_results([MockExecResult { + rows_affected: 1, + ..Default::default() + }]) + .into_connection(); + let config = Config::default(); + + let req = IdentityProviderUpdate { + name: Some("idp".into()), + oidc_discovery_url: Some(Some("url".into())), + oidc_client_id: Some(Some("oidccid".into())), + oidc_client_secret: Some(Some("oidccs".into())), + oidc_response_mode: Some(Some("oidcrm".into())), + oidc_response_types: Some(Some(vec!["t1".into(), "t2".into()])), + jwks_url: Some(Some("http://jwks".into())), + jwt_validation_pubkeys: Some(Some(vec!["jt1".into(), "jt2".into()])), + bound_issuer: Some(Some("bi".into())), + default_mapping_name: Some(Some("dummy".into())), + provider_config: Some(Some(json!({"foo": "bar"}))), + }; + + assert_eq!( + update(&config, &db, "1", req).await.unwrap(), + get_idp_mock("1").try_into().unwrap() + ); + // Checking transaction log + assert_eq!( + db.into_transaction_log(), + [ + Transaction::from_sql_and_values( + DatabaseBackend::Postgres, + r#"SELECT "federated_identity_provider"."id", "federated_identity_provider"."name", "federated_identity_provider"."domain_id", "federated_identity_provider"."oidc_discovery_url", "federated_identity_provider"."oidc_client_id", "federated_identity_provider"."oidc_client_secret", "federated_identity_provider"."oidc_response_mode", "federated_identity_provider"."oidc_response_types", "federated_identity_provider"."jwks_url", "federated_identity_provider"."jwt_validation_pubkeys", "federated_identity_provider"."bound_issuer", "federated_identity_provider"."default_mapping_name", "federated_identity_provider"."provider_config" FROM "federated_identity_provider" WHERE "federated_identity_provider"."id" = $1 LIMIT $2"#, + ["1".into(), 1u64.into()] + ), + Transaction::from_sql_and_values( + DatabaseBackend::Postgres, + r#"UPDATE "federated_identity_provider" SET "name" = $1, "oidc_discovery_url" = $2, "oidc_client_id" = $3, "oidc_client_secret" = $4, "oidc_response_mode" = $5, "oidc_response_types" = $6, "jwks_url" = $7, "jwt_validation_pubkeys" = $8, "bound_issuer" = $9, "default_mapping_name" = $10, "provider_config" = $11 WHERE "federated_identity_provider"."id" = $12 RETURNING "id", "name", "domain_id", "oidc_discovery_url", "oidc_client_id", "oidc_client_secret", "oidc_response_mode", "oidc_response_types", "jwks_url", "jwt_validation_pubkeys", "bound_issuer", "default_mapping_name", "provider_config""#, + [ + "idp".into(), + "url".into(), + "oidccid".into(), + "oidccs".into(), + "oidcrm".into(), + "t1,t2".into(), + "http://jwks".into(), + "jt1,jt2".into(), + "bi".into(), + "dummy".into(), + json!({"foo": "bar"}).into(), + "1".into(), + ] + ), + ] + ); + } +} diff --git a/src/federation/backends/sql/mapping.rs b/src/federation/backends/sql/mapping.rs index c3db072f..82daa60f 100644 --- a/src/federation/backends/sql/mapping.rs +++ b/src/federation/backends/sql/mapping.rs @@ -12,60 +12,23 @@ // // SPDX-License-Identifier: Apache-2.0 -use sea_orm::DatabaseConnection; -use sea_orm::entity::*; -use sea_orm::query::*; - -use crate::config::Config; use crate::db::entity::{ - federated_mapping as db_federated_mapping, prelude::FederatedMapping as DbFederatedMapping, - sea_orm_active_enums::MappingType as db_mapping_type, + federated_mapping as db_federated_mapping, sea_orm_active_enums::MappingType as db_mapping_type, }; use crate::federation::backends::error::FederationDatabaseError; use crate::federation::types::*; -pub async fn get>( - _conf: &Config, - db: &DatabaseConnection, - id: I, -) -> Result, FederationDatabaseError> { - let select = DbFederatedMapping::find_by_id(id.as_ref()); - - let entry: Option = select.one(db).await?; - entry.map(TryInto::try_into).transpose() -} +mod create; +mod delete; +mod get; +mod list; +mod update; -pub async fn list( - _conf: &Config, - db: &DatabaseConnection, - params: &MappingListParameters, -) -> Result, FederationDatabaseError> { - let mut select = DbFederatedMapping::find(); - - if let Some(val) = ¶ms.name { - select = select.filter(db_federated_mapping::Column::Name.eq(val)); - } - - if let Some(val) = ¶ms.domain_id { - select = select.filter(db_federated_mapping::Column::DomainId.eq(val)); - } - - if let Some(val) = ¶ms.idp_id { - select = select.filter(db_federated_mapping::Column::IdpId.eq(val)); - } - - if let Some(val) = ¶ms.r#type { - select = select.filter(db_federated_mapping::Column::r#Type.eq(db_mapping_type::from(val))); - } - - let db_entities: Vec = select.all(db).await?; - let results: Result, _> = db_entities - .into_iter() - .map(TryInto::::try_into) - .collect(); - - results -} +pub use create::create; +pub use delete::delete; +pub use get::get; +pub use list::list; +pub use update::update; impl From for db_mapping_type { fn from(value: MappingType) -> db_mapping_type { @@ -75,6 +38,7 @@ impl From for db_mapping_type { } } } + impl From<&MappingType> for db_mapping_type { fn from(value: &MappingType) -> db_mapping_type { match value { @@ -84,165 +48,6 @@ impl From<&MappingType> for db_mapping_type { } } -pub async fn create( - _conf: &Config, - db: &DatabaseConnection, - mapping: Mapping, -) -> Result { - let entry = db_federated_mapping::ActiveModel { - id: Set(mapping.id.clone()), - domain_id: Set(mapping.domain_id.clone()), - name: Set(mapping.name.clone()), - idp_id: Set(mapping.idp_id.clone()), - r#type: Set(mapping.r#type.into()), - allowed_redirect_uris: mapping - .allowed_redirect_uris - .clone() - .map(|x| Set(x.join(","))) - .unwrap_or(NotSet) - .into(), - user_id_claim: Set(mapping.user_id_claim.clone()), - user_name_claim: Set(mapping.user_name_claim.clone()), - domain_id_claim: mapping - .domain_id_claim - .clone() - .map(Set) - .unwrap_or(NotSet) - .into(), - groups_claim: mapping - .groups_claim - .clone() - .map(Set) - .unwrap_or(NotSet) - .into(), - bound_audiences: mapping - .bound_audiences - .clone() - .map(|x| Set(x.join(","))) - .unwrap_or(NotSet) - .into(), - bound_subject: mapping - .bound_subject - .clone() - .map(Set) - .unwrap_or(NotSet) - .into(), - bound_claims: mapping - .bound_claims - .clone() - .map(|x| Set(Some(x))) - .unwrap_or(NotSet), - oidc_scopes: mapping - .oidc_scopes - .clone() - .map(|x| Set(x.join(","))) - .unwrap_or(NotSet) - .into(), - token_user_id: mapping - .token_user_id - .clone() - .map(Set) - .unwrap_or(NotSet) - .into(), - token_role_ids: mapping - .token_role_ids - .clone() - .map(|x| Set(x.join(","))) - .unwrap_or(NotSet) - .into(), - token_project_id: mapping - .token_project_id - .clone() - .map(Set) - .unwrap_or(NotSet) - .into(), - }; - - let db_entry: db_federated_mapping::Model = entry.insert(db).await?; - - db_entry.try_into() -} - -pub async fn update>( - _conf: &Config, - db: &DatabaseConnection, - id: S, - mapping: MappingUpdate, -) -> Result { - if let Some(current) = DbFederatedMapping::find_by_id(id.as_ref()).one(db).await? { - let mut entry: db_federated_mapping::ActiveModel = current.into(); - if let Some(val) = mapping.name { - entry.name = Set(val.to_owned()); - } - if let Some(val) = mapping.idp_id { - entry.idp_id = Set(val.to_owned()); - } - if let Some(val) = mapping.r#type { - entry.r#type = Set(val.into()); - } - if let Some(val) = mapping.allowed_redirect_uris { - entry.allowed_redirect_uris = Set(val.clone().map(|x| x.join(","))); - } - if let Some(val) = mapping.user_id_claim { - entry.user_id_claim = Set(val.to_owned()); - } - if let Some(val) = mapping.user_name_claim { - entry.user_name_claim = Set(val.to_owned()); - } - if let Some(val) = mapping.domain_id_claim { - entry.domain_id_claim = Set(Some(val.to_owned())); - } - if let Some(val) = mapping.groups_claim { - entry.groups_claim = Set(val.to_owned()); - } - if let Some(val) = mapping.bound_audiences { - entry.bound_audiences = Set(val.clone().map(|x| x.join(","))); - } - if let Some(val) = mapping.bound_subject { - entry.bound_subject = Set(val.to_owned()); - } - if let Some(val) = &mapping.bound_claims { - entry.bound_claims = Set(Some(val.clone())); - } - if let Some(val) = mapping.oidc_scopes { - entry.oidc_scopes = Set(val.clone().map(|x| x.join(","))); - } - if let Some(val) = mapping.token_user_id { - entry.token_user_id = Set(val.to_owned()); - } - if let Some(val) = mapping.token_role_ids { - entry.token_role_ids = Set(val.clone().map(|x| x.join(","))); - } - if let Some(val) = mapping.token_project_id { - entry.token_project_id = Set(val.to_owned()); - } - - let db_entry: db_federated_mapping::Model = entry.update(db).await?; - db_entry.try_into() - } else { - Err(FederationDatabaseError::MappingNotFound( - id.as_ref().to_string(), - )) - } -} - -pub async fn delete>( - _conf: &Config, - db: &DatabaseConnection, - id: S, -) -> Result<(), FederationDatabaseError> { - let res = DbFederatedMapping::delete_by_id(id.as_ref()) - .exec(db) - .await?; - if res.rows_affected == 1 { - Ok(()) - } else { - Err(FederationDatabaseError::MappingNotFound( - id.as_ref().to_string(), - )) - } -} - impl TryFrom for Mapping { type Error = FederationDatabaseError; @@ -304,15 +109,12 @@ impl TryFrom for Mapping { #[cfg(test)] mod tests { - use sea_orm::{DatabaseBackend, MockDatabase, MockExecResult, Transaction}; - use serde_json::json; - use crate::config::Config; use crate::db::entity::federated_mapping; use super::*; - fn get_mapping_mock>(id: S) -> federated_mapping::Model { + pub(super) fn get_mapping_mock>(id: S) -> federated_mapping::Model { federated_mapping::Model { id: id.as_ref().into(), name: "name".into(), @@ -333,242 +135,4 @@ mod tests { token_project_id: None, } } - - #[tokio::test] - async fn test_get() { - let db = MockDatabase::new(DatabaseBackend::Postgres) - .append_query_results([vec![get_mapping_mock("1")]]) - .into_connection(); - let config = Config::default(); - assert_eq!( - get(&config, &db, "1").await.unwrap().unwrap(), - Mapping { - id: "1".into(), - name: "name".into(), - domain_id: Some("did".into()), - idp_id: "idp".into(), - user_id_claim: "sub".into(), - user_name_claim: "preferred_username".into(), - domain_id_claim: Some("domain_id".into()), - ..Default::default() - } - ); - - assert_eq!( - db.into_transaction_log(), - [Transaction::from_sql_and_values( - DatabaseBackend::Postgres, - r#"SELECT "federated_mapping"."id", "federated_mapping"."name", "federated_mapping"."idp_id", "federated_mapping"."domain_id", CAST("federated_mapping"."type" AS "text"), "federated_mapping"."allowed_redirect_uris", "federated_mapping"."user_id_claim", "federated_mapping"."user_name_claim", "federated_mapping"."domain_id_claim", "federated_mapping"."groups_claim", "federated_mapping"."bound_audiences", "federated_mapping"."bound_subject", "federated_mapping"."bound_claims", "federated_mapping"."oidc_scopes", "federated_mapping"."token_user_id", "federated_mapping"."token_role_ids", "federated_mapping"."token_project_id" FROM "federated_mapping" WHERE "federated_mapping"."id" = $1 LIMIT $2"#, - ["1".into(), 1u64.into()] - ),] - ); - } - - #[tokio::test] - async fn test_list() { - let db = MockDatabase::new(DatabaseBackend::Postgres) - .append_query_results([vec![get_mapping_mock("1")]]) - .append_query_results([vec![get_mapping_mock("1")]]) - .into_connection(); - let config = Config::default(); - assert!( - list(&config, &db, &MappingListParameters::default()) - .await - .is_ok() - ); - assert_eq!( - list( - &config, - &db, - &MappingListParameters { - name: Some("mapping_name".into()), - domain_id: Some("did".into()), - idp_id: Some("idp".into()), - r#type: Some(MappingType::Jwt) - } - ) - .await - .unwrap(), - vec![Mapping { - id: "1".into(), - name: "name".into(), - domain_id: Some("did".into()), - idp_id: "idp".into(), - user_id_claim: "sub".into(), - user_name_claim: "preferred_username".into(), - domain_id_claim: Some("domain_id".into()), - ..Default::default() - }] - ); - - assert_eq!( - db.into_transaction_log(), - [ - Transaction::from_sql_and_values( - DatabaseBackend::Postgres, - r#"SELECT "federated_mapping"."id", "federated_mapping"."name", "federated_mapping"."idp_id", "federated_mapping"."domain_id", CAST("federated_mapping"."type" AS "text"), "federated_mapping"."allowed_redirect_uris", "federated_mapping"."user_id_claim", "federated_mapping"."user_name_claim", "federated_mapping"."domain_id_claim", "federated_mapping"."groups_claim", "federated_mapping"."bound_audiences", "federated_mapping"."bound_subject", "federated_mapping"."bound_claims", "federated_mapping"."oidc_scopes", "federated_mapping"."token_user_id", "federated_mapping"."token_role_ids", "federated_mapping"."token_project_id" FROM "federated_mapping""#, - [] - ), - Transaction::from_sql_and_values( - DatabaseBackend::Postgres, - r#"SELECT "federated_mapping"."id", "federated_mapping"."name", "federated_mapping"."idp_id", "federated_mapping"."domain_id", CAST("federated_mapping"."type" AS "text"), "federated_mapping"."allowed_redirect_uris", "federated_mapping"."user_id_claim", "federated_mapping"."user_name_claim", "federated_mapping"."domain_id_claim", "federated_mapping"."groups_claim", "federated_mapping"."bound_audiences", "federated_mapping"."bound_subject", "federated_mapping"."bound_claims", "federated_mapping"."oidc_scopes", "federated_mapping"."token_user_id", "federated_mapping"."token_role_ids", "federated_mapping"."token_project_id" FROM "federated_mapping" WHERE "federated_mapping"."name" = $1 AND "federated_mapping"."domain_id" = $2 AND "federated_mapping"."idp_id" = $3 AND "federated_mapping"."type" = (CAST($4 AS "federated_mapping_type"))"#, - [ - "mapping_name".into(), - "did".into(), - "idp".into(), - "jwt".into() - ] - ), - ] - ); - } - - #[tokio::test] - async fn test_create() { - let db = MockDatabase::new(DatabaseBackend::Postgres) - .append_query_results([vec![get_mapping_mock("1")]]) - .into_connection(); - let config = Config::default(); - - let req = Mapping { - id: "1".into(), - name: "mapping".into(), - domain_id: Some("foo_domain".into()), - r#type: MappingType::default(), - idp_id: "idp".into(), - allowed_redirect_uris: Some(vec!["url".into()]), - user_id_claim: "sub".into(), - user_name_claim: "preferred_username".into(), - domain_id_claim: Some("domain_id".into()), - groups_claim: Some("groups".into()), - bound_audiences: Some(vec!["a1".into(), "a2".into()]), - bound_subject: Some("subject".into()), - bound_claims: Some(json!({"department": "foo"})), - //claim_mappings: Some(json!({"foo": "bar"})), - oidc_scopes: Some(vec!["oidc".into(), "oauth".into()]), - token_user_id: Some("uid".into()), - token_role_ids: Some(vec!["r1".into(), "r2".into()]), - token_project_id: Some("pid".into()), - }; - - assert_eq!( - create(&config, &db, req).await.unwrap(), - get_mapping_mock("1").try_into().unwrap() - ); - assert_eq!( - db.into_transaction_log(), - [Transaction::from_sql_and_values( - DatabaseBackend::Postgres, - r#"INSERT INTO "federated_mapping" ("id", "name", "idp_id", "domain_id", "type", "allowed_redirect_uris", "user_id_claim", "user_name_claim", "domain_id_claim", "groups_claim", "bound_audiences", "bound_subject", "bound_claims", "oidc_scopes", "token_user_id", "token_role_ids", "token_project_id") VALUES ($1, $2, $3, $4, CAST($5 AS "federated_mapping_type"), $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17) RETURNING "id", "name", "idp_id", "domain_id", CAST("type" AS "text"), "allowed_redirect_uris", "user_id_claim", "user_name_claim", "domain_id_claim", "groups_claim", "bound_audiences", "bound_subject", "bound_claims", "oidc_scopes", "token_user_id", "token_role_ids", "token_project_id""#, - [ - "1".into(), - "mapping".into(), - "idp".into(), - "foo_domain".into(), - "oidc".into(), - "url".into(), - "sub".into(), - "preferred_username".into(), - "domain_id".into(), - "groups".into(), - "a1,a2".into(), - "subject".into(), - json!({"department": "foo"}).into(), - "oidc,oauth".into(), - "uid".into(), - "r1,r2".into(), - "pid".into(), - ] - ),] - ); - } - - #[tokio::test] - async fn test_update() { - let db = MockDatabase::new(DatabaseBackend::Postgres) - .append_query_results([vec![get_mapping_mock("1")], vec![get_mapping_mock("1")]]) - .append_exec_results([MockExecResult { - rows_affected: 1, - ..Default::default() - }]) - .into_connection(); - let config = Config::default(); - - let req = MappingUpdate { - name: Some("name".into()), - idp_id: Some("idp".into()), - r#type: MappingType::default().into(), - allowed_redirect_uris: Some(Some(vec!["url".into()])), - user_id_claim: Some("sub".into()), - user_name_claim: Some("preferred_username".into()), - domain_id_claim: Some("domain_id".into()), - groups_claim: Some(Some("groups".into())), - bound_audiences: Some(Some(vec!["a1".into(), "a2".into()])), - bound_subject: Some(Some("subject".into())), - bound_claims: Some(json!({"department": "foo"})), - //claim_mappings: Some(json!({"foo": "bar"})), - oidc_scopes: Some(Some(vec!["oidc".into(), "oauth".into()])), - token_user_id: Some(Some("uid".into())), - token_role_ids: Some(Some(vec!["r1".into(), "r2".into()])), - token_project_id: Some(Some("pid".into())), - }; - - assert_eq!( - update(&config, &db, "1", req).await.unwrap(), - get_mapping_mock("1").try_into().unwrap() - ); - assert_eq!( - db.into_transaction_log(), - [ - Transaction::from_sql_and_values( - DatabaseBackend::Postgres, - r#"SELECT "federated_mapping"."id", "federated_mapping"."name", "federated_mapping"."idp_id", "federated_mapping"."domain_id", CAST("federated_mapping"."type" AS "text"), "federated_mapping"."allowed_redirect_uris", "federated_mapping"."user_id_claim", "federated_mapping"."user_name_claim", "federated_mapping"."domain_id_claim", "federated_mapping"."groups_claim", "federated_mapping"."bound_audiences", "federated_mapping"."bound_subject", "federated_mapping"."bound_claims", "federated_mapping"."oidc_scopes", "federated_mapping"."token_user_id", "federated_mapping"."token_role_ids", "federated_mapping"."token_project_id" FROM "federated_mapping" WHERE "federated_mapping"."id" = $1 LIMIT $2"#, - ["1".into(), 1u64.into()] - ), - Transaction::from_sql_and_values( - DatabaseBackend::Postgres, - r#"UPDATE "federated_mapping" SET "name" = $1, "idp_id" = $2, "type" = CAST($3 AS "federated_mapping_type"), "allowed_redirect_uris" = $4, "user_id_claim" = $5, "user_name_claim" = $6, "domain_id_claim" = $7, "groups_claim" = $8, "bound_audiences" = $9, "bound_subject" = $10, "bound_claims" = $11, "oidc_scopes" = $12, "token_user_id" = $13, "token_role_ids" = $14, "token_project_id" = $15 WHERE "federated_mapping"."id" = $16 RETURNING "id", "name", "idp_id", "domain_id", CAST("type" AS "text"), "allowed_redirect_uris", "user_id_claim", "user_name_claim", "domain_id_claim", "groups_claim", "bound_audiences", "bound_subject", "bound_claims", "oidc_scopes", "token_user_id", "token_role_ids", "token_project_id""#, - [ - "name".into(), - "idp".into(), - "oidc".into(), - "url".into(), - "sub".into(), - "preferred_username".into(), - "domain_id".into(), - "groups".into(), - "a1,a2".into(), - "subject".into(), - json!({"department": "foo"}).into(), - "oidc,oauth".into(), - "uid".into(), - "r1,r2".into(), - "pid".into(), - "1".into() - ] - ), - ] - ); - } - - #[tokio::test] - async fn test_delete() { - let db = MockDatabase::new(DatabaseBackend::Postgres) - .append_exec_results([MockExecResult { - rows_affected: 1, - ..Default::default() - }]) - .into_connection(); - let config = Config::default(); - - delete(&config, &db, "id").await.unwrap(); - assert_eq!( - db.into_transaction_log(), - [Transaction::from_sql_and_values( - DatabaseBackend::Postgres, - r#"DELETE FROM "federated_mapping" WHERE "federated_mapping"."id" = $1"#, - ["id".into()] - ),] - ); - } } diff --git a/src/federation/backends/sql/mapping/create.rs b/src/federation/backends/sql/mapping/create.rs new file mode 100644 index 00000000..7ce8879f --- /dev/null +++ b/src/federation/backends/sql/mapping/create.rs @@ -0,0 +1,171 @@ +// 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::DatabaseConnection; +use sea_orm::entity::*; + +use crate::config::Config; +use crate::db::entity::federated_mapping as db_federated_mapping; +use crate::federation::backends::error::FederationDatabaseError; +use crate::federation::types::*; + +pub async fn create( + _conf: &Config, + db: &DatabaseConnection, + mapping: Mapping, +) -> Result { + let entry = db_federated_mapping::ActiveModel { + id: Set(mapping.id.clone()), + domain_id: Set(mapping.domain_id.clone()), + name: Set(mapping.name.clone()), + idp_id: Set(mapping.idp_id.clone()), + r#type: Set(mapping.r#type.into()), + allowed_redirect_uris: mapping + .allowed_redirect_uris + .clone() + .map(|x| Set(x.join(","))) + .unwrap_or(NotSet) + .into(), + user_id_claim: Set(mapping.user_id_claim.clone()), + user_name_claim: Set(mapping.user_name_claim.clone()), + domain_id_claim: mapping + .domain_id_claim + .clone() + .map(Set) + .unwrap_or(NotSet) + .into(), + groups_claim: mapping + .groups_claim + .clone() + .map(Set) + .unwrap_or(NotSet) + .into(), + bound_audiences: mapping + .bound_audiences + .clone() + .map(|x| Set(x.join(","))) + .unwrap_or(NotSet) + .into(), + bound_subject: mapping + .bound_subject + .clone() + .map(Set) + .unwrap_or(NotSet) + .into(), + bound_claims: mapping + .bound_claims + .clone() + .map(|x| Set(Some(x))) + .unwrap_or(NotSet), + oidc_scopes: mapping + .oidc_scopes + .clone() + .map(|x| Set(x.join(","))) + .unwrap_or(NotSet) + .into(), + token_user_id: mapping + .token_user_id + .clone() + .map(Set) + .unwrap_or(NotSet) + .into(), + token_role_ids: mapping + .token_role_ids + .clone() + .map(|x| Set(x.join(","))) + .unwrap_or(NotSet) + .into(), + token_project_id: mapping + .token_project_id + .clone() + .map(Set) + .unwrap_or(NotSet) + .into(), + }; + + let db_entry: db_federated_mapping::Model = entry.insert(db).await?; + + db_entry.try_into() +} + +#[cfg(test)] +mod tests { + use sea_orm::{DatabaseBackend, MockDatabase, Transaction}; + use serde_json::json; + + use crate::config::Config; + + use super::super::tests::get_mapping_mock; + use super::*; + + #[tokio::test] + async fn test_create() { + let db = MockDatabase::new(DatabaseBackend::Postgres) + .append_query_results([vec![get_mapping_mock("1")]]) + .into_connection(); + let config = Config::default(); + + let req = Mapping { + id: "1".into(), + name: "mapping".into(), + domain_id: Some("foo_domain".into()), + r#type: MappingType::default(), + idp_id: "idp".into(), + allowed_redirect_uris: Some(vec!["url".into()]), + user_id_claim: "sub".into(), + user_name_claim: "preferred_username".into(), + domain_id_claim: Some("domain_id".into()), + groups_claim: Some("groups".into()), + bound_audiences: Some(vec!["a1".into(), "a2".into()]), + bound_subject: Some("subject".into()), + bound_claims: Some(json!({"department": "foo"})), + //claim_mappings: Some(json!({"foo": "bar"})), + oidc_scopes: Some(vec!["oidc".into(), "oauth".into()]), + token_user_id: Some("uid".into()), + token_role_ids: Some(vec!["r1".into(), "r2".into()]), + token_project_id: Some("pid".into()), + }; + + assert_eq!( + create(&config, &db, req).await.unwrap(), + get_mapping_mock("1").try_into().unwrap() + ); + assert_eq!( + db.into_transaction_log(), + [Transaction::from_sql_and_values( + DatabaseBackend::Postgres, + r#"INSERT INTO "federated_mapping" ("id", "name", "idp_id", "domain_id", "type", "allowed_redirect_uris", "user_id_claim", "user_name_claim", "domain_id_claim", "groups_claim", "bound_audiences", "bound_subject", "bound_claims", "oidc_scopes", "token_user_id", "token_role_ids", "token_project_id") VALUES ($1, $2, $3, $4, CAST($5 AS "federated_mapping_type"), $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17) RETURNING "id", "name", "idp_id", "domain_id", CAST("type" AS "text"), "allowed_redirect_uris", "user_id_claim", "user_name_claim", "domain_id_claim", "groups_claim", "bound_audiences", "bound_subject", "bound_claims", "oidc_scopes", "token_user_id", "token_role_ids", "token_project_id""#, + [ + "1".into(), + "mapping".into(), + "idp".into(), + "foo_domain".into(), + "oidc".into(), + "url".into(), + "sub".into(), + "preferred_username".into(), + "domain_id".into(), + "groups".into(), + "a1,a2".into(), + "subject".into(), + json!({"department": "foo"}).into(), + "oidc,oauth".into(), + "uid".into(), + "r1,r2".into(), + "pid".into(), + ] + ),] + ); + } +} diff --git a/src/federation/backends/sql/mapping/delete.rs b/src/federation/backends/sql/mapping/delete.rs new file mode 100644 index 00000000..257529f5 --- /dev/null +++ b/src/federation/backends/sql/mapping/delete.rs @@ -0,0 +1,67 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +use sea_orm::DatabaseConnection; +use sea_orm::entity::*; + +use crate::config::Config; +use crate::db::entity::prelude::FederatedMapping as DbFederatedMapping; +use crate::federation::backends::error::FederationDatabaseError; + +pub async fn delete>( + _conf: &Config, + db: &DatabaseConnection, + id: S, +) -> Result<(), FederationDatabaseError> { + let res = DbFederatedMapping::delete_by_id(id.as_ref()) + .exec(db) + .await?; + if res.rows_affected == 1 { + Ok(()) + } else { + Err(FederationDatabaseError::MappingNotFound( + id.as_ref().to_string(), + )) + } +} + +#[cfg(test)] +mod tests { + use sea_orm::{DatabaseBackend, MockDatabase, MockExecResult, Transaction}; + + use crate::config::Config; + + use super::*; + + #[tokio::test] + async fn test_delete() { + let db = MockDatabase::new(DatabaseBackend::Postgres) + .append_exec_results([MockExecResult { + rows_affected: 1, + ..Default::default() + }]) + .into_connection(); + let config = Config::default(); + + delete(&config, &db, "id").await.unwrap(); + assert_eq!( + db.into_transaction_log(), + [Transaction::from_sql_and_values( + DatabaseBackend::Postgres, + r#"DELETE FROM "federated_mapping" WHERE "federated_mapping"."id" = $1"#, + ["id".into()] + ),] + ); + } +} diff --git a/src/federation/backends/sql/mapping/get.rs b/src/federation/backends/sql/mapping/get.rs new file mode 100644 index 00000000..94207334 --- /dev/null +++ b/src/federation/backends/sql/mapping/get.rs @@ -0,0 +1,74 @@ +// 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::DatabaseConnection; +use sea_orm::entity::*; + +use crate::config::Config; +use crate::db::entity::{ + federated_mapping as db_federated_mapping, prelude::FederatedMapping as DbFederatedMapping, +}; +use crate::federation::backends::error::FederationDatabaseError; +use crate::federation::types::*; + +pub async fn get>( + _conf: &Config, + db: &DatabaseConnection, + id: I, +) -> Result, FederationDatabaseError> { + let select = DbFederatedMapping::find_by_id(id.as_ref()); + + let entry: Option = select.one(db).await?; + entry.map(TryInto::try_into).transpose() +} + +#[cfg(test)] +mod tests { + use sea_orm::{DatabaseBackend, MockDatabase, Transaction}; + + use crate::config::Config; + + use super::super::tests::get_mapping_mock; + use super::*; + + #[tokio::test] + async fn test_get() { + let db = MockDatabase::new(DatabaseBackend::Postgres) + .append_query_results([vec![get_mapping_mock("1")]]) + .into_connection(); + let config = Config::default(); + assert_eq!( + get(&config, &db, "1").await.unwrap().unwrap(), + Mapping { + id: "1".into(), + name: "name".into(), + domain_id: Some("did".into()), + idp_id: "idp".into(), + user_id_claim: "sub".into(), + user_name_claim: "preferred_username".into(), + domain_id_claim: Some("domain_id".into()), + ..Default::default() + } + ); + + assert_eq!( + db.into_transaction_log(), + [Transaction::from_sql_and_values( + DatabaseBackend::Postgres, + r#"SELECT "federated_mapping"."id", "federated_mapping"."name", "federated_mapping"."idp_id", "federated_mapping"."domain_id", CAST("federated_mapping"."type" AS "text"), "federated_mapping"."allowed_redirect_uris", "federated_mapping"."user_id_claim", "federated_mapping"."user_name_claim", "federated_mapping"."domain_id_claim", "federated_mapping"."groups_claim", "federated_mapping"."bound_audiences", "federated_mapping"."bound_subject", "federated_mapping"."bound_claims", "federated_mapping"."oidc_scopes", "federated_mapping"."token_user_id", "federated_mapping"."token_role_ids", "federated_mapping"."token_project_id" FROM "federated_mapping" WHERE "federated_mapping"."id" = $1 LIMIT $2"#, + ["1".into(), 1u64.into()] + ),] + ); + } +} diff --git a/src/federation/backends/sql/mapping/list.rs b/src/federation/backends/sql/mapping/list.rs new file mode 100644 index 00000000..87685d04 --- /dev/null +++ b/src/federation/backends/sql/mapping/list.rs @@ -0,0 +1,126 @@ +// 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::DatabaseConnection; +use sea_orm::entity::*; +use sea_orm::query::*; + +use crate::config::Config; +use crate::db::entity::{ + federated_mapping as db_federated_mapping, prelude::FederatedMapping as DbFederatedMapping, + sea_orm_active_enums::MappingType as db_mapping_type, +}; +use crate::federation::backends::error::FederationDatabaseError; +use crate::federation::types::*; + +pub async fn list( + _conf: &Config, + db: &DatabaseConnection, + params: &MappingListParameters, +) -> Result, FederationDatabaseError> { + let mut select = DbFederatedMapping::find(); + + if let Some(val) = ¶ms.name { + select = select.filter(db_federated_mapping::Column::Name.eq(val)); + } + + if let Some(val) = ¶ms.domain_id { + select = select.filter(db_federated_mapping::Column::DomainId.eq(val)); + } + + if let Some(val) = ¶ms.idp_id { + select = select.filter(db_federated_mapping::Column::IdpId.eq(val)); + } + + if let Some(val) = ¶ms.r#type { + select = select.filter(db_federated_mapping::Column::r#Type.eq(db_mapping_type::from(val))); + } + + let db_entities: Vec = select.all(db).await?; + let results: Result, _> = db_entities + .into_iter() + .map(TryInto::::try_into) + .collect(); + + results +} + +#[cfg(test)] +mod tests { + use sea_orm::{DatabaseBackend, MockDatabase, Transaction}; + + use crate::config::Config; + + use super::super::tests::get_mapping_mock; + use super::*; + + #[tokio::test] + async fn test_list() { + let db = MockDatabase::new(DatabaseBackend::Postgres) + .append_query_results([vec![get_mapping_mock("1")]]) + .append_query_results([vec![get_mapping_mock("1")]]) + .into_connection(); + let config = Config::default(); + assert!( + list(&config, &db, &MappingListParameters::default()) + .await + .is_ok() + ); + assert_eq!( + list( + &config, + &db, + &MappingListParameters { + name: Some("mapping_name".into()), + domain_id: Some("did".into()), + idp_id: Some("idp".into()), + r#type: Some(MappingType::Jwt) + } + ) + .await + .unwrap(), + vec![Mapping { + id: "1".into(), + name: "name".into(), + domain_id: Some("did".into()), + idp_id: "idp".into(), + user_id_claim: "sub".into(), + user_name_claim: "preferred_username".into(), + domain_id_claim: Some("domain_id".into()), + ..Default::default() + }] + ); + + assert_eq!( + db.into_transaction_log(), + [ + Transaction::from_sql_and_values( + DatabaseBackend::Postgres, + r#"SELECT "federated_mapping"."id", "federated_mapping"."name", "federated_mapping"."idp_id", "federated_mapping"."domain_id", CAST("federated_mapping"."type" AS "text"), "federated_mapping"."allowed_redirect_uris", "federated_mapping"."user_id_claim", "federated_mapping"."user_name_claim", "federated_mapping"."domain_id_claim", "federated_mapping"."groups_claim", "federated_mapping"."bound_audiences", "federated_mapping"."bound_subject", "federated_mapping"."bound_claims", "federated_mapping"."oidc_scopes", "federated_mapping"."token_user_id", "federated_mapping"."token_role_ids", "federated_mapping"."token_project_id" FROM "federated_mapping""#, + [] + ), + Transaction::from_sql_and_values( + DatabaseBackend::Postgres, + r#"SELECT "federated_mapping"."id", "federated_mapping"."name", "federated_mapping"."idp_id", "federated_mapping"."domain_id", CAST("federated_mapping"."type" AS "text"), "federated_mapping"."allowed_redirect_uris", "federated_mapping"."user_id_claim", "federated_mapping"."user_name_claim", "federated_mapping"."domain_id_claim", "federated_mapping"."groups_claim", "federated_mapping"."bound_audiences", "federated_mapping"."bound_subject", "federated_mapping"."bound_claims", "federated_mapping"."oidc_scopes", "federated_mapping"."token_user_id", "federated_mapping"."token_role_ids", "federated_mapping"."token_project_id" FROM "federated_mapping" WHERE "federated_mapping"."name" = $1 AND "federated_mapping"."domain_id" = $2 AND "federated_mapping"."idp_id" = $3 AND "federated_mapping"."type" = (CAST($4 AS "federated_mapping_type"))"#, + [ + "mapping_name".into(), + "did".into(), + "idp".into(), + "jwt".into() + ] + ), + ] + ); + } +} diff --git a/src/federation/backends/sql/mapping/update.rs b/src/federation/backends/sql/mapping/update.rs new file mode 100644 index 00000000..b31a6ba4 --- /dev/null +++ b/src/federation/backends/sql/mapping/update.rs @@ -0,0 +1,165 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +use sea_orm::DatabaseConnection; +use sea_orm::entity::*; + +use crate::config::Config; +use crate::db::entity::{ + federated_mapping as db_federated_mapping, prelude::FederatedMapping as DbFederatedMapping, +}; +use crate::federation::backends::error::FederationDatabaseError; +use crate::federation::types::*; + +pub async fn update>( + _conf: &Config, + db: &DatabaseConnection, + id: S, + mapping: MappingUpdate, +) -> Result { + if let Some(current) = DbFederatedMapping::find_by_id(id.as_ref()).one(db).await? { + let mut entry: db_federated_mapping::ActiveModel = current.into(); + if let Some(val) = mapping.name { + entry.name = Set(val.to_owned()); + } + if let Some(val) = mapping.idp_id { + entry.idp_id = Set(val.to_owned()); + } + if let Some(val) = mapping.r#type { + entry.r#type = Set(val.into()); + } + if let Some(val) = mapping.allowed_redirect_uris { + entry.allowed_redirect_uris = Set(val.clone().map(|x| x.join(","))); + } + if let Some(val) = mapping.user_id_claim { + entry.user_id_claim = Set(val.to_owned()); + } + if let Some(val) = mapping.user_name_claim { + entry.user_name_claim = Set(val.to_owned()); + } + if let Some(val) = mapping.domain_id_claim { + entry.domain_id_claim = Set(Some(val.to_owned())); + } + if let Some(val) = mapping.groups_claim { + entry.groups_claim = Set(val.to_owned()); + } + if let Some(val) = mapping.bound_audiences { + entry.bound_audiences = Set(val.clone().map(|x| x.join(","))); + } + if let Some(val) = mapping.bound_subject { + entry.bound_subject = Set(val.to_owned()); + } + if let Some(val) = &mapping.bound_claims { + entry.bound_claims = Set(Some(val.clone())); + } + if let Some(val) = mapping.oidc_scopes { + entry.oidc_scopes = Set(val.clone().map(|x| x.join(","))); + } + if let Some(val) = mapping.token_user_id { + entry.token_user_id = Set(val.to_owned()); + } + if let Some(val) = mapping.token_role_ids { + entry.token_role_ids = Set(val.clone().map(|x| x.join(","))); + } + if let Some(val) = mapping.token_project_id { + entry.token_project_id = Set(val.to_owned()); + } + + let db_entry: db_federated_mapping::Model = entry.update(db).await?; + db_entry.try_into() + } else { + Err(FederationDatabaseError::MappingNotFound( + id.as_ref().to_string(), + )) + } +} + +#[cfg(test)] +mod tests { + use sea_orm::{DatabaseBackend, MockDatabase, MockExecResult, Transaction}; + use serde_json::json; + + use crate::config::Config; + + use super::super::tests::get_mapping_mock; + use super::*; + + #[tokio::test] + async fn test_update() { + let db = MockDatabase::new(DatabaseBackend::Postgres) + .append_query_results([vec![get_mapping_mock("1")], vec![get_mapping_mock("1")]]) + .append_exec_results([MockExecResult { + rows_affected: 1, + ..Default::default() + }]) + .into_connection(); + let config = Config::default(); + + let req = MappingUpdate { + name: Some("name".into()), + idp_id: Some("idp".into()), + r#type: MappingType::default().into(), + allowed_redirect_uris: Some(Some(vec!["url".into()])), + user_id_claim: Some("sub".into()), + user_name_claim: Some("preferred_username".into()), + domain_id_claim: Some("domain_id".into()), + groups_claim: Some(Some("groups".into())), + bound_audiences: Some(Some(vec!["a1".into(), "a2".into()])), + bound_subject: Some(Some("subject".into())), + bound_claims: Some(json!({"department": "foo"})), + //claim_mappings: Some(json!({"foo": "bar"})), + oidc_scopes: Some(Some(vec!["oidc".into(), "oauth".into()])), + token_user_id: Some(Some("uid".into())), + token_role_ids: Some(Some(vec!["r1".into(), "r2".into()])), + token_project_id: Some(Some("pid".into())), + }; + + assert_eq!( + update(&config, &db, "1", req).await.unwrap(), + get_mapping_mock("1").try_into().unwrap() + ); + assert_eq!( + db.into_transaction_log(), + [ + Transaction::from_sql_and_values( + DatabaseBackend::Postgres, + r#"SELECT "federated_mapping"."id", "federated_mapping"."name", "federated_mapping"."idp_id", "federated_mapping"."domain_id", CAST("federated_mapping"."type" AS "text"), "federated_mapping"."allowed_redirect_uris", "federated_mapping"."user_id_claim", "federated_mapping"."user_name_claim", "federated_mapping"."domain_id_claim", "federated_mapping"."groups_claim", "federated_mapping"."bound_audiences", "federated_mapping"."bound_subject", "federated_mapping"."bound_claims", "federated_mapping"."oidc_scopes", "federated_mapping"."token_user_id", "federated_mapping"."token_role_ids", "federated_mapping"."token_project_id" FROM "federated_mapping" WHERE "federated_mapping"."id" = $1 LIMIT $2"#, + ["1".into(), 1u64.into()] + ), + Transaction::from_sql_and_values( + DatabaseBackend::Postgres, + r#"UPDATE "federated_mapping" SET "name" = $1, "idp_id" = $2, "type" = CAST($3 AS "federated_mapping_type"), "allowed_redirect_uris" = $4, "user_id_claim" = $5, "user_name_claim" = $6, "domain_id_claim" = $7, "groups_claim" = $8, "bound_audiences" = $9, "bound_subject" = $10, "bound_claims" = $11, "oidc_scopes" = $12, "token_user_id" = $13, "token_role_ids" = $14, "token_project_id" = $15 WHERE "federated_mapping"."id" = $16 RETURNING "id", "name", "idp_id", "domain_id", CAST("type" AS "text"), "allowed_redirect_uris", "user_id_claim", "user_name_claim", "domain_id_claim", "groups_claim", "bound_audiences", "bound_subject", "bound_claims", "oidc_scopes", "token_user_id", "token_role_ids", "token_project_id""#, + [ + "name".into(), + "idp".into(), + "oidc".into(), + "url".into(), + "sub".into(), + "preferred_username".into(), + "domain_id".into(), + "groups".into(), + "a1,a2".into(), + "subject".into(), + json!({"department": "foo"}).into(), + "oidc,oauth".into(), + "uid".into(), + "r1,r2".into(), + "pid".into(), + "1".into() + ] + ), + ] + ); + } +}