Skip to content

Commit 30ee1a2

Browse files
committed
feat(auth): add config for auth keys and associated permissions
1 parent 1d47c19 commit 30ee1a2

File tree

3 files changed

+150
-1
lines changed

3 files changed

+150
-1
lines changed

objectstore-server/src/auth.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
use serde::{Deserialize, Serialize};
2+
3+
/// Permissions that control whether different operations are authorized.
4+
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Hash)]
5+
pub enum Permission {
6+
/// The permission required to read objects from objectstore.
7+
#[serde(rename = "object.read")]
8+
ObjectRead,
9+
10+
/// The permission required to write/overwrite objects in objectstore.
11+
#[serde(rename = "object.write")]
12+
ObjectWrite,
13+
14+
/// The permission required to delete objects from objectstore.
15+
#[serde(rename = "object.delete")]
16+
ObjectDelete,
17+
}

objectstore-server/src/config.rs

Lines changed: 132 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
//! ```
3333
3434
use std::borrow::Cow;
35-
use std::collections::BTreeMap;
35+
use std::collections::{BTreeMap, HashSet};
3636
use std::fmt;
3737
use std::net::SocketAddr;
3838
use std::path::{Path, PathBuf};
@@ -43,6 +43,8 @@ use secrecy::{CloneableSecret, SecretBox, SerializableSecret, zeroize::Zeroize};
4343
use serde::{Deserialize, Serialize};
4444
use tracing::level_filters::LevelFilter;
4545

46+
use crate::auth::Permission;
47+
4648
/// Environment variable prefix for all configuration options.
4749
const ENV_PREFIX: &str = "OS__";
4850

@@ -667,6 +669,44 @@ pub struct Metrics {
667669
pub tags: BTreeMap<String, String>,
668670
}
669671

672+
/// A key that may be used to verify a request's `Authorization` header and its
673+
/// associated permissions. May contain multiple key versions to facilitate rotation.
674+
#[derive(Debug, Clone, Deserialize, Serialize)]
675+
pub struct AuthZVerificationKey {
676+
/// Versions of this key's key material which may be used to verify signatures.
677+
///
678+
/// If a key is being rotated, the old and new versions of that key should both be
679+
/// configured so objectstore can verify signatures while the updated key is still
680+
/// rolling out. Otherwise, this should only contain the most recent version of a key.
681+
pub key_versions: Vec<SecretBox<ConfigSecret>>,
682+
683+
/// The maximum set of permissions that this key's signer is authorized to grant.
684+
///
685+
/// If a request's `Authorization` header grants full permission but it was signed by
686+
/// a key that is only allowed to grant read permission, then the request only has
687+
/// read permission.
688+
#[serde(default)]
689+
pub max_permissions: HashSet<Permission>,
690+
}
691+
692+
/// Configuration for content-based authorization.
693+
#[derive(Debug, Default, Clone, Deserialize, Serialize)]
694+
pub struct AuthZ {
695+
/// Whether to enforce content-based authorization or not.
696+
///
697+
/// If this is set to `false`, checks are still performed but failures will not result
698+
/// in `403 Unauthorized` responses.
699+
pub enforce: bool,
700+
701+
/// Keys that may be used to verify a request's `Authorization` header.
702+
///
703+
/// This field is a container that is keyed on a key's ID. When verifying a JWT
704+
/// from the `Authorization` header, the `kid` field should be read from the JWT
705+
/// header and used to index into this map to select the appropriate key.
706+
#[serde(default)]
707+
pub keys: BTreeMap<String, AuthZVerificationKey>,
708+
}
709+
670710
/// Main configuration struct for the objectstore server.
671711
///
672712
/// This is the top-level configuration that combines all server settings including networking,
@@ -779,6 +819,12 @@ pub struct Config {
779819
/// Optional configuration for submitting internal metrics to Datadog. See [`Metrics`] for
780820
/// configuration options.
781821
pub metrics: Metrics,
822+
823+
/// Content-based authorization configuration.
824+
///
825+
/// Controls the verification and enforcement of content-based access control based on the
826+
/// JWT in a request's `Authorization` header.
827+
pub auth: AuthZ,
782828
}
783829

784830
impl Default for Config {
@@ -797,6 +843,7 @@ impl Default for Config {
797843
logging: Logging::default(),
798844
sentry: Sentry::default(),
799845
metrics: Metrics::default(),
846+
auth: AuthZ::default(),
800847
}
801848
}
802849
}
@@ -952,4 +999,88 @@ mod tests {
952999
Ok(())
9531000
});
9541001
}
1002+
1003+
#[test]
1004+
fn configure_auth_with_env() {
1005+
figment::Jail::expect_with(|jail| {
1006+
jail.set_env("OS__AUTH__ENFORCE", "true");
1007+
jail.set_env(
1008+
"OS__AUTH__KEYS",
1009+
r#"{kid1={key_versions=["abcde","fghij","this is a test\n multiline string\nend of string\n"],max_permissions=["object.read", "object.write"],}, kid2={key_versions=["12345"],}}"#,
1010+
);
1011+
1012+
let config = Config::load(None).unwrap();
1013+
1014+
assert_eq!(config.auth.enforce, true);
1015+
1016+
let kid1 = config.auth.keys.get("kid1").unwrap();
1017+
assert_eq!(kid1.key_versions[0].expose_secret().as_str(), "abcde");
1018+
assert_eq!(kid1.key_versions[1].expose_secret().as_str(), "fghij");
1019+
assert_eq!(
1020+
kid1.key_versions[2].expose_secret().as_str(),
1021+
"this is a test\n multiline string\nend of string\n"
1022+
);
1023+
assert_eq!(
1024+
kid1.max_permissions,
1025+
HashSet::from([Permission::ObjectRead, Permission::ObjectWrite])
1026+
);
1027+
1028+
let kid2 = config.auth.keys.get("kid2").unwrap();
1029+
assert_eq!(kid2.key_versions[0].expose_secret().as_str(), "12345");
1030+
assert_eq!(kid2.max_permissions, HashSet::new());
1031+
1032+
Ok(())
1033+
});
1034+
}
1035+
#[test]
1036+
fn configure_auth_with_yaml() {
1037+
let mut tempfile = tempfile::NamedTempFile::new().unwrap();
1038+
tempfile
1039+
.write_all(
1040+
br#"
1041+
auth:
1042+
enforce: true
1043+
keys:
1044+
kid1:
1045+
key_versions:
1046+
- "abcde"
1047+
- "fghij"
1048+
- |
1049+
this is a test
1050+
multiline string
1051+
end of string
1052+
max_permissions:
1053+
- "object.read"
1054+
- "object.write"
1055+
kid2:
1056+
key_versions:
1057+
- "12345"
1058+
"#,
1059+
)
1060+
.unwrap();
1061+
1062+
figment::Jail::expect_with(|_jail| {
1063+
let config = Config::load(Some(tempfile.path())).unwrap();
1064+
1065+
assert_eq!(config.auth.enforce, true);
1066+
1067+
let kid1 = config.auth.keys.get("kid1").unwrap();
1068+
assert_eq!(kid1.key_versions[0].expose_secret().as_str(), "abcde");
1069+
assert_eq!(kid1.key_versions[1].expose_secret().as_str(), "fghij");
1070+
assert_eq!(
1071+
kid1.key_versions[2].expose_secret().as_str(),
1072+
"this is a test\n multiline string\nend of string\n"
1073+
);
1074+
assert_eq!(
1075+
kid1.max_permissions,
1076+
HashSet::from([Permission::ObjectRead, Permission::ObjectWrite])
1077+
);
1078+
1079+
let kid2 = config.auth.keys.get("kid2").unwrap();
1080+
assert_eq!(kid2.key_versions[0].expose_secret().as_str(), "12345");
1081+
assert_eq!(kid2.max_permissions, HashSet::new());
1082+
1083+
Ok(())
1084+
});
1085+
}
9551086
}

objectstore-server/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
//! This builds on top of the [`objectstore_service`], and exposes the underlying storage layer as
44
//! an `HTTP` layer which can serve files directly to *external clients* and our SDK.
55
6+
pub mod auth;
67
pub mod cli;
78
pub mod config;
89
pub mod endpoints;

0 commit comments

Comments
 (0)