Skip to main content

unitycatalog_storage_proxy/
testing.rs

1//! An in-memory [`StorageProxyBackend`] for exercising the wire contract without a
2//! cloud.
3//!
4//! [`InMemoryStorageProxyBackend`] wraps an [`object_store::memory::InMemory`] and
5//! authorizes exactly one securable + key-prefix, so the confused-deputy and
6//! read-only-scope tests have something concrete to be rejected against. Every
7//! handler path (ranged `get_opts`, conditional `put_opts`, streaming
8//! `put_multipart`) is supported by `InMemory`, so the full contract is testable
9//! here.
10
11use std::sync::Arc;
12
13use object_store::DynObjectStore;
14use object_store::memory::InMemory;
15use object_store::prefix::PrefixStore;
16
17use crate::backend::{
18    ProxyCapabilities, ProxyReq, Securable, StorageProxyBackend, reject_key_traversal,
19};
20use crate::error::{ProxyError, ProxyResult};
21
22/// An in-memory backend authorizing a single securable and key prefix.
23#[derive(Clone)]
24pub struct InMemoryStorageProxyBackend {
25    store: Arc<InMemory>,
26    /// The one securable this backend authorizes.
27    allowed: Securable,
28    /// The key prefix within the securable the caller may address (`""` = any key
29    /// under the securable root).
30    prefix: String,
31    /// Whether writes (`PUT`) are permitted.
32    allow_write: bool,
33}
34
35impl InMemoryStorageProxyBackend {
36    /// A backend authorizing the whole of the given `table:` securable, read+write,
37    /// over a fresh empty store.
38    pub fn table(full_name: impl Into<String>) -> Self {
39        Self {
40            store: Arc::new(InMemory::new()),
41            allowed: Securable::Table {
42                full_name: full_name.into(),
43            },
44            prefix: String::new(),
45            allow_write: true,
46        }
47    }
48
49    /// A backend authorizing the whole of the given `vol:` securable, read+write.
50    pub fn volume(full_name: impl Into<String>) -> Self {
51        Self {
52            store: Arc::new(InMemory::new()),
53            allowed: Securable::Volume {
54                full_name: full_name.into(),
55            },
56            prefix: String::new(),
57            allow_write: true,
58        }
59    }
60
61    /// Restrict authorized keys to those under `prefix` (drives out-of-scope tests).
62    pub fn with_prefix(mut self, prefix: impl Into<String>) -> Self {
63        self.prefix = prefix.into();
64        self
65    }
66
67    /// Make the backend read-only (drives read-only-scope `PUT` rejection tests).
68    pub fn read_only(mut self) -> Self {
69        self.allow_write = false;
70        self
71    }
72
73    /// Borrow the backing store, e.g. to seed objects in a test.
74    pub fn store(&self) -> Arc<InMemory> {
75        self.store.clone()
76    }
77}
78
79#[async_trait::async_trait]
80impl<Cx: Send + Sync + 'static> StorageProxyBackend<Cx> for InMemoryStorageProxyBackend {
81    fn capabilities(&self) -> ProxyCapabilities {
82        ProxyCapabilities { enabled: true }
83    }
84
85    async fn authorize(&self, req: &ProxyReq, _cx: &Cx) -> ProxyResult<()> {
86        reject_key_traversal(&req.key)?;
87        if req.securable != self.allowed {
88            return Err(ProxyError::PermissionDenied(format!(
89                "securable {:?} is not authorized",
90                req.securable
91            )));
92        }
93        if !self.prefix.is_empty() && !req.key.starts_with(&self.prefix) {
94            return Err(ProxyError::OutOfScope(format!(
95                "key `{}` is outside the authorized prefix `{}`",
96                req.key, self.prefix
97            )));
98        }
99        if req.verb.is_write() && !self.allow_write {
100            return Err(ProxyError::PermissionDenied(
101                "write not permitted for this scope".into(),
102            ));
103        }
104        Ok(())
105    }
106
107    async fn open(&self, _req: &ProxyReq, _cx: &Cx) -> ProxyResult<Arc<DynObjectStore>> {
108        // A real backend prefixes at the securable's storage root; the in-memory
109        // store's root already stands in for that root, so return it directly (the
110        // authorized key prefix is enforced in `authorize`, not by re-prefixing).
111        Ok(self.store.clone())
112    }
113}
114
115/// A [`PrefixStore`]-wrapping helper, mirroring how a real arm roots `req.key`
116/// under a securable prefix. Exposed for tests that want to assert prefix
117/// confinement explicitly.
118pub fn prefixed(store: Arc<InMemory>, prefix: &str) -> Arc<DynObjectStore> {
119    Arc::new(PrefixStore::new(store, prefix))
120}