Compare commits
10 Commits
main
...
aws-kms-se
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
baaefeda37 | ||
|
|
f32c3b3c88 | ||
|
|
18a1acc212 | ||
|
|
45978a566f | ||
|
|
6e0b85c5d5 | ||
|
|
9177c1f944 | ||
|
|
c053c04601 | ||
|
|
6acd508c02 | ||
|
|
68c8388976 | ||
|
|
c57bc88aad |
26
backend/.sqlx/query-243588f12c62aa7913bc3a4ae11ecee8a1e03863735036b1192fca143f27641c.json
generated
Normal file
26
backend/.sqlx/query-243588f12c62aa7913bc3a4ae11ecee8a1e03863735036b1192fca143f27641c.json
generated
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT workspace_id, path FROM variable WHERE is_secret = true AND value LIKE '$aws_sm:%'",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "workspace_id",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "path",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "243588f12c62aa7913bc3a4ae11ecee8a1e03863735036b1192fca143f27641c"
|
||||
}
|
||||
25
backend/Cargo.lock
generated
25
backend/Cargo.lock
generated
@@ -1006,6 +1006,30 @@ dependencies = [
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-sdk-secretsmanager"
|
||||
version = "1.100.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26b0b2985427bb081e54e759468d3af89fa2ccb17fb8b9e5b704ae2f8da10a3b"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-runtime",
|
||||
"aws-smithy-async",
|
||||
"aws-smithy-http 0.63.4",
|
||||
"aws-smithy-json 0.62.4",
|
||||
"aws-smithy-observability",
|
||||
"aws-smithy-runtime",
|
||||
"aws-smithy-runtime-api",
|
||||
"aws-smithy-types",
|
||||
"aws-types",
|
||||
"bytes",
|
||||
"fastrand",
|
||||
"http 0.2.12",
|
||||
"http 1.4.0",
|
||||
"regex-lite",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-sdk-sqs"
|
||||
version = "1.77.0"
|
||||
@@ -16722,6 +16746,7 @@ dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-sdk-bedrockruntime",
|
||||
"aws-sdk-rds",
|
||||
"aws-sdk-secretsmanager",
|
||||
"aws-sdk-sts",
|
||||
"aws-smithy-types",
|
||||
"aws-smithy-types-convert",
|
||||
|
||||
@@ -560,6 +560,7 @@ aws-sdk-bedrock = "1.129.0"
|
||||
aws-sdk-bedrockruntime = "=1.122.0"
|
||||
aws-credential-types = "^1"
|
||||
aws-smithy-types = "^1"
|
||||
aws-sdk-secretsmanager = "^1"
|
||||
aws-sdk-sqs = "=1.77.0"
|
||||
aws-sdk-sts = "=1.79.0"
|
||||
aws-sdk-sso = "=1.77.0"
|
||||
|
||||
@@ -1 +1 @@
|
||||
50fdd6b517c46e506b251b9fbe1c6218f601d369
|
||||
8e5b77ef1f07b5b6620e540fea580f1a3d7f7d8b
|
||||
|
||||
@@ -41,7 +41,7 @@ use serde::{Deserialize, Serialize};
|
||||
use windmill_common::ee_oss::{send_critical_alert, CriticalAlertKind, CriticalErrorChannel};
|
||||
#[cfg(all(feature = "private", feature = "enterprise"))]
|
||||
use windmill_common::secret_backend::{
|
||||
AzureKeyVaultSettings, SecretMigrationReport, VaultSettings,
|
||||
AwsSecretsManagerSettings, AzureKeyVaultSettings, SecretMigrationReport, VaultSettings,
|
||||
};
|
||||
use windmill_common::{
|
||||
ai_cache::bump_instance_ai_config_revision,
|
||||
@@ -131,6 +131,15 @@ pub fn global_service() -> Router {
|
||||
.route(
|
||||
"/migrate_secrets_from_azure_kv",
|
||||
post(migrate_secrets_from_azure_kv),
|
||||
)
|
||||
.route("/test_aws_sm_backend", post(test_aws_sm_backend))
|
||||
.route(
|
||||
"/migrate_secrets_to_aws_sm",
|
||||
post(migrate_secrets_to_aws_sm),
|
||||
)
|
||||
.route(
|
||||
"/migrate_secrets_from_aws_sm",
|
||||
post(migrate_secrets_from_aws_sm),
|
||||
);
|
||||
|
||||
#[cfg(feature = "parquet")]
|
||||
@@ -1284,6 +1293,43 @@ pub async fn migrate_secrets_from_azure_kv(
|
||||
Ok(Json(report))
|
||||
}
|
||||
|
||||
/// Test connection to AWS Secrets Manager
|
||||
#[cfg(all(feature = "private", feature = "enterprise"))]
|
||||
pub async fn test_aws_sm_backend(
|
||||
Extension(db): Extension<DB>,
|
||||
authed: ApiAuthed,
|
||||
Json(settings): Json<AwsSecretsManagerSettings>,
|
||||
) -> Result<String> {
|
||||
require_super_admin(&db, &authed.email).await?;
|
||||
windmill_common::secret_backend::test_aws_sm_connection(&settings).await?;
|
||||
Ok("Successfully connected to AWS Secrets Manager".to_string())
|
||||
}
|
||||
|
||||
/// Migrate existing secrets from database to AWS Secrets Manager
|
||||
#[cfg(all(feature = "private", feature = "enterprise"))]
|
||||
pub async fn migrate_secrets_to_aws_sm(
|
||||
Extension(db): Extension<DB>,
|
||||
authed: ApiAuthed,
|
||||
Json(settings): Json<AwsSecretsManagerSettings>,
|
||||
) -> JsonResult<SecretMigrationReport> {
|
||||
require_super_admin(&db, &authed.email).await?;
|
||||
let report = windmill_common::secret_backend::migrate_secrets_to_aws_sm(&db, &settings).await?;
|
||||
Ok(Json(report))
|
||||
}
|
||||
|
||||
/// Migrate secrets from AWS Secrets Manager back to database
|
||||
#[cfg(all(feature = "private", feature = "enterprise"))]
|
||||
pub async fn migrate_secrets_from_aws_sm(
|
||||
Extension(db): Extension<DB>,
|
||||
authed: ApiAuthed,
|
||||
Json(settings): Json<AwsSecretsManagerSettings>,
|
||||
) -> JsonResult<SecretMigrationReport> {
|
||||
require_super_admin(&db, &authed.email).await?;
|
||||
let report =
|
||||
windmill_common::secret_backend::migrate_secrets_from_aws_sm(&db, &settings).await?;
|
||||
Ok(Json(report))
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// JWKS Endpoint for Vault JWT Authentication
|
||||
// ============================================================================
|
||||
|
||||
@@ -1855,6 +1855,66 @@ paths:
|
||||
schema:
|
||||
$ref: "#/components/schemas/SecretMigrationReport"
|
||||
|
||||
/settings/test_aws_sm_backend:
|
||||
post:
|
||||
summary: test connection to AWS Secrets Manager
|
||||
operationId: testAwsSmBackend
|
||||
tags:
|
||||
- setting
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/AwsSecretsManagerSettings"
|
||||
responses:
|
||||
"200":
|
||||
description: connection test result
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/settings/migrate_secrets_to_aws_sm:
|
||||
post:
|
||||
summary: migrate secrets from database to AWS Secrets Manager
|
||||
operationId: migrateSecretsToAwsSm
|
||||
tags:
|
||||
- setting
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/AwsSecretsManagerSettings"
|
||||
responses:
|
||||
"200":
|
||||
description: migration report
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/SecretMigrationReport"
|
||||
|
||||
/settings/migrate_secrets_from_aws_sm:
|
||||
post:
|
||||
summary: migrate secrets from AWS Secrets Manager to database
|
||||
operationId: migrateSecretsFromAwsSm
|
||||
tags:
|
||||
- setting
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/AwsSecretsManagerSettings"
|
||||
responses:
|
||||
"200":
|
||||
description: migration report
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/SecretMigrationReport"
|
||||
|
||||
/users/email:
|
||||
get:
|
||||
summary: get current user email (if logged in)
|
||||
@@ -19405,6 +19465,27 @@ components:
|
||||
type: string
|
||||
description: Static Bearer token for testing/development (optional, if provided this is used instead of OAuth2 authentication)
|
||||
|
||||
AwsSecretsManagerSettings:
|
||||
type: object
|
||||
required:
|
||||
- region
|
||||
properties:
|
||||
region:
|
||||
type: string
|
||||
description: AWS region (e.g., us-east-1)
|
||||
access_key_id:
|
||||
type: string
|
||||
description: AWS Access Key ID (optional, uses default credential chain if not provided)
|
||||
secret_access_key:
|
||||
type: string
|
||||
description: AWS Secret Access Key (optional)
|
||||
endpoint_url:
|
||||
type: string
|
||||
description: Custom endpoint URL for testing (e.g., LocalStack)
|
||||
prefix:
|
||||
type: string
|
||||
description: Prefix for secret names (e.g., windmill/)
|
||||
|
||||
SecretMigrationFailure:
|
||||
type: object
|
||||
required:
|
||||
|
||||
@@ -29,8 +29,8 @@ use windmill_common::secret_backend::{database::DatabaseBackend, SecretBackend};
|
||||
use windmill_common::{
|
||||
global_settings::{load_value_from_global_settings, SECRET_BACKEND_SETTING},
|
||||
secret_backend::{
|
||||
AzureKeyVaultBackend, AzureKeyVaultSettings, SecretBackendConfig, VaultBackend,
|
||||
VaultSettings,
|
||||
AwsSecretsManagerBackend, AwsSecretsManagerSettings, AzureKeyVaultBackend,
|
||||
AzureKeyVaultSettings, SecretBackendConfig, VaultBackend, VaultSettings,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -62,6 +62,18 @@ lazy_static::lazy_static! {
|
||||
static ref AZURE_KV_BACKEND_CACHE: RwLock<Option<CachedAzureKvBackend>> = RwLock::new(None);
|
||||
}
|
||||
|
||||
// Cached AWS Secrets Manager backend
|
||||
#[cfg(all(feature = "private", feature = "enterprise"))]
|
||||
struct CachedAwsSmBackend {
|
||||
backend: Arc<dyn SecretBackend>,
|
||||
settings: AwsSecretsManagerSettings,
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "private", feature = "enterprise"))]
|
||||
lazy_static::lazy_static! {
|
||||
static ref AWS_SM_BACKEND_CACHE: RwLock<Option<CachedAwsSmBackend>> = RwLock::new(None);
|
||||
}
|
||||
|
||||
/// Get the current secret backend based on global settings (EE only)
|
||||
#[cfg(all(feature = "private", feature = "enterprise"))]
|
||||
async fn get_secret_backend(db: &DB) -> Result<Arc<dyn SecretBackend>> {
|
||||
@@ -78,6 +90,9 @@ async fn get_secret_backend(db: &DB) -> Result<Arc<dyn SecretBackend>> {
|
||||
SecretBackendConfig::AzureKeyVault(settings) => {
|
||||
get_or_create_azure_kv_backend(db, settings).await
|
||||
}
|
||||
SecretBackendConfig::AwsSecretsManager(settings) => {
|
||||
get_or_create_aws_sm_backend(db, settings).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,6 +176,37 @@ async fn get_or_create_azure_kv_backend(
|
||||
Ok(backend)
|
||||
}
|
||||
|
||||
/// Get a cached AWS SM backend or create a new one if settings changed
|
||||
#[cfg(all(feature = "private", feature = "enterprise"))]
|
||||
async fn get_or_create_aws_sm_backend(
|
||||
_db: &DB,
|
||||
settings: AwsSecretsManagerSettings,
|
||||
) -> Result<Arc<dyn SecretBackend>> {
|
||||
{
|
||||
let cache = AWS_SM_BACKEND_CACHE.read().await;
|
||||
if let Some(ref cached) = *cache {
|
||||
if cached.settings == settings {
|
||||
return Ok(cached.backend.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut cache = AWS_SM_BACKEND_CACHE.write().await;
|
||||
|
||||
if let Some(ref cached) = *cache {
|
||||
if cached.settings == settings {
|
||||
return Ok(cached.backend.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let backend: Arc<dyn SecretBackend> =
|
||||
Arc::new(AwsSecretsManagerBackend::new_with_client(settings.clone()).await?);
|
||||
|
||||
*cache = Some(CachedAwsSmBackend { backend: backend.clone(), settings });
|
||||
|
||||
Ok(backend)
|
||||
}
|
||||
|
||||
/// Check if an external secret backend is currently configured (EE only)
|
||||
#[cfg(all(feature = "private", feature = "enterprise"))]
|
||||
async fn is_vault_backend_configured(db: &DB) -> Result<bool> {
|
||||
@@ -171,7 +217,7 @@ async fn is_vault_backend_configured(db: &DB) -> Result<bool> {
|
||||
|
||||
Ok(matches!(
|
||||
config,
|
||||
SecretBackendConfig::HashiCorpVault(_) | SecretBackendConfig::AzureKeyVault(_)
|
||||
SecretBackendConfig::HashiCorpVault(_) | SecretBackendConfig::AzureKeyVault(_) | SecretBackendConfig::AwsSecretsManager(_)
|
||||
))
|
||||
}
|
||||
|
||||
@@ -187,10 +233,16 @@ fn is_azure_kv_stored_value(value: &str) -> bool {
|
||||
value.starts_with("$azure_kv:")
|
||||
}
|
||||
|
||||
/// Check if a value is stored in AWS Secrets Manager
|
||||
#[cfg(all(feature = "private", feature = "enterprise"))]
|
||||
fn is_aws_sm_stored_value(value: &str) -> bool {
|
||||
value.starts_with("$aws_sm:")
|
||||
}
|
||||
|
||||
/// Check if a value is stored in any external secret backend
|
||||
#[cfg(all(feature = "private", feature = "enterprise"))]
|
||||
fn is_external_stored_value(value: &str) -> bool {
|
||||
is_vault_stored_value(value) || is_azure_kv_stored_value(value)
|
||||
is_vault_stored_value(value) || is_azure_kv_stored_value(value) || is_aws_sm_stored_value(value)
|
||||
}
|
||||
|
||||
/// Bulk rename secrets in Vault when a path prefix changes (e.g., user rename)
|
||||
@@ -235,6 +287,8 @@ pub async fn rename_vault_secrets_with_prefix(
|
||||
// Determine the marker prefix from the stored value
|
||||
let marker_prefix = if is_azure_kv_stored_value(&value) {
|
||||
"$azure_kv:"
|
||||
} else if is_aws_sm_stored_value(&value) {
|
||||
"$aws_sm:"
|
||||
} else {
|
||||
"$vault:"
|
||||
};
|
||||
|
||||
@@ -9,7 +9,7 @@ default = []
|
||||
enterprise = ["dep:aws-config"]
|
||||
instance_config_schema = ["dep:schemars"]
|
||||
local_reports = ["dep:rsa", "dep:aes-gcm"]
|
||||
private = ["dep:aws-sdk-rds"]
|
||||
private = ["dep:aws-sdk-rds", "dep:aws-sdk-secretsmanager"]
|
||||
jemalloc = ["dep:tikv-jemalloc-ctl"]
|
||||
tantivy = []
|
||||
prometheus = ["dep:prometheus"]
|
||||
@@ -80,6 +80,7 @@ postgres-native-tls.workspace = true
|
||||
native-tls.workspace = true
|
||||
|
||||
aws-smithy-types-convert = { workspace = true, optional = true }
|
||||
aws-sdk-secretsmanager = { workspace = true, optional = true }
|
||||
aws-sdk-rds = { workspace = true, optional = true }
|
||||
indexmap.workspace = true
|
||||
bytes.workspace = true
|
||||
|
||||
@@ -899,7 +899,7 @@ const SENSITIVE_SETTINGS: &[&str] = &[
|
||||
/// Maps a top-level key to the sub-field names that must be redacted.
|
||||
const NESTED_SENSITIVE_FIELDS: &[(&str, &[&str])] = &[
|
||||
("smtp_settings", &["smtp_password"]),
|
||||
("secret_backend", &["token", "client_secret"]),
|
||||
("secret_backend", &["token", "client_secret", "secret_access_key"]),
|
||||
(
|
||||
"object_store_cache_config",
|
||||
&["secret_key", "serviceAccountKey"],
|
||||
|
||||
69
backend/windmill-common/src/secret_backend/aws_sm_oss.rs
Normal file
69
backend/windmill-common/src/secret_backend/aws_sm_oss.rs
Normal file
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Author: Windmill Labs, Inc
|
||||
* Copyright (C) Windmill Labs, Inc - All Rights Reserved
|
||||
* Unauthorized copying of this file, via any medium is strictly prohibited.
|
||||
*/
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::db::DB;
|
||||
use crate::error::{Error, Result};
|
||||
|
||||
use super::{AwsSecretsManagerSettings, SecretBackend, SecretMigrationReport};
|
||||
|
||||
pub struct AwsSecretsManagerBackend;
|
||||
|
||||
impl AwsSecretsManagerBackend {
|
||||
pub fn new(_settings: AwsSecretsManagerSettings) -> Self {
|
||||
AwsSecretsManagerBackend
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl SecretBackend for AwsSecretsManagerBackend {
|
||||
async fn get_secret(&self, _workspace_id: &str, _path: &str) -> Result<String> {
|
||||
Err(Error::internal_err(
|
||||
"AWS Secrets Manager integration requires Enterprise Edition".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
async fn set_secret(&self, _workspace_id: &str, _path: &str, _value: &str) -> Result<()> {
|
||||
Err(Error::internal_err(
|
||||
"AWS Secrets Manager integration requires Enterprise Edition".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
async fn delete_secret(&self, _workspace_id: &str, _path: &str) -> Result<()> {
|
||||
Err(Error::internal_err(
|
||||
"AWS Secrets Manager integration requires Enterprise Edition".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
fn backend_name(&self) -> &'static str {
|
||||
"aws_secrets_manager"
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn test_aws_sm_connection(_settings: &AwsSecretsManagerSettings) -> Result<()> {
|
||||
Err(Error::internal_err(
|
||||
"AWS Secrets Manager integration requires Enterprise Edition".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn migrate_secrets_to_aws_sm(
|
||||
_db: &DB,
|
||||
_settings: &AwsSecretsManagerSettings,
|
||||
) -> Result<SecretMigrationReport> {
|
||||
Err(Error::internal_err(
|
||||
"AWS Secrets Manager integration requires Enterprise Edition".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn migrate_secrets_from_aws_sm(
|
||||
_db: &DB,
|
||||
_settings: &AwsSecretsManagerSettings,
|
||||
) -> Result<SecretMigrationReport> {
|
||||
Err(Error::internal_err(
|
||||
"AWS Secrets Manager integration requires Enterprise Edition".to_string(),
|
||||
))
|
||||
}
|
||||
@@ -22,6 +22,10 @@ pub mod vault_oss;
|
||||
pub mod azure_kv_ee;
|
||||
pub mod azure_kv_oss;
|
||||
|
||||
#[cfg(feature = "private")]
|
||||
pub mod aws_sm_ee;
|
||||
pub mod aws_sm_oss;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
@@ -37,6 +41,12 @@ pub use azure_kv_ee::*;
|
||||
#[cfg(not(feature = "private"))]
|
||||
pub use azure_kv_oss::*;
|
||||
|
||||
#[cfg(feature = "private")]
|
||||
pub use aws_sm_ee::*;
|
||||
|
||||
#[cfg(not(feature = "private"))]
|
||||
pub use aws_sm_oss::*;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -91,6 +101,8 @@ pub enum SecretBackendConfig {
|
||||
HashiCorpVault(VaultSettings),
|
||||
/// Store secrets in Azure Key Vault (Enterprise Edition only)
|
||||
AzureKeyVault(AzureKeyVaultSettings),
|
||||
/// Store secrets in AWS Secrets Manager (Enterprise Edition only)
|
||||
AwsSecretsManager(AwsSecretsManagerSettings),
|
||||
}
|
||||
|
||||
impl Default for SecretBackendConfig {
|
||||
@@ -136,6 +148,25 @@ pub struct AzureKeyVaultSettings {
|
||||
pub token: Option<String>,
|
||||
}
|
||||
|
||||
/// Settings for AWS Secrets Manager integration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct AwsSecretsManagerSettings {
|
||||
/// AWS region (e.g., "us-east-1")
|
||||
pub region: String,
|
||||
/// Static AWS access key ID (optional - uses default credential chain if not provided)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub access_key_id: Option<String>,
|
||||
/// Static AWS secret access key (optional)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub secret_access_key: Option<String>,
|
||||
/// Custom endpoint URL for LocalStack/testing (optional)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub endpoint_url: Option<String>,
|
||||
/// Prefix for secret names in AWS Secrets Manager (e.g., "windmill/")
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub prefix: Option<String>,
|
||||
}
|
||||
|
||||
/// Result of a secret migration operation
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SecretMigrationReport {
|
||||
|
||||
@@ -79,6 +79,13 @@ pub async fn create_secret_backend(
|
||||
);
|
||||
Ok(Arc::new(DatabaseBackend::new(db)))
|
||||
}
|
||||
SecretBackendConfig::AwsSecretsManager(_) => {
|
||||
tracing::warn!(
|
||||
"AWS Secrets Manager is configured but requires Enterprise Edition. \
|
||||
Falling back to database backend."
|
||||
);
|
||||
Ok(Arc::new(DatabaseBackend::new(db)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
264
backend/windmill-common/tests/aws_sm_integration.rs
Normal file
264
backend/windmill-common/tests/aws_sm_integration.rs
Normal file
@@ -0,0 +1,264 @@
|
||||
//! Integration tests for AWS Secrets Manager secret backend.
|
||||
//!
|
||||
//! These tests require a running LocalStack instance with the `secretsmanager` service.
|
||||
//!
|
||||
//! ## Setup
|
||||
//!
|
||||
//! 1. Start LocalStack:
|
||||
//!
|
||||
//! ```bash
|
||||
//! docker run -d --name localstack -p 4566:4566 \
|
||||
//! -e SERVICES=secretsmanager \
|
||||
//! localstack/localstack:3.8
|
||||
//! ```
|
||||
//!
|
||||
//! 2. Run the tests:
|
||||
//!
|
||||
//! ```bash
|
||||
//! RUN_AWS_SM_TESTS=1 cargo test -p windmill-common --features private,enterprise \
|
||||
//! aws_sm_integration -- --nocapture
|
||||
//! ```
|
||||
//!
|
||||
//! ## Environment variables
|
||||
//!
|
||||
//! - `RUN_AWS_SM_TESTS=1` - Required to run the tests
|
||||
//! - `AWS_SM_ENDPOINT` - LocalStack endpoint (default: http://localhost:4566)
|
||||
//! - `AWS_SM_REGION` - AWS region (default: us-east-1)
|
||||
|
||||
#[cfg(all(feature = "private", feature = "enterprise"))]
|
||||
mod tests {
|
||||
use windmill_common::secret_backend::{
|
||||
test_aws_sm_connection, AwsSecretsManagerBackend, AwsSecretsManagerSettings,
|
||||
SecretBackend,
|
||||
};
|
||||
|
||||
fn should_run_aws_sm_tests() -> bool {
|
||||
std::env::var("RUN_AWS_SM_TESTS")
|
||||
.map(|v| v == "1" || v.to_lowercase() == "true")
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
macro_rules! skip_if_no_aws_sm {
|
||||
() => {
|
||||
if !should_run_aws_sm_tests() {
|
||||
println!("Skipping test: RUN_AWS_SM_TESTS=1 not set");
|
||||
println!("To run: RUN_AWS_SM_TESTS=1 cargo test -p windmill-common --features private,enterprise aws_sm_integration -- --nocapture");
|
||||
println!("Requires: docker run -d -p 4566:4566 -e SERVICES=secretsmanager localstack/localstack:3.8");
|
||||
return;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fn test_settings() -> AwsSecretsManagerSettings {
|
||||
AwsSecretsManagerSettings {
|
||||
region: std::env::var("AWS_SM_REGION").unwrap_or_else(|_| "us-east-1".to_string()),
|
||||
access_key_id: Some("test".to_string()),
|
||||
secret_access_key: Some("test".to_string()),
|
||||
endpoint_url: Some(
|
||||
std::env::var("AWS_SM_ENDPOINT")
|
||||
.unwrap_or_else(|_| "http://localhost:4566".to_string()),
|
||||
),
|
||||
prefix: Some("windmill-test/".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_connection() {
|
||||
skip_if_no_aws_sm!();
|
||||
let result = test_aws_sm_connection(&test_settings()).await;
|
||||
assert!(result.is_ok(), "Connection test failed: {:?}", result.err());
|
||||
println!(" ✓ Connection test passed");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_create_and_get_secret() {
|
||||
skip_if_no_aws_sm!();
|
||||
let backend = AwsSecretsManagerBackend::new_with_client(test_settings())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
backend
|
||||
.set_secret("test-ws", "u/admin/my_secret", "super-secret-value")
|
||||
.await
|
||||
.unwrap();
|
||||
let value = backend.get_secret("test-ws", "u/admin/my_secret").await.unwrap();
|
||||
assert_eq!(value, "super-secret-value");
|
||||
|
||||
backend.delete_secret("test-ws", "u/admin/my_secret").await.unwrap();
|
||||
println!(" ✓ Create + Get + Delete roundtrip passed");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_update_secret() {
|
||||
skip_if_no_aws_sm!();
|
||||
let backend = AwsSecretsManagerBackend::new_with_client(test_settings())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
backend
|
||||
.set_secret("test-ws", "u/admin/update_test", "original")
|
||||
.await
|
||||
.unwrap();
|
||||
backend
|
||||
.set_secret("test-ws", "u/admin/update_test", "updated-value")
|
||||
.await
|
||||
.unwrap();
|
||||
let value = backend
|
||||
.get_secret("test-ws", "u/admin/update_test")
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(value, "updated-value");
|
||||
|
||||
backend
|
||||
.delete_secret("test-ws", "u/admin/update_test")
|
||||
.await
|
||||
.unwrap();
|
||||
println!(" ✓ Update secret passed");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_delete_and_verify_gone() {
|
||||
skip_if_no_aws_sm!();
|
||||
let backend = AwsSecretsManagerBackend::new_with_client(test_settings())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
backend
|
||||
.set_secret("test-ws", "u/admin/delete_test", "to-be-deleted")
|
||||
.await
|
||||
.unwrap();
|
||||
backend
|
||||
.delete_secret("test-ws", "u/admin/delete_test")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let result = backend.get_secret("test-ws", "u/admin/delete_test").await;
|
||||
assert!(
|
||||
matches!(result, Err(windmill_common::error::Error::NotFound(_))),
|
||||
"Expected NotFound after delete, got: {:?}",
|
||||
result
|
||||
);
|
||||
println!(" ✓ Delete + verify gone passed");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_delete_nonexistent_is_ok() {
|
||||
skip_if_no_aws_sm!();
|
||||
let backend = AwsSecretsManagerBackend::new_with_client(test_settings())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let result = backend
|
||||
.delete_secret("test-ws", "u/admin/never_existed")
|
||||
.await;
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"Delete of nonexistent should be Ok, got: {:?}",
|
||||
result.err()
|
||||
);
|
||||
println!(" ✓ Delete nonexistent is Ok");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_get_nonexistent_returns_not_found() {
|
||||
skip_if_no_aws_sm!();
|
||||
let backend = AwsSecretsManagerBackend::new_with_client(test_settings())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let result = backend
|
||||
.get_secret("test-ws", "u/admin/does_not_exist")
|
||||
.await;
|
||||
assert!(
|
||||
matches!(result, Err(windmill_common::error::Error::NotFound(_))),
|
||||
"Expected NotFound, got: {:?}",
|
||||
result
|
||||
);
|
||||
println!(" ✓ Get nonexistent returns NotFound");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_unicode_and_special_chars() {
|
||||
skip_if_no_aws_sm!();
|
||||
let backend = AwsSecretsManagerBackend::new_with_client(test_settings())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let secret_value = "p@$$w0rd with unicode: \u{1F512}\u{1F511} and <xml>&\"quotes\"";
|
||||
backend
|
||||
.set_secret("test-ws", "u/admin/unicode_test", secret_value)
|
||||
.await
|
||||
.unwrap();
|
||||
let retrieved = backend
|
||||
.get_secret("test-ws", "u/admin/unicode_test")
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(retrieved, secret_value);
|
||||
|
||||
backend
|
||||
.delete_secret("test-ws", "u/admin/unicode_test")
|
||||
.await
|
||||
.unwrap();
|
||||
println!(" ✓ Unicode and special chars passed");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_workspace_isolation() {
|
||||
skip_if_no_aws_sm!();
|
||||
let backend = AwsSecretsManagerBackend::new_with_client(test_settings())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Create secrets in different workspaces with the same path
|
||||
backend
|
||||
.set_secret("workspace-a", "u/admin/shared_name", "value-a")
|
||||
.await
|
||||
.unwrap();
|
||||
backend
|
||||
.set_secret("workspace-b", "u/admin/shared_name", "value-b")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Each workspace should see its own value
|
||||
let a = backend
|
||||
.get_secret("workspace-a", "u/admin/shared_name")
|
||||
.await
|
||||
.unwrap();
|
||||
let b = backend
|
||||
.get_secret("workspace-b", "u/admin/shared_name")
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(a, "value-a");
|
||||
assert_eq!(b, "value-b");
|
||||
|
||||
// Cleanup
|
||||
backend
|
||||
.delete_secret("workspace-a", "u/admin/shared_name")
|
||||
.await
|
||||
.unwrap();
|
||||
backend
|
||||
.delete_secret("workspace-b", "u/admin/shared_name")
|
||||
.await
|
||||
.unwrap();
|
||||
println!(" ✓ Workspace isolation passed");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_backend_name() {
|
||||
skip_if_no_aws_sm!();
|
||||
let backend = AwsSecretsManagerBackend::new_with_client(test_settings())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(backend.backend_name(), "aws_secrets_manager");
|
||||
println!(" ✓ Backend name is correct");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(all(feature = "private", feature = "enterprise")))]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn test_aws_sm_requires_enterprise() {
|
||||
println!("AWS Secrets Manager integration tests require Enterprise Edition features");
|
||||
println!("Run with: cargo test -p windmill-common --features private,enterprise");
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@ use windmill_common::{
|
||||
#[cfg(all(feature = "private", feature = "enterprise"))]
|
||||
use windmill_common::{
|
||||
global_settings::{load_value_from_global_settings, SECRET_BACKEND_SETTING},
|
||||
secret_backend::{AzureKeyVaultBackend, AzureKeyVaultSettings, SecretBackendConfig, VaultBackend, VaultSettings},
|
||||
secret_backend::{AwsSecretsManagerBackend, AwsSecretsManagerSettings, AzureKeyVaultBackend, AzureKeyVaultSettings, SecretBackendConfig, VaultBackend, VaultSettings},
|
||||
};
|
||||
|
||||
#[cfg(all(feature = "private", feature = "enterprise"))]
|
||||
@@ -56,6 +56,18 @@ lazy_static::lazy_static! {
|
||||
static ref AZURE_KV_BACKEND_CACHE: RwLock<Option<CachedAzureKvBackend>> = RwLock::new(None);
|
||||
}
|
||||
|
||||
// Cached AWS Secrets Manager backend
|
||||
#[cfg(all(feature = "private", feature = "enterprise"))]
|
||||
struct CachedAwsSmBackend {
|
||||
backend: Arc<dyn SecretBackend>,
|
||||
settings: AwsSecretsManagerSettings,
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "private", feature = "enterprise"))]
|
||||
lazy_static::lazy_static! {
|
||||
static ref AWS_SM_BACKEND_CACHE: RwLock<Option<CachedAwsSmBackend>> = RwLock::new(None);
|
||||
}
|
||||
|
||||
/// Get the current secret backend based on global settings
|
||||
///
|
||||
/// OSS: Always returns DatabaseBackend
|
||||
@@ -80,6 +92,9 @@ pub async fn get_secret_backend(db: &DB) -> Result<Arc<dyn SecretBackend>> {
|
||||
SecretBackendConfig::AzureKeyVault(settings) => {
|
||||
get_or_create_azure_kv_backend(db, settings).await
|
||||
}
|
||||
SecretBackendConfig::AwsSecretsManager(settings) => {
|
||||
get_or_create_aws_sm_backend(db, settings).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,6 +178,37 @@ async fn get_or_create_azure_kv_backend(
|
||||
Ok(backend)
|
||||
}
|
||||
|
||||
/// Get a cached AWS SM backend or create a new one if settings changed
|
||||
#[cfg(all(feature = "private", feature = "enterprise"))]
|
||||
async fn get_or_create_aws_sm_backend(
|
||||
_db: &DB,
|
||||
settings: AwsSecretsManagerSettings,
|
||||
) -> Result<Arc<dyn SecretBackend>> {
|
||||
{
|
||||
let cache = AWS_SM_BACKEND_CACHE.read().await;
|
||||
if let Some(ref cached) = *cache {
|
||||
if cached.settings == settings {
|
||||
return Ok(cached.backend.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut cache = AWS_SM_BACKEND_CACHE.write().await;
|
||||
|
||||
if let Some(ref cached) = *cache {
|
||||
if cached.settings == settings {
|
||||
return Ok(cached.backend.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let backend: Arc<dyn SecretBackend> =
|
||||
Arc::new(AwsSecretsManagerBackend::new_with_client(settings.clone()).await?);
|
||||
|
||||
*cache = Some(CachedAwsSmBackend { backend: backend.clone(), settings });
|
||||
|
||||
Ok(backend)
|
||||
}
|
||||
|
||||
/// Check if a Vault backend is currently configured
|
||||
///
|
||||
/// OSS: Always returns false
|
||||
@@ -179,7 +225,7 @@ pub async fn is_vault_backend_configured(db: &DB) -> Result<bool> {
|
||||
None => SecretBackendConfig::default(),
|
||||
};
|
||||
|
||||
Ok(matches!(config, SecretBackendConfig::HashiCorpVault(_) | SecretBackendConfig::AzureKeyVault(_)))
|
||||
Ok(matches!(config, SecretBackendConfig::HashiCorpVault(_) | SecretBackendConfig::AzureKeyVault(_) | SecretBackendConfig::AwsSecretsManager(_)))
|
||||
}
|
||||
|
||||
/// Get a secret value using the configured backend
|
||||
@@ -209,6 +255,9 @@ pub async fn get_secret_value(
|
||||
"azure_key_vault" => {
|
||||
backend.get_secret(workspace_id, path).await
|
||||
}
|
||||
"aws_secrets_manager" => {
|
||||
backend.get_secret(workspace_id, path).await
|
||||
}
|
||||
_ => Err(Error::internal_err(format!(
|
||||
"Unknown backend: {}",
|
||||
backend.backend_name()
|
||||
@@ -243,6 +292,10 @@ pub async fn store_secret_value(
|
||||
backend.set_secret(workspace_id, path, plain_value).await?;
|
||||
Ok(format!("$azure_kv:{}", path))
|
||||
}
|
||||
"aws_secrets_manager" => {
|
||||
backend.set_secret(workspace_id, path, plain_value).await?;
|
||||
Ok(format!("$aws_sm:{}", path))
|
||||
}
|
||||
_ => Err(Error::internal_err(format!(
|
||||
"Unknown backend: {}",
|
||||
backend.backend_name()
|
||||
@@ -278,9 +331,14 @@ pub fn is_azure_kv_stored_value(value: &str) -> bool {
|
||||
value.starts_with("$azure_kv:")
|
||||
}
|
||||
|
||||
/// Check if a value is stored in AWS Secrets Manager (indicated by the $aws_sm: prefix)
|
||||
pub fn is_aws_sm_stored_value(value: &str) -> bool {
|
||||
value.starts_with("$aws_sm:")
|
||||
}
|
||||
|
||||
/// Check if a value is stored in any external secret backend
|
||||
pub fn is_external_stored_value(value: &str) -> bool {
|
||||
is_vault_stored_value(value) || is_azure_kv_stored_value(value)
|
||||
is_vault_stored_value(value) || is_azure_kv_stored_value(value) || is_aws_sm_stored_value(value)
|
||||
}
|
||||
|
||||
/// Rename a secret in Vault when a variable path changes (EE only)
|
||||
@@ -308,6 +366,14 @@ pub async fn rename_vault_secret(
|
||||
);
|
||||
return Ok(Some(format!("$azure_kv:{}", new_path)));
|
||||
}
|
||||
if is_aws_sm_stored_value(current_value) {
|
||||
tracing::warn!(
|
||||
"Variable has $aws_sm: prefix but AWS Secrets Manager requires Enterprise Edition. \
|
||||
Updating DB reference to {}",
|
||||
new_path
|
||||
);
|
||||
return Ok(Some(format!("$aws_sm:{}", new_path)));
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
@@ -325,6 +391,8 @@ pub async fn rename_vault_secret(
|
||||
|
||||
let marker_prefix = if current_value.starts_with("$azure_kv:") {
|
||||
"$azure_kv:"
|
||||
} else if current_value.starts_with("$aws_sm:") {
|
||||
"$aws_sm:"
|
||||
} else {
|
||||
"$vault:"
|
||||
};
|
||||
@@ -405,6 +473,8 @@ pub async fn rename_vault_secrets_with_prefix(
|
||||
|
||||
let marker_prefix = if value.starts_with("$azure_kv:") {
|
||||
"$azure_kv:"
|
||||
} else if value.starts_with("$aws_sm:") {
|
||||
"$aws_sm:"
|
||||
} else {
|
||||
"$vault:"
|
||||
};
|
||||
|
||||
@@ -683,11 +683,12 @@ export const settings: Record<string, Setting[]> = {
|
||||
{
|
||||
label: 'Backend type',
|
||||
description:
|
||||
'By default, secrets are encrypted and stored in the database. Enterprise Edition supports HashiCorp Vault and Azure Key Vault as external secret stores.',
|
||||
'By default, secrets are encrypted and stored in the database. Enterprise Edition supports HashiCorp Vault, Azure Key Vault, and AWS Secrets Manager as external secret backends.',
|
||||
key: 'secret_backend',
|
||||
fieldType: 'secret_backend',
|
||||
storage: 'setting',
|
||||
ee_only: 'HashiCorp Vault and Azure Key Vault integrations are Enterprise Edition features'
|
||||
ee_only:
|
||||
'HashiCorp Vault, Azure Key Vault, and AWS Secrets Manager integrations are Enterprise Edition features'
|
||||
}
|
||||
],
|
||||
'GitHub App': [
|
||||
|
||||
@@ -19,24 +19,19 @@
|
||||
|
||||
let { values, disabled = false }: Props = $props()
|
||||
|
||||
// Initialize default values if not set
|
||||
$effect(() => {
|
||||
if (!$values['secret_backend']) {
|
||||
$values['secret_backend'] = { type: 'Database' }
|
||||
}
|
||||
})
|
||||
|
||||
let selectedType: 'Database' | 'HashiCorpVault' | 'AzureKeyVault' = $derived(
|
||||
let selectedType: 'Database' | 'HashiCorpVault' | 'AzureKeyVault' | 'AwsSecretsManager' = $derived(
|
||||
$values['secret_backend']?.type ?? 'Database'
|
||||
)
|
||||
|
||||
// Derive auth method from current config
|
||||
// We check jwt_role === null because setAuthMethod explicitly sets jwt_role to null for token mode
|
||||
// and sets token to null for jwt mode. This allows empty token values while still tracking the selection.
|
||||
let authMethod: 'token' | 'jwt' = $derived.by(() => {
|
||||
const config = $values['secret_backend']
|
||||
if (!config || config.type !== 'HashiCorpVault') return 'jwt'
|
||||
// If jwt_role is explicitly null, we're in token mode; otherwise jwt mode
|
||||
return config.jwt_role === null ? 'token' : 'jwt'
|
||||
})
|
||||
|
||||
@@ -52,15 +47,17 @@
|
||||
let migrateToAzureKvModalOpen = $state(false)
|
||||
let migrateFromAzureKvModalOpen = $state(false)
|
||||
|
||||
// Check if Vault option should be disabled (non-EE)
|
||||
let testingAwsSmConnection = $state(false)
|
||||
let migratingToAwsSm = $state(false)
|
||||
let migratingFromAwsSm = $state(false)
|
||||
let migrateToAwsSmModalOpen = $state(false)
|
||||
let migrateFromAwsSmModalOpen = $state(false)
|
||||
|
||||
let vaultDisabled = $derived(!$enterpriseLicense)
|
||||
|
||||
function setBackendType(type: string | undefined) {
|
||||
if (!type) return
|
||||
// Prevent selecting Vault in non-EE
|
||||
if (type === 'HashiCorpVault' && vaultDisabled) {
|
||||
return
|
||||
}
|
||||
if ((type === 'HashiCorpVault' || type === 'AzureKeyVault' || type === 'AwsSecretsManager') && vaultDisabled) return
|
||||
if (type === 'Database') {
|
||||
$values['secret_backend'] = { type: 'Database' }
|
||||
} else if (type === 'HashiCorpVault') {
|
||||
@@ -73,7 +70,6 @@
|
||||
token: $values['secret_backend']?.token ?? null
|
||||
}
|
||||
} else if (type === 'AzureKeyVault') {
|
||||
if (vaultDisabled) return
|
||||
$values['secret_backend'] = {
|
||||
type: 'AzureKeyVault',
|
||||
vault_url: $values['secret_backend']?.vault_url ?? '',
|
||||
@@ -82,31 +78,24 @@
|
||||
client_secret: $values['secret_backend']?.client_secret ?? null,
|
||||
token: $values['secret_backend']?.token ?? null
|
||||
}
|
||||
} else if (type === 'AwsSecretsManager') {
|
||||
$values['secret_backend'] = {
|
||||
type: 'AwsSecretsManager',
|
||||
region: $values['secret_backend']?.region ?? 'us-east-1',
|
||||
access_key_id: $values['secret_backend']?.access_key_id ?? null,
|
||||
secret_access_key: $values['secret_backend']?.secret_access_key ?? null,
|
||||
endpoint_url: $values['secret_backend']?.endpoint_url ?? null,
|
||||
prefix: $values['secret_backend']?.prefix ?? 'windmill/'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setAuthMethod(method: string | undefined) {
|
||||
if (
|
||||
!method ||
|
||||
!$values['secret_backend'] ||
|
||||
$values['secret_backend'].type !== 'HashiCorpVault'
|
||||
)
|
||||
return
|
||||
|
||||
if (!method || !$values['secret_backend'] || $values['secret_backend'].type !== 'HashiCorpVault') return
|
||||
if (method === 'token') {
|
||||
// Clear JWT role when switching to token auth
|
||||
$values['secret_backend'] = {
|
||||
...$values['secret_backend'],
|
||||
jwt_role: null,
|
||||
token: $values['secret_backend'].token ?? ''
|
||||
}
|
||||
$values['secret_backend'] = { ...$values['secret_backend'], jwt_role: null, token: $values['secret_backend'].token ?? '' }
|
||||
} else if (method === 'jwt') {
|
||||
// Clear token when switching to JWT auth
|
||||
$values['secret_backend'] = {
|
||||
...$values['secret_backend'],
|
||||
token: null,
|
||||
jwt_role: $values['secret_backend'].jwt_role ?? 'windmill-secrets'
|
||||
}
|
||||
$values['secret_backend'] = { ...$values['secret_backend'], token: null, jwt_role: $values['secret_backend'].jwt_role ?? 'windmill-secrets' }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,96 +110,47 @@
|
||||
}
|
||||
|
||||
async function testVaultConnection() {
|
||||
if (!$values['secret_backend'] || $values['secret_backend'].type !== 'HashiCorpVault') {
|
||||
return
|
||||
}
|
||||
|
||||
if (!$values['secret_backend'] || $values['secret_backend'].type !== 'HashiCorpVault') return
|
||||
testingConnection = true
|
||||
try {
|
||||
await SettingService.testSecretBackend({
|
||||
requestBody: getVaultSettings()
|
||||
})
|
||||
await SettingService.testSecretBackend({ requestBody: getVaultSettings() })
|
||||
sendUserToast('Successfully connected to HashiCorp Vault')
|
||||
} catch (error: any) {
|
||||
sendUserToast('Failed to connect to Vault: ' + error.message, true)
|
||||
} finally {
|
||||
testingConnection = false
|
||||
}
|
||||
} finally { testingConnection = false }
|
||||
}
|
||||
|
||||
async function migrateSecretsToVault() {
|
||||
if (!$values['secret_backend'] || $values['secret_backend'].type !== 'HashiCorpVault') {
|
||||
return
|
||||
}
|
||||
|
||||
if (!$values['secret_backend'] || $values['secret_backend'].type !== 'HashiCorpVault') return
|
||||
migratingToVault = true
|
||||
try {
|
||||
const report = await SettingService.migrateSecretsToVault({
|
||||
requestBody: getVaultSettings()
|
||||
})
|
||||
if (report.failed_count > 0) {
|
||||
sendUserToast(
|
||||
`Migration completed with errors: ${report.migrated_count}/${report.total_secrets} secrets migrated, ${report.failed_count} failed`,
|
||||
true
|
||||
)
|
||||
console.error('Migration failures:', report.failures)
|
||||
} else {
|
||||
sendUserToast(
|
||||
`Successfully migrated ${report.migrated_count}/${report.total_secrets} secrets to Vault`
|
||||
)
|
||||
}
|
||||
} catch (error: any) {
|
||||
sendUserToast('Failed to migrate secrets to Vault: ' + error.message, true)
|
||||
} finally {
|
||||
migratingToVault = false
|
||||
migrateToVaultModalOpen = false
|
||||
}
|
||||
const report = await SettingService.migrateSecretsToVault({ requestBody: getVaultSettings() })
|
||||
if (report.failed_count > 0) sendUserToast(`Migration: ${report.migrated_count}/${report.total_secrets} migrated, ${report.failed_count} failed`, true)
|
||||
else sendUserToast(`Migrated ${report.migrated_count}/${report.total_secrets} secrets to Vault`)
|
||||
} catch (error: any) { sendUserToast('Failed: ' + error.message, true) }
|
||||
finally { migratingToVault = false; migrateToVaultModalOpen = false }
|
||||
}
|
||||
|
||||
async function migrateSecretsToDatabase() {
|
||||
if (!$values['secret_backend'] || $values['secret_backend'].type !== 'HashiCorpVault') {
|
||||
return
|
||||
}
|
||||
|
||||
if (!$values['secret_backend'] || $values['secret_backend'].type !== 'HashiCorpVault') return
|
||||
migratingToDatabase = true
|
||||
try {
|
||||
const report = await SettingService.migrateSecretsToDatabase({
|
||||
requestBody: getVaultSettings()
|
||||
})
|
||||
if (report.failed_count > 0) {
|
||||
sendUserToast(
|
||||
`Migration completed with errors: ${report.migrated_count}/${report.total_secrets} secrets migrated, ${report.failed_count} failed`,
|
||||
true
|
||||
)
|
||||
console.error('Migration failures:', report.failures)
|
||||
} else {
|
||||
sendUserToast(
|
||||
`Successfully migrated ${report.migrated_count}/${report.total_secrets} secrets to database`
|
||||
)
|
||||
}
|
||||
} catch (error: any) {
|
||||
sendUserToast('Failed to migrate secrets to database: ' + error.message, true)
|
||||
} finally {
|
||||
migratingToDatabase = false
|
||||
migrateToDatabaseModalOpen = false
|
||||
}
|
||||
const report = await SettingService.migrateSecretsToDatabase({ requestBody: getVaultSettings() })
|
||||
if (report.failed_count > 0) sendUserToast(`Migration: ${report.migrated_count}/${report.total_secrets} migrated, ${report.failed_count} failed`, true)
|
||||
else sendUserToast(`Migrated ${report.migrated_count}/${report.total_secrets} secrets to database`)
|
||||
} catch (error: any) { sendUserToast('Failed: ' + error.message, true) }
|
||||
finally { migratingToDatabase = false; migrateToDatabaseModalOpen = false }
|
||||
}
|
||||
|
||||
function isVaultConfigValid(): boolean {
|
||||
if (!$values['secret_backend'] || $values['secret_backend'].type !== 'HashiCorpVault') {
|
||||
return false
|
||||
}
|
||||
if (!$values['secret_backend'] || $values['secret_backend'].type !== 'HashiCorpVault') return false
|
||||
const hasAddress = $values['secret_backend'].address?.trim() !== ''
|
||||
const hasMountPath = $values['secret_backend'].mount_path?.trim() !== ''
|
||||
const hasToken = $values['secret_backend'].token?.trim()
|
||||
const hasJwtRole = $values['secret_backend'].jwt_role?.trim()
|
||||
|
||||
// Must have address and mount path, plus either token OR jwt_role (not both)
|
||||
return hasAddress && hasMountPath && (hasToken || hasJwtRole)
|
||||
}
|
||||
|
||||
// Get the base URL for JWKS endpoint instructions (from instance settings)
|
||||
|
||||
function getAzureKvSettings() {
|
||||
return {
|
||||
vault_url: $values['secret_backend'].vault_url,
|
||||
@@ -227,11 +167,8 @@
|
||||
try {
|
||||
await SettingService.testAzureKvBackend({ requestBody: getAzureKvSettings() })
|
||||
sendUserToast('Successfully connected to Azure Key Vault')
|
||||
} catch (error: any) {
|
||||
sendUserToast('Failed to connect to Azure Key Vault: ' + error.message, true)
|
||||
} finally {
|
||||
testingAzureKvConnection = false
|
||||
}
|
||||
} catch (error: any) { sendUserToast('Failed: ' + error.message, true) }
|
||||
finally { testingAzureKvConnection = false }
|
||||
}
|
||||
|
||||
async function migrateSecretsToAzureKv() {
|
||||
@@ -239,18 +176,10 @@
|
||||
migratingToAzureKv = true
|
||||
try {
|
||||
const report = await SettingService.migrateSecretsToAzureKv({ requestBody: getAzureKvSettings() })
|
||||
if (report.failed_count > 0) {
|
||||
sendUserToast(`Migration completed with errors: ${report.migrated_count}/${report.total_secrets} secrets migrated, ${report.failed_count} failed`, true)
|
||||
console.error('Migration failures:', report.failures)
|
||||
} else {
|
||||
sendUserToast(`Successfully migrated ${report.migrated_count}/${report.total_secrets} secrets to Azure Key Vault`)
|
||||
}
|
||||
} catch (error: any) {
|
||||
sendUserToast('Failed to migrate secrets to Azure Key Vault: ' + error.message, true)
|
||||
} finally {
|
||||
migratingToAzureKv = false
|
||||
migrateToAzureKvModalOpen = false
|
||||
}
|
||||
if (report.failed_count > 0) sendUserToast(`Migration: ${report.migrated_count}/${report.total_secrets} migrated, ${report.failed_count} failed`, true)
|
||||
else sendUserToast(`Migrated ${report.migrated_count}/${report.total_secrets} secrets to Azure Key Vault`)
|
||||
} catch (error: any) { sendUserToast('Failed: ' + error.message, true) }
|
||||
finally { migratingToAzureKv = false; migrateToAzureKvModalOpen = false }
|
||||
}
|
||||
|
||||
async function migrateSecretsFromAzureKv() {
|
||||
@@ -258,18 +187,10 @@
|
||||
migratingFromAzureKv = true
|
||||
try {
|
||||
const report = await SettingService.migrateSecretsFromAzureKv({ requestBody: getAzureKvSettings() })
|
||||
if (report.failed_count > 0) {
|
||||
sendUserToast(`Migration completed with errors: ${report.migrated_count}/${report.total_secrets} secrets migrated, ${report.failed_count} failed`, true)
|
||||
console.error('Migration failures:', report.failures)
|
||||
} else {
|
||||
sendUserToast(`Successfully migrated ${report.migrated_count}/${report.total_secrets} secrets to database`)
|
||||
}
|
||||
} catch (error: any) {
|
||||
sendUserToast('Failed to migrate secrets to database: ' + error.message, true)
|
||||
} finally {
|
||||
migratingFromAzureKv = false
|
||||
migrateFromAzureKvModalOpen = false
|
||||
}
|
||||
if (report.failed_count > 0) sendUserToast(`Migration: ${report.migrated_count}/${report.total_secrets} migrated, ${report.failed_count} failed`, true)
|
||||
else sendUserToast(`Migrated ${report.migrated_count}/${report.total_secrets} secrets to database`)
|
||||
} catch (error: any) { sendUserToast('Failed: ' + error.message, true) }
|
||||
finally { migratingFromAzureKv = false; migrateFromAzureKvModalOpen = false }
|
||||
}
|
||||
|
||||
function isAzureKvConfigValid(): boolean {
|
||||
@@ -282,43 +203,69 @@
|
||||
)
|
||||
}
|
||||
|
||||
function getAwsSmSettings() {
|
||||
return {
|
||||
region: $values['secret_backend'].region,
|
||||
access_key_id: $values['secret_backend'].access_key_id || undefined,
|
||||
secret_access_key: $values['secret_backend'].secret_access_key || undefined,
|
||||
endpoint_url: $values['secret_backend'].endpoint_url || undefined,
|
||||
prefix: $values['secret_backend'].prefix || undefined
|
||||
}
|
||||
}
|
||||
|
||||
async function testAwsSmConnection() {
|
||||
if (!$values['secret_backend'] || $values['secret_backend'].type !== 'AwsSecretsManager') return
|
||||
testingAwsSmConnection = true
|
||||
try {
|
||||
await SettingService.testAwsSmBackend({ requestBody: getAwsSmSettings() })
|
||||
sendUserToast('Successfully connected to AWS Secrets Manager')
|
||||
} catch (error: any) { sendUserToast('Failed: ' + error.message, true) }
|
||||
finally { testingAwsSmConnection = false }
|
||||
}
|
||||
|
||||
async function migrateSecretsToAwsSm() {
|
||||
if (!$values['secret_backend'] || $values['secret_backend'].type !== 'AwsSecretsManager') return
|
||||
migratingToAwsSm = true
|
||||
try {
|
||||
const report = await SettingService.migrateSecretsToAwsSm({ requestBody: getAwsSmSettings() })
|
||||
if (report.failed_count > 0) sendUserToast(`Migration: ${report.migrated_count}/${report.total_secrets} migrated, ${report.failed_count} failed`, true)
|
||||
else sendUserToast(`Migrated ${report.migrated_count}/${report.total_secrets} secrets to AWS Secrets Manager`)
|
||||
} catch (error: any) { sendUserToast('Failed: ' + error.message, true) }
|
||||
finally { migratingToAwsSm = false; migrateToAwsSmModalOpen = false }
|
||||
}
|
||||
|
||||
async function migrateSecretsFromAwsSm() {
|
||||
if (!$values['secret_backend'] || $values['secret_backend'].type !== 'AwsSecretsManager') return
|
||||
migratingFromAwsSm = true
|
||||
try {
|
||||
const report = await SettingService.migrateSecretsFromAwsSm({ requestBody: getAwsSmSettings() })
|
||||
if (report.failed_count > 0) sendUserToast(`Migration: ${report.migrated_count}/${report.total_secrets} migrated, ${report.failed_count} failed`, true)
|
||||
else sendUserToast(`Migrated ${report.migrated_count}/${report.total_secrets} secrets to database`)
|
||||
} catch (error: any) { sendUserToast('Failed: ' + error.message, true) }
|
||||
finally { migratingFromAwsSm = false; migrateFromAwsSmModalOpen = false }
|
||||
}
|
||||
|
||||
function isAwsSmConfigValid(): boolean {
|
||||
if (!$values['secret_backend'] || $values['secret_backend'].type !== 'AwsSecretsManager') return false
|
||||
return $values['secret_backend'].region?.trim() !== ''
|
||||
}
|
||||
|
||||
let baseUrl = $derived($values['base_url'] ?? 'https://your-windmill-instance.com')
|
||||
</script>
|
||||
|
||||
<div class="space-y-6">
|
||||
<!-- Backend Type Selector -->
|
||||
<div class="flex flex-col gap-2 mt-1">
|
||||
<ToggleButtonGroup selected={selectedType} onSelected={(v) => setBackendType(v)}>
|
||||
{#snippet children({ item: toggleButton })}
|
||||
<ToggleButton
|
||||
value="Database"
|
||||
label="Database"
|
||||
tooltip="Store secrets encrypted in the database (default)"
|
||||
item={toggleButton}
|
||||
/>
|
||||
<ToggleButton
|
||||
value="HashiCorpVault"
|
||||
label="HashiCorp Vault (Beta)"
|
||||
tooltip={vaultDisabled
|
||||
? 'HashiCorp Vault integration requires Enterprise Edition'
|
||||
: 'Store secrets in HashiCorp Vault (Beta feature)'}
|
||||
item={toggleButton}
|
||||
disabled={vaultDisabled}
|
||||
/>
|
||||
<ToggleButton
|
||||
value="AzureKeyVault"
|
||||
label="Azure Key Vault"
|
||||
tooltip={vaultDisabled
|
||||
? 'Azure Key Vault integration requires Enterprise Edition'
|
||||
: 'Store secrets in Azure Key Vault'}
|
||||
item={toggleButton}
|
||||
disabled={vaultDisabled}
|
||||
/>
|
||||
<ToggleButton value="Database" label="Database" tooltip="Store secrets encrypted in the database (default)" item={toggleButton} />
|
||||
<ToggleButton value="HashiCorpVault" label="HashiCorp Vault (Beta)" tooltip={vaultDisabled ? 'Requires Enterprise Edition' : 'Store secrets in HashiCorp Vault'} item={toggleButton} disabled={vaultDisabled} />
|
||||
<ToggleButton value="AzureKeyVault" label="Azure Key Vault" tooltip={vaultDisabled ? 'Requires Enterprise Edition' : 'Store secrets in Azure Key Vault'} item={toggleButton} disabled={vaultDisabled} />
|
||||
<ToggleButton value="AwsSecretsManager" label="AWS Secrets Manager (Beta)" tooltip={vaultDisabled ? 'Requires Enterprise Edition' : 'Store secrets in AWS Secrets Manager'} item={toggleButton} disabled={vaultDisabled} />
|
||||
{/snippet}
|
||||
</ToggleButtonGroup>
|
||||
{#if vaultDisabled}
|
||||
<div class="flex items-center gap-1">
|
||||
<EEOnly>HashiCorp Vault and Azure Key Vault integrations require Enterprise Edition</EEOnly>
|
||||
<EEOnly>External secret store integrations require Enterprise Edition</EEOnly>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -328,125 +275,54 @@
|
||||
<Database class="text-primary" size={20} />
|
||||
<div>
|
||||
<p class="text-sm font-medium text-emphasis">Database Storage (Default)</p>
|
||||
<p class="text-xs text-secondary">
|
||||
Secrets are encrypted using workspace-specific keys and stored in the PostgreSQL database.
|
||||
</p>
|
||||
<p class="text-xs text-secondary">Secrets are encrypted using workspace-specific keys and stored in the PostgreSQL database.</p>
|
||||
</div>
|
||||
</div>
|
||||
{:else if selectedType === 'HashiCorpVault'}
|
||||
<!-- Vault Configuration -->
|
||||
<div class="space-y-4 p-4 border rounded-lg">
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<Lock class="text-primary" size={20} />
|
||||
<div>
|
||||
<p class="text-sm font-medium text-emphasis">
|
||||
HashiCorp Vault Configuration
|
||||
<span
|
||||
class="ml-2 px-1.5 py-0.5 text-2xs font-medium bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200 rounded"
|
||||
>Beta</span
|
||||
>
|
||||
</p>
|
||||
<p class="text-xs text-secondary">
|
||||
Store secrets in an external HashiCorp Vault instance.
|
||||
</p>
|
||||
<p class="text-sm font-medium text-emphasis">HashiCorp Vault Configuration <span class="ml-2 px-1.5 py-0.5 text-2xs font-medium bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200 rounded">Beta</span></p>
|
||||
<p class="text-xs text-secondary">Store secrets in an external HashiCorp Vault instance.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4">
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="vault_address" class="block text-xs font-semibold text-emphasis"
|
||||
>Vault Address</label
|
||||
>
|
||||
<TextInput
|
||||
inputProps={{
|
||||
type: 'text',
|
||||
id: 'vault_address',
|
||||
placeholder: 'https://vault.company.com:8200',
|
||||
disabled: disabled
|
||||
}}
|
||||
bind:value={$values['secret_backend'].address}
|
||||
/>
|
||||
<label for="vault_address" class="block text-xs font-semibold text-emphasis">Vault Address</label>
|
||||
<TextInput inputProps={{ type: 'text', id: 'vault_address', placeholder: 'https://vault.company.com:8200', disabled }} bind:value={$values['secret_backend'].address} />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="vault_mount_path" class="block text-xs font-semibold text-emphasis"
|
||||
>KV Mount Path</label
|
||||
>
|
||||
<label for="vault_mount_path" class="block text-xs font-semibold text-emphasis">KV Mount Path</label>
|
||||
<span class="text-2xs text-secondary">The KV v2 secrets engine mount path in Vault</span>
|
||||
<TextInput
|
||||
inputProps={{
|
||||
type: 'text',
|
||||
id: 'vault_mount_path',
|
||||
placeholder: 'windmill',
|
||||
disabled: disabled
|
||||
}}
|
||||
bind:value={$values['secret_backend'].mount_path}
|
||||
/>
|
||||
<TextInput inputProps={{ type: 'text', id: 'vault_mount_path', placeholder: 'windmill', disabled }} bind:value={$values['secret_backend'].mount_path} />
|
||||
</div>
|
||||
|
||||
<!-- Authentication Method Toggle -->
|
||||
<div class="flex flex-col gap-2">
|
||||
<span class="block text-xs font-semibold text-emphasis">Authentication Method</span>
|
||||
<ToggleButtonGroup selected={authMethod} onSelected={(v) => setAuthMethod(v)}>
|
||||
{#snippet children({ item: toggleButton })}
|
||||
<ToggleButton
|
||||
value="jwt"
|
||||
label="JWT Auth"
|
||||
tooltip="Authenticate using Windmill-signed JWTs (recommended for production)"
|
||||
item={toggleButton}
|
||||
{disabled}
|
||||
/>
|
||||
<ToggleButton
|
||||
value="token"
|
||||
label="Static Token"
|
||||
tooltip="Use a static Vault token (for testing/development)"
|
||||
item={toggleButton}
|
||||
{disabled}
|
||||
/>
|
||||
<ToggleButton value="jwt" label="JWT Auth" tooltip="Authenticate using Windmill-signed JWTs" item={toggleButton} {disabled} />
|
||||
<ToggleButton value="token" label="Static Token" tooltip="Use a static Vault token" item={toggleButton} {disabled} />
|
||||
{/snippet}
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
|
||||
{#if authMethod === 'token'}
|
||||
<div class="flex flex-col gap-1 p-3 bg-surface-secondary rounded-lg">
|
||||
<label for="vault_token" class="block text-xs font-semibold text-emphasis"
|
||||
>Vault Token</label
|
||||
>
|
||||
<span class="text-2xs text-secondary"
|
||||
>Static token for authentication. Recommended only for testing/development.</span
|
||||
>
|
||||
<label for="vault_token" class="block text-xs font-semibold text-emphasis">Vault Token</label>
|
||||
<span class="text-2xs text-secondary">Static token. Recommended only for testing/development.</span>
|
||||
<Password bind:password={$values['secret_backend'].token} small {disabled} />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-col gap-2 p-3 bg-surface-secondary rounded-lg">
|
||||
<label for="vault_jwt_role" class="block text-xs font-semibold text-emphasis"
|
||||
>JWT Auth Role</label
|
||||
>
|
||||
<span class="text-2xs text-secondary"
|
||||
>The JWT authentication role configured in Vault.</span
|
||||
>
|
||||
<TextInput
|
||||
inputProps={{
|
||||
type: 'text',
|
||||
id: 'vault_jwt_role',
|
||||
placeholder: 'windmill-secrets',
|
||||
disabled: disabled
|
||||
}}
|
||||
bind:value={$values['secret_backend'].jwt_role}
|
||||
/>
|
||||
|
||||
<!-- Vault JWT Setup Instructions -->
|
||||
<label for="vault_jwt_role" class="block text-xs font-semibold text-emphasis">JWT Auth Role</label>
|
||||
<span class="text-2xs text-secondary">The JWT authentication role configured in Vault.</span>
|
||||
<TextInput inputProps={{ type: 'text', id: 'vault_jwt_role', placeholder: 'windmill-secrets', disabled }} bind:value={$values['secret_backend'].jwt_role} />
|
||||
<details class="mt-2">
|
||||
<summary class="text-xs font-medium text-secondary cursor-pointer hover:text-primary"
|
||||
>Vault JWT Setup Instructions</summary
|
||||
>
|
||||
<summary class="text-xs font-medium text-secondary cursor-pointer hover:text-primary">Vault JWT Setup Instructions</summary>
|
||||
<div class="mt-2 p-2 bg-surface rounded text-2xs text-secondary space-y-2">
|
||||
<p>Configure Vault to accept JWTs from Windmill:</p>
|
||||
<div
|
||||
class="bg-gray-100 dark:bg-gray-800 p-2 rounded font-mono text-2xs overflow-x-auto"
|
||||
>
|
||||
<pre
|
||||
># Enable JWT auth method
|
||||
<div class="bg-gray-100 dark:bg-gray-800 p-2 rounded font-mono text-2xs overflow-x-auto">
|
||||
<pre># Enable JWT auth method
|
||||
vault auth enable jwt
|
||||
|
||||
# Configure JWT auth with Windmill's JWKS endpoint
|
||||
@@ -470,382 +346,162 @@ vault write auth/jwt/role/windmill-secrets \
|
||||
bound_audiences="{baseUrl}" \
|
||||
user_claim="email" \
|
||||
policies="windmill-secrets" \
|
||||
ttl="1h"</pre
|
||||
>
|
||||
ttl="1h"</pre>
|
||||
</div>
|
||||
<p class="text-yellow-600 dark:text-yellow-400">
|
||||
Replace <code>windmill-secrets</code> with your role name if different.
|
||||
</p>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="vault_namespace" class="block text-xs font-semibold text-emphasis"
|
||||
>Namespace (optional)</label
|
||||
>
|
||||
<span class="text-2xs text-secondary"
|
||||
>Vault Enterprise namespace (leave empty if not using namespaces)</span
|
||||
>
|
||||
<TextInput
|
||||
inputProps={{
|
||||
type: 'text',
|
||||
id: 'vault_namespace',
|
||||
placeholder: 'admin/my-namespace',
|
||||
disabled: disabled
|
||||
}}
|
||||
bind:value={$values['secret_backend'].namespace}
|
||||
/>
|
||||
<label for="vault_namespace" class="block text-xs font-semibold text-emphasis">Namespace (optional)</label>
|
||||
<span class="text-2xs text-secondary">Vault Enterprise namespace</span>
|
||||
<TextInput inputProps={{ type: 'text', id: 'vault_namespace', placeholder: 'admin/my-namespace', disabled }} bind:value={$values['secret_backend'].namespace} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex flex-col gap-4 pt-4 border-t">
|
||||
<div class="flex gap-2">
|
||||
<Button
|
||||
unifiedSize="md"
|
||||
variant="accent"
|
||||
onclick={testVaultConnection}
|
||||
disabled={disabled || !isVaultConfigValid() || testingConnection}
|
||||
loading={testingConnection}
|
||||
startIcon={{ icon: Server }}
|
||||
>
|
||||
Test Connection
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- Migration Section -->
|
||||
<Button unifiedSize="md" variant="accent" onclick={testVaultConnection} disabled={disabled || !isVaultConfigValid() || testingConnection} loading={testingConnection} startIcon={{ icon: Server }}>Test Connection</Button>
|
||||
<div class="flex flex-col gap-4 pt-4 border-t">
|
||||
<span class="block text-xs font-semibold text-emphasis">Secret Migration</span>
|
||||
<span class="text-2xs text-secondary">
|
||||
Migrate secrets between the database and HashiCorp Vault. Original values are NOT
|
||||
deleted to allow for rollback.
|
||||
</span>
|
||||
|
||||
<span class="text-2xs text-secondary">Original values are NOT deleted to allow for rollback.</span>
|
||||
<div class="flex gap-4">
|
||||
<!-- Database to Vault -->
|
||||
<div class="flex-1 p-3 border rounded-lg">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<Database size={16} />
|
||||
<ArrowRight size={16} />
|
||||
<Lock size={16} />
|
||||
</div>
|
||||
<div class="flex items-center gap-2 mb-2"><Database size={16} /><ArrowRight size={16} /><Lock size={16} /></div>
|
||||
<p class="text-xs font-medium mb-2">Database → Vault</p>
|
||||
<p class="text-2xs text-secondary mb-3">
|
||||
Decrypt secrets from database and store in Vault
|
||||
</p>
|
||||
<Button
|
||||
unifiedSize="sm"
|
||||
variant="default"
|
||||
onclick={() => (migrateToVaultModalOpen = true)}
|
||||
disabled={disabled || !isVaultConfigValid() || migratingToVault}
|
||||
startIcon={{ icon: ArrowRight }}
|
||||
>
|
||||
Migrate to Vault
|
||||
</Button>
|
||||
<Button unifiedSize="sm" variant="default" onclick={() => (migrateToVaultModalOpen = true)} disabled={disabled || !isVaultConfigValid() || migratingToVault} startIcon={{ icon: ArrowRight }}>Migrate to Vault</Button>
|
||||
</div>
|
||||
|
||||
<!-- Vault to Database -->
|
||||
<div class="flex-1 p-3 border rounded-lg">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<Lock size={16} />
|
||||
<ArrowLeft size={16} />
|
||||
<Database size={16} />
|
||||
</div>
|
||||
<div class="flex items-center gap-2 mb-2"><Lock size={16} /><ArrowLeft size={16} /><Database size={16} /></div>
|
||||
<p class="text-xs font-medium mb-2">Vault → Database</p>
|
||||
<p class="text-2xs text-secondary mb-3">
|
||||
Read secrets from Vault and encrypt in database
|
||||
</p>
|
||||
<Button
|
||||
unifiedSize="sm"
|
||||
variant="default"
|
||||
onclick={() => (migrateToDatabaseModalOpen = true)}
|
||||
disabled={disabled || !isVaultConfigValid() || migratingToDatabase}
|
||||
startIcon={{ icon: ArrowLeft }}
|
||||
>
|
||||
Migrate to Database
|
||||
</Button>
|
||||
<Button unifiedSize="sm" variant="default" onclick={() => (migrateToDatabaseModalOpen = true)} disabled={disabled || !isVaultConfigValid() || migratingToDatabase} startIcon={{ icon: ArrowLeft }}>Migrate to Database</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else if selectedType === 'AzureKeyVault'}
|
||||
<!-- Azure Key Vault Configuration -->
|
||||
<div class="space-y-4 p-4 border rounded-lg">
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<Cloud class="text-primary" size={20} />
|
||||
<div>
|
||||
<p class="text-sm font-medium text-emphasis">Azure Key Vault Configuration</p>
|
||||
<p class="text-xs text-secondary">
|
||||
Store secrets in an Azure Key Vault instance.
|
||||
</p>
|
||||
<p class="text-xs text-secondary">Store secrets in an Azure Key Vault instance.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4">
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="azure_vault_url" class="block text-xs font-semibold text-emphasis"
|
||||
>Vault URL</label
|
||||
>
|
||||
<TextInput
|
||||
inputProps={{
|
||||
type: 'text',
|
||||
id: 'azure_vault_url',
|
||||
placeholder: 'https://my-vault.vault.azure.net',
|
||||
disabled: disabled
|
||||
}}
|
||||
bind:value={$values['secret_backend'].vault_url}
|
||||
/>
|
||||
<label for="azure_vault_url" class="block text-xs font-semibold text-emphasis">Vault URL</label>
|
||||
<TextInput inputProps={{ type: 'text', id: 'azure_vault_url', placeholder: 'https://my-vault.vault.azure.net', disabled }} bind:value={$values['secret_backend'].vault_url} />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="azure_tenant_id" class="block text-xs font-semibold text-emphasis"
|
||||
>Tenant ID</label
|
||||
>
|
||||
<span class="text-2xs text-secondary">The Azure Active Directory tenant ID</span>
|
||||
<TextInput
|
||||
inputProps={{
|
||||
type: 'text',
|
||||
id: 'azure_tenant_id',
|
||||
placeholder: '00000000-0000-0000-0000-000000000000',
|
||||
disabled: disabled
|
||||
}}
|
||||
bind:value={$values['secret_backend'].tenant_id}
|
||||
/>
|
||||
<label for="azure_tenant_id" class="block text-xs font-semibold text-emphasis">Tenant ID</label>
|
||||
<TextInput inputProps={{ type: 'text', id: 'azure_tenant_id', placeholder: '00000000-0000-0000-0000-000000000000', disabled }} bind:value={$values['secret_backend'].tenant_id} />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="azure_client_id" class="block text-xs font-semibold text-emphasis"
|
||||
>Client ID</label
|
||||
>
|
||||
<span class="text-2xs text-secondary">The Azure AD application (service principal) client ID</span>
|
||||
<TextInput
|
||||
inputProps={{
|
||||
type: 'text',
|
||||
id: 'azure_client_id',
|
||||
placeholder: '00000000-0000-0000-0000-000000000000',
|
||||
disabled: disabled
|
||||
}}
|
||||
bind:value={$values['secret_backend'].client_id}
|
||||
/>
|
||||
<label for="azure_client_id" class="block text-xs font-semibold text-emphasis">Client ID</label>
|
||||
<TextInput inputProps={{ type: 'text', id: 'azure_client_id', placeholder: '00000000-0000-0000-0000-000000000000', disabled }} bind:value={$values['secret_backend'].client_id} />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1 p-3 bg-surface-secondary rounded-lg">
|
||||
<label for="azure_client_secret" class="block text-xs font-semibold text-emphasis"
|
||||
>Client Secret</label
|
||||
>
|
||||
<span class="text-2xs text-secondary">The Azure AD application client secret for authentication</span>
|
||||
<label for="azure_client_secret" class="block text-xs font-semibold text-emphasis">Client Secret</label>
|
||||
<Password bind:password={$values['secret_backend'].client_secret} small {disabled} />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1 p-3 bg-surface-secondary rounded-lg">
|
||||
<label for="azure_token" class="block text-xs font-semibold text-emphasis"
|
||||
>Token (optional)</label
|
||||
>
|
||||
<span class="text-2xs text-secondary">Static Bearer token for testing/development. If provided, OAuth2 authentication is skipped.</span>
|
||||
<label for="azure_token" class="block text-xs font-semibold text-emphasis">Token (optional)</label>
|
||||
<span class="text-2xs text-secondary">Static Bearer token for testing. If provided, OAuth2 is skipped.</span>
|
||||
<Password bind:password={$values['secret_backend'].token} small {disabled} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex flex-col gap-4 pt-4 border-t">
|
||||
<div class="flex gap-2">
|
||||
<Button
|
||||
unifiedSize="md"
|
||||
variant="accent"
|
||||
onclick={testAzureKvConnection}
|
||||
disabled={disabled || !isAzureKvConfigValid() || testingAzureKvConnection}
|
||||
loading={testingAzureKvConnection}
|
||||
startIcon={{ icon: Server }}
|
||||
>
|
||||
Test Connection
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- Migration Section -->
|
||||
<Button unifiedSize="md" variant="accent" onclick={testAzureKvConnection} disabled={disabled || !isAzureKvConfigValid() || testingAzureKvConnection} loading={testingAzureKvConnection} startIcon={{ icon: Server }}>Test Connection</Button>
|
||||
<div class="flex flex-col gap-4 pt-4 border-t">
|
||||
<span class="block text-xs font-semibold text-emphasis">Secret Migration</span>
|
||||
<span class="text-2xs text-secondary">
|
||||
Migrate secrets between the database and Azure Key Vault. Original values are NOT
|
||||
deleted to allow for rollback.
|
||||
</span>
|
||||
|
||||
<span class="text-2xs text-secondary">Original values are NOT deleted to allow for rollback.</span>
|
||||
<div class="flex gap-4">
|
||||
<!-- Database to Azure KV -->
|
||||
<div class="flex-1 p-3 border rounded-lg">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<Database size={16} />
|
||||
<ArrowRight size={16} />
|
||||
<Cloud size={16} />
|
||||
</div>
|
||||
<div class="flex items-center gap-2 mb-2"><Database size={16} /><ArrowRight size={16} /><Cloud size={16} /></div>
|
||||
<p class="text-xs font-medium mb-2">Database → Azure Key Vault</p>
|
||||
<p class="text-2xs text-secondary mb-3">
|
||||
Decrypt secrets from database and store in Azure Key Vault
|
||||
</p>
|
||||
<Button
|
||||
unifiedSize="sm"
|
||||
variant="default"
|
||||
onclick={() => (migrateToAzureKvModalOpen = true)}
|
||||
disabled={disabled || !isAzureKvConfigValid() || migratingToAzureKv}
|
||||
startIcon={{ icon: ArrowRight }}
|
||||
>
|
||||
Migrate to Azure Key Vault
|
||||
</Button>
|
||||
<Button unifiedSize="sm" variant="default" onclick={() => (migrateToAzureKvModalOpen = true)} disabled={disabled || !isAzureKvConfigValid() || migratingToAzureKv} startIcon={{ icon: ArrowRight }}>Migrate to Azure KV</Button>
|
||||
</div>
|
||||
|
||||
<!-- Azure KV to Database -->
|
||||
<div class="flex-1 p-3 border rounded-lg">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<Cloud size={16} />
|
||||
<ArrowLeft size={16} />
|
||||
<Database size={16} />
|
||||
</div>
|
||||
<div class="flex items-center gap-2 mb-2"><Cloud size={16} /><ArrowLeft size={16} /><Database size={16} /></div>
|
||||
<p class="text-xs font-medium mb-2">Azure Key Vault → Database</p>
|
||||
<p class="text-2xs text-secondary mb-3">
|
||||
Read secrets from Azure Key Vault and encrypt in database
|
||||
</p>
|
||||
<Button
|
||||
unifiedSize="sm"
|
||||
variant="default"
|
||||
onclick={() => (migrateFromAzureKvModalOpen = true)}
|
||||
disabled={disabled || !isAzureKvConfigValid() || migratingFromAzureKv}
|
||||
startIcon={{ icon: ArrowLeft }}
|
||||
>
|
||||
Migrate to Database
|
||||
</Button>
|
||||
<Button unifiedSize="sm" variant="default" onclick={() => (migrateFromAzureKvModalOpen = true)} disabled={disabled || !isAzureKvConfigValid() || migratingFromAzureKv} startIcon={{ icon: ArrowLeft }}>Migrate to Database</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else if selectedType === 'AwsSecretsManager'}
|
||||
<div class="space-y-4 p-4 border rounded-lg">
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<Cloud class="text-primary" size={20} />
|
||||
<div>
|
||||
<p class="text-sm font-medium text-emphasis">AWS Secrets Manager Configuration <span class="ml-2 px-1.5 py-0.5 text-2xs font-medium bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200 rounded">Beta</span></p>
|
||||
<p class="text-xs text-secondary">Store secrets in AWS Secrets Manager.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-4">
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="aws_sm_region" class="block text-xs font-semibold text-emphasis">Region</label>
|
||||
<TextInput inputProps={{ type: 'text', id: 'aws_sm_region', placeholder: 'us-east-1', disabled }} bind:value={$values['secret_backend'].region} />
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="aws_sm_access_key_id" class="block text-xs font-semibold text-emphasis">Access Key ID (optional)</label>
|
||||
<span class="text-2xs text-secondary">If not provided, the default AWS credential chain is used (env vars, instance profile, EKS pod identity)</span>
|
||||
<TextInput inputProps={{ type: 'text', id: 'aws_sm_access_key_id', placeholder: 'AKIA...', disabled }} bind:value={$values['secret_backend'].access_key_id} />
|
||||
</div>
|
||||
<div class="flex flex-col gap-1 p-3 bg-surface-secondary rounded-lg">
|
||||
<label for="aws_sm_secret_access_key" class="block text-xs font-semibold text-emphasis">Secret Access Key (optional)</label>
|
||||
<Password bind:password={$values['secret_backend'].secret_access_key} small {disabled} />
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="aws_sm_prefix" class="block text-xs font-semibold text-emphasis">Secret Name Prefix (optional)</label>
|
||||
<span class="text-2xs text-secondary">Prefix for secret names in AWS Secrets Manager (default: windmill/)</span>
|
||||
<TextInput inputProps={{ type: 'text', id: 'aws_sm_prefix', placeholder: 'windmill/', disabled }} bind:value={$values['secret_backend'].prefix} />
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="aws_sm_endpoint_url" class="block text-xs font-semibold text-emphasis">Endpoint URL (optional)</label>
|
||||
<span class="text-2xs text-secondary">Custom endpoint for LocalStack or other compatible services</span>
|
||||
<TextInput inputProps={{ type: 'text', id: 'aws_sm_endpoint_url', placeholder: 'http://localhost:4566', disabled }} bind:value={$values['secret_backend'].endpoint_url} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-4 pt-4 border-t">
|
||||
<Button unifiedSize="md" variant="accent" onclick={testAwsSmConnection} disabled={disabled || !isAwsSmConfigValid() || testingAwsSmConnection} loading={testingAwsSmConnection} startIcon={{ icon: Server }}>Test Connection</Button>
|
||||
<div class="flex flex-col gap-4 pt-4 border-t">
|
||||
<span class="block text-xs font-semibold text-emphasis">Secret Migration</span>
|
||||
<span class="text-2xs text-secondary">Original values are NOT deleted to allow for rollback.</span>
|
||||
<div class="flex gap-4">
|
||||
<div class="flex-1 p-3 border rounded-lg">
|
||||
<div class="flex items-center gap-2 mb-2"><Database size={16} /><ArrowRight size={16} /><Cloud size={16} /></div>
|
||||
<p class="text-xs font-medium mb-2">Database → AWS Secrets Manager</p>
|
||||
<Button unifiedSize="sm" variant="default" onclick={() => (migrateToAwsSmModalOpen = true)} disabled={disabled || !isAwsSmConfigValid() || migratingToAwsSm} startIcon={{ icon: ArrowRight }}>Migrate to AWS SM</Button>
|
||||
</div>
|
||||
<div class="flex-1 p-3 border rounded-lg">
|
||||
<div class="flex items-center gap-2 mb-2"><Cloud size={16} /><ArrowLeft size={16} /><Database size={16} /></div>
|
||||
<p class="text-xs font-medium mb-2">AWS Secrets Manager → Database</p>
|
||||
<Button unifiedSize="sm" variant="default" onclick={() => (migrateFromAwsSmModalOpen = true)} disabled={disabled || !isAwsSmConfigValid() || migratingFromAwsSm} startIcon={{ icon: ArrowLeft }}>Migrate to Database</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Migrate to Azure Key Vault Modal -->
|
||||
<ConfirmationModal
|
||||
title="Migrate Secrets to Azure Key Vault"
|
||||
confirmationText="Migrate"
|
||||
open={migrateToAzureKvModalOpen}
|
||||
loading={migratingToAzureKv}
|
||||
type="reload"
|
||||
onCanceled={() => {
|
||||
migrateToAzureKvModalOpen = false
|
||||
}}
|
||||
onConfirmed={migrateSecretsToAzureKv}
|
||||
>
|
||||
{#snippet children()}
|
||||
<div class="flex flex-col gap-2">
|
||||
<p>
|
||||
This will migrate all existing secrets from the database to Azure Key Vault. The process
|
||||
will:
|
||||
</p>
|
||||
<ol class="list-decimal list-inside text-sm space-y-1">
|
||||
<li>Read all encrypted secrets from the database</li>
|
||||
<li>Decrypt them using the workspace encryption keys</li>
|
||||
<li>Store them in Azure Key Vault</li>
|
||||
</ol>
|
||||
<p class="text-yellow-600 dark:text-yellow-400 text-sm mt-2">
|
||||
Note: Database values are NOT deleted automatically. You can manually clear them after
|
||||
verifying the migration was successful.
|
||||
</p>
|
||||
<p>Are you sure you want to proceed?</p>
|
||||
</div>
|
||||
{/snippet}
|
||||
<ConfirmationModal title="Migrate to AWS Secrets Manager" confirmationText="Migrate" open={migrateToAwsSmModalOpen} loading={migratingToAwsSm} type="reload" onCanceled={() => { migrateToAwsSmModalOpen = false }} onConfirmed={migrateSecretsToAwsSm}>
|
||||
{#snippet children()}<div class="flex flex-col gap-2"><p>This will copy all secrets from the database to AWS Secrets Manager.</p><p class="text-yellow-600 dark:text-yellow-400 text-sm">Database values are NOT deleted automatically.</p></div>{/snippet}
|
||||
</ConfirmationModal>
|
||||
|
||||
<!-- Migrate from Azure Key Vault to Database Modal -->
|
||||
<ConfirmationModal
|
||||
title="Migrate Secrets to Database"
|
||||
confirmationText="Migrate"
|
||||
open={migrateFromAzureKvModalOpen}
|
||||
loading={migratingFromAzureKv}
|
||||
type="reload"
|
||||
onCanceled={() => {
|
||||
migrateFromAzureKvModalOpen = false
|
||||
}}
|
||||
onConfirmed={migrateSecretsFromAzureKv}
|
||||
>
|
||||
{#snippet children()}
|
||||
<div class="flex flex-col gap-2">
|
||||
<p>
|
||||
This will migrate all secrets from Azure Key Vault back to the database. The process will:
|
||||
</p>
|
||||
<ol class="list-decimal list-inside text-sm space-y-1">
|
||||
<li>List all secrets in Azure Key Vault for each workspace</li>
|
||||
<li>Read each secret value from Azure Key Vault</li>
|
||||
<li>Encrypt and store them in the database</li>
|
||||
</ol>
|
||||
<p class="text-yellow-600 dark:text-yellow-400 text-sm mt-2">
|
||||
Note: Azure Key Vault values are NOT deleted automatically. Only secrets that already exist in the
|
||||
database will be updated.
|
||||
</p>
|
||||
<p>Are you sure you want to proceed?</p>
|
||||
</div>
|
||||
{/snippet}
|
||||
<ConfirmationModal title="Migrate to Database" confirmationText="Migrate" open={migrateFromAwsSmModalOpen} loading={migratingFromAwsSm} type="reload" onCanceled={() => { migrateFromAwsSmModalOpen = false }} onConfirmed={migrateSecretsFromAwsSm}>
|
||||
{#snippet children()}<div class="flex flex-col gap-2"><p>This will copy all secrets from AWS Secrets Manager back to the database.</p><p class="text-yellow-600 dark:text-yellow-400 text-sm">AWS Secrets Manager values are NOT deleted automatically.</p></div>{/snippet}
|
||||
</ConfirmationModal>
|
||||
|
||||
<!-- Migrate to Vault Modal -->
|
||||
<ConfirmationModal
|
||||
title="Migrate Secrets to Vault"
|
||||
confirmationText="Migrate"
|
||||
open={migrateToVaultModalOpen}
|
||||
loading={migratingToVault}
|
||||
type="reload"
|
||||
onCanceled={() => {
|
||||
migrateToVaultModalOpen = false
|
||||
}}
|
||||
onConfirmed={migrateSecretsToVault}
|
||||
>
|
||||
{#snippet children()}
|
||||
<div class="flex flex-col gap-2">
|
||||
<p>
|
||||
This will migrate all existing secrets from the database to HashiCorp Vault. The process
|
||||
will:
|
||||
</p>
|
||||
<ol class="list-decimal list-inside text-sm space-y-1">
|
||||
<li>Read all encrypted secrets from the database</li>
|
||||
<li>Decrypt them using the workspace encryption keys</li>
|
||||
<li>Store them in HashiCorp Vault under the configured mount path</li>
|
||||
</ol>
|
||||
<p class="text-yellow-600 dark:text-yellow-400 text-sm mt-2">
|
||||
Note: Database values are NOT deleted automatically. You can manually clear them after
|
||||
verifying the migration was successful.
|
||||
</p>
|
||||
<p>Are you sure you want to proceed?</p>
|
||||
</div>
|
||||
{/snippet}
|
||||
<ConfirmationModal title="Migrate to Azure Key Vault" confirmationText="Migrate" open={migrateToAzureKvModalOpen} loading={migratingToAzureKv} type="reload" onCanceled={() => { migrateToAzureKvModalOpen = false }} onConfirmed={migrateSecretsToAzureKv}>
|
||||
{#snippet children()}<div class="flex flex-col gap-2"><p>This will copy all secrets to Azure Key Vault.</p><p class="text-yellow-600 dark:text-yellow-400 text-sm">Database values are NOT deleted automatically.</p></div>{/snippet}
|
||||
</ConfirmationModal>
|
||||
|
||||
<!-- Migrate to Database Modal -->
|
||||
<ConfirmationModal
|
||||
title="Migrate Secrets to Database"
|
||||
confirmationText="Migrate"
|
||||
open={migrateToDatabaseModalOpen}
|
||||
loading={migratingToDatabase}
|
||||
type="reload"
|
||||
onCanceled={() => {
|
||||
migrateToDatabaseModalOpen = false
|
||||
}}
|
||||
onConfirmed={migrateSecretsToDatabase}
|
||||
>
|
||||
{#snippet children()}
|
||||
<div class="flex flex-col gap-2">
|
||||
<p>
|
||||
This will migrate all secrets from HashiCorp Vault back to the database. The process will:
|
||||
</p>
|
||||
<ol class="list-decimal list-inside text-sm space-y-1">
|
||||
<li>List all secrets in Vault for each workspace</li>
|
||||
<li>Read each secret value from Vault</li>
|
||||
<li>Encrypt and store them in the database</li>
|
||||
</ol>
|
||||
<p class="text-yellow-600 dark:text-yellow-400 text-sm mt-2">
|
||||
Note: Vault values are NOT deleted automatically. Only secrets that already exist in the
|
||||
database will be updated.
|
||||
</p>
|
||||
<p>Are you sure you want to proceed?</p>
|
||||
</div>
|
||||
{/snippet}
|
||||
<ConfirmationModal title="Migrate to Database" confirmationText="Migrate" open={migrateFromAzureKvModalOpen} loading={migratingFromAzureKv} type="reload" onCanceled={() => { migrateFromAzureKvModalOpen = false }} onConfirmed={migrateSecretsFromAzureKv}>
|
||||
{#snippet children()}<div class="flex flex-col gap-2"><p>This will copy all secrets from Azure Key Vault back to the database.</p></div>{/snippet}
|
||||
</ConfirmationModal>
|
||||
<ConfirmationModal title="Migrate to Vault" confirmationText="Migrate" open={migrateToVaultModalOpen} loading={migratingToVault} type="reload" onCanceled={() => { migrateToVaultModalOpen = false }} onConfirmed={migrateSecretsToVault}>
|
||||
{#snippet children()}<div class="flex flex-col gap-2"><p>This will copy all secrets to HashiCorp Vault.</p><p class="text-yellow-600 dark:text-yellow-400 text-sm">Database values are NOT deleted automatically.</p></div>{/snippet}
|
||||
</ConfirmationModal>
|
||||
<ConfirmationModal title="Migrate to Database" confirmationText="Migrate" open={migrateToDatabaseModalOpen} loading={migratingToDatabase} type="reload" onCanceled={() => { migrateToDatabaseModalOpen = false }} onConfirmed={migrateSecretsToDatabase}>
|
||||
{#snippet children()}<div class="flex flex-col gap-2"><p>This will copy all secrets from Vault back to the database.</p></div>{/snippet}
|
||||
</ConfirmationModal>
|
||||
|
||||
Reference in New Issue
Block a user