1
1
use std:: path:: { Path , PathBuf } ;
2
2
3
- use serde:: { Deserialize , Serialize } ;
3
+ use serde:: Deserialize ;
4
4
use tokio:: fs;
5
5
6
6
use crate :: {
7
7
authentication_manager:: ServiceAccount ,
8
8
custom_service_account:: ApplicationCredentials ,
9
9
default_authorized_user:: { ConfigDefaultCredentials , UserCredentials } ,
10
+ service_account_impersonation:: ImpersonatedServiceAccount ,
10
11
types:: HyperClient ,
11
12
CustomServiceAccount , Error ,
12
13
} ;
@@ -15,7 +16,7 @@ use crate::{
15
16
// https://github.com/golang/oauth2/blob/a835fc4358f6852f50c4c5c33fddcd1adade5b0a/google/google.go#L158
16
17
// Currently not implementing external account credentials
17
18
// Currently not implementing impersonating service accounts (coming soon !)
18
- #[ derive( Serialize , Deserialize , Debug ) ]
19
+ #[ derive( Deserialize , Debug ) ]
19
20
#[ serde( tag = "type" , rename_all = "snake_case" ) ]
20
21
pub ( crate ) enum FlexibleCredentialSource {
21
22
// This credential parses the `key.json` file created when running
@@ -24,6 +25,9 @@ pub(crate) enum FlexibleCredentialSource {
24
25
// This credential parses the `~/.config/gcloud/application_default_credentials.json` file
25
26
// created when running `gcloud auth application-default login`
26
27
AuthorizedUser ( UserCredentials ) ,
28
+ // This credential parses the `~/.config/gcloud/application_default_credentials.json` file
29
+ // created when running `gcloud auth application-default login --impersonate-service-account <service account>`
30
+ ImpersonatedServiceAccount ( ImpersonatedServiceAccountCredentials ) ,
27
31
}
28
32
29
33
impl FlexibleCredentialSource {
@@ -62,6 +66,30 @@ impl FlexibleCredentialSource {
62
66
ConfigDefaultCredentials :: from_user_credentials ( creds, client) . await ?;
63
67
Ok ( Box :: new ( service_account) )
64
68
}
69
+ FlexibleCredentialSource :: ImpersonatedServiceAccount ( creds) => {
70
+ let source_creds: Box < dyn ServiceAccount > = match * creds. source_credentials {
71
+ FlexibleCredentialSource :: AuthorizedUser ( creds) => {
72
+ let service_account =
73
+ ConfigDefaultCredentials :: from_user_credentials ( creds, client) . await ?;
74
+ Box :: new ( service_account)
75
+ }
76
+ FlexibleCredentialSource :: ServiceAccount ( creds) => {
77
+ let service_account = CustomServiceAccount :: new ( creds) ?;
78
+ Box :: new ( service_account)
79
+ }
80
+ FlexibleCredentialSource :: ImpersonatedServiceAccount ( _) => {
81
+ return Err ( Error :: NestedImpersonation )
82
+ }
83
+ } ;
84
+
85
+ let service_account = ImpersonatedServiceAccount :: new (
86
+ source_creds,
87
+ creds. service_account_impersonation_url ,
88
+ creds. delegates ,
89
+ ) ;
90
+
91
+ Ok ( Box :: new ( service_account) )
92
+ }
65
93
}
66
94
}
67
95
@@ -76,6 +104,17 @@ impl FlexibleCredentialSource {
76
104
}
77
105
}
78
106
107
+ // This credential uses the `source_credentials` to get a token
108
+ // and then uses that token to get a token impersonating the service
109
+ // account specified by `service_account_impersonation_url`.
110
+ // refresh logic https://github.com/golang/oauth2/blob/a835fc4358f6852f50c4c5c33fddcd1adade5b0a/google/internal/externalaccount/impersonate.go#L57
111
+ #[ derive( Deserialize , Debug ) ]
112
+ pub ( crate ) struct ImpersonatedServiceAccountCredentials {
113
+ service_account_impersonation_url : String ,
114
+ source_credentials : Box < FlexibleCredentialSource > ,
115
+ delegates : Vec < String > ,
116
+ }
117
+
79
118
#[ cfg( test) ]
80
119
mod tests {
81
120
use crate :: { flexible_credential_source:: FlexibleCredentialSource , types} ;
@@ -192,4 +231,67 @@ mod tests {
192
231
) ;
193
232
}
194
233
}
234
+
235
+ #[ tokio:: test]
236
+ async fn test_parse_impersonating_service_account ( ) {
237
+ let impersonate_from_user_creds = r#"{
238
+ "delegates": [],
239
+ "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test_account@test_project.iam.gserviceaccount.com:generateAccessToken",
240
+ "source_credentials": {
241
+ "client_id": "***id***.apps.googleusercontent.com",
242
+ "client_secret": "***secret***",
243
+ "refresh_token": "***refresh***",
244
+ "type": "authorized_user",
245
+ "quota_project_id": "test_project"
246
+ },
247
+ "type": "impersonated_service_account"
248
+ }"# ;
249
+
250
+ let cred_source: FlexibleCredentialSource =
251
+ serde_json:: from_str ( impersonate_from_user_creds) . expect ( "Valid creds to parse" ) ;
252
+
253
+ assert ! ( matches!(
254
+ cred_source,
255
+ FlexibleCredentialSource :: ImpersonatedServiceAccount ( _)
256
+ ) ) ;
257
+
258
+ let impersonate_from_service_key = r#"{
259
+ "delegates": [],
260
+ "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test_account@test_project.iam.gserviceaccount.com:generateAccessToken",
261
+ "source_credentials": {
262
+ "private_key_id": "268f54e43a1af97cfc71731688434f45aca15c8b",
263
+ "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC5M5y3WwsRk8NX\npF9fKaZukNspot9Ecmk1PAkupcHLKVhalwPxU4sMNWXgM9H2LTWSvvyOT//rDQpn\n3SGYri/lMhzb4lI8h10E7k6zyFQUPujxkXFBkMOzhIDUgtiiht0WvIw6M8nbaPqI\nxn/aYmPsFhvJfKCthYAt2UUz+D3enI9QjCuhic8iSMnvKT8m0QkOG2eALYGUaLF1\ngRkbV4BiBUGZfXfNEBdux3Wf4kNUau32LA0XotomlvNvf1oH77v5Hc1R/KMMIk5F\nJWVBuAr4jwkN9hwtOozpJ/52wSpddxsZuj+0nP1a3f0UyvrmMnuwszardPK39BoH\nJ+5+HZM3AgMBAAECggEADrHZrXK73hkrVrjkGFjlq8Ayo4sYzAWH84Ff+SONzODq\n8cUpuuw2DDHwc2mpLy9HIO2mfGQ8mhneyX7yO3sWscjYIVpDzCmxZ8LA2+L5SOH0\n+bXglqM14/iPgE0hg0PQJw2u0q9pRM9/kXquilVkOEdIzSPmW95L3Vdv9j+sKQ2A\nOL23l4dsaG4+i1lWRBKiGsLh1kB9FRnm4BzcOxd3WGooy7L1/jo9BoYRss1YABls\nmmyZ9f7r28zjclhpOBkE3OXX0zNbp4yIu1O1Bt9X2p87EOuYqlFA5eEvDbiTPZbk\n6wKEX3BPUkeIo8OaGvsGhHCWx0lv/sDPw/UofycOgQKBgQD4BD059aXEV13Byc5D\nh8LQSejjeM/Vx+YeCFI66biaIOvUs+unyxkH+qxXTuW6AgOgcvrJo93xkyAZ9SeR\nc6Vj9g5mZ5vqSJz5Hg8h8iZBAYtf40qWq0pHcmUIm2Z9LvrG5ZFHU5EEcCtLyBVS\nAv+pLLLf3OsAkJuuqTAgygBbOwKBgQC/KcBa9sUg2u9qIpq020UOW/n4KFWhSJ8h\ngXqqmjOnPqmDc5AnYg1ZdYdqSSgdiK8lJpRL/S2UjYUQp3H+56z0eK/b1iKM51n+\n6D80nIxWeKJ+n7VKI7cBXwc/KokaXgkz0It2UEZSlhPUMImnYcOvGIZ7cMr3Q6mf\n6FwD15UQNQKBgQDyAsDz454DvvS/+noJL1qMAPL9tI+pncwQljIXRqVZ0LIO9hoH\nu4kLXjH5aAWGwhxj3o6VYA9cgSIb8jrQFbbXmexnRMbBkGWMOSavCykE2cr0oEfS\nSgbLPPcVtP4HPWZ72tsubH7fg8zbv7v+MOrkW7eX9mxiOrmPb4yFElfSrQKBgA7y\nMLvr91WuSHG/6uChFDEfN9gTLz7A8tAn03NrQwace5xveKHbpLeN3NyOg7hra2Y4\nMfgO/3VR60l2Dg+kBX3HwdgqUeE6ZWrstaRjaQWJwQqtafs196T/zQ0/QiDxoT6P\n25eQhy8F1N8OPHT9y9Lw0/LqyrOycpyyCh+yx1DRAoGAJ/6dlhyQnwSfMAe3mfRC\noiBQG6FkyoeXHHYcoQ/0cSzwp0BwBlar1Z28P7KTGcUNqV+YfK9nF47eoLaTLCmG\nG5du0Ds6m2Eg0sOBBqXHnw6R1PC878tgT/XokNxIsVlF5qRz88q7Rn0J1lzB7+Tl\n2HSAcyIUcmr0gxlhRmC2Jq4=\n-----END PRIVATE KEY-----\n",
264
+ "client_email": "[email protected] ",
265
+ "client_id": "gopher.apps.googleusercontent.com",
266
+ "token_uri": "https://accounts.google.com/o/gophers/token",
267
+ "type": "service_account",
268
+ "audience": "https://testservice.googleapis.com/",
269
+ "project_id": "test_project"
270
+ },
271
+ "type": "impersonated_service_account"
272
+ }"# ;
273
+
274
+ let cred_source: FlexibleCredentialSource =
275
+ serde_json:: from_str ( impersonate_from_service_key) . expect ( "Valid creds to parse" ) ;
276
+
277
+ assert ! ( matches!(
278
+ cred_source,
279
+ FlexibleCredentialSource :: ImpersonatedServiceAccount ( _)
280
+ ) ) ;
281
+
282
+ let client = types:: client ( ) ;
283
+ let creds = cred_source
284
+ . try_into_service_account ( & client)
285
+ . await
286
+ . expect ( "Valid creds to parse" ) ;
287
+
288
+ assert_eq ! (
289
+ creds
290
+ . project_id( & client)
291
+ . await
292
+ . expect( "Project ID to be present" ) ,
293
+ "test_project" . to_string( ) ,
294
+ "Project ID should be parsed"
295
+ ) ;
296
+ }
195
297
}
0 commit comments