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
7 changes: 5 additions & 2 deletions clients/rust/tests/e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,9 +172,12 @@ async fn stores_under_given_key() {

#[tokio::test]
async fn stores_structured_keys() {
let server = TestServer::new().await;
let server = test_server().await;

let client = Client::builder(server.url("/")).build().unwrap();
let client = Client::builder(server.url("/"))
.token(test_token_generator())
.build()
.unwrap();
let usecase = Usecase::new("usecase");
let session = client.session(usecase.for_project(12345, 1337)).unwrap();

Expand Down
1 change: 1 addition & 0 deletions devservices/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ services:
OS__STORAGE__TYPE: "filesystem"
OS__STORAGE__PATH: "/data"
OS__LOGGING__LEVEL: "debug"
OS__AUTH__ENFORCE: "false"
command: run
healthcheck:
test: ["CMD", "/bin/entrypoint", "healthcheck"]
Expand Down
3 changes: 3 additions & 0 deletions objectstore-server/config/emulators.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ storage:
endpoint: http://localhost:8087
bucket: test-bucket

auth:
enforce: false

logging:
level: trace
format: auto
Expand Down
3 changes: 3 additions & 0 deletions objectstore-server/config/local.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ storage:
type: filesystem
path: data

auth:
enforce: false

logging:
level: trace
format: auto
Expand Down
10 changes: 5 additions & 5 deletions objectstore-server/docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,9 @@ A request flows through several layers before reaching the storage service:
## Authentication & Authorization

Objectstore uses **JWT tokens with EdDSA signatures** (Ed25519) for
authentication. Auth enforcement is optional and controlled by the
[`auth.enforce`](config) config flag, allowing unauthenticated development
setups.
authentication. Auth enforcement is **enabled by default** and controlled by the
[`auth.enforce`](config) config flag. Set `enforce: false` explicitly for
unauthenticated development setups.

### Token Structure

Expand Down Expand Up @@ -100,8 +100,8 @@ this precedence (highest wins):
1. **Environment variables** — prefixed with `OS__`, using `__` as a nested
separator. Example: `OS__STORAGE__TYPE=tiered`
2. **YAML file** — passed via the `-c` / `--config` CLI flag
3. **Defaults** — sensible development defaults (local filesystem backend,
auth disabled)
3. **Defaults** — sensible defaults (local filesystem backend,
auth enabled)

Key configuration sections:
- `storage` — backend type and connection parameters; use `type: tiered` for
Expand Down
58 changes: 55 additions & 3 deletions objectstore-server/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -333,12 +333,13 @@ pub struct AuthZVerificationKey {
}

/// Configuration for content-based authorization.
#[derive(Debug, Default, Deserialize, Serialize)]
#[derive(Debug, Deserialize, Serialize)]
pub struct AuthZ {
/// Whether to enforce content-based authorization or not.
///
/// If this is set to `false`, checks are still performed but failures will not result
/// in `403 Unauthorized` responses.
/// Defaults to `true`. If this is set to `false`, checks are still performed but failures
/// will not result in `403 Unauthorized` responses.
#[serde(default = "default_enforce")]
pub enforce: bool,

/// Keys that may be used to verify a request's auth token.
Expand All @@ -352,6 +353,19 @@ pub struct AuthZ {
pub keys: BTreeMap<String, AuthZVerificationKey>,
}

fn default_enforce() -> bool {
true
}

impl Default for AuthZ {
fn default() -> Self {
Self {
enforce: true,
keys: BTreeMap::new(),
}
}
}
Comment thread
cursor[bot] marked this conversation as resolved.

/// Main configuration struct for the objectstore server.
///
/// This is the top-level configuration that combines all server settings including networking,
Expand Down Expand Up @@ -892,6 +906,44 @@ mod tests {
});
}

#[test]
fn auth_enforce_defaults_to_true() {
figment::Jail::expect_with(|_jail| {
let config = Config::load(None).unwrap();
assert!(config.auth.enforce);
Ok(())
});
}

#[test]
fn auth_enforce_defaults_to_true_when_omitted_from_yaml() {
let mut tempfile = tempfile::NamedTempFile::new().unwrap();
tempfile
.write_all(
br#"
auth:
keys: {}
"#,
)
.unwrap();

figment::Jail::expect_with(|_jail| {
let config = Config::load(Some(tempfile.path())).unwrap();
assert!(config.auth.enforce);
Ok(())
});
}

#[test]
fn auth_enforce_can_be_disabled() {
figment::Jail::expect_with(|jail| {
jail.set_env("OS__AUTH__ENFORCE", "false");
let config = Config::load(None).unwrap();
assert!(!config.auth.enforce);
Ok(())
});
}

#[test]
fn configure_killswitches_with_yaml() {
let mut tempfile = tempfile::NamedTempFile::new().unwrap();
Expand Down
38 changes: 38 additions & 0 deletions objectstore-server/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ impl Services {
service.start();

let key_directory = PublicKeyDirectory::try_from(&config.auth)?;
if config.auth.enforce && key_directory.keys.is_empty() {
anyhow::bail!(
"Auth enforcement is enabled but no keys are configured. Either disable auth enforcement (dev/test environments) or configure a public key."
);
}
Comment thread
cursor[bot] marked this conversation as resolved.
let rate_limiter = RateLimiter::new(config.rate_limits.clone());
rate_limiter.start();

Expand Down Expand Up @@ -132,3 +137,36 @@ async fn track_runtime_metrics(interval: Duration) {
);
}
}

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

#[tokio::test]
async fn enforce_without_keys_fails_startup() {
let config = Config {
auth: crate::config::AuthZ {
enforce: true,
..Default::default()
},
..Default::default()
};
let err = Services::spawn(config).await.unwrap_err();
assert!(
err.to_string()
.contains("Auth enforcement is enabled but no keys are configured"),
);
}

#[tokio::test]
async fn no_enforce_without_keys_starts_ok() {
let config = Config {
auth: crate::config::AuthZ {
enforce: false,
..Default::default()
},
..Default::default()
};
assert!(Services::spawn(config).await.is_ok());
}
}
28 changes: 28 additions & 0 deletions objectstore-server/tests/limits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,10 @@ async fn test_killswitches() -> Result<()> {
#[tokio::test]
async fn test_throughput_global_rps_limit() -> Result<()> {
let server = TestServer::with_config(Config {
auth: AuthZ {
enforce: false,
..Default::default()
},
rate_limits: RateLimits {
throughput: ThroughputLimits {
global_rps: Some(2),
Expand Down Expand Up @@ -159,6 +163,10 @@ async fn test_throughput_global_rps_limit() -> Result<()> {
#[tokio::test]
async fn test_throughput_usecase_pct_limit() -> Result<()> {
let server = TestServer::with_config(Config {
auth: AuthZ {
enforce: false,
..Default::default()
},
rate_limits: RateLimits {
throughput: ThroughputLimits {
global_rps: Some(100),
Expand Down Expand Up @@ -213,6 +221,10 @@ async fn test_throughput_usecase_pct_limit() -> Result<()> {
#[tokio::test]
async fn test_throughput_scope_pct_limit() -> Result<()> {
let server = TestServer::with_config(Config {
auth: AuthZ {
enforce: false,
..Default::default()
},
rate_limits: RateLimits {
throughput: ThroughputLimits {
global_rps: Some(100),
Expand Down Expand Up @@ -267,6 +279,10 @@ async fn test_throughput_scope_pct_limit() -> Result<()> {
#[tokio::test]
async fn test_throughput_rule() -> Result<()> {
let server = TestServer::with_config(Config {
auth: AuthZ {
enforce: false,
..Default::default()
},
rate_limits: RateLimits {
throughput: ThroughputLimits {
global_rps: None,
Expand Down Expand Up @@ -331,6 +347,10 @@ async fn test_throughput_rule() -> Result<()> {
#[tokio::test]
async fn test_bandwidth_global_bps_limit() -> Result<()> {
let server = TestServer::with_config(Config {
auth: AuthZ {
enforce: false,
..Default::default()
},
rate_limits: RateLimits {
bandwidth: BandwidthLimits {
global_bps: Some(500),
Expand Down Expand Up @@ -385,6 +405,10 @@ async fn test_bandwidth_global_bps_limit() -> Result<()> {
#[tokio::test]
async fn test_bandwidth_usecase_pct_limit() -> Result<()> {
let server = TestServer::with_config(Config {
auth: AuthZ {
enforce: false,
..Default::default()
},
rate_limits: RateLimits {
bandwidth: BandwidthLimits {
global_bps: Some(100_000),
Expand Down Expand Up @@ -446,6 +470,10 @@ async fn test_bandwidth_usecase_pct_limit() -> Result<()> {
#[tokio::test]
async fn test_bandwidth_scope_pct_limit() -> Result<()> {
let server = TestServer::with_config(Config {
auth: AuthZ {
enforce: false,
..Default::default()
},
rate_limits: RateLimits {
bandwidth: BandwidthLimits {
global_bps: Some(100_000),
Expand Down
3 changes: 2 additions & 1 deletion objectstore-server/tests/stresstest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ async fn test_basic() {
.env("OS__HTTP_ADDR", &addr)
.env("OS__STORAGE__TYPE", "filesystem")
.env("OS__STORAGE__PATH", tempdir.path().display().to_string())
.env("OS__LOGGING__LEVEL", "warn");
.env("OS__LOGGING__LEVEL", "warn")
.env("OS__AUTH__ENFORCE", "false");
if let Ok(statsd_host) = std::env::var("STATSD_HOST") {
cmd.env("OS__METRICS__ADDR", statsd_host);
}
Expand Down
Loading