Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,4 @@ members = [
resolver = "2"

[workspace.package]
version = "0.5.0"
version = "0.5.1"
4 changes: 2 additions & 2 deletions crates/store-pg/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,13 @@ environment-protocol = { path = "../environment-protocol" }
futures-util = "0.3"
async-trait = "0.1"
mcp = { path = "../mcp" }
object_store = { version = "0.14", features = ["aws"] }
object_store = { version = "0.14", features = ["aws", "azure"] }
profiles = { path = "../profiles" }
rand = "0.10"
sha2 = "0.11"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sqlx = { version = "0.9", default-features = false, features = ["runtime-tokio", "postgres", "uuid", "json"] }
sqlx = { version = "0.9", default-features = false, features = ["runtime-tokio", "tls-rustls-ring-native-roots", "postgres", "uuid", "json"] }
thiserror = "2"
tracing = "0.1"
tokio = { version = "1", features = ["rt"] }
Expand Down
87 changes: 87 additions & 0 deletions crates/store-pg/src/azure.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
use std::{fmt, sync::Arc};

use object_store::{
ObjectStore,
azure::{AzureConfigKey, MicrosoftAzureBuilder},
};

/// Static Azure credentials supplied by the deployment. Debug output is redacted.
#[derive(Clone)]
pub enum AzureObjectStoreCredential {
AccessKey(String),
SasToken(String),
}

impl fmt::Debug for AzureObjectStoreCredential {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::AccessKey(_) => "AccessKey([REDACTED])",
Self::SasToken(_) => "SasToken([REDACTED])",
})
}
}

#[derive(Clone)]
pub struct AzureObjectStoreConfig {
pub account: String,
pub container: String,
pub credential: AzureObjectStoreCredential,
/// Account endpoint, including the account path when using an emulator.
pub endpoint: Option<String>,
}

impl fmt::Debug for AzureObjectStoreConfig {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("AzureObjectStoreConfig")
.field("account", &self.account)
.field("container", &self.container)
.field("credential", &self.credential)
.field("endpoint_configured", &self.endpoint.is_some())
.finish()
}
}

/// Build the native Azure backend without changing CAS keys or metadata.
/// Credentials are explicit so a missing Secret cannot fall back to another identity.
pub fn build_azure_object_store(
config: AzureObjectStoreConfig,
) -> Result<Arc<dyn ObjectStore>, object_store::Error> {
let mut builder = MicrosoftAzureBuilder::new()
.with_account(config.account)
.with_container_name(config.container);
builder = match config.credential {
AzureObjectStoreCredential::AccessKey(key) => builder.with_access_key(key),
AzureObjectStoreCredential::SasToken(token) => {
builder.with_config(AzureConfigKey::SasKey, token.trim_start_matches('?'))
}
};
if let Some(endpoint) = config.endpoint {
builder = builder
.with_allow_http(endpoint.starts_with("http://"))
.with_endpoint(endpoint);
}
Ok(Arc::new(builder.build()?))
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn azure_config_debug_redacts_both_credential_types_and_endpoint() {
for credential in [
AzureObjectStoreCredential::AccessKey("secret-key".into()),
AzureObjectStoreCredential::SasToken("sig=secret-sas".into()),
] {
let config = AzureObjectStoreConfig {
account: "account".into(),
container: "container".into(),
credential,
endpoint: Some("https://secret-endpoint.example".into()),
};
let debug = format!("{config:?}");
assert!(!debug.contains("secret-"));
assert!(debug.contains("[REDACTED]"));
}
}
}
73 changes: 4 additions & 69 deletions crates/store-pg/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
mod access;
mod api_keys;
mod auth;
mod azure;
mod blob;
mod blob_cache;
mod bots;
Expand All @@ -21,6 +22,7 @@ mod oauth;
mod object;
mod profile;
mod providers;
mod s3;
mod session;
pub use session::PurgeSessionError;
mod shared;
Expand All @@ -31,14 +33,15 @@ use std::sync::Arc;

use base64::Engine as _;
use object_store::ObjectStore;
use object_store::aws::AmazonS3Builder;
use sqlx::{PgPool, postgres::PgPoolOptions};
use thiserror::Error;
use uuid::Uuid;

pub use access::AccessFilter;
pub use access::{AccessStoreError, PgAccessStore, ResourceAccess};
pub use azure::{AzureObjectStoreConfig, AzureObjectStoreCredential, build_azure_object_store};
pub use model_defaults::ModelDefaultsStoreError;
pub use s3::{S3ObjectStoreConfig, build_s3_object_store};

/// A session page with the access summary each view carries.
#[derive(Clone, Debug)]
Expand Down Expand Up @@ -139,74 +142,6 @@ impl PgStoreConfig {
}
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct S3ObjectStoreConfig {
pub bucket: String,
pub endpoint: Option<String>,
pub region: String,
pub access_key_id: Option<String>,
pub secret_access_key: Option<String>,
pub force_path_style: bool,
}

impl S3ObjectStoreConfig {
pub fn new(bucket: impl Into<String>) -> Self {
Self {
bucket: bucket.into(),
endpoint: None,
region: "us-east-1".to_owned(),
access_key_id: None,
secret_access_key: None,
force_path_style: false,
}
}

pub fn with_endpoint(mut self, endpoint: impl Into<String>) -> Self {
self.endpoint = Some(endpoint.into());
self
}

pub fn with_region(mut self, region: impl Into<String>) -> Self {
self.region = region.into();
self
}

pub fn with_access_key_id(mut self, access_key_id: impl Into<String>) -> Self {
self.access_key_id = Some(access_key_id.into());
self
}

pub fn with_secret_access_key(mut self, secret_access_key: impl Into<String>) -> Self {
self.secret_access_key = Some(secret_access_key.into());
self
}

pub fn with_force_path_style(mut self, force_path_style: bool) -> Self {
self.force_path_style = force_path_style;
self
}
}

pub fn build_s3_object_store(
config: S3ObjectStoreConfig,
) -> Result<Arc<dyn ObjectStore>, object_store::Error> {
let mut builder = AmazonS3Builder::new()
.with_bucket_name(config.bucket)
.with_region(config.region)
.with_virtual_hosted_style_request(!config.force_path_style);
if let Some(endpoint) = config.endpoint {
let allow_http = endpoint.starts_with("http://");
builder = builder.with_endpoint(endpoint).with_allow_http(allow_http);
}
if let Some(access_key_id) = config.access_key_id {
builder = builder.with_access_key_id(access_key_id);
}
if let Some(secret_access_key) = config.secret_access_key {
builder = builder.with_secret_access_key(secret_access_key);
}
Ok(Arc::new(builder.build()?))
}

#[derive(Clone)]
pub struct PgStore {
pub(crate) pool: PgPool,
Expand Down
83 changes: 83 additions & 0 deletions crates/store-pg/src/s3.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
use std::{fmt, sync::Arc};

use object_store::{ObjectStore, aws::AmazonS3Builder};

#[derive(Clone, PartialEq, Eq)]
pub struct S3ObjectStoreConfig {
pub bucket: String,
pub endpoint: Option<String>,
pub region: String,
pub access_key_id: Option<String>,
pub secret_access_key: Option<String>,
pub force_path_style: bool,
}

impl fmt::Debug for S3ObjectStoreConfig {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("S3ObjectStoreConfig")
.field("bucket", &self.bucket)
.field("region", &self.region)
.field("endpoint_configured", &self.endpoint.is_some())
.field("credentials_configured", &self.secret_access_key.is_some())
.field("force_path_style", &self.force_path_style)
.finish()
}
}

impl S3ObjectStoreConfig {
pub fn new(bucket: impl Into<String>) -> Self {
Self {
bucket: bucket.into(),
endpoint: None,
region: "us-east-1".to_owned(),
access_key_id: None,
secret_access_key: None,
force_path_style: false,
}
}

pub fn with_endpoint(mut self, endpoint: impl Into<String>) -> Self {
self.endpoint = Some(endpoint.into());
self
}

pub fn with_region(mut self, region: impl Into<String>) -> Self {
self.region = region.into();
self
}

pub fn with_access_key_id(mut self, access_key_id: impl Into<String>) -> Self {
self.access_key_id = Some(access_key_id.into());
self
}

pub fn with_secret_access_key(mut self, secret_access_key: impl Into<String>) -> Self {
self.secret_access_key = Some(secret_access_key.into());
self
}

pub fn with_force_path_style(mut self, force_path_style: bool) -> Self {
self.force_path_style = force_path_style;
self
}
}

pub fn build_s3_object_store(
config: S3ObjectStoreConfig,
) -> Result<Arc<dyn ObjectStore>, object_store::Error> {
let mut builder = AmazonS3Builder::new()
.with_bucket_name(config.bucket)
.with_region(config.region)
.with_virtual_hosted_style_request(!config.force_path_style);
if let Some(endpoint) = config.endpoint {
let allow_http = endpoint.starts_with("http://");
builder = builder.with_endpoint(endpoint).with_allow_http(allow_http);
}
if let Some(access_key_id) = config.access_key_id {
builder = builder.with_access_key_id(access_key_id);
}
if let Some(secret_access_key) = config.secret_access_key {
builder = builder.with_secret_access_key(secret_access_key);
}
Ok(Arc::new(builder.build()?))
}
Loading
Loading