mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-25 23:10:11 +00:00
The Slack channel capabilities.json declares secret names in lowercase (slack_bot_token) but the web UI stored them in UPPERCASE (SLACK_BOT_TOKEN), causing credential injection to fail with "not_authed". Changes: - CreateSecretParams::new() normalizes name to lowercase on creation - All SecretsStore lookups (get, exists, delete, is_accessible) now lowercase the name parameter before querying - Applied to all three backends: PostgreSQL, libSQL, InMemory - CredentialInjector::is_secret_allowed() uses case-insensitive comparison Co-authored-by: Claude Opus 4.6 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
f62937d482
commit
ec31e83a7d
+32
-19
@@ -129,6 +129,7 @@ impl SecretsStore for PostgresSecretsStore {
|
||||
}
|
||||
|
||||
async fn get(&self, user_id: &str, name: &str) -> Result<Secret, SecretError> {
|
||||
let name = name.to_lowercase();
|
||||
let client = self
|
||||
.pool
|
||||
.get()
|
||||
@@ -176,6 +177,7 @@ impl SecretsStore for PostgresSecretsStore {
|
||||
}
|
||||
|
||||
async fn exists(&self, user_id: &str, name: &str) -> Result<bool, SecretError> {
|
||||
let name = name.to_lowercase();
|
||||
let client = self
|
||||
.pool
|
||||
.get()
|
||||
@@ -218,6 +220,7 @@ impl SecretsStore for PostgresSecretsStore {
|
||||
}
|
||||
|
||||
async fn delete(&self, user_id: &str, name: &str) -> Result<bool, SecretError> {
|
||||
let name = name.to_lowercase();
|
||||
let client = self
|
||||
.pool
|
||||
.get()
|
||||
@@ -263,21 +266,23 @@ impl SecretsStore for PostgresSecretsStore {
|
||||
secret_name: &str,
|
||||
allowed_secrets: &[String],
|
||||
) -> Result<bool, SecretError> {
|
||||
let secret_name_lower = secret_name.to_lowercase();
|
||||
// First check if the secret exists
|
||||
if !self.exists(user_id, secret_name).await? {
|
||||
if !self.exists(user_id, &secret_name_lower).await? {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// Check if secret is in the allowed list
|
||||
// Supports glob patterns: "openai_*" matches "openai_api_key"
|
||||
for pattern in allowed_secrets {
|
||||
if pattern == secret_name {
|
||||
let pattern_lower = pattern.to_lowercase();
|
||||
if pattern_lower == secret_name_lower {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
// Simple glob: * matches any suffix
|
||||
if let Some(prefix) = pattern.strip_suffix('*')
|
||||
&& secret_name.starts_with(prefix)
|
||||
if let Some(prefix) = pattern_lower.strip_suffix('*')
|
||||
&& secret_name_lower.starts_with(prefix)
|
||||
{
|
||||
return Ok(true);
|
||||
}
|
||||
@@ -415,6 +420,7 @@ impl SecretsStore for LibSqlSecretsStore {
|
||||
}
|
||||
|
||||
async fn get(&self, user_id: &str, name: &str) -> Result<Secret, SecretError> {
|
||||
let name = name.to_lowercase();
|
||||
let conn = self.connect().await?;
|
||||
let mut rows = conn
|
||||
.query(
|
||||
@@ -424,7 +430,7 @@ impl SecretsStore for LibSqlSecretsStore {
|
||||
FROM secrets
|
||||
WHERE user_id = ?1 AND name = ?2
|
||||
"#,
|
||||
libsql::params![user_id, name],
|
||||
libsql::params![user_id, name.as_str()],
|
||||
)
|
||||
.await
|
||||
.map_err(|e| SecretError::Database(e.to_string()))?;
|
||||
@@ -460,11 +466,12 @@ impl SecretsStore for LibSqlSecretsStore {
|
||||
}
|
||||
|
||||
async fn exists(&self, user_id: &str, name: &str) -> Result<bool, SecretError> {
|
||||
let name = name.to_lowercase();
|
||||
let conn = self.connect().await?;
|
||||
let mut rows = conn
|
||||
.query(
|
||||
"SELECT 1 FROM secrets WHERE user_id = ?1 AND name = ?2",
|
||||
libsql::params![user_id, name],
|
||||
libsql::params![user_id, name.as_str()],
|
||||
)
|
||||
.await
|
||||
.map_err(|e| SecretError::Database(e.to_string()))?;
|
||||
@@ -501,11 +508,12 @@ impl SecretsStore for LibSqlSecretsStore {
|
||||
}
|
||||
|
||||
async fn delete(&self, user_id: &str, name: &str) -> Result<bool, SecretError> {
|
||||
let name = name.to_lowercase();
|
||||
let conn = self.connect().await?;
|
||||
let affected = conn
|
||||
.execute(
|
||||
"DELETE FROM secrets WHERE user_id = ?1 AND name = ?2",
|
||||
libsql::params![user_id, name],
|
||||
libsql::params![user_id, name.as_str()],
|
||||
)
|
||||
.await
|
||||
.map_err(|e| SecretError::Database(e.to_string()))?;
|
||||
@@ -537,17 +545,19 @@ impl SecretsStore for LibSqlSecretsStore {
|
||||
secret_name: &str,
|
||||
allowed_secrets: &[String],
|
||||
) -> Result<bool, SecretError> {
|
||||
if !self.exists(user_id, secret_name).await? {
|
||||
let secret_name_lower = secret_name.to_lowercase();
|
||||
if !self.exists(user_id, &secret_name_lower).await? {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
for pattern in allowed_secrets {
|
||||
if pattern == secret_name {
|
||||
let pattern_lower = pattern.to_lowercase();
|
||||
if pattern_lower == secret_name_lower {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
if let Some(prefix) = pattern.strip_suffix('*')
|
||||
&& secret_name.starts_with(prefix)
|
||||
if let Some(prefix) = pattern_lower.strip_suffix('*')
|
||||
&& secret_name_lower.starts_with(prefix)
|
||||
{
|
||||
return Ok(true);
|
||||
}
|
||||
@@ -701,13 +711,14 @@ pub mod in_memory {
|
||||
}
|
||||
|
||||
async fn get(&self, user_id: &str, name: &str) -> Result<Secret, SecretError> {
|
||||
let name = name.to_lowercase();
|
||||
let secret = self
|
||||
.secrets
|
||||
.read()
|
||||
.await
|
||||
.get(&(user_id.to_string(), name.to_string()))
|
||||
.get(&(user_id.to_string(), name.clone()))
|
||||
.cloned()
|
||||
.ok_or_else(|| SecretError::NotFound(name.to_string()))?;
|
||||
.ok_or_else(|| SecretError::NotFound(name.clone()))?;
|
||||
|
||||
if let Some(expires_at) = secret.expires_at
|
||||
&& expires_at < Utc::now()
|
||||
@@ -733,7 +744,7 @@ pub mod in_memory {
|
||||
.secrets
|
||||
.read()
|
||||
.await
|
||||
.contains_key(&(user_id.to_string(), name.to_string())))
|
||||
.contains_key(&(user_id.to_string(), name.to_lowercase())))
|
||||
}
|
||||
|
||||
async fn list(&self, user_id: &str) -> Result<Vec<SecretRef>, SecretError> {
|
||||
@@ -755,7 +766,7 @@ pub mod in_memory {
|
||||
.secrets
|
||||
.write()
|
||||
.await
|
||||
.remove(&(user_id.to_string(), name.to_string()))
|
||||
.remove(&(user_id.to_string(), name.to_lowercase()))
|
||||
.is_some())
|
||||
}
|
||||
|
||||
@@ -769,15 +780,17 @@ pub mod in_memory {
|
||||
secret_name: &str,
|
||||
allowed_secrets: &[String],
|
||||
) -> Result<bool, SecretError> {
|
||||
if !self.exists(user_id, secret_name).await? {
|
||||
let secret_name_lower = secret_name.to_lowercase();
|
||||
if !self.exists(user_id, &secret_name_lower).await? {
|
||||
return Ok(false);
|
||||
}
|
||||
for pattern in allowed_secrets {
|
||||
if pattern == secret_name {
|
||||
let pattern_lower = pattern.to_lowercase();
|
||||
if pattern_lower == secret_name_lower {
|
||||
return Ok(true);
|
||||
}
|
||||
if let Some(prefix) = pattern.strip_suffix('*')
|
||||
&& secret_name.starts_with(prefix)
|
||||
if let Some(prefix) = pattern_lower.strip_suffix('*')
|
||||
&& secret_name_lower.starts_with(prefix)
|
||||
{
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
@@ -171,9 +171,12 @@ pub struct CreateSecretParams {
|
||||
}
|
||||
|
||||
impl CreateSecretParams {
|
||||
/// Create new secret params. The name is normalized to lowercase for
|
||||
/// case-insensitive matching (capabilities.json uses lowercase names
|
||||
/// like `slack_bot_token`, but UIs may store `SLACK_BOT_TOKEN`).
|
||||
pub fn new(name: impl Into<String>, value: impl Into<String>) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
name: name.into().to_lowercase(),
|
||||
value: SecretString::from(value.into()),
|
||||
provider: None,
|
||||
expires_at: None,
|
||||
|
||||
@@ -235,14 +235,16 @@ impl CredentialInjector {
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Check if a secret name is in the allowed list.
|
||||
/// Check if a secret name is in the allowed list (case-insensitive).
|
||||
fn is_secret_allowed(&self, name: &str) -> bool {
|
||||
let name_lower = name.to_lowercase();
|
||||
for pattern in &self.allowed_secrets {
|
||||
if pattern == name {
|
||||
let pattern_lower = pattern.to_lowercase();
|
||||
if pattern_lower == name_lower {
|
||||
return true;
|
||||
}
|
||||
if let Some(prefix) = pattern.strip_suffix('*')
|
||||
&& name.starts_with(prefix)
|
||||
if let Some(prefix) = pattern_lower.strip_suffix('*')
|
||||
&& name_lower.starts_with(prefix)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user