@@ -24,19 +24,22 @@ use crate::client::s3::{
24
24
ListResponse ,
25
25
} ;
26
26
use crate :: client:: GetOptionsExt ;
27
- use crate :: gcp:: { GcpCredential , GcpCredentialProvider , STORE } ;
27
+ use crate :: gcp:: { GcpCredential , GcpCredentialProvider , GcpSigningCredentialProvider , STORE } ;
28
28
use crate :: multipart:: PartId ;
29
29
use crate :: path:: { Path , DELIMITER } ;
30
+ use crate :: util:: hex_encode;
30
31
use crate :: {
31
32
ClientOptions , GetOptions , ListResult , MultipartId , PutMode , PutOptions , PutResult , Result ,
32
33
RetryConfig ,
33
34
} ;
34
35
use async_trait:: async_trait;
36
+ use base64:: prelude:: BASE64_STANDARD ;
37
+ use base64:: Engine ;
35
38
use bytes:: { Buf , Bytes } ;
36
39
use percent_encoding:: { percent_encode, utf8_percent_encode, NON_ALPHANUMERIC } ;
37
40
use reqwest:: header:: HeaderName ;
38
41
use reqwest:: { header, Client , Method , RequestBuilder , Response , StatusCode } ;
39
- use serde:: Serialize ;
42
+ use serde:: { Deserialize , Serialize } ;
40
43
use snafu:: { OptionExt , ResultExt , Snafu } ;
41
44
use std:: sync:: Arc ;
42
45
@@ -101,6 +104,15 @@ enum Error {
101
104
102
105
#[ snafu( display( "Got invalid multipart response: {}" , source) ) ]
103
106
InvalidMultipartResponse { source : quick_xml:: de:: DeError } ,
107
+
108
+ #[ snafu( display( "Error signing blob: {}" , source) ) ]
109
+ SignBlobRequest { source : crate :: client:: retry:: Error } ,
110
+
111
+ #[ snafu( display( "Got invalid signing blob repsonse: {}" , source) ) ]
112
+ InvalidSignBlobResponse { source : reqwest:: Error } ,
113
+
114
+ #[ snafu( display( "Got invalid signing blob signature: {}" , source) ) ]
115
+ InvalidSignBlobSignature { source : base64:: DecodeError } ,
104
116
}
105
117
106
118
impl From < Error > for crate :: Error {
@@ -123,13 +135,39 @@ pub struct GoogleCloudStorageConfig {
123
135
124
136
pub credentials : GcpCredentialProvider ,
125
137
138
+ pub signing_credentials : GcpSigningCredentialProvider ,
139
+
126
140
pub bucket_name : String ,
127
141
128
142
pub retry_config : RetryConfig ,
129
143
130
144
pub client_options : ClientOptions ,
131
145
}
132
146
147
+ impl GoogleCloudStorageConfig {
148
+ pub fn new (
149
+ base_url : String ,
150
+ credentials : GcpCredentialProvider ,
151
+ signing_credentials : GcpSigningCredentialProvider ,
152
+ bucket_name : String ,
153
+ retry_config : RetryConfig ,
154
+ client_options : ClientOptions ,
155
+ ) -> Self {
156
+ Self {
157
+ base_url,
158
+ credentials,
159
+ signing_credentials,
160
+ bucket_name,
161
+ retry_config,
162
+ client_options,
163
+ }
164
+ }
165
+
166
+ pub fn path_url ( & self , path : & Path ) -> String {
167
+ format ! ( "{}/{}/{}" , self . base_url, self . bucket_name, path)
168
+ }
169
+ }
170
+
133
171
/// A builder for a put request allowing customisation of the headers and query string
134
172
pub struct PutRequest < ' a > {
135
173
path : & ' a Path ,
@@ -163,6 +201,21 @@ impl<'a> PutRequest<'a> {
163
201
}
164
202
}
165
203
204
+ /// Sign Blob Request Body
205
+ #[ derive( Debug , Serialize ) ]
206
+ struct SignBlobBody {
207
+ /// The payload to sign
208
+ payload : String ,
209
+ }
210
+
211
+ /// Sign Blob Response
212
+ #[ derive( Deserialize ) ]
213
+ #[ serde( rename_all = "camelCase" ) ]
214
+ struct SignBlobResponse {
215
+ /// The signature for the payload
216
+ signed_blob : String ,
217
+ }
218
+
166
219
#[ derive( Debug ) ]
167
220
pub struct GoogleCloudStorageClient {
168
221
config : GoogleCloudStorageConfig ,
@@ -197,6 +250,54 @@ impl GoogleCloudStorageClient {
197
250
self . config . credentials . get_credential ( ) . await
198
251
}
199
252
253
+ /// Create a signature from a string-to-sign using Google Cloud signBlob method.
254
+ /// form like:
255
+ /// ```plaintext
256
+ /// curl -X POST --data-binary @JSON_FILE_NAME \
257
+ /// -H "Authorization: Bearer OAUTH2_TOKEN" \
258
+ /// -H "Content-Type: application/json" \
259
+ /// "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/SERVICE_ACCOUNT_EMAIL:signBlob"
260
+ /// ```
261
+ ///
262
+ /// 'JSON_FILE_NAME' is a file containing the following JSON object:
263
+ /// ```plaintext
264
+ /// {
265
+ /// "payload": "REQUEST_INFORMATION"
266
+ /// }
267
+ /// ```
268
+ pub async fn sign_blob ( & self , string_to_sign : & str , client_email : & str ) -> Result < String > {
269
+ let credential = self . get_credential ( ) . await ?;
270
+ let body = SignBlobBody {
271
+ payload : BASE64_STANDARD . encode ( string_to_sign) ,
272
+ } ;
273
+
274
+ let url = format ! (
275
+ "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/{}:signBlob" ,
276
+ client_email
277
+ ) ;
278
+
279
+ let response = self
280
+ . client
281
+ . post ( & url)
282
+ . bearer_auth ( & credential. bearer )
283
+ . json ( & body)
284
+ . send_retry ( & self . config . retry_config )
285
+ . await
286
+ . context ( SignBlobRequestSnafu ) ?;
287
+
288
+ //If successful, the signature is returned in the signedBlob field in the response.
289
+ let response = response
290
+ . json :: < SignBlobResponse > ( )
291
+ . await
292
+ . context ( InvalidSignBlobResponseSnafu ) ?;
293
+
294
+ let signed_blob = BASE64_STANDARD
295
+ . decode ( response. signed_blob )
296
+ . context ( InvalidSignBlobSignatureSnafu ) ?;
297
+
298
+ Ok ( hex_encode ( & signed_blob) )
299
+ }
300
+
200
301
pub fn object_url ( & self , path : & Path ) -> String {
201
302
let encoded = utf8_percent_encode ( path. as_ref ( ) , NON_ALPHANUMERIC ) ;
202
303
format ! (
0 commit comments