Skip to content
86 changes: 10 additions & 76 deletions clients/rust/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,9 @@ use std::time::Duration;

use bytes::Bytes;
use futures_util::stream::BoxStream;
use objectstore_types::ExpirationPolicy;
use objectstore_types::{Compression, ExpirationPolicy, scope};
use url::Url;

pub use objectstore_types::Compression;

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

#[derive(Debug)]
Expand Down Expand Up @@ -218,28 +216,14 @@ impl Usecase {
#[derive(Debug)]
pub(crate) struct ScopeInner {
usecase: Usecase,
scope: String,
scopes: scope::Scopes,
}

impl ScopeInner {
#[inline]
pub(crate) fn usecase(&self) -> &Usecase {
&self.usecase
}

fn as_path_segment(&self) -> &str {
if self.scope.is_empty() {
"_"
} else {
&self.scope
}
}
}

impl std::fmt::Display for ScopeInner {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.scope)
}
}

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

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

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

/// Extends this Scope by creating a new sub-scope nested within it.
Expand All @@ -277,62 +259,13 @@ impl Scope {
V: std::fmt::Display,
{
let result = self.0.and_then(|mut inner| {
Self::validate_key(key)?;

let value = value.to_string();
Self::validate_value(&value)?;

if !inner.scope.is_empty() {
inner.scope.push(';');
}
inner.scope.push_str(key);
inner.scope.push('=');
inner.scope.push_str(&value);

inner.scopes.push(key, value)?;
Ok(inner)
});

Self(result)
}

/// Characters allowed in a Scope's key and value.
/// These are the URL safe characters, except for `.` which we use as separator between
/// key and value of Scope components.
const ALLOWED_CHARS: &[u8] =
b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-()$!+'";

/// Validates that a scope key contains only allowed characters and is not empty.
fn validate_key(key: &str) -> crate::Result<()> {
if key.is_empty() {
return Err(crate::Error::InvalidScope {
message: "Scope key cannot be empty".to_string(),
});
}
if key.bytes().all(|b| Self::ALLOWED_CHARS.contains(&b)) {
Ok(())
} else {
Err(crate::Error::InvalidScope {
message: format!("Invalid scope key '{key}'."),
})
}
}

/// Validates that a scope value contains only allowed characters and is not empty.
fn validate_value(value: &str) -> crate::Result<()> {
if value.is_empty() {
return Err(crate::Error::InvalidScope {
message: "Scope value cannot be empty".to_string(),
});
}
if value.bytes().all(|b| Self::ALLOWED_CHARS.contains(&b)) {
Ok(())
} else {
Err(crate::Error::InvalidScope {
message: format!("Invalid scope value '{value}'."),
})
}
}

/// Creates a session for this scope using the given client.
///
/// # Errors
Expand Down Expand Up @@ -366,9 +299,10 @@ pub(crate) struct ClientInner {
/// .timeout(Duration::from_secs(1))
/// .propagate_traces(true)
/// .build()?;
/// let usecase = Usecase::new("my_app");
///
/// let session = client.session(usecase.for_project(12345, 1337))?;
/// let session = Usecase::new("my_app")
/// .for_project(12345, 1337)
/// .session(&client)?;
///
/// let response = session.put("hello world").send().await?;
///
Expand Down Expand Up @@ -439,7 +373,7 @@ impl Session {
.push("v1")
.push("objects")
.push(&self.scope.usecase.name)
.push(self.scope.as_path_segment())
.push(&self.scope.scopes.as_api_path().to_string())
.extend(object_key.split("/"));
drop(segments);

Expand Down
7 changes: 2 additions & 5 deletions clients/rust/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,8 @@ pub enum Error {
#[error(transparent)]
Metadata(#[from] objectstore_types::Error),
/// Error when scope validation fails.
#[error("{message}")]
InvalidScope {
/// The validation error message.
message: String,
},
#[error("invalid scope: {0}")]
InvalidScope(#[from] objectstore_types::scope::InvalidScopeError),
/// Error when URL manipulation fails.
#[error("{message}")]
InvalidUrl {
Expand Down
19 changes: 2 additions & 17 deletions objectstore-server/src/auth/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,14 @@ use std::collections::{BTreeMap, HashSet};

use jsonwebtoken::{Algorithm, DecodingKey, Header, TokenData, Validation, decode, decode_header};
use objectstore_service::id::ObjectContext;
use objectstore_types::Permission;
use secrecy::ExposeSecret;
use serde::{Deserialize, Serialize};
use thiserror::Error;

use crate::auth::util::StringOrWildcard;
use crate::config::AuthZ;

/// Permissions that control whether different operations are authorized.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Hash)]
pub enum Permission {
/// The permission required to read objects from objectstore.
#[serde(rename = "object.read")]
ObjectRead,

/// The permission required to write/overwrite objects in objectstore.
#[serde(rename = "object.write")]
ObjectWrite,

/// The permission required to delete objects from objectstore.
#[serde(rename = "object.delete")]
ObjectDelete,
}

/// `AuthContext` encapsulates the verified content of things like authorization tokens.
///
/// [`AuthContext::assert_authorized`] can be used to check whether a request is authorized to
Expand Down Expand Up @@ -237,7 +222,7 @@ impl AuthContext {
mod tests {
use super::*;
use crate::config::{AuthZVerificationKey, ConfigSecret};
use objectstore_service::id::{Scope, Scopes};
use objectstore_types::scope::{Scope, Scopes};
use secrecy::SecretBox;
use serde_json::json;

Expand Down
4 changes: 2 additions & 2 deletions objectstore-server/src/auth/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ use axum::extract::FromRequestParts;
use axum::http::{StatusCode, header, request::Parts};
use objectstore_service::id::{ObjectContext, ObjectId};
use objectstore_service::{PayloadStream, StorageService};
use objectstore_types::Metadata;
use objectstore_types::{Metadata, Permission};

use crate::auth::{AuthContext, AuthError, Permission};
use crate::auth::{AuthContext, AuthError};
use crate::state::ServiceState;

const BEARER_PREFIX: &str = "Bearer ";
Expand Down
3 changes: 1 addition & 2 deletions objectstore-server/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,11 @@ use std::time::Duration;

use anyhow::Result;
use figment::providers::{Env, Format, Serialized, Yaml};
use objectstore_types::Permission;
use secrecy::{CloneableSecret, SecretBox, SerializableSecret, zeroize::Zeroize};
use serde::{Deserialize, Serialize};
use tracing::level_filters::LevelFilter;

use crate::auth::Permission;

/// Environment variable prefix for all configuration options.
const ENV_PREFIX: &str = "OS__";

Expand Down
3 changes: 2 additions & 1 deletion objectstore-server/src/endpoints/objects.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ use axum::response::{IntoResponse, Response};
use axum::routing;
use axum::{Json, Router};
use futures_util::{StreamExt, TryStreamExt};
use objectstore_service::id::{ObjectContext, ObjectId, Scope, Scopes};
use objectstore_service::id::{ObjectContext, ObjectId};
use objectstore_types::Metadata;
use objectstore_types::scope::{Scope, Scopes};
use serde::{Deserialize, Serialize, de};

use crate::auth::AuthAwareService;
Expand Down
3 changes: 2 additions & 1 deletion objectstore-service/src/backend/bigtable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,8 @@ fn micros_to_time(micros: i64) -> Option<SystemTime> {
mod tests {
use std::collections::BTreeMap;

use crate::id::{ObjectContext, Scope, Scopes};
use crate::id::ObjectContext;
use objectstore_types::scope::{Scope, Scopes};

use super::*;

Expand Down
3 changes: 2 additions & 1 deletion objectstore-service/src/backend/gcs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -435,7 +435,8 @@ impl Backend for GcsBackend {
mod tests {
use std::collections::BTreeMap;

use crate::id::{ObjectContext, Scope, Scopes};
use crate::id::ObjectContext;
use objectstore_types::scope::{Scope, Scopes};

use super::*;

Expand Down
3 changes: 2 additions & 1 deletion objectstore-service/src/backend/local_fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,8 @@ mod tests {
use futures_util::TryStreamExt;
use objectstore_types::{Compression, ExpirationPolicy};

use crate::id::{ObjectContext, Scope, Scopes};
use crate::id::ObjectContext;
use objectstore_types::scope::{Scope, Scopes};

use super::*;

Expand Down
Loading