From eda9cd500e9533e471edf655b6340f875b90d98d Mon Sep 17 00:00:00 2001 From: "ayush.jain@juspay.in" Date: Mon, 23 Sep 2024 18:45:20 +0530 Subject: [PATCH] feat: Tenant specific config support via .cac.toml --- .env.example | 3 - Cargo.lock | 2 + Cargo.toml | 1 + crates/cac_toml/Cargo.toml | 2 +- .../src/api/config/handlers.rs | 48 ++++--- .../src/api/context/handlers.rs | 122 ++++++++-------- .../src/api/context/helpers.rs | 23 ++-- .../src/api/dimension/handlers.rs | 40 +++--- .../service_utils/src/middlewares/tenant.rs | 12 ++ crates/service_utils/src/service/types.rs | 38 ++--- crates/superposition/Cargo.toml | 2 + crates/superposition/Superposition.cac.toml | 9 ++ crates/superposition/src/app_state.rs | 89 ++++++++++++ crates/superposition/src/main.rs | 130 +++--------------- crates/superposition_types/src/lib.rs | 44 +++++- 15 files changed, 295 insertions(+), 270 deletions(-) create mode 100644 crates/superposition/Superposition.cac.toml create mode 100644 crates/superposition/src/app_state.rs diff --git a/.env.example b/.env.example index 7bd1a080..8e81a063 100644 --- a/.env.example +++ b/.env.example @@ -16,7 +16,6 @@ CAC_HOST="http://localhost:8080" API_HOSTNAME="http://localhost:8080" SUPERPOSITION_VERSION="v0.1.0" HOSTNAME="---" -MJOS_ALLOWED_ORIGINS=https://potato.in,https://onion.in,http://localhost:8080 ACTIX_KEEP_ALIVE=120 MAX_DB_CONNECTION_POOL_SIZE=3 ENABLE_TENANT_AND_SCOPE=true @@ -24,5 +23,3 @@ TENANTS=dev,test TENANT_MIDDLEWARE_EXCLUSION_LIST="/health,/assets/favicon.ico,/pkg/frontend.js,/pkg,/pkg/frontend_bg.wasm,/pkg/tailwind.css,/pkg/style.css,/assets,/admin,/" SERVICE_PREFIX="" SERVICE_NAME="CAC" -MANDATORY_DIMENSIONS="" -# MANDATORY_DIMENSIONS="dev:clientId,fare;test:fare" diff --git a/Cargo.lock b/Cargo.lock index 51638364..01f43e97 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4814,6 +4814,7 @@ dependencies = [ "blake3", "bytes", "cac_client", + "cac_toml", "chrono", "context_aware_config", "derive_more", @@ -4842,6 +4843,7 @@ dependencies = [ "strum", "strum_macros", "superposition_types", + "toml 0.8.8", "tracing-log", "urlencoding", "uuid", diff --git a/Cargo.toml b/Cargo.toml index a2c7bba5..232ed2f7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -71,6 +71,7 @@ leptos-use = "0.10.3" mime = "0.3.17" aws-sdk-kms = {version = "1.38.0"} aws-config = { version = "1.1.7", features = ["behavior-version-latest"] } +toml = { version = "0.8.8", features = ["preserve_order"] } [workspace.lints.clippy] mod_module_files = "warn" diff --git a/crates/cac_toml/Cargo.toml b/crates/cac_toml/Cargo.toml index ef48ccdb..71caa74b 100644 --- a/crates/cac_toml/Cargo.toml +++ b/crates/cac_toml/Cargo.toml @@ -17,4 +17,4 @@ path = "src/bin.rs" clap = { version = "4.3.0", features = ["derive"] } pest = "2.6" pest_derive = "2.6" -toml = { version = "0.8.8", features = ["preserve_order"] } +toml = { workspace = true } diff --git a/crates/context_aware_config/src/api/config/handlers.rs b/crates/context_aware_config/src/api/config/handlers.rs index 95e733a7..dc8793ff 100644 --- a/crates/context_aware_config/src/api/config/handlers.rs +++ b/crates/context_aware_config/src/api/config/handlers.rs @@ -1,19 +1,6 @@ use std::{collections::HashMap, str::FromStr}; -use super::helpers::{ - filter_config_by_dimensions, filter_config_by_prefix, get_query_params_map, -}; -use super::types::{Config, Context}; -use crate::api::context::{ - delete_context_api, hash, put, validate_dimensions_and_calculate_priority, PutReq, -}; -use crate::api::dimension::get_all_dimension_schema_map; -use crate::{ - db::schema::{config_versions::dsl as config_versions, event_log::dsl as event_log}, - helpers::generate_cac, -}; use actix_http::header::HeaderValue; -use actix_web::web::Data; use actix_web::{get, put, web, HttpRequest, HttpResponse, HttpResponseBuilder, Scope}; use cac_client::{eval_cac, eval_cac_with_reasoning, MergeStrategy}; use chrono::{DateTime, NaiveDateTime, TimeZone, Timelike, Utc}; @@ -22,19 +9,33 @@ use diesel::{ r2d2::{ConnectionManager, PooledConnection}, ExpressionMethods, PgConnection, QueryDsl, RunQueryDsl, }; -use serde_json::{json, Map, Value}; -use service_utils::service::types::{AppState, Tenant}; -use superposition_macros::{bad_argument, db_error, unexpected_error}; -use superposition_types::{result as superposition, Cac, Condition, Overrides, User}; - use itertools::Itertools; use jsonschema::JSONSchema; +use serde_json::{json, Map, Value}; use service_utils::{ helpers::extract_dimensions, service::types::{AppHeader, DbConnection}, }; +use superposition_macros::{bad_argument, db_error, unexpected_error}; +use superposition_types::{ + result as superposition, Cac, Condition, Overrides, TenantConfig, User, +}; use uuid::Uuid; +use crate::api::context::{ + delete_context_api, hash, put, validate_dimensions_and_calculate_priority, PutReq, +}; +use crate::api::dimension::get_all_dimension_schema_map; +use crate::{ + db::schema::{config_versions::dsl as config_versions, event_log::dsl as event_log}, + helpers::generate_cac, +}; + +use super::helpers::{ + filter_config_by_dimensions, filter_config_by_prefix, get_query_params_map, +}; +use super::types::{Config, Context}; + pub fn endpoints() -> Scope { Scope::new("") .service(get) @@ -365,8 +366,7 @@ fn construct_new_payload( async fn reduce_config_key( user: User, conn: &mut PooledConnection>, - state: &Data, - tenant: &Tenant, + tenant_config: &TenantConfig, mut og_contexts: Vec, mut og_overrides: HashMap, check_key: &str, @@ -454,7 +454,7 @@ async fn reduce_config_key( if is_approve { let _ = delete_context_api(cid.clone(), user.clone(), conn); if let Ok(put_req) = construct_new_payload(request_payload) { - let _ = put(put_req, conn, false, &user, state, tenant); + let _ = put(put_req, conn, false, &user, &tenant_config); } } @@ -497,8 +497,7 @@ async fn reduce_config( req: HttpRequest, user: User, db_conn: DbConnection, - state: Data, - tenant: Tenant, + tenant_config: TenantConfig, ) -> superposition::Result { let DbConnection(mut conn) = db_conn; let is_approve = req @@ -517,8 +516,7 @@ async fn reduce_config( config = reduce_config_key( user.clone(), &mut conn, - &state, - &tenant, + &tenant_config, contexts.clone(), overrides.clone(), key.as_str(), diff --git a/crates/context_aware_config/src/api/context/handlers.rs b/crates/context_aware_config/src/api/context/handlers.rs index 2b946dc5..7dbec67b 100644 --- a/crates/context_aware_config/src/api/context/handlers.rs +++ b/crates/context_aware_config/src/api/context/handlers.rs @@ -1,10 +1,34 @@ extern crate base64; + +use std::collections::HashMap; use std::str; -use crate::helpers::{ - add_config_version, calculate_context_priority, json_to_sorted_string, - validate_context_jsonschema, +use actix_web::{ + delete, get, post, put, + web::{Data, Json, Path, Query}, + HttpResponse, Responder, Scope, +}; +use chrono::Utc; +use diesel::{ + delete, + r2d2::{ConnectionManager, PooledConnection}, + result::{DatabaseErrorKind::*, Error::DatabaseError}, + upsert::excluded, + Connection, ExpressionMethods, PgConnection, QueryDsl, RunQueryDsl, }; +use jsonschema::{Draft, JSONSchema, ValidationError}; +use serde_json::{from_value, json, Map, Value}; +use service_utils::{ + helpers::{parse_config_tags, validation_err_to_str}, + service::types::{AppHeader, AppState, CustomHeaders, DbConnection}, +}; +use superposition_macros::{ + bad_argument, db_error, not_found, unexpected_error, validation_error, +}; +use superposition_types::{ + result as superposition, SuperpositionUser, TenantConfig, User, +}; + use crate::{ api::{ context::types::{ @@ -20,40 +44,17 @@ use crate::{ default_configs::dsl, }, }, + helpers::{ + add_config_version, calculate_context_priority, json_to_sorted_string, + validate_context_jsonschema, + }, }; -use actix_web::web::Data; -use service_utils::service::types::{AppHeader, AppState, CustomHeaders, Tenant}; - -use actix_web::{ - delete, get, post, put, - web::{Json, Path, Query}, - HttpResponse, Responder, Scope, -}; -use chrono::Utc; -use diesel::{ - delete, - r2d2::{ConnectionManager, PooledConnection}, - result::{DatabaseErrorKind::*, Error::DatabaseError}, - upsert::excluded, - Connection, ExpressionMethods, PgConnection, QueryDsl, RunQueryDsl, -}; -use jsonschema::{Draft, JSONSchema, ValidationError}; -use serde_json::{from_value, json, Map, Value}; -use service_utils::helpers::{parse_config_tags, validation_err_to_str}; -use service_utils::service::types::DbConnection; -use std::collections::HashMap; -use superposition_types::{SuperpositionUser, User}; use super::helpers::{ validate_condition_with_functions, validate_condition_with_mandatory_dimensions, validate_override_with_functions, }; -use superposition_macros::{ - bad_argument, db_error, not_found, unexpected_error, validation_error, -}; -use superposition_types::result as superposition; - pub fn endpoints() -> Scope { Scope::new("") .service(put_handler) @@ -193,8 +194,7 @@ fn create_ctx_from_put_req( req: Json, conn: &mut DBConnection, user: &User, - state: &Data, - tenant: &Tenant, + tenant_config: &TenantConfig, ) -> superposition::Result { let ctx_condition = req.context.to_owned().into_inner(); let condition_val = json!(ctx_condition); @@ -202,8 +202,7 @@ fn create_ctx_from_put_req( let ctx_override = json!(r_override.to_owned()); validate_condition_with_mandatory_dimensions( &ctx_condition, - &state.mandatory_dimensions, - tenant, + &tenant_config.mandatory_dimensions, )?; validate_override_with_default_configs(conn, &r_override)?; validate_condition_with_functions(conn, &ctx_condition)?; @@ -307,11 +306,10 @@ pub fn put( conn: &mut PooledConnection>, already_under_txn: bool, user: &User, - state: &Data, - tenant: &Tenant, + tenant_config: &TenantConfig, ) -> superposition::Result { use contexts::dsl::contexts; - let new_ctx = create_ctx_from_put_req(req, conn, user, &state, tenant)?; + let new_ctx = create_ctx_from_put_req(req, conn, user, tenant_config)?; if already_under_txn { diesel::sql_query("SAVEPOINT put_ctx_savepoint").execute(conn)?; @@ -340,11 +338,11 @@ async fn put_handler( req: Json, mut db_conn: DbConnection, user: User, - tenant: Tenant, + tenant_config: TenantConfig, ) -> superposition::Result { let tags = parse_config_tags(custom_headers.config_tags)?; db_conn.transaction::<_, superposition::AppError, _>(|transaction_conn| { - let put_response = put(req, transaction_conn, true, &user, &state, &tenant) + let put_response = put(req, transaction_conn, true, &user, &tenant_config) .map_err(|err: superposition::AppError| { log::info!("context put failed with error: {:?}", err); err @@ -365,11 +363,10 @@ fn override_helper( conn: &mut PooledConnection>, already_under_txn: bool, user: &User, - state: &Data, - tenant: Tenant, + tenant_config: &TenantConfig, ) -> superposition::Result { use contexts::dsl::contexts; - let new_ctx = create_ctx_from_put_req(req, conn, user, state, &tenant)?; + let new_ctx = create_ctx_from_put_req(req, conn, user, &tenant_config)?; if already_under_txn { diesel::sql_query("SAVEPOINT insert_ctx_savepoint").execute(conn)?; } @@ -397,12 +394,12 @@ async fn update_override_handler( req: Json, mut db_conn: DbConnection, user: User, - tenant: Tenant, + tenant_config: TenantConfig, ) -> superposition::Result { let tags = parse_config_tags(custom_headers.config_tags)?; db_conn.transaction::<_, superposition::AppError, _>(|transaction_conn| { let override_resp = - override_helper(req, transaction_conn, true, &user, &state, tenant).map_err( + override_helper(req, transaction_conn, true, &user, &tenant_config).map_err( |err: superposition::AppError| { log::info!("context put failed with error: {:?}", err); err @@ -425,8 +422,7 @@ fn r#move( conn: &mut PooledConnection>, already_under_txn: bool, user: &User, - state: &Data, - tenant: &Tenant, + tenant_config: &TenantConfig, ) -> superposition::Result { use contexts::dsl; let req = req.into_inner(); @@ -441,8 +437,7 @@ fn r#move( )?; validate_condition_with_mandatory_dimensions( &req.context.into_inner(), - &state.mandatory_dimensions, - tenant, + &tenant_config.mandatory_dimensions, )?; if priority == 0 { @@ -519,7 +514,7 @@ async fn move_handler( req: Json, mut db_conn: DbConnection, user: User, - tenant: Tenant, + tenant_config: TenantConfig, ) -> superposition::Result { let tags = parse_config_tags(custom_headers.config_tags)?; db_conn.transaction::<_, superposition::AppError, _>(|transaction_conn| { @@ -529,8 +524,7 @@ async fn move_handler( transaction_conn, true, &user, - &state, - &tenant, + &tenant_config, ) .map_err(|err| { log::info!("move api failed with error: {:?}", err); @@ -669,7 +663,7 @@ async fn bulk_operations( reqs: Json>, db_conn: DbConnection, user: User, - tenant: Tenant, + tenant_config: TenantConfig, ) -> superposition::Result { use contexts::dsl::contexts; let DbConnection(mut conn) = db_conn; @@ -680,18 +674,15 @@ async fn bulk_operations( for action in reqs.into_inner().into_iter() { match action { ContextAction::Put(put_req) => { - let put_resp = put( - Json(put_req), - transaction_conn, - true, - &user, - &state, - &tenant, - ) - .map_err(|err| { - log::error!("Failed at insert into contexts due to {:?}", err); - err - })?; + let put_resp = + put(Json(put_req), transaction_conn, true, &user, &tenant_config) + .map_err(|err| { + log::error!( + "Failed at insert into contexts due to {:?}", + err + ); + err + })?; response.push(ContextBulkResponse::Put(put_resp)); } ContextAction::Delete(ctx_id) => { @@ -725,8 +716,7 @@ async fn bulk_operations( transaction_conn, true, &user, - &state, - &tenant, + &tenant_config, ) .map_err(|err| { log::error!("Failed at moving context reponse due to {:?}", err); diff --git a/crates/context_aware_config/src/api/context/helpers.rs b/crates/context_aware_config/src/api/context/helpers.rs index c1c56468..071ea4b0 100644 --- a/crates/context_aware_config/src/api/context/helpers.rs +++ b/crates/context_aware_config/src/api/context/helpers.rs @@ -1,8 +1,15 @@ extern crate base64; + +use std::collections::HashMap; +use std::str; + use base64::prelude::*; +use diesel::{ + r2d2::{ConnectionManager, PooledConnection}, + ExpressionMethods, PgConnection, QueryDsl, RunQueryDsl, +}; +use serde_json::{Map, Value}; use service_utils::helpers::extract_dimensions; -use service_utils::service::types::Tenant; -use std::str; use superposition_macros::{unexpected_error, validation_error}; use superposition_types::{result as superposition, Condition}; @@ -15,23 +22,15 @@ use crate::{ dimensions::{self}, }, }; -use diesel::{ - r2d2::{ConnectionManager, PooledConnection}, - ExpressionMethods, PgConnection, QueryDsl, RunQueryDsl, -}; -use serde_json::{Map, Value}; -use std::collections::HashMap; + type DBConnection = PooledConnection>; pub fn validate_condition_with_mandatory_dimensions( context: &Condition, - mandatory_dimensions: &Map, - tenant: &Tenant, + mandatory_dimensions: &Vec, ) -> superposition::Result<()> { let context_map = extract_dimensions(context)?; let dimensions_list: Vec = context_map.keys().cloned().collect(); - let mandatory_dimensions = - Tenant::get_mandatory_dimensions(&tenant, mandatory_dimensions); let all_mandatory_present = mandatory_dimensions .iter() .all(|dimension| dimensions_list.contains(dimension)); diff --git a/crates/context_aware_config/src/api/dimension/handlers.rs b/crates/context_aware_config/src/api/dimension/handlers.rs index 750eb5f2..3ea20444 100644 --- a/crates/context_aware_config/src/api/dimension/handlers.rs +++ b/crates/context_aware_config/src/api/dimension/handlers.rs @@ -1,11 +1,3 @@ -use crate::{ - api::dimension::{types::CreateReq, utils::get_dimension_usage_context_ids}, - db::{ - models::Dimension, - schema::{dimensions, dimensions::dsl::*}, - }, - helpers::validate_jsonschema, -}; use actix_web::{ delete, get, put, web::{self, Data, Path}, @@ -17,10 +9,20 @@ use diesel::{ }; use jsonschema::{Draft, JSONSchema}; use serde_json::Value; +use service_utils::service::types::{AppState, DbConnection}; use superposition_macros::{bad_argument, not_found, unexpected_error}; -use superposition_types::{result as superposition, SuperpositionUser, User}; +use superposition_types::{ + result as superposition, SuperpositionUser, TenantConfig, User, +}; -use service_utils::service::types::{AppState, DbConnection, Tenant}; +use crate::{ + api::dimension::{types::CreateReq, utils::get_dimension_usage_context_ids}, + db::{ + models::Dimension, + schema::{dimensions, dimensions::dsl::*}, + }, + helpers::validate_jsonschema, +}; use super::types::{DeleteReq, DimensionWithMandatory}; @@ -37,7 +39,7 @@ async fn create( req: web::Json, user: User, db_conn: DbConnection, - tenant: Tenant, + tenant_config: TenantConfig, ) -> superposition::Result { let DbConnection(mut conn) = db_conn; @@ -88,10 +90,9 @@ async fn create( match upsert { Ok(upserted_dimension) => { - let mandatory_dimensions = - Tenant::get_mandatory_dimensions(&tenant, &state.mandatory_dimensions); - let is_mandatory = - mandatory_dimensions.contains(&upserted_dimension.dimension); + let is_mandatory = tenant_config + .mandatory_dimensions + .contains(&upserted_dimension.dimension); Ok(HttpResponse::Created().json(DimensionWithMandatory::new( upserted_dimension, is_mandatory, @@ -119,20 +120,17 @@ async fn create( #[get("")] async fn get( db_conn: DbConnection, - state: Data, - tenant: Tenant, + tenant_config: TenantConfig, ) -> superposition::Result { let DbConnection(mut conn) = db_conn; let result: Vec = dimensions.get_results(&mut conn)?; - let mandatory_dimensions = - Tenant::get_mandatory_dimensions(&tenant, &state.mandatory_dimensions); - let dimensions_with_mandatory: Vec = result .into_iter() .map(|ele| { - let is_mandatory = mandatory_dimensions.contains(&ele.dimension); + let is_mandatory = + tenant_config.mandatory_dimensions.contains(&ele.dimension); DimensionWithMandatory::new(ele, is_mandatory) }) .collect(); diff --git a/crates/service_utils/src/middlewares/tenant.rs b/crates/service_utils/src/middlewares/tenant.rs index ff8e0089..7cf76487 100644 --- a/crates/service_utils/src/middlewares/tenant.rs +++ b/crates/service_utils/src/middlewares/tenant.rs @@ -134,7 +134,19 @@ where } }; + let tenant_config = app_state + .tenant_configs + .get(&validated_tenant.0) + .cloned() + .ok_or_else(|| { + error::ErrorInternalServerError(format!( + "tenant config not found for {}", + validated_tenant.0 + )) + })?; + req.extensions_mut().insert(validated_tenant); + req.extensions_mut().insert(tenant_config); } let res = srv.call(req).await?; diff --git a/crates/service_utils/src/service/types.rs b/crates/service_utils/src/service/types.rs index 5e296b66..fb71a6be 100644 --- a/crates/service_utils/src/service/types.rs +++ b/crates/service_utils/src/service/types.rs @@ -1,19 +1,19 @@ -use crate::db::pgschema_manager::{PgSchemaConnection, PgSchemaManager}; -use derive_more::{Deref, DerefMut}; -use jsonschema::JSONSchema; -use serde_json::{json, Map, Value}; - +use std::sync::Mutex; use std::{ - collections::HashSet, + collections::{HashMap, HashSet}, future::{ready, Ready}, str::FromStr, sync::Arc, }; use actix_web::{error, web::Data, Error, FromRequest, HttpMessage}; - +use derive_more::{Deref, DerefMut}; +use jsonschema::JSONSchema; +use serde_json::json; use snowflake::SnowflakeIdGenerator; -use std::sync::Mutex; +use superposition_types::TenantConfig; + +use crate::db::pgschema_manager::{PgSchemaConnection, PgSchemaManager}; pub struct ExperimentationFlags { pub allow_same_keys_overlapping_ctx: bool, @@ -49,7 +49,7 @@ pub struct AppState { pub enable_tenant_and_scope: bool, pub tenant_middleware_exclusion_list: HashSet, pub service_prefix: String, - pub mandatory_dimensions: Map, + pub tenant_configs: HashMap, } impl FromStr for AppEnv { @@ -177,26 +177,6 @@ impl FromRequest for Tenant { } } -impl Tenant { - pub fn get_mandatory_dimensions( - &self, - mandatory_dimensions: &Map, - ) -> Vec { - mandatory_dimensions - .get(&self.0) - .and_then(|v| v.as_array()) - .map_or_else( - || vec![], - |arr| { - arr.iter() - .filter_map(|value| value.as_str()) - .map(String::from) - .collect() - }, - ) - } -} - #[derive(Deref, DerefMut)] pub struct DbConnection(pub PgSchemaConnection); impl FromRequest for DbConnection { diff --git a/crates/superposition/Cargo.toml b/crates/superposition/Cargo.toml index 9d5adab8..867e503c 100644 --- a/crates/superposition/Cargo.toml +++ b/crates/superposition/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +cac_toml = { path = "../cac_toml" } cac_client = { path = "../cac_client" } frontend = { path = "../frontend" } service_utils = { path = "../service_utils" } @@ -57,6 +58,7 @@ leptos_meta = { workspace = true } leptos_router = { workspace = true } actix-files = { version = "0.6" } anyhow = { workspace = true } +toml = { workspace = true } [lints] workspace = true diff --git a/crates/superposition/Superposition.cac.toml b/crates/superposition/Superposition.cac.toml new file mode 100644 index 00000000..9324d633 --- /dev/null +++ b/crates/superposition/Superposition.cac.toml @@ -0,0 +1,9 @@ +[default-config] +mandatory_dimensions = { "value" = [ +], "schema" = { "type" = "array", "items" = { "type" = "number" } } } + +[dimensions] +tenant = { schema = { "type" = "string", "enum" = ["test", "dev"] } } + +[context."$tenant == 'dev'"] +mandatory_dimensions = [] diff --git a/crates/superposition/src/app_state.rs b/crates/superposition/src/app_state.rs new file mode 100644 index 00000000..b62a90fc --- /dev/null +++ b/crates/superposition/src/app_state.rs @@ -0,0 +1,89 @@ +use std::{ + collections::{HashMap, HashSet}, + sync::{Arc, Mutex}, +}; + +use cac_toml::ContextAwareConfig; +use context_aware_config::helpers::get_meta_schema; +use service_utils::{ + db::utils::init_pool_manager, + helpers::{get_from_env_or_default, get_from_env_unsafe}, + service::types::{AppState, ExperimentationFlags}, +}; +use snowflake::SnowflakeIdGenerator; +use superposition_types::TenantConfig; + +const TENANT_CONFIG_FILE: &str = "crates/superposition/Superposition.cac.toml"; + +pub async fn get( + service_prefix: String, + base: &String, + tenants: &HashSet, +) -> AppState { + let cac_host = + get_from_env_unsafe::("CAC_HOST").expect("CAC host is not set") + base; + let max_pool_size = get_from_env_or_default("MAX_DB_CONNECTION_POOL_SIZE", 2); + let app_env = get_from_env_unsafe("APP_ENV").expect("APP_ENV is not set"); + let enable_tenant_and_scope = get_from_env_unsafe("ENABLE_TENANT_AND_SCOPE") + .expect("ENABLE_TENANT_AND_SCOPE is not set"); + + let cac = ContextAwareConfig::parse(TENANT_CONFIG_FILE) + .expect(&format!("File {TENANT_CONFIG_FILE} not found")); + + let tenant_configs = tenants + .clone() + .into_iter() + .filter_map(|tenant| { + serde_json::to_value(cac.get_resolved_config(&HashMap::from_iter(vec![( + String::from("tenant"), + toml::Value::String(tenant.clone()), + )]))) + .and_then(serde_json::from_value::) + .map(|config| ((tenant, config))) + .ok() + }) + .collect::>(); + + let snowflake_generator = Arc::new(Mutex::new(SnowflakeIdGenerator::new(1, 1))); + + AppState { + db_pool: init_pool_manager( + tenants.clone(), + enable_tenant_and_scope, + app_env, + max_pool_size, + ) + .await, + cac_host, + cac_version: get_from_env_unsafe("SUPERPOSITION_VERSION") + .expect("SUPERPOSITION_VERSION is not set"), + experimentation_flags: ExperimentationFlags { + allow_same_keys_overlapping_ctx: get_from_env_unsafe( + "ALLOW_SAME_KEYS_OVERLAPPING_CTX", + ) + .expect("ALLOW_SAME_KEYS_OVERLAPPING_CTX not set"), + allow_diff_keys_overlapping_ctx: get_from_env_unsafe( + "ALLOW_DIFF_KEYS_OVERLAPPING_CTX", + ) + .expect("ALLOW_DIFF_KEYS_OVERLAPPING_CTX not set"), + allow_same_keys_non_overlapping_ctx: get_from_env_unsafe( + "ALLOW_SAME_KEYS_NON_OVERLAPPING_CTX", + ) + .expect("ALLOW_SAME_KEYS_NON_OVERLAPPING_CTX not set"), + }, + snowflake_generator, + meta_schema: get_meta_schema(), + app_env, + enable_tenant_and_scope, + tenants: tenants.clone(), + tenant_middleware_exclusion_list: get_from_env_unsafe::( + "TENANT_MIDDLEWARE_EXCLUSION_LIST", + ) + .expect("TENANT_MIDDLEWARE_EXCLUSION_LIST is not set") + .split(',') + .map(String::from) + .collect::>(), + service_prefix, + tenant_configs, + } +} diff --git a/crates/superposition/src/main.rs b/crates/superposition/src/main.rs index 97c482d7..fd201e08 100644 --- a/crates/superposition/src/main.rs +++ b/crates/superposition/src/main.rs @@ -1,44 +1,28 @@ -use actix_web::dev::Service; -use actix_web::middleware::Compress; -use actix_web::web::PathConfig; -use actix_web::HttpMessage; -use actix_web::{web, web::get, web::scope, web::Data, App, HttpResponse, HttpServer}; -use context_aware_config::api::*; -use context_aware_config::helpers::get_meta_schema; -use experimentation_platform::api::*; -use serde_json::{Map, Value}; -use std::sync::Arc; -use std::{collections::HashSet, io::Result}; -use superposition_types::User; +mod app_state; -use snowflake::SnowflakeIdGenerator; -use std::{sync::Mutex, time::Duration}; +use std::{collections::HashSet, io::Result, time::Duration}; use actix_files::Files; +use actix_web::{ + dev::Service, + middleware::Compress, + web::{self, get, scope, Data, PathConfig}, + App, HttpMessage, HttpResponse, HttpServer, +}; +use context_aware_config::api::*; +use experimentation_platform::api::*; use frontend::app::*; use frontend::types::Envs as UIEnvs; use leptos::*; use leptos_actix::{generate_route_list, LeptosRoutes}; use service_utils::{ - db::pgschema_manager::PgSchemaManager, - db::utils::init_pool_manager, - helpers::{get_from_env_or_default, get_from_env_unsafe}, + helpers::get_from_env_unsafe, middlewares::{ app_scope::AppExecutionScopeMiddlewareFactory, tenant::TenantMiddlewareFactory, }, - service::types::{AppEnv, AppScope, AppState, ExperimentationFlags}, + service::types::AppScope, }; - -#[actix_web::get("favicon.ico")] -async fn favicon( - leptos_options: actix_web::web::Data, -) -> actix_web::Result { - let leptos_options = leptos_options.into_inner(); - let site_root = &leptos_options.site_root; - Ok(actix_files::NamedFile::open(format!( - "{site_root}/favicon.ico" - ))?) -} +use superposition_types::User; #[actix_web::main] async fn main() -> Result<()> { @@ -64,69 +48,16 @@ async fn main() -> Result<()> { prefix => "/".to_owned() + prefix, }; - let cac_host: String = get_from_env_unsafe("CAC_HOST").expect("CAC host is not set"); let cac_port: u16 = get_from_env_unsafe("PORT").unwrap_or(8080); - let cac_version: String = get_from_env_unsafe("SUPERPOSITION_VERSION") - .expect("SUPERPOSITION_VERSION is not set"); - let max_pool_size = get_from_env_or_default("MAX_DB_CONNECTION_POOL_SIZE", 2); let api_host: String = get_from_env_unsafe("API_HOSTNAME").expect("API_HOSTNAME is not set"); - let app_env: AppEnv = get_from_env_unsafe("APP_ENV").expect("APP_ENV is not set"); - let enable_tenant_and_scope: bool = get_from_env_unsafe("ENABLE_TENANT_AND_SCOPE") - .expect("ENABLE_TENANT_AND_SCOPE is not set"); + let tenants: HashSet = get_from_env_unsafe::("TENANTS") .expect("TENANTS is not set") .split(',') .map(|tenant| tenant.to_string()) .collect::>(); - let tenant_middleware_exclusion_list = - get_from_env_unsafe::("TENANT_MIDDLEWARE_EXCLUSION_LIST") - .expect("TENANT_MIDDLEWARE_EXCLUSION_LIST is not set") - .split(',') - .map(String::from) - .collect::>(); - let mandatory_dimensions: Map = - get_from_env_unsafe::("MANDATORY_DIMENSIONS") - .expect("MANDATORY_DIMENSIONS is not set") - .split(';') - .filter_map(|ele| { - let arr: Vec<&str> = ele.split(':').collect(); - if arr.len() == 2 { - let key = arr[0].to_string(); - let values = arr[1] - .split(',') - .map(String::from) - .map(Value::String) - .collect(); - Some((key.trim().to_string(), Value::Array(values))) - } else { - None - } - }) - .collect(); - - let schema_manager: PgSchemaManager = init_pool_manager( - tenants.clone(), - enable_tenant_and_scope, - app_env, - max_pool_size, - ) - .await; - - /****** EXPERIMENTATION PLATFORM ENVs *********/ - - let allow_same_keys_overlapping_ctx: bool = - get_from_env_unsafe("ALLOW_SAME_KEYS_OVERLAPPING_CTX") - .expect("ALLOW_SAME_KEYS_OVERLAPPING_CTX not set"); - let allow_diff_keys_overlapping_ctx: bool = - get_from_env_unsafe("ALLOW_DIFF_KEYS_OVERLAPPING_CTX") - .expect("ALLOW_DIFF_KEYS_OVERLAPPING_CTX not set"); - let allow_same_keys_non_overlapping_ctx: bool = - get_from_env_unsafe("ALLOW_SAME_KEYS_NON_OVERLAPPING_CTX") - .expect("ALLOW_SAME_KEYS_NON_OVERLAPPING_CTX not set"); - - /****** EXPERIMENTATION PLATFORM ENVs *********/ /* Frontend configurations */ let ui_redirect_path = match tenants.iter().next() { @@ -136,7 +67,7 @@ async fn main() -> Result<()> { let ui_envs = UIEnvs { service_prefix: service_prefix_str, - tenants: tenants.clone().into_iter().collect::>(), + tenants: tenants.clone().into_iter().collect::>(), host: api_host.clone(), }; @@ -148,13 +79,13 @@ async fn main() -> Result<()> { view! { } }); - let snowflake_generator = Arc::new(Mutex::new(SnowflakeIdGenerator::new(1, 1))); + let app_state = + Data::new(app_state::get(service_prefix_str.to_owned(), &base, &tenants).await); HttpServer::new(move || { let leptos_options = &conf.leptos_options; let site_root = &leptos_options.site_root; let leptos_envs = ui_envs.clone(); - let cac_host = cac_host.to_owned() + base.as_str(); App::new() .wrap(Compress::default()) .wrap_fn(|req, srv| { @@ -163,36 +94,13 @@ async fn main() -> Result<()> { srv.call(req) }) .wrap(TenantMiddlewareFactory) - .app_data(Data::new(AppState { - db_pool: schema_manager.clone(), - cac_host: cac_host.to_owned(), - cac_version: cac_version.to_owned(), - - experimentation_flags: ExperimentationFlags { - allow_same_keys_overlapping_ctx: allow_same_keys_overlapping_ctx - .to_owned(), - allow_diff_keys_overlapping_ctx: allow_diff_keys_overlapping_ctx - .to_owned(), - allow_same_keys_non_overlapping_ctx: - allow_same_keys_non_overlapping_ctx.to_owned(), - }, - - snowflake_generator: snowflake_generator.clone(), - meta_schema: get_meta_schema(), - app_env: app_env.to_owned(), - enable_tenant_and_scope: enable_tenant_and_scope.to_owned(), - tenants: tenants.to_owned(), - tenant_middleware_exclusion_list: tenant_middleware_exclusion_list - .to_owned(), - service_prefix: service_prefix_str.to_owned(), - mandatory_dimensions: mandatory_dimensions.to_owned(), - })) + .app_data(app_state.clone()) .app_data(PathConfig::default().error_handler(|err, _| { actix_web::error::ErrorBadRequest(err) })) .wrap( actix_web::middleware::DefaultHeaders::new() - .add(("X-SERVER-VERSION", cac_version.to_string())) + .add(("X-SERVER-VERSION", app_state.cac_version.to_string())) .add(("Cache-Control", "no-store".to_string())) ) .service(web::redirect("/", ui_redirect_path.to_string())) diff --git a/crates/superposition_types/src/lib.rs b/crates/superposition_types/src/lib.rs index 99d439ad..780ce91f 100644 --- a/crates/superposition_types/src/lib.rs +++ b/crates/superposition_types/src/lib.rs @@ -55,7 +55,7 @@ impl Default for User { impl From> for User { fn from(value: Box) -> Self { - User { + Self { email: value.get_email(), username: value.get_username(), auth_token: value.get_auth_token(), @@ -69,7 +69,7 @@ impl FromRequest for User { type Future = Ready>; fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { - if let Some(user) = req.extensions().get::() { + if let Some(user) = req.extensions().get::() { ready(Ok(user.to_owned())) } else { error!("No user was found while validating token"); @@ -257,6 +257,46 @@ impl Display for RegexEnum { } } +pub trait SuperpositionTenantConfig { + fn get_mandatory_dimensions(&self) -> Vec; +} + +#[derive(Clone, Deserialize)] +pub struct TenantConfig { + pub mandatory_dimensions: Vec, +} + +impl SuperpositionTenantConfig for TenantConfig { + fn get_mandatory_dimensions(&self) -> Vec { + self.mandatory_dimensions.clone() + } +} + +impl From> for TenantConfig { + fn from(value: Box) -> Self { + Self { + mandatory_dimensions: value.get_mandatory_dimensions(), + } + } +} + +impl FromRequest for TenantConfig { + type Error = actix_web::error::Error; + type Future = Ready>; + + fn from_request( + req: &actix_web::HttpRequest, + _: &mut actix_web::dev::Payload, + ) -> Self::Future { + let result = req.extensions().get::().cloned().ok_or_else(|| { + log::error!("Tenant config not found"); + error::ErrorInternalServerError("Tenant config not found") + }); + + ready(result) + } +} + #[cfg(test)] mod tests { use super::*;