use crate::server::auth::{
cookie_helpers::CookieSettings,
jwt::JwtValidator,
jwt_signer::JwtSigner,
oauth::OAuthClient,
token_storage::{InMemoryTokenStore, TokenStore},
};
use crate::server::encryption::EncryptionProvider;
use crate::server::registry::{
models::OciClientAuthConfig, providers::OciClientAuthProvider, RegistryProvider,
};
#[cfg(feature = "backend")]
use crate::server::registry::{models::EcrConfig, providers::EcrProvider};
use crate::server::settings::{
AuthSettings, EncryptionSettings, RegistrySettings, ServerSettings, Settings,
};
use anyhow::{Context, Result};
use sqlx::postgres::PgPoolOptions;
use sqlx::PgPool;
use std::sync::Arc;
use std::time::Duration;
#[cfg(feature = "backend")]
use crate::server::deployment::controller::{
DeploymentBackend, KubernetesController, KubernetesControllerConfig,
};
#[derive(Clone)]
pub struct ControllerState {
pub db_pool: PgPool,
pub encryption_provider: Option<Arc<dyn EncryptionProvider>>,
}
#[derive(Clone)]
pub struct AppState {
pub db_pool: PgPool,
pub jwt_validator: Arc<JwtValidator>,
pub jwt_signer: Arc<JwtSigner>,
pub oauth_client: Arc<OAuthClient>,
pub registry_provider: Arc<dyn RegistryProvider>,
pub oci_client: Arc<crate::server::oci::OciClient>,
pub admin_users: Arc<Vec<String>>,
pub auth_settings: Arc<AuthSettings>,
pub server_settings: Arc<ServerSettings>,
pub token_store: Arc<dyn TokenStore>,
pub cookie_settings: CookieSettings,
pub public_url: String,
pub encryption_provider: Option<Arc<dyn EncryptionProvider>>,
pub deployment_backend: Arc<dyn crate::server::deployment::controller::DeploymentBackend>,
pub extension_registry: Arc<crate::server::extensions::registry::ExtensionRegistry>,
pub oauth_state_store:
Arc<moka::future::Cache<String, crate::server::extensions::providers::oauth::OAuthState>>,
pub oauth_code_store: Arc<
moka::future::Cache<String, crate::server::extensions::providers::oauth::OAuthCodeState>,
>,
pub encrypt_rate_limiter: Arc<moka::future::Cache<String, u32>>,
pub access_classes:
Arc<std::collections::HashMap<String, crate::server::settings::AccessClass>>,
pub production_ingress_url_template: Option<String>,
pub staging_ingress_url_template: Option<String>,
}
async fn init_encryption_provider(
encryption_settings: Option<&EncryptionSettings>,
) -> Result<Option<Arc<dyn EncryptionProvider>>> {
if let Some(encryption_config) = encryption_settings {
match encryption_config {
EncryptionSettings::Local { key } => {
use crate::server::encryption::providers::local::LocalEncryptionProvider;
let provider = LocalEncryptionProvider::new(key)
.context("Failed to initialize local encryption provider")?;
tracing::info!("Testing local encryption provider...");
test_encryption_provider(&provider).await?;
tracing::info!("✓ Local AES-256-GCM encryption provider initialized and validated");
Ok(Some(Arc::new(provider)))
}
#[cfg(feature = "backend")]
EncryptionSettings::AwsKms {
region,
key_id,
access_key_id,
secret_access_key,
} => {
use crate::server::encryption::providers::aws_kms::AwsKmsEncryptionProvider;
let provider = AwsKmsEncryptionProvider::new(
region,
key_id.clone(),
access_key_id.clone(),
secret_access_key.clone(),
)
.await
.context("Failed to initialize AWS KMS encryption provider")?;
tracing::info!("Testing AWS KMS encryption provider with key {}...", key_id);
test_encryption_provider(&provider).await.with_context(|| {
format!(
"KMS provider initialized but encryption test failed. \
Please verify: 1) Key ARN/ID '{}' is valid, \
2) AWS credentials are available, \
3) IAM permissions include kms:Encrypt and kms:Decrypt, \
4) Key is enabled and not pending deletion",
key_id
)
})?;
tracing::info!("✓ AWS KMS encryption provider initialized and validated");
Ok(Some(Arc::new(provider)))
}
#[cfg(not(feature = "backend"))]
EncryptionSettings::AwsKms { key_id, .. } => {
anyhow::bail!(
"AWS KMS encryption is configured (key: {}) but the 'aws' feature is not enabled. \
Please rebuild with --features aws or use a pre-built binary with AWS support.",
key_id
)
}
}
} else {
tracing::info!("No encryption provider configured - secret environment variables will not be available");
Ok(None)
}
}
async fn test_encryption_provider(provider: &dyn EncryptionProvider) -> Result<()> {
const TEST_PLAINTEXT: &str = "rise-encryption-test-12345";
let ciphertext = provider
.encrypt(TEST_PLAINTEXT)
.await
.context("Encryption test failed")?;
let decrypted = provider
.decrypt(&ciphertext)
.await
.context("Decryption test failed")?;
if decrypted != TEST_PLAINTEXT {
anyhow::bail!("Encryption round-trip test failed: decrypted value does not match original");
}
Ok(())
}
#[cfg(feature = "backend")]
async fn init_kubernetes_backend(
settings: &Settings,
controller_state: Arc<ControllerState>,
registry_provider: Arc<dyn RegistryProvider>,
) -> Result<Arc<dyn DeploymentBackend>> {
use crate::server::settings::DeploymentControllerSettings;
if let Some(DeploymentControllerSettings::Kubernetes {
kubeconfig,
production_ingress_url_template,
staging_ingress_url_template,
ingress_port,
ingress_schema,
auth_backend_url,
auth_signin_url,
namespace_labels,
namespace_annotations,
ingress_annotations,
ingress_tls_secret_name,
custom_domain_tls_mode,
custom_domain_ingress_annotations,
node_selector,
image_pull_secret_name,
access_classes,
host_aliases,
extra_service_token_audiences,
ingress_controller_namespace,
ingress_controller_labels,
network_policy_allow_kube_apiserver,
network_policy_egress_allow_cidrs,
pod_security_enabled,
pod_resources,
health_probes,
..
}) = &settings.deployment_controller
{
rustls::crypto::ring::default_provider()
.install_default()
.ok();
let kube_config = if kubeconfig.is_some() {
kube::Config::from_kubeconfig(&kube::config::KubeConfigOptions {
context: None,
cluster: None,
user: None,
})
.await?
} else {
kube::Config::infer().await? };
let kube_client = kube::Client::try_from(kube_config)?;
let parsed_backend_address =
crate::server::settings::BackendAddress::from_url(auth_backend_url)?;
let filtered_access_classes: std::collections::HashMap<_, _> = access_classes
.iter()
.filter_map(|(k, v)| v.as_ref().map(|ac| (k.clone(), ac.clone())))
.collect();
let k8s_backend = KubernetesController::new(
(*controller_state).clone(),
kube_client,
KubernetesControllerConfig {
production_ingress_url_template: production_ingress_url_template.clone(),
staging_ingress_url_template: staging_ingress_url_template.clone(),
ingress_port: *ingress_port,
ingress_schema: ingress_schema.clone(),
registry_provider,
auth_backend_url: auth_backend_url.clone(),
auth_signin_url: auth_signin_url.clone(),
backend_address: Some(parsed_backend_address),
namespace_labels: namespace_labels.clone(),
namespace_annotations: namespace_annotations.clone(),
ingress_annotations: ingress_annotations.clone(),
ingress_tls_secret_name: ingress_tls_secret_name.clone(),
custom_domain_tls_mode: custom_domain_tls_mode.clone(),
custom_domain_ingress_annotations: custom_domain_ingress_annotations.clone(),
node_selector: node_selector.clone(),
image_pull_secret_name: image_pull_secret_name.clone(),
access_classes: filtered_access_classes,
host_aliases: host_aliases.clone(),
extra_service_token_audiences: extra_service_token_audiences.clone(),
ingress_controller_namespace: ingress_controller_namespace.clone(),
ingress_controller_labels: ingress_controller_labels.clone(),
network_policy_allow_kube_apiserver: *network_policy_allow_kube_apiserver,
network_policy_egress_allow_cidrs: network_policy_egress_allow_cidrs.clone(),
pod_security_enabled: *pod_security_enabled,
pod_resources: pod_resources.clone(),
health_probes: health_probes.clone(),
},
)?;
k8s_backend.test_connection().await?;
tracing::info!("✓ Kubernetes deployment backend initialized and connection tested");
Ok(Arc::new(k8s_backend) as Arc<dyn DeploymentBackend>)
} else {
anyhow::bail!("Deployment controller not configured. Please add deployment_controller configuration with type: kubernetes")
}
}
impl AppState {
pub fn is_admin(&self, user_email: &str) -> bool {
crate::server::auth::admin::is_admin_user(&self.admin_users, user_email)
}
async fn run_migrations(pool: &PgPool) -> Result<()> {
tracing::info!("Running database migrations...");
sqlx::migrate!("./migrations")
.run(pool)
.await
.context("Failed to run migrations")?;
tracing::info!("Migrations completed successfully");
Ok(())
}
pub async fn new(settings: &Settings) -> Result<Self> {
tracing::info!("Initializing AppState for HTTP server");
let db_pool = PgPoolOptions::new()
.max_connections(10)
.connect(&settings.database.url)
.await
.context("Failed to connect to PostgreSQL")?;
tracing::info!("Successfully connected to PostgreSQL");
Self::run_migrations(&db_pool).await?;
let jwt_validator = Arc::new(JwtValidator::new());
let jwt_signer = Arc::new(
JwtSigner::new(
&settings.server.jwt_signing_secret,
settings.server.public_url.clone(),
settings.server.jwt_expiry_seconds,
settings.server.jwt_claims.clone(),
settings.server.rs256_private_key_pem.as_deref(),
settings.server.rs256_public_key_pem.as_deref(),
)
.context("Failed to initialize JWT signer for ingress authentication")?,
);
tracing::info!(
"Initialized JWT signer for ingress authentication (expiry: {}s)",
settings.server.jwt_expiry_seconds
);
let oauth_client = Arc::new(
OAuthClient::new(
settings.auth.issuer.clone(),
settings.auth.client_id.clone(),
settings.auth.client_secret.clone(),
settings.auth.authorize_url.clone(),
settings.auth.token_url.clone(),
)
.await?,
);
let registry_provider: Arc<dyn RegistryProvider> = match &settings.registry {
Some(registry_config) => match registry_config {
#[cfg(feature = "backend")]
RegistrySettings::Ecr {
region,
account_id,
repo_prefix,
push_role_arn,
auto_remove,
access_key_id,
secret_access_key,
} => {
let ecr_config = EcrConfig {
region: region.clone(),
account_id: account_id.clone(),
repo_prefix: repo_prefix.clone(),
push_role_arn: push_role_arn.clone(),
auto_remove: *auto_remove,
access_key_id: access_key_id.clone(),
secret_access_key: secret_access_key.clone(),
};
let provider = EcrProvider::new(ecr_config)
.await
.context("Failed to initialize ECR registry provider")?;
tracing::info!("Initialized ECR registry provider");
Arc::new(provider)
}
#[cfg(not(feature = "backend"))]
RegistrySettings::Ecr { account_id, .. } => {
anyhow::bail!(
"AWS ECR registry is configured (account: {}) but the 'aws' feature is not enabled. \
Please rebuild with --features aws or use a pre-built binary with AWS support.",
account_id
)
}
RegistrySettings::OciClientAuth {
registry_url,
namespace,
client_registry_url,
} => {
let oci_config = OciClientAuthConfig {
registry_url: registry_url.clone(),
namespace: namespace.clone(),
client_registry_url: client_registry_url.clone(),
};
let provider = OciClientAuthProvider::new(oci_config)
.context("Failed to initialize OCI client-auth registry provider")?;
tracing::info!(
"Initialized OCI client-auth registry provider at {}",
registry_url
);
Arc::new(provider)
}
},
None => {
anyhow::bail!(
"Registry provider is required for server operation. \
Please configure a registry in settings (ECR or OCI client-auth)"
)
}
};
let oci_client = Arc::new(
crate::server::oci::OciClient::new().context("Failed to initialize OCI client")?,
);
tracing::info!("Initialized OCI client for registry digest resolution");
let admin_users = Arc::new(settings.auth.admin_users.clone());
if !admin_users.is_empty() {
tracing::info!("Configured {} admin user(s)", admin_users.len());
}
let auth_settings = Arc::new(settings.auth.clone());
let server_settings = Arc::new(settings.server.clone());
let token_store: Arc<dyn TokenStore> =
Arc::new(InMemoryTokenStore::new(Duration::from_secs(600)));
tracing::info!("Initialized in-memory token store for OAuth2 state");
let cookie_settings = CookieSettings {
domain: settings.server.cookie_domain.clone(),
secure: settings.server.cookie_secure,
};
tracing::info!(
"Configured session cookies with domain={:?}, secure={}",
if cookie_settings.domain.is_empty() {
"current-host-only"
} else {
&cookie_settings.domain
},
cookie_settings.secure
);
if !cookie_settings.domain.is_empty() {
#[cfg(feature = "backend")]
if let Some(crate::server::settings::DeploymentControllerSettings::Kubernetes {
auth_signin_url: signin_url,
..
}) = &settings.deployment_controller
{
if let Ok(parsed) = url::Url::parse(signin_url) {
if let Some(host) = parsed.host_str() {
let cookie_domain_normalized =
cookie_settings.domain.trim_start_matches('.');
if !host.ends_with(cookie_domain_normalized)
&& host != cookie_domain_normalized
{
tracing::warn!(
"âš Cookie domain mismatch: cookie_domain='{}' but auth_signin_url host='{}'. \
Cookies may not work correctly. Consider setting cookie_domain to '.{}' or \
ensure signin URL uses a matching domain.",
cookie_settings.domain,
host,
host.split('.').skip(1).collect::<Vec<_>>().join(".")
);
}
}
}
if signin_url.contains("localhost") || signin_url.contains("127.0.0.1") {
tracing::warn!(
"âš Cookie domain '{}' set but auth_signin_url uses localhost. \
Use a proper domain name (e.g., rise.local) instead of localhost for cookie sharing.",
cookie_settings.domain
);
}
}
}
let public_url = settings.server.public_url.clone();
tracing::info!("Public URL: {}", public_url);
if let Some(ref docs_dir) = settings.server.docs_dir {
tracing::info!("Documentation directory: {}", docs_dir);
} else {
tracing::warn!("No docs_dir configured — documentation endpoints will return 404");
}
if let Some(frontend_proxy_url) = &settings.server.frontend_dev_proxy_url {
tracing::info!(
"Frontend dev proxy enabled: backend will proxy UI requests to {}",
frontend_proxy_url
);
}
let encryption_provider = init_encryption_provider(settings.encryption.as_ref()).await?;
#[cfg(not(feature = "backend"))]
compile_error!(
"At least one deployment backend must be enabled. Please build with --features k8s"
);
#[cfg(feature = "backend")]
let deployment_backend = {
let controller_state = Arc::new(ControllerState {
db_pool: db_pool.clone(),
encryption_provider: encryption_provider.clone(),
});
init_kubernetes_backend(settings, controller_state, registry_provider.clone()).await?
};
#[allow(unused_mut)]
let mut extension_registry = crate::server::extensions::registry::ExtensionRegistry::new();
if let Some(ref extensions_config) = settings.extensions {
#[allow(clippy::never_loop)]
for provider_config in &extensions_config.providers {
match provider_config {
#[cfg(feature = "backend")]
crate::server::settings::ExtensionProviderConfig::AwsRdsProvisioner {
region,
instance_size,
disk_size,
instance_id_template,
instance_id_prefix,
default_engine_version,
vpc_security_group_ids,
db_subnet_group_name,
backup_retention_days,
backup_window,
maintenance_window,
access_key_id,
secret_access_key,
} => {
tracing::info!("Initializing AWS RDS extension provider");
let mut aws_config_builder =
aws_config::defaults(aws_config::BehaviorVersion::latest())
.region(aws_config::Region::new(region.clone()));
if let (Some(key_id), Some(secret_key)) = (access_key_id, secret_access_key)
{
aws_config_builder = aws_config_builder.credentials_provider(
aws_sdk_sts::config::Credentials::new(
key_id,
secret_key,
None,
None,
"static-credentials",
),
);
}
let aws_config = aws_config_builder.load().await;
let rds_client = aws_sdk_rds::Client::new(&aws_config);
let encryption_provider = encryption_provider.clone().ok_or_else(|| {
anyhow::anyhow!("Encryption provider required for AWS RDS extension")
})?;
let aws_rds_provisioner =
crate::server::extensions::providers::aws_rds::AwsRdsProvisioner::new(
crate::server::extensions::providers::aws_rds::AwsRdsProvisionerConfig {
rds_client,
db_pool: db_pool.clone(),
encryption_provider,
region: region.clone(),
instance_size: instance_size.clone(),
disk_size: *disk_size,
instance_id_template: instance_id_template.clone(),
instance_id_prefix: instance_id_prefix.clone(),
default_engine_version: default_engine_version.clone(),
vpc_security_group_ids: vpc_security_group_ids.clone(),
db_subnet_group_name: db_subnet_group_name.clone(),
backup_retention_days: *backup_retention_days,
backup_window: backup_window.clone(),
maintenance_window: maintenance_window.clone(),
}
)
.await?;
let aws_rds_arc: Arc<dyn crate::server::extensions::Extension> =
Arc::new(aws_rds_provisioner);
extension_registry.register_type(aws_rds_arc.clone());
aws_rds_arc.start();
tracing::info!("AWS RDS extension provider initialized and started");
}
#[allow(unreachable_patterns)]
_ => {
}
}
}
}
tracing::info!("Initializing OAuth extension provider");
let encryption_provider_for_oauth = encryption_provider
.clone()
.ok_or_else(|| anyhow::anyhow!("Encryption provider required for OAuth extension"))?;
let oauth_provider = crate::server::extensions::providers::oauth::OAuthProvider::new(
crate::server::extensions::providers::oauth::OAuthProviderConfig {
db_pool: db_pool.clone(),
encryption_provider: encryption_provider_for_oauth,
http_client: reqwest::Client::new(),
api_domain: public_url.clone(),
},
);
let oauth_provider_arc: Arc<dyn crate::server::extensions::Extension> =
Arc::new(oauth_provider);
extension_registry.register_type(oauth_provider_arc.clone());
oauth_provider_arc.start();
tracing::info!("OAuth extension provider initialized and started");
#[cfg(feature = "backend")]
if let Some(ref extensions_config) = settings.extensions {
for provider_config in &extensions_config.providers {
#[allow(irrefutable_let_patterns)]
if let crate::server::settings::ExtensionProviderConfig::SnowflakeOAuthProvisioner {
account,
user,
role,
warehouse,
auth,
integration_name_prefix,
default_blocked_roles,
default_scopes,
refresh_token_validity_seconds,
} = provider_config
{
tracing::info!("Initializing Snowflake OAuth provisioner");
let snowflake_oauth_provisioner =
crate::server::extensions::providers::snowflake_oauth::SnowflakeOAuthProvisioner::new(
crate::server::extensions::providers::snowflake_oauth::SnowflakeOAuthProvisionerConfig {
db_pool: db_pool.clone(),
encryption_provider: encryption_provider.clone()
.ok_or_else(|| anyhow::anyhow!("Encryption provider required for Snowflake OAuth provisioner"))?,
http_client: reqwest::Client::new(),
api_domain: public_url.clone(),
oauth_provider: Some(oauth_provider_arc.clone()),
account: account.clone(),
user: user.clone(),
role: role.clone(),
warehouse: warehouse.clone(),
auth: auth.clone(),
integration_name_prefix: integration_name_prefix.clone(),
default_blocked_roles: default_blocked_roles.clone(),
default_scopes: default_scopes.clone(),
refresh_token_validity_seconds: *refresh_token_validity_seconds,
},
);
snowflake_oauth_provisioner
.validate_credentials()
.await
.context("Failed to validate Snowflake credentials during startup")?;
let snowflake_oauth_arc: Arc<dyn crate::server::extensions::Extension> =
Arc::new(snowflake_oauth_provisioner);
extension_registry.register_type(snowflake_oauth_arc.clone());
snowflake_oauth_arc.start();
tracing::info!("Snowflake OAuth provisioner initialized and started");
}
}
}
let extension_registry = Arc::new(extension_registry);
let oauth_state_store = Arc::new(
moka::future::Cache::builder()
.time_to_live(Duration::from_secs(600))
.max_capacity(10_000) .build(),
);
tracing::info!("Initialized OAuth state store for OAuth extensions");
let oauth_code_store = Arc::new(
moka::future::Cache::builder()
.time_to_live(Duration::from_secs(300))
.max_capacity(10_000) .build(),
);
tracing::info!("Initialized OAuth authorization code store for secure backend flow");
let encrypt_rate_limiter = Arc::new(
moka::future::Cache::builder()
.time_to_live(Duration::from_secs(3600)) .max_capacity(10_000) .build(),
);
tracing::info!("Initialized encrypt endpoint rate limiter (100 req/hour per user)");
let (access_classes, production_ingress_url_template, staging_ingress_url_template) =
if let Some(crate::server::settings::DeploymentControllerSettings::Kubernetes {
access_classes,
production_ingress_url_template,
staging_ingress_url_template,
..
}) = &settings.deployment_controller
{
let filtered: std::collections::HashMap<_, _> = access_classes
.iter()
.filter_map(|(k, v)| v.as_ref().map(|ac| (k.clone(), ac.clone())))
.collect();
(
Arc::new(filtered),
Some(production_ingress_url_template.clone()),
staging_ingress_url_template.clone(),
)
} else {
(Arc::new(std::collections::HashMap::new()), None, None)
};
Ok(Self {
db_pool,
jwt_validator,
jwt_signer,
oauth_client,
registry_provider,
oci_client,
admin_users,
auth_settings,
server_settings,
token_store,
cookie_settings,
public_url,
encryption_provider,
deployment_backend,
extension_registry,
oauth_state_store,
oauth_code_store,
encrypt_rate_limiter,
access_classes,
production_ingress_url_template,
staging_ingress_url_template,
})
}
}