Skip to main content

loonfs_objectstore/presign/
issuer.rs

1//! Issuer contract and values for presigned direct transfers, in both
2//! directions: the create-only writes `direct_put` authorizes, and the
3//! reads `direct_get` authorizes.
4
5use crate::object_store::Result;
6use loonfs_api::{ContentRef, StorageChecksum};
7use std::collections::BTreeMap;
8use std::time::{Duration, SystemTime};
9
10/// Describes one immutable create-only write to authorize for a client.
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct PresignedPutRequest<'a> {
13    /// Logical unscoped object key that the issuer resolves beneath its configured prefix.
14    pub object_key: &'a str,
15    /// Expected digest and byte length the provider must enforce for the request body.
16    pub content_ref: &'a ContentRef,
17    /// Lifetime of the issued capability measured from the supplied signing time.
18    pub expires_in: Duration,
19}
20
21/// Describes one read of an existing content object to authorize for a client.
22///
23/// There is nothing to bind but the key. A write has to carry the digest
24/// and the create-only precondition into the signature because the bytes do
25/// not exist yet and the provider is the only party that can refuse them; a
26/// read is of bytes this deployment already verified, and the reader checks
27/// what arrives against the reference it was handed.
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct PresignedGetRequest<'a> {
30    /// Logical unscoped object key that the issuer resolves beneath its configured prefix.
31    pub object_key: &'a str,
32    /// Lifetime of the issued capability measured from the supplied signing time.
33    pub expires_in: Duration,
34}
35
36/// Describes one part of an open multipart upload to authorize for a client.
37#[derive(Debug, Clone, PartialEq, Eq)]
38pub struct PresignedPartRequest<'a> {
39    /// Logical unscoped object key the finished upload assembles into.
40    pub object_key: &'a str,
41    /// Provider-side upload the part belongs to.
42    pub provider_upload_id: &'a str,
43    /// One-based part number.
44    pub part_number: u32,
45    /// Checksum the provider must enforce on this part's bytes.
46    pub part_checksum: &'a StorageChecksum,
47    /// Lifetime of the issued capability measured from the supplied signing time.
48    pub expires_in: Duration,
49}
50
51/// Carries a short-lived HTTP capability and every request header covered by its signature.
52#[derive(Debug, Clone, PartialEq, Eq)]
53pub struct PresignedUrl {
54    /// HTTP method the client must use exactly as issued.
55    pub method: String,
56    /// Complete provider URL including authentication query parameters.
57    pub url: String,
58    /// Signed headers and values the client must send unchanged.
59    pub headers: BTreeMap<String, String>,
60    /// Unix-millisecond instant after which the provider rejects the capability.
61    pub expires_at_ms: u64,
62}
63
64/// Issues short-lived transfer capabilities in both directions, and in
65/// doing so carries the write-time enforcement contract the rest of the
66/// system leans on:
67///
68/// - The signed request must make the provider verify that the uploaded
69///   body hashes to the content ref's digest and reject anything else
70///   (S3-family: a signed `x-amz-checksum-sha256` header).
71/// - The signed request must be create-only, so an existing object is never
72///   replaced through a transfer capability (S3-family: a signed
73///   `if-none-match: *` header).
74/// - Both requirements ride the signature: a client cannot drop or alter
75///   them without invalidating the capability.
76///
77/// Because every issuer guarantees this, `direct_put` completion proves an
78/// upload by existence and size alone — it never reads content back. A
79/// provider that cannot enforce digest verification and create-only
80/// preconditions in a presigned request must not implement this trait; the
81/// deployment then reports `direct_put` as unsupported instead of falling
82/// back to weaker verification.
83///
84/// Reads ride the same trait for a plainer reason: a deployment that lets a
85/// client write an object directly must be able to hand that object back,
86/// and it can only do that where it can sign a read of it. So the two
87/// directions are offered together or not at all, and a store with no
88/// issuer proxies both.
89pub trait ObjectTransferIssuer: Send + Sync + std::fmt::Debug {
90    /// Issues a create-only write capability bound to the requested content identity.
91    ///
92    /// Issuance fails for invalid keys, unsupported content-reference kinds,
93    /// invalid expiry policy, unusable signing time, or malformed provider configuration.
94    fn presign_put(
95        &self,
96        request: PresignedPutRequest<'_>,
97        now: SystemTime,
98    ) -> Result<PresignedUrl>;
99
100    /// Issues a write capability for one part of an open multipart upload.
101    ///
102    /// A part is not the object, so this one is not create-only: re-issuing
103    /// a part is how a client retries a transfer that failed halfway, and
104    /// the provider takes the last write. What still rides the signature is
105    /// the part's checksum, which both providers enforce on the way in — so
106    /// no part of the eventual object is ever bytes the client did not
107    /// declare.
108    fn presign_multipart_part(
109        &self,
110        request: PresignedPartRequest<'_>,
111        now: SystemTime,
112    ) -> Result<PresignedUrl>;
113
114    /// Issues a read capability for one content object.
115    ///
116    /// One capability serves the whole transfer, however many requests the
117    /// client makes with it. A presigned URL signs a named set of headers,
118    /// and `Range` is not among them — so a reader may range, resume after
119    /// a broken connection, or fetch in parallel windows on the one URL,
120    /// and the signature is unaffected. Implementors must keep it that way:
121    /// signing a `Range` header would bind a capability to one window and
122    /// turn every resumption into another round trip to this server.
123    ///
124    /// Issuance fails for invalid keys, invalid expiry policy, unusable
125    /// signing time, or malformed provider configuration.
126    fn presign_get(
127        &self,
128        request: PresignedGetRequest<'_>,
129        now: SystemTime,
130    ) -> Result<PresignedUrl>;
131}