1#![cfg_attr(docsrs, feature(doc_cfg))]
2
3use faucet_core::FaucetError;
7use google_cloud_storage::client::{Storage, StorageControl};
8use schemars::JsonSchema;
9use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
18#[serde(tag = "type", content = "config", rename_all = "snake_case")]
19pub enum GcsCredentials {
20 ServiceAccountJsonFile { path: String },
22 ServiceAccountJsonInline { json: String },
26 #[default]
30 ApplicationDefault,
31 Anonymous,
36}
37
38pub async fn build_credentials(
41 creds: &GcsCredentials,
42) -> Result<google_cloud_auth::credentials::Credentials, FaucetError> {
43 match creds {
44 GcsCredentials::ApplicationDefault => google_cloud_auth::credentials::Builder::default()
45 .build()
46 .map_err(|e| FaucetError::Auth(format!("GCS auth (ADC): {e}"))),
47 GcsCredentials::Anonymous => {
48 Ok(google_cloud_auth::credentials::anonymous::Builder::new().build())
49 }
50 GcsCredentials::ServiceAccountJsonFile { path } => {
51 let bytes = tokio::fs::read(path).await.map_err(|e| {
52 FaucetError::Auth(format!(
53 "GCS auth: could not read service-account key from '{path}': {e}"
54 ))
55 })?;
56 let value: serde_json::Value = serde_json::from_slice(&bytes).map_err(|e| {
57 FaucetError::Auth(format!(
58 "GCS auth: service-account key at '{path}' is not valid JSON: {e}"
59 ))
60 })?;
61 google_cloud_auth::credentials::service_account::Builder::new(value)
62 .build()
63 .map_err(|e| FaucetError::Auth(format!("GCS auth (service account): {e}")))
64 }
65 GcsCredentials::ServiceAccountJsonInline { json } => {
66 let value: serde_json::Value = serde_json::from_str(json).map_err(|e| {
67 FaucetError::Auth(format!(
68 "GCS auth: inline service-account key is not valid JSON: {e}"
69 ))
70 })?;
71 google_cloud_auth::credentials::service_account::Builder::new(value)
72 .build()
73 .map_err(|e| FaucetError::Auth(format!("GCS auth (service account): {e}")))
74 }
75 }
76}
77
78pub async fn build_storage(
82 creds: &GcsCredentials,
83 storage_host: Option<&str>,
84) -> Result<Storage, FaucetError> {
85 let credentials = build_credentials(creds).await?;
86 let mut builder = Storage::builder().with_credentials(credentials);
87 if let Some(host) = storage_host {
88 builder = builder.with_endpoint(host.to_string());
89 }
90 builder
91 .build()
92 .await
93 .map_err(|e| FaucetError::Auth(format!("GCS client build failed: {e}")))
94}
95
96pub async fn build_storage_control(
98 creds: &GcsCredentials,
99 storage_host: Option<&str>,
100) -> Result<StorageControl, FaucetError> {
101 let credentials = build_credentials(creds).await?;
102 let mut builder = StorageControl::builder().with_credentials(credentials);
103 if let Some(host) = storage_host {
104 builder = builder.with_endpoint(host.to_string());
105 }
106 builder
107 .build()
108 .await
109 .map_err(|e| FaucetError::Auth(format!("GCS control client build failed: {e}")))
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115 use serde_json::json;
116
117 #[test]
118 fn credentials_serde_application_default() {
119 let creds = GcsCredentials::ApplicationDefault;
120 let v = serde_json::to_value(&creds).unwrap();
121 assert_eq!(v, json!({"type": "application_default"}));
122 let back: GcsCredentials = serde_json::from_value(v).unwrap();
123 assert!(matches!(back, GcsCredentials::ApplicationDefault));
124 }
125
126 #[test]
127 fn credentials_serde_service_account_json_file() {
128 let creds = GcsCredentials::ServiceAccountJsonFile {
129 path: "/run/secrets/sa.json".into(),
130 };
131 let v = serde_json::to_value(&creds).unwrap();
132 assert_eq!(
133 v,
134 json!({"type": "service_account_json_file", "config": {"path": "/run/secrets/sa.json"}})
135 );
136 let back: GcsCredentials = serde_json::from_value(v).unwrap();
137 assert!(
138 matches!(back, GcsCredentials::ServiceAccountJsonFile { path } if path == "/run/secrets/sa.json")
139 );
140 }
141
142 #[test]
143 fn credentials_serde_service_account_json_inline() {
144 let creds = GcsCredentials::ServiceAccountJsonInline {
145 json: "{\"client_email\":\"x@y\"}".into(),
146 };
147 let v = serde_json::to_value(&creds).unwrap();
148 assert_eq!(v["type"], "service_account_json_inline");
149 assert!(
150 v["config"]["json"]
151 .as_str()
152 .unwrap()
153 .contains("client_email")
154 );
155 let back: GcsCredentials = serde_json::from_value(v).unwrap();
156 assert!(matches!(
157 back,
158 GcsCredentials::ServiceAccountJsonInline { .. }
159 ));
160 }
161
162 #[test]
163 fn credentials_default_is_application_default() {
164 let creds = GcsCredentials::default();
165 assert!(matches!(creds, GcsCredentials::ApplicationDefault));
166 }
167
168 #[test]
169 fn credentials_serde_anonymous() {
170 let creds = GcsCredentials::Anonymous;
171 let v = serde_json::to_value(&creds).unwrap();
172 assert_eq!(v, json!({"type": "anonymous"}));
173 let back: GcsCredentials = serde_json::from_value(v).unwrap();
174 assert!(matches!(back, GcsCredentials::Anonymous));
175 }
176
177 #[tokio::test]
178 async fn build_credentials_anonymous_succeeds() {
179 let creds = build_credentials(&GcsCredentials::Anonymous).await.unwrap();
180 let _ = creds;
185 }
186
187 #[tokio::test]
188 async fn build_credentials_rejects_missing_file() {
189 let creds = GcsCredentials::ServiceAccountJsonFile {
190 path: "/definitely/does/not/exist/sa.json".into(),
191 };
192 let err = build_credentials(&creds).await.unwrap_err();
193 assert!(matches!(err, FaucetError::Auth(_)));
194 let msg = err.to_string();
195 assert!(
196 msg.contains("could not read") || msg.contains("No such file"),
197 "unexpected error: {msg}"
198 );
199 }
200
201 #[tokio::test]
202 async fn build_credentials_rejects_invalid_inline_json() {
203 let creds = GcsCredentials::ServiceAccountJsonInline {
204 json: "not-json".into(),
205 };
206 let err = build_credentials(&creds).await.unwrap_err();
207 assert!(matches!(err, FaucetError::Auth(_)));
208 }
209}