1use reqwest::Response;
2use std::sync::Arc;
3use token_source::TokenSource;
4
5use crate::http::Error;
6
7#[derive(Clone)]
8pub struct ServiceAccountClient {
9 ts: Option<Arc<dyn TokenSource>>,
10 v1_endpoint: String,
11 http: reqwest_middleware::ClientWithMiddleware,
12}
13
14impl ServiceAccountClient {
15 pub(crate) fn new(
16 ts: Option<Arc<dyn TokenSource>>,
17 endpoint: &str,
18 http: reqwest_middleware::ClientWithMiddleware,
19 ) -> Self {
20 Self {
21 ts,
22 v1_endpoint: format!("{endpoint}/v1"),
23 http,
24 }
25 }
26
27 #[cfg_attr(feature = "trace", tracing::instrument(skip_all))]
28 pub async fn sign_blob(&self, name: &str, payload: &[u8]) -> Result<Vec<u8>, Error> {
29 let url = format!("{}/{}:signBlob", self.v1_endpoint, name);
30 let request = SignBlobRequest { payload };
31 let request = self
32 .http
33 .post(url)
34 .json(&request)
35 .header("X-Goog-Api-Client", "rust")
36 .header(reqwest::header::USER_AGENT, "google-cloud-storage");
37 let request = match &self.ts {
38 Some(ts) => {
39 let token = ts.token().await.map_err(Error::TokenSource)?;
40 request.header(reqwest::header::AUTHORIZATION, token)
41 }
42 None => request,
43 };
44 let response = request.send().await?;
45 let response = ServiceAccountClient::check_response_status(response).await?;
46 Ok(response.json::<SignBlobResponse>().await?.signed_blob)
47 }
48
49 async fn check_response_status(response: Response) -> Result<Response, Error> {
51 match response.error_for_status_ref() {
53 Ok(_) => Ok(response),
54 Err(error) => match response.text().await {
55 Ok(raw) => Err(Error::RawResponse(error, raw)),
56 Err(_) => Err(Error::HttpClient(error)),
57 },
58 }
59 }
60}
61
62#[derive(serde::Serialize)]
63struct SignBlobRequest<'a> {
64 #[serde(with = "super::base64")]
65 payload: &'a [u8],
66}
67
68#[derive(serde::Deserialize)]
69#[serde(rename_all = "camelCase")]
70struct SignBlobResponse {
71 #[serde(with = "super::base64")]
72 signed_blob: Vec<u8>,
73}
74
75#[cfg(test)]
76mod test {
77 use reqwest::Client;
78 use reqwest_middleware::ClientBuilder;
79 use serial_test::serial;
80
81 use google_cloud_auth::project::Config;
82 use google_cloud_auth::token::DefaultTokenSourceProvider;
83 use token_source::TokenSourceProvider;
84
85 use crate::http::service_account_client::ServiceAccountClient;
86
87 async fn client() -> (ServiceAccountClient, String) {
88 let tsp = DefaultTokenSourceProvider::new(
89 Config::default().with_scopes(&["https://www.googleapis.com/auth/cloud-platform"]),
90 )
91 .await
92 .unwrap();
93 let email = tsp.source_credentials.clone().unwrap().client_email.unwrap();
94 let ts = tsp.token_source();
95 (
96 ServiceAccountClient::new(
97 Some(ts),
98 "https://iamcredentials.googleapis.com",
99 ClientBuilder::new(Client::default()).build(),
100 ),
101 email,
102 )
103 }
104
105 #[tokio::test]
107 #[serial]
108 pub async fn sign_blob_test() {
109 let (client, email) = client().await;
110 let body = vec![
111 71, 79, 79, 71, 52, 45, 82, 83, 65, 45, 83, 72, 65, 50, 53, 54, 10, 50, 48, 50, 50, 48, 55, 48, 57, 84, 50,
112 51, 52, 56, 48, 56, 90, 10, 50, 48, 50, 50, 48, 55, 48, 57, 47, 97, 117, 116, 111, 47, 115, 116, 111, 114,
113 97, 103, 101, 47, 103, 111, 111, 103, 52, 95, 114, 101, 113, 117, 101, 115, 116, 10, 98, 101, 97, 48, 48,
114 49, 100, 98, 48, 50, 97, 56, 98, 55, 101, 101, 54, 50, 102, 50, 54, 53, 99, 101, 50, 52, 54, 53, 51, 49,
115 97, 98, 50, 54, 101, 102, 49, 97, 48, 97, 99, 100, 102, 102, 55, 99, 54, 55, 49, 100, 101, 56, 49, 100, 56,
116 56, 98, 50, 56, 101, 55, 48, 98, 101,
117 ];
118 let data = client
119 .sign_blob(&format!("projects/-/serviceAccounts/{email}"), &body)
120 .await
121 .unwrap();
122 assert_eq!(256, data.len());
123 }
124}