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}