Skip to main content

spiral/
storage_binding.rs

1//! Scandal bindings from the storage bindings that Spiral's storage service issues.
2
3use crate::api::tables::{
4    DirectRead as IssuedDirectRead, StorageAccess, StorageBinding as IssuedBinding, StorageMode,
5    VendedCredential,
6};
7use crate::scandal::{
8    DirectRead, GcsSession, S3Session, ScandalError, StorageBinding, StorageCredential,
9};
10
11impl StorageBinding {
12    /// The scandal binding of `root` that storage's `binding` authorizes, with its deadline: an
13    /// SPFS capability, signed at the binding's endpoint and read directly at the provider when
14    /// a read-only binding carries a direct read, or a provider session vended for the binding's
15    /// provider root. When `root`, the root that a Table's object references name, differs from
16    /// the binding's, it becomes the alias.
17    pub fn from_storage(root: String, binding: IssuedBinding) -> Result<Self, ScandalError> {
18        let expires_at = Some(binding.expires_at);
19        match binding.access {
20            StorageAccess::Signed(access) => {
21                let direct_read = match access.direct_read {
22                    Some(direct) if binding.mode == StorageMode::ReadOnly => {
23                        Some(direct_read(direct, &root)?)
24                    }
25                    _ => None,
26                };
27                Ok(Self {
28                    root_alias: (root != binding.root_uri).then_some(binding.root_uri),
29                    root_uri: root,
30                    credential: Some(StorageCredential(access.token.0)),
31                    // Storage access of older stacks names no endpoint.
32                    endpoint: (!access.endpoint.is_empty()).then_some(access.endpoint),
33                    expires_at,
34                    direct_read,
35                })
36            }
37            StorageAccess::Vended(access) => {
38                let credential = session(access.credential, &root)?;
39                let root_alias = (root != binding.root_uri).then_some(root);
40                Ok(Self {
41                    root_uri: binding.root_uri,
42                    root_alias,
43                    credential: Some(credential),
44                    endpoint: None,
45                    expires_at,
46                    direct_read: None,
47                })
48            }
49            StorageAccess::Unknown { .. } => Err(unsupported(&root)),
50        }
51    }
52
53    /// The scandal binding of a resource's whole storage, which `spiral://` addresses name
54    /// relative to its root.
55    pub fn for_resource(binding: IssuedBinding) -> Result<Self, ScandalError> {
56        Self::from_storage(binding.root_uri.clone(), binding)
57    }
58}
59
60fn direct_read(direct: IssuedDirectRead, root: &str) -> Result<DirectRead, ScandalError> {
61    Ok(DirectRead {
62        root_uri: direct.uri,
63        credential: session(direct.credential, root)?,
64    })
65}
66
67/// The provider session `credential` grants, as scandal reads it.
68fn session(credential: VendedCredential, root: &str) -> Result<StorageCredential, ScandalError> {
69    let json = match credential {
70        VendedCredential::AwsSession(credential) => serde_json::to_string(&S3Session {
71            access_key_id: credential.access_key_id,
72            secret_access_key: credential.secret_access_key.0,
73            session_token: credential.session_token.0,
74            region: credential.region,
75            endpoint: credential.endpoint,
76            expires_at_micros: credential.expires_at_micros.0,
77        }),
78        VendedCredential::GcsAccessToken(credential) => serde_json::to_string(&GcsSession {
79            access_token: credential.access_token.0,
80            expires_at_micros: credential.expires_at_micros.0,
81        }),
82        _ => return Err(unsupported(root)),
83    };
84    json.map(StorageCredential)
85        .map_err(|error| ScandalError::Internal {
86            message: error.to_string(),
87        })
88}
89
90fn unsupported(root: &str) -> ScandalError {
91    ScandalError::InvalidArgument {
92        message: format!("storage at {root} is neither signed by SPFS nor in object storage"),
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99    use crate::api::tables::{
100        GcsAccessToken, HuggingFaceToken, SecretToken, SignedAccess, VendedAccess,
101    };
102
103    fn vended_binding(credential: VendedCredential) -> IssuedBinding {
104        IssuedBinding {
105            root_uri: "gs://acme/lake/tables/t1/".to_owned(),
106            mode: StorageMode::ReadOnly,
107            expires_at: 0.into(),
108            access: StorageAccess::Vended(VendedAccess { credential }),
109        }
110    }
111
112    /// A vended binding reaches scandal at its provider root with its session as the
113    /// credential, aliased by the table's recorded root when that differs, and a credential that
114    /// opens no object store is refused.
115    #[test]
116    fn a_vended_binding_carries_its_session_to_scandal() {
117        let root = "gs://acme/lake/tables/t1/".to_owned();
118        let gcs = || {
119            vended_binding(VendedCredential::GcsAccessToken(GcsAccessToken {
120                access_token: SecretToken("ya29.vended".into()),
121                expires_at_micros: 7.into(),
122            }))
123        };
124        let binding = StorageBinding::from_storage(root.clone(), gcs()).unwrap();
125        assert_eq!(binding.root_uri, root);
126        assert_eq!(binding.root_alias, None);
127        let session: GcsSession = serde_json::from_str(binding.credential().unwrap()).unwrap();
128        assert_eq!(session.access_token, "ya29.vended");
129        assert_eq!(session.expires_at_micros, 7);
130
131        let aliased =
132            StorageBinding::from_storage("spfsu://4b44-alias/".to_owned(), gcs()).unwrap();
133        assert_eq!(aliased.root_uri, root);
134        assert_eq!(aliased.root_alias.as_deref(), Some("spfsu://4b44-alias/"));
135
136        let refused = StorageBinding::from_storage(
137            root,
138            vended_binding(VendedCredential::HuggingFaceToken(HuggingFaceToken {
139                token: SecretToken("hf".into()),
140                expires_at_micros: 7.into(),
141                endpoint: "https://huggingface.co".to_owned(),
142            })),
143        );
144        assert!(refused.is_err());
145    }
146
147    fn signed_binding(mode: StorageMode, endpoint: &str) -> IssuedBinding {
148        IssuedBinding {
149            root_uri: "spfsu://4b44-alias/".to_owned(),
150            mode,
151            expires_at: 1_900_000_000.into(),
152            access: StorageAccess::Signed(SignedAccess {
153                endpoint: endpoint.to_owned(),
154                token: SecretToken("capability".into()),
155                direct_read: Some(IssuedDirectRead {
156                    uri: "gs://acme/lake/tables/t1/".to_owned(),
157                    credential: VendedCredential::GcsAccessToken(GcsAccessToken {
158                        access_token: SecretToken("ya29.vended".into()),
159                        expires_at_micros: 7.into(),
160                    }),
161                }),
162            }),
163        }
164    }
165
166    /// A signed binding reaches scandal with the endpoint that signs it, its deadline and,
167    /// read-only, its direct read; older stacks name no endpoint.
168    #[test]
169    fn a_signed_binding_carries_its_terms_to_scandal() {
170        let binding = StorageBinding::for_resource(signed_binding(
171            StorageMode::ReadOnly,
172            "https://spfs.example/",
173        ))
174        .unwrap();
175        assert_eq!(binding.endpoint.as_deref(), Some("https://spfs.example/"));
176        assert_eq!(binding.expires_at.map(u64::from), Some(1_900_000_000));
177        let direct = binding.direct_read.unwrap();
178        assert_eq!(direct.root_uri, "gs://acme/lake/tables/t1/");
179        let session: GcsSession = serde_json::from_str(&direct.credential.0).unwrap();
180        assert_eq!(session.access_token, "ya29.vended");
181
182        let writable =
183            StorageBinding::for_resource(signed_binding(StorageMode::ReadWrite, "")).unwrap();
184        assert_eq!(writable.direct_read, None);
185        assert_eq!(writable.endpoint, None);
186    }
187}