Skip to content

Commit ac6f38b

Browse files
committed
feat(auth): rust client support for bearer auth
1 parent b511816 commit ac6f38b

File tree

8 files changed

+99
-2
lines changed

8 files changed

+99
-2
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ futures-util = "0.3.31"
3636
http = "1.3.1"
3737
humantime = "2.2.0"
3838
humantime-serde = "1.1.1"
39+
jsonwebtoken = { version = "10.2.0", features = ["rust_crypto"] }
3940
merni = "0.1.3"
4041
mimalloc = { version = "0.1.48", features = ["v3", "override"] }
4142
rand = "0.9.1"

clients/rust/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ async-compression = { version = "0.4.27", features = ["tokio", "zstd"] }
1515
bytes = { workspace = true }
1616
futures-util = { workspace = true }
1717
infer = { version = "0.19.0", default-features = false }
18+
jsonwebtoken = { workspace = true }
1819
objectstore-types = { workspace = true }
1920
reqwest = { workspace = true, features = ["json", "stream"] }
2021
sentry-core = { version = ">=0.41", features = ["client"] }

clients/rust/src/auth.rs

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
use std::collections::{BTreeMap, HashSet};
2+
3+
use jsonwebtoken::{Algorithm, EncodingKey, Header, encode, get_current_timestamp};
4+
use objectstore_types::Permission;
5+
use serde::{Deserialize, Serialize};
6+
7+
use crate::ScopeInner;
8+
9+
const DEFAULT_EXPIRY_SECONDS: u64 = 60;
10+
const DEFAULT_PERMISSIONS: [Permission; 3] = [
11+
Permission::ObjectRead,
12+
Permission::ObjectWrite,
13+
Permission::ObjectDelete,
14+
];
15+
16+
struct SecretKey {
17+
kid: String,
18+
secret_key: String,
19+
}
20+
21+
struct TokenGenerator {
22+
kid: String,
23+
encoding_key: EncodingKey,
24+
expiry_seconds: u64,
25+
permissions: HashSet<Permission>,
26+
}
27+
28+
#[derive(Serialize, Deserialize)]
29+
struct JwtRes {
30+
#[serde(rename = "os:usecase")]
31+
usecase: String,
32+
33+
#[serde(flatten)]
34+
scopes: BTreeMap<String, String>,
35+
}
36+
37+
#[derive(Serialize, Deserialize)]
38+
struct JwtClaims {
39+
exp: u64,
40+
permissions: HashSet<Permission>,
41+
res: JwtRes,
42+
}
43+
44+
impl TokenGenerator {
45+
pub fn new(secret_key: SecretKey) -> crate::Result<TokenGenerator> {
46+
let encoding_key = EncodingKey::from_ed_pem(secret_key.secret_key.as_bytes())?;
47+
Ok(TokenGenerator {
48+
kid: secret_key.kid,
49+
encoding_key,
50+
expiry_seconds: DEFAULT_EXPIRY_SECONDS,
51+
permissions: HashSet::from(DEFAULT_PERMISSIONS),
52+
})
53+
}
54+
55+
pub fn expiry_seconds(mut self, expiry_seconds: u64) -> Self {
56+
self.expiry_seconds = expiry_seconds;
57+
self
58+
}
59+
60+
pub fn permissions(mut self, permissions: &[Permission]) -> Self {
61+
self.permissions = HashSet::from_iter(permissions.iter().cloned());
62+
self
63+
}
64+
65+
pub fn sign_for_scope(&self, scope: &ScopeInner) -> crate::Result<String> {
66+
let claims = JwtClaims {
67+
exp: get_current_timestamp() + self.expiry_seconds,
68+
permissions: self.permissions.clone(),
69+
res: JwtRes {
70+
usecase: scope.usecase().name().into(),
71+
scopes: scope
72+
.scopes()
73+
.iter()
74+
.map(|scope| (scope.name().to_string(), scope.value().to_string()))
75+
.collect(),
76+
},
77+
};
78+
79+
let mut header = Header::new(Algorithm::EdDSA);
80+
header.kid = Some(self.kid.clone());
81+
82+
Ok(encode(&header, &claims, &self.encoding_key)?)
83+
}
84+
}

clients/rust/src/client.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use futures_util::stream::BoxStream;
77
use objectstore_types::ExpirationPolicy;
88
use url::Url;
99

10-
pub use objectstore_types::{Compression, scope as scope_types};
10+
pub use objectstore_types::{scope as scope_types, Compression};
1111

1212
const USER_AGENT: &str = concat!("objectstore-client/", env!("CARGO_PKG_VERSION"));
1313

@@ -227,6 +227,11 @@ impl ScopeInner {
227227
&self.usecase
228228
}
229229

230+
#[inline]
231+
pub(crate) fn scopes(&self) -> &scope_types::Scopes {
232+
&self.scopes
233+
}
234+
230235
fn as_path_segment(&self) -> String {
231236
if self.scopes.is_empty() {
232237
return "_".to_string();

clients/rust/src/error.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ pub enum Error {
1616
/// Error when scope validation fails.
1717
#[error(transparent)]
1818
InvalidScope(#[from] objectstore_types::scope::InvalidScopeError),
19+
/// Error when creating auth tokens, such as invalid keys.
20+
#[error(transparent)]
21+
TokenError(#[from] jsonwebtoken::errors::Error),
1922
/// Error when URL manipulation fails.
2023
#[error("{message}")]
2124
InvalidUrl {

clients/rust/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
#![warn(missing_docs)]
33
#![warn(missing_debug_implementations)]
44

5+
mod auth;
56
mod client;
67
mod delete;
78
mod error;
@@ -11,6 +12,7 @@ pub mod utils;
1112

1213
pub use objectstore_types::{Compression, ExpirationPolicy};
1314

15+
pub use auth::*;
1416
pub use client::*;
1517
pub use delete::*;
1618
pub use error::*;

objectstore-server/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ figment = { version = "0.10.19", features = ["env", "test", "yaml"] }
2121
futures-util = { workspace = true }
2222
humantime = { workspace = true }
2323
humantime-serde = { workspace = true }
24-
jsonwebtoken = { version = "10.2.0", features = ["rust_crypto"] }
24+
jsonwebtoken = { workspace = true }
2525
merni = { workspace = true }
2626
mimalloc = { workspace = true }
2727
num_cpus = "1.17.0"

0 commit comments

Comments
 (0)