Skip to content

Commit e59ed38

Browse files
authored
feat: Implement static killswitches (#239)
This implements a first version of killswitches. Switches are configured through objectstore config, so in order to create a killswitch one has to apply new config and restart the service. Note that all scope values must be strings. Examples for configuration look like this: ```yaml killswitches: # Drop all requests for a specific usecase, regardless of scopes - usecase: broken_usecase # Drop requests from a specific org, all projects, all usecases - scopes: org: "42" # Drop requests from a specific project, all usecases - scopes: org: "42" # can also be omitted project: "4711" # Combination of usecase and org - usecase: attachments scopes: org: "42" ``` At a later stage, we can replace the static configuration with a runtime system.
1 parent e9a6e0f commit e59ed38

File tree

4 files changed

+287
-27
lines changed

4 files changed

+287
-27
lines changed

objectstore-server/src/config.rs

Lines changed: 84 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ use secrecy::{CloneableSecret, SecretBox, SerializableSecret, zeroize::Zeroize};
4545
use serde::{Deserialize, Serialize};
4646
use tracing::level_filters::LevelFilter;
4747

48+
use crate::killswitches::Killswitches;
49+
4850
/// Environment variable prefix for all configuration options.
4951
const ENV_PREFIX: &str = "OS__";
5052

@@ -92,7 +94,7 @@ impl Zeroize for ConfigSecret {
9294
/// The `type` field in YAML or `__TYPE` in environment variables determines which variant is used.
9395
///
9496
/// Used in: [`Config::high_volume_storage`], [`Config::long_term_storage`]
95-
#[derive(Debug, Clone, Deserialize, Serialize)]
97+
#[derive(Debug, Deserialize, Serialize)]
9698
#[serde(tag = "type", rename_all = "lowercase")]
9799
pub enum Storage {
98100
/// Local filesystem storage backend (type `"filesystem"`).
@@ -306,7 +308,7 @@ pub enum Storage {
306308
/// Controls the threading behavior of the server's async runtime.
307309
///
308310
/// Used in: [`Config::runtime`]
309-
#[derive(Debug, Clone, Deserialize, Serialize)]
311+
#[derive(Debug, Deserialize, Serialize)]
310312
#[serde(default)]
311313
pub struct Runtime {
312314
/// Number of worker threads for the server runtime.
@@ -355,7 +357,7 @@ impl Default for Runtime {
355357
/// tracing. Sentry is disabled by default and only enabled when a DSN is provided.
356358
///
357359
/// Used in: [`Config::sentry`]
358-
#[derive(Debug, Clone, Deserialize, Serialize)]
360+
#[derive(Debug, Deserialize, Serialize)]
359361
pub struct Sentry {
360362
/// Sentry DSN (Data Source Name).
361363
///
@@ -607,7 +609,7 @@ mod display_fromstr {
607609
/// Controls the verbosity and format of log output. Logs are always written to stderr.
608610
///
609611
/// Used in: [`Config::logging`]
610-
#[derive(Debug, Clone, Deserialize, Serialize)]
612+
#[derive(Debug, Deserialize, Serialize)]
611613
pub struct Logging {
612614
/// Minimum log level to output.
613615
///
@@ -664,7 +666,7 @@ impl Default for Logging {
664666
/// Metrics configuration.
665667
///
666668
/// Configures submission of internal metrics to Datadog.
667-
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
669+
#[derive(Debug, Default, Deserialize, Serialize)]
668670
pub struct Metrics {
669671
/// Datadog [API key] for metrics.
670672
///
@@ -710,7 +712,7 @@ pub struct Metrics {
710712

711713
/// A key that may be used to verify a request's `Authorization` header and its
712714
/// associated permissions. May contain multiple key versions to facilitate rotation.
713-
#[derive(Debug, Clone, Deserialize, Serialize)]
715+
#[derive(Debug, Deserialize, Serialize)]
714716
pub struct AuthZVerificationKey {
715717
/// Versions of this key's key material which may be used to verify signatures.
716718
///
@@ -729,7 +731,7 @@ pub struct AuthZVerificationKey {
729731
}
730732

731733
/// Configuration for content-based authorization.
732-
#[derive(Debug, Default, Clone, Deserialize, Serialize)]
734+
#[derive(Debug, Default, Deserialize, Serialize)]
733735
pub struct AuthZ {
734736
/// Whether to enforce content-based authorization or not.
735737
///
@@ -758,7 +760,7 @@ pub struct AuthZ {
758760
///
759761
/// See individual field documentation for details on each configuration option, including
760762
/// defaults and environment variables.
761-
#[derive(Debug, Clone, Deserialize, Serialize)]
763+
#[derive(Debug, Deserialize, Serialize)]
762764
pub struct Config {
763765
/// HTTP server bind address.
764766
///
@@ -864,6 +866,9 @@ pub struct Config {
864866
/// Controls the verification and enforcement of content-based access control based on the
865867
/// JWT in a request's `Authorization` header.
866868
pub auth: AuthZ,
869+
870+
/// A list of matchers for requests to discard without processing.
871+
pub killswitches: Killswitches,
867872
}
868873

869874
impl Default for Config {
@@ -883,6 +888,7 @@ impl Default for Config {
883888
sentry: Sentry::default(),
884889
metrics: Metrics::default(),
885890
auth: AuthZ::default(),
891+
killswitches: Killswitches::default(),
886892
}
887893
}
888894
}
@@ -920,6 +926,8 @@ mod tests {
920926

921927
use secrecy::ExposeSecret;
922928

929+
use crate::killswitches::Killswitch;
930+
923931
use super::*;
924932

925933
#[test]
@@ -1071,29 +1079,30 @@ mod tests {
10711079
Ok(())
10721080
});
10731081
}
1082+
10741083
#[test]
10751084
fn configure_auth_with_yaml() {
10761085
let mut tempfile = tempfile::NamedTempFile::new().unwrap();
10771086
tempfile
10781087
.write_all(
10791088
br#"
1080-
auth:
1081-
enforce: true
1082-
keys:
1083-
kid1:
1084-
key_versions:
1085-
- "abcde"
1086-
- "fghij"
1087-
- |
1088-
this is a test
1089-
multiline string
1090-
end of string
1091-
max_permissions:
1092-
- "object.read"
1093-
- "object.write"
1094-
kid2:
1095-
key_versions:
1096-
- "12345"
1089+
auth:
1090+
enforce: true
1091+
keys:
1092+
kid1:
1093+
key_versions:
1094+
- "abcde"
1095+
- "fghij"
1096+
- |
1097+
this is a test
1098+
multiline string
1099+
end of string
1100+
max_permissions:
1101+
- "object.read"
1102+
- "object.write"
1103+
kid2:
1104+
key_versions:
1105+
- "12345"
10971106
"#,
10981107
)
10991108
.unwrap();
@@ -1122,4 +1131,54 @@ mod tests {
11221131
Ok(())
11231132
});
11241133
}
1134+
1135+
#[test]
1136+
fn configure_killswitches_with_yaml() {
1137+
let mut tempfile = tempfile::NamedTempFile::new().unwrap();
1138+
tempfile
1139+
.write_all(
1140+
br#"
1141+
killswitches:
1142+
- usecase: broken_usecase
1143+
- scopes:
1144+
org: "42"
1145+
- scopes:
1146+
org: "42"
1147+
project: "4711"
1148+
- usecase: attachments
1149+
scopes:
1150+
org: "42"
1151+
"#,
1152+
)
1153+
.unwrap();
1154+
1155+
figment::Jail::expect_with(|_jail| {
1156+
let expected = [
1157+
Killswitch {
1158+
usecase: Some("broken_usecase".into()),
1159+
scopes: BTreeMap::new(),
1160+
},
1161+
Killswitch {
1162+
usecase: None,
1163+
scopes: BTreeMap::from([("org".into(), "42".into())]),
1164+
},
1165+
Killswitch {
1166+
usecase: None,
1167+
scopes: BTreeMap::from([
1168+
("org".into(), "42".into()),
1169+
("project".into(), "4711".into()),
1170+
]),
1171+
},
1172+
Killswitch {
1173+
usecase: Some("attachments".into()),
1174+
scopes: BTreeMap::from([("org".into(), "42".into())]),
1175+
},
1176+
];
1177+
1178+
let config = Config::load(Some(tempfile.path())).unwrap();
1179+
assert_eq!(&config.killswitches.0, &expected,);
1180+
1181+
Ok(())
1182+
});
1183+
}
11251184
}

objectstore-server/src/extractors/id.rs

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,41 @@ use std::borrow::Cow;
33
use axum::extract::rejection::PathRejection;
44
use axum::extract::{FromRequestParts, Path};
55
use axum::http::request::Parts;
6+
use axum::response::{IntoResponse, Response};
67
use objectstore_service::id::{ObjectContext, ObjectId};
78
use objectstore_types::scope::{EMPTY_SCOPES, Scope, Scopes};
89
use serde::{Deserialize, de};
910

1011
use crate::extractors::Xt;
1112
use crate::state::ServiceState;
1213

14+
#[derive(Debug)]
15+
pub enum ObjectRejection {
16+
Path(PathRejection),
17+
Killswitched,
18+
}
19+
20+
impl IntoResponse for ObjectRejection {
21+
fn into_response(self) -> Response {
22+
match self {
23+
ObjectRejection::Path(rejection) => rejection.into_response(),
24+
ObjectRejection::Killswitched => (
25+
axum::http::StatusCode::FORBIDDEN,
26+
"Object access is disabled for this scope through killswitches",
27+
)
28+
.into_response(),
29+
}
30+
}
31+
}
32+
33+
impl From<PathRejection> for ObjectRejection {
34+
fn from(rejection: PathRejection) -> Self {
35+
ObjectRejection::Path(rejection)
36+
}
37+
}
38+
1339
impl FromRequestParts<ServiceState> for Xt<ObjectId> {
14-
type Rejection = PathRejection;
40+
type Rejection = ObjectRejection;
1541

1642
async fn from_request_parts(
1743
parts: &mut Parts,
@@ -23,6 +49,10 @@ impl FromRequestParts<ServiceState> for Xt<ObjectId> {
2349
populate_sentry_context(id.context());
2450
sentry::configure_scope(|s| s.set_extra("key", id.key().into()));
2551

52+
if state.config.killswitches.matches(id.context()) {
53+
return Err(ObjectRejection::Killswitched);
54+
}
55+
2656
Ok(Xt(id))
2757
}
2858
}
@@ -66,7 +96,7 @@ where
6696
}
6797

6898
impl FromRequestParts<ServiceState> for Xt<ObjectContext> {
69-
type Rejection = PathRejection;
99+
type Rejection = ObjectRejection;
70100

71101
async fn from_request_parts(
72102
parts: &mut Parts,
@@ -80,6 +110,10 @@ impl FromRequestParts<ServiceState> for Xt<ObjectContext> {
80110

81111
populate_sentry_context(&context);
82112

113+
if state.config.killswitches.matches(&context) {
114+
return Err(ObjectRejection::Killswitched);
115+
}
116+
83117
Ok(Xt(context))
84118
}
85119
}

0 commit comments

Comments
 (0)