Skip to content

Commit 0ad3d0e

Browse files
ref: move Permission, Scope, Scopes to objectstore_types and use in Rust client (#236)
Signing tokens in the client requires structured scope information which we currently don't have. We have a structured scope type in `objectstore_service` already so this PR moves that type to `objectstore_types` and integrates it into the Rust client. As a driveby, it also moves the `Permission` type in preparation for the actual token signing impl. --- Co-authored-by: Jan Michael Auer <[email protected]>
1 parent 2fca084 commit 0ad3d0e

File tree

14 files changed

+331
-253
lines changed

14 files changed

+331
-253
lines changed

clients/rust/src/client.rs

Lines changed: 10 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,9 @@ use std::time::Duration;
44

55
use bytes::Bytes;
66
use futures_util::stream::BoxStream;
7-
use objectstore_types::ExpirationPolicy;
7+
use objectstore_types::{Compression, ExpirationPolicy, scope};
88
use url::Url;
99

10-
pub use objectstore_types::Compression;
11-
1210
const USER_AGENT: &str = concat!("objectstore-client/", env!("CARGO_PKG_VERSION"));
1311

1412
#[derive(Debug)]
@@ -218,28 +216,14 @@ impl Usecase {
218216
#[derive(Debug)]
219217
pub(crate) struct ScopeInner {
220218
usecase: Usecase,
221-
scope: String,
219+
scopes: scope::Scopes,
222220
}
223221

224222
impl ScopeInner {
225223
#[inline]
226224
pub(crate) fn usecase(&self) -> &Usecase {
227225
&self.usecase
228226
}
229-
230-
fn as_path_segment(&self) -> &str {
231-
if self.scope.is_empty() {
232-
"_"
233-
} else {
234-
&self.scope
235-
}
236-
}
237-
}
238-
239-
impl std::fmt::Display for ScopeInner {
240-
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
241-
f.write_str(&self.scope)
242-
}
243227
}
244228

245229
/// A [`Scope`] is a sequence of key-value pairs that defines a (possibly nested) namespace within a
@@ -257,18 +241,16 @@ impl Scope {
257241
pub fn new(usecase: Usecase) -> Self {
258242
Self(Ok(ScopeInner {
259243
usecase,
260-
scope: String::new(),
244+
scopes: scope::Scopes::empty(),
261245
}))
262246
}
263247

264248
fn for_organization(usecase: Usecase, organization: u64) -> Self {
265-
let scope = format!("org={}", organization);
266-
Self(Ok(ScopeInner { usecase, scope }))
249+
Self::new(usecase).push("org", organization)
267250
}
268251

269252
fn for_project(usecase: Usecase, organization: u64, project: u64) -> Self {
270-
let scope = format!("org={};project={}", organization, project);
271-
Self(Ok(ScopeInner { usecase, scope }))
253+
Self::for_organization(usecase, organization).push("project", project)
272254
}
273255

274256
/// Extends this Scope by creating a new sub-scope nested within it.
@@ -277,62 +259,13 @@ impl Scope {
277259
V: std::fmt::Display,
278260
{
279261
let result = self.0.and_then(|mut inner| {
280-
Self::validate_key(key)?;
281-
282-
let value = value.to_string();
283-
Self::validate_value(&value)?;
284-
285-
if !inner.scope.is_empty() {
286-
inner.scope.push(';');
287-
}
288-
inner.scope.push_str(key);
289-
inner.scope.push('=');
290-
inner.scope.push_str(&value);
291-
262+
inner.scopes.push(key, value)?;
292263
Ok(inner)
293264
});
294265

295266
Self(result)
296267
}
297268

298-
/// Characters allowed in a Scope's key and value.
299-
/// These are the URL safe characters, except for `.` which we use as separator between
300-
/// key and value of Scope components.
301-
const ALLOWED_CHARS: &[u8] =
302-
b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-()$!+'";
303-
304-
/// Validates that a scope key contains only allowed characters and is not empty.
305-
fn validate_key(key: &str) -> crate::Result<()> {
306-
if key.is_empty() {
307-
return Err(crate::Error::InvalidScope {
308-
message: "Scope key cannot be empty".to_string(),
309-
});
310-
}
311-
if key.bytes().all(|b| Self::ALLOWED_CHARS.contains(&b)) {
312-
Ok(())
313-
} else {
314-
Err(crate::Error::InvalidScope {
315-
message: format!("Invalid scope key '{key}'."),
316-
})
317-
}
318-
}
319-
320-
/// Validates that a scope value contains only allowed characters and is not empty.
321-
fn validate_value(value: &str) -> crate::Result<()> {
322-
if value.is_empty() {
323-
return Err(crate::Error::InvalidScope {
324-
message: "Scope value cannot be empty".to_string(),
325-
});
326-
}
327-
if value.bytes().all(|b| Self::ALLOWED_CHARS.contains(&b)) {
328-
Ok(())
329-
} else {
330-
Err(crate::Error::InvalidScope {
331-
message: format!("Invalid scope value '{value}'."),
332-
})
333-
}
334-
}
335-
336269
/// Creates a session for this scope using the given client.
337270
///
338271
/// # Errors
@@ -366,9 +299,10 @@ pub(crate) struct ClientInner {
366299
/// .timeout(Duration::from_secs(1))
367300
/// .propagate_traces(true)
368301
/// .build()?;
369-
/// let usecase = Usecase::new("my_app");
370302
///
371-
/// let session = client.session(usecase.for_project(12345, 1337))?;
303+
/// let session = Usecase::new("my_app")
304+
/// .for_project(12345, 1337)
305+
/// .session(&client)?;
372306
///
373307
/// let response = session.put("hello world").send().await?;
374308
///
@@ -439,7 +373,7 @@ impl Session {
439373
.push("v1")
440374
.push("objects")
441375
.push(&self.scope.usecase.name)
442-
.push(self.scope.as_path_segment())
376+
.push(&self.scope.scopes.as_api_path().to_string())
443377
.extend(object_key.split("/"));
444378
drop(segments);
445379

clients/rust/src/error.rs

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,8 @@ pub enum Error {
1414
#[error(transparent)]
1515
Metadata(#[from] objectstore_types::Error),
1616
/// Error when scope validation fails.
17-
#[error("{message}")]
18-
InvalidScope {
19-
/// The validation error message.
20-
message: String,
21-
},
17+
#[error("invalid scope: {0}")]
18+
InvalidScope(#[from] objectstore_types::scope::InvalidScopeError),
2219
/// Error when URL manipulation fails.
2320
#[error("{message}")]
2421
InvalidUrl {

objectstore-server/src/auth/context.rs

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,29 +2,14 @@ use std::collections::{BTreeMap, HashSet};
22

33
use jsonwebtoken::{Algorithm, DecodingKey, Header, TokenData, Validation, decode, decode_header};
44
use objectstore_service::id::ObjectContext;
5+
use objectstore_types::Permission;
56
use secrecy::ExposeSecret;
67
use serde::{Deserialize, Serialize};
78
use thiserror::Error;
89

910
use crate::auth::util::StringOrWildcard;
1011
use crate::config::AuthZ;
1112

12-
/// Permissions that control whether different operations are authorized.
13-
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Hash)]
14-
pub enum Permission {
15-
/// The permission required to read objects from objectstore.
16-
#[serde(rename = "object.read")]
17-
ObjectRead,
18-
19-
/// The permission required to write/overwrite objects in objectstore.
20-
#[serde(rename = "object.write")]
21-
ObjectWrite,
22-
23-
/// The permission required to delete objects from objectstore.
24-
#[serde(rename = "object.delete")]
25-
ObjectDelete,
26-
}
27-
2813
/// `AuthContext` encapsulates the verified content of things like authorization tokens.
2914
///
3015
/// [`AuthContext::assert_authorized`] can be used to check whether a request is authorized to
@@ -237,7 +222,7 @@ impl AuthContext {
237222
mod tests {
238223
use super::*;
239224
use crate::config::{AuthZVerificationKey, ConfigSecret};
240-
use objectstore_service::id::{Scope, Scopes};
225+
use objectstore_types::scope::{Scope, Scopes};
241226
use secrecy::SecretBox;
242227
use serde_json::json;
243228

objectstore-server/src/auth/service.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ use axum::extract::FromRequestParts;
22
use axum::http::{StatusCode, header, request::Parts};
33
use objectstore_service::id::{ObjectContext, ObjectId};
44
use objectstore_service::{PayloadStream, StorageService};
5-
use objectstore_types::Metadata;
5+
use objectstore_types::{Metadata, Permission};
66

7-
use crate::auth::{AuthContext, AuthError, Permission};
7+
use crate::auth::{AuthContext, AuthError};
88
use crate::state::ServiceState;
99

1010
const BEARER_PREFIX: &str = "Bearer ";

objectstore-server/src/config.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,11 @@ use std::time::Duration;
4040

4141
use anyhow::Result;
4242
use figment::providers::{Env, Format, Serialized, Yaml};
43+
use objectstore_types::Permission;
4344
use secrecy::{CloneableSecret, SecretBox, SerializableSecret, zeroize::Zeroize};
4445
use serde::{Deserialize, Serialize};
4546
use tracing::level_filters::LevelFilter;
4647

47-
use crate::auth::Permission;
48-
4948
/// Environment variable prefix for all configuration options.
5049
const ENV_PREFIX: &str = "OS__";
5150

objectstore-server/src/endpoints/objects.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@ use axum::response::{IntoResponse, Response};
1010
use axum::routing;
1111
use axum::{Json, Router};
1212
use futures_util::{StreamExt, TryStreamExt};
13-
use objectstore_service::id::{ObjectContext, ObjectId, Scope, Scopes};
13+
use objectstore_service::id::{ObjectContext, ObjectId};
1414
use objectstore_types::Metadata;
15+
use objectstore_types::scope::{Scope, Scopes};
1516
use serde::{Deserialize, Serialize, de};
1617

1718
use crate::auth::AuthAwareService;

objectstore-service/src/backend/bigtable.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -326,7 +326,8 @@ fn micros_to_time(micros: i64) -> Option<SystemTime> {
326326
mod tests {
327327
use std::collections::BTreeMap;
328328

329-
use crate::id::{ObjectContext, Scope, Scopes};
329+
use crate::id::ObjectContext;
330+
use objectstore_types::scope::{Scope, Scopes};
330331

331332
use super::*;
332333

objectstore-service/src/backend/gcs.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -435,7 +435,8 @@ impl Backend for GcsBackend {
435435
mod tests {
436436
use std::collections::BTreeMap;
437437

438-
use crate::id::{ObjectContext, Scope, Scopes};
438+
use crate::id::ObjectContext;
439+
use objectstore_types::scope::{Scope, Scopes};
439440

440441
use super::*;
441442

objectstore-service/src/backend/local_fs.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,8 @@ mod tests {
108108
use futures_util::TryStreamExt;
109109
use objectstore_types::{Compression, ExpirationPolicy};
110110

111-
use crate::id::{ObjectContext, Scope, Scopes};
111+
use crate::id::ObjectContext;
112+
use objectstore_types::scope::{Scope, Scopes};
112113

113114
use super::*;
114115

0 commit comments

Comments
 (0)