Skip to content

Commit 14c9d11

Browse files
feat: add data_source_google_service_account_jwt (#6239) (#12107)
Signed-off-by: Modular Magician <[email protected]>
1 parent 182ace8 commit 14c9d11

6 files changed

+247
-0
lines changed

.changelog/6239.txt

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
```release-note:new-datasource
2+
`google_service_account_jwt`
3+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package google
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
7+
iamcredentials "google.golang.org/api/iamcredentials/v1"
8+
9+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
10+
)
11+
12+
func dataSourceGoogleServiceAccountJwt() *schema.Resource {
13+
return &schema.Resource{
14+
Read: dataSourceGoogleServiceAccountJwtRead,
15+
Schema: map[string]*schema.Schema{
16+
"payload": {
17+
Type: schema.TypeString,
18+
Required: true,
19+
Description: `A JSON-encoded JWT claims set that will be included in the signed JWT.`,
20+
},
21+
"target_service_account": {
22+
Type: schema.TypeString,
23+
Required: true,
24+
ValidateFunc: validateRegexp("(" + strings.Join(PossibleServiceAccountNames, "|") + ")"),
25+
},
26+
"delegates": {
27+
Type: schema.TypeSet,
28+
Optional: true,
29+
Elem: &schema.Schema{
30+
Type: schema.TypeString,
31+
ValidateFunc: validateRegexp(ServiceAccountLinkRegex),
32+
},
33+
},
34+
"jwt": {
35+
Type: schema.TypeString,
36+
Sensitive: true,
37+
Computed: true,
38+
},
39+
},
40+
}
41+
}
42+
43+
func dataSourceGoogleServiceAccountJwtRead(d *schema.ResourceData, meta interface{}) error {
44+
config := meta.(*Config)
45+
46+
userAgent, err := generateUserAgentString(d, config.userAgent)
47+
48+
if err != nil {
49+
return err
50+
}
51+
52+
name := fmt.Sprintf("projects/-/serviceAccounts/%s", d.Get("target_service_account").(string))
53+
54+
jwtRequest := &iamcredentials.SignJwtRequest{
55+
Payload: d.Get("payload").(string),
56+
Delegates: convertStringSet(d.Get("delegates").(*schema.Set)),
57+
}
58+
59+
service := config.NewIamCredentialsClient(userAgent)
60+
61+
jwtResponse, err := service.Projects.ServiceAccounts.SignJwt(name, jwtRequest).Do()
62+
63+
if err != nil {
64+
return fmt.Errorf("error calling iamcredentials.SignJwt: %w", err)
65+
}
66+
67+
d.SetId(name)
68+
69+
if err := d.Set("jwt", jwtResponse.SignedJwt); err != nil {
70+
return fmt.Errorf("error setting jwt attribute: %w", err)
71+
}
72+
73+
return nil
74+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package google
2+
3+
import (
4+
"bytes"
5+
"encoding/base64"
6+
"encoding/json"
7+
"errors"
8+
"strings"
9+
"testing"
10+
11+
"fmt"
12+
13+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
14+
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
15+
)
16+
17+
const (
18+
jwtTestSubject = "custom-subject"
19+
jwtTestFoo = "bar"
20+
jwtTestComplexFooNested = "baz"
21+
)
22+
23+
type jwtTestPayload struct {
24+
Subject string `json:"sub"`
25+
26+
Foo string `json:"foo"`
27+
28+
ComplexFoo struct {
29+
Nested string `json:"nested"`
30+
} `json:"complexFoo"`
31+
}
32+
33+
func testAccCheckServiceAccountJwtValue(name, audience string) resource.TestCheckFunc {
34+
return func(s *terraform.State) error {
35+
ms := s.RootModule()
36+
37+
rs, ok := ms.Resources[name]
38+
39+
if !ok {
40+
return fmt.Errorf("can't find %s in state", name)
41+
}
42+
43+
jwtString, ok := rs.Primary.Attributes["jwt"]
44+
45+
if !ok {
46+
return fmt.Errorf("jwt not found")
47+
}
48+
49+
jwtParts := strings.Split(jwtString, ".")
50+
51+
if len(jwtParts) != 3 {
52+
return errors.New("jwt does not appear well-formed")
53+
}
54+
55+
decoded, err := base64.RawURLEncoding.DecodeString(jwtParts[1])
56+
57+
if err != nil {
58+
return fmt.Errorf("could not base64 decode jwt body: %w", err)
59+
}
60+
61+
var payload jwtTestPayload
62+
63+
err = json.NewDecoder(bytes.NewBuffer(decoded)).Decode(&payload)
64+
65+
if err != nil {
66+
return fmt.Errorf("could not decode jwt payload: %w", err)
67+
}
68+
69+
if payload.Subject != jwtTestSubject {
70+
return fmt.Errorf("invalid 'sub', expected '%s', got '%s'", jwtTestSubject, payload.Subject)
71+
}
72+
73+
if payload.Foo != jwtTestFoo {
74+
return fmt.Errorf("invalid 'foo', expected '%s', got '%s'", jwtTestFoo, payload.Foo)
75+
}
76+
77+
if payload.ComplexFoo.Nested != jwtTestComplexFooNested {
78+
return fmt.Errorf("invalid 'foo', expected '%s', got '%s'", jwtTestComplexFooNested, payload.ComplexFoo.Nested)
79+
}
80+
81+
return nil
82+
}
83+
}
84+
85+
func TestAccDataSourceGoogleServiceAccountJwt(t *testing.T) {
86+
t.Parallel()
87+
88+
resourceName := "data.google_service_account_jwt.default"
89+
serviceAccount := getTestServiceAccountFromEnv(t)
90+
targetServiceAccountEmail := BootstrapServiceAccount(t, getTestProjectFromEnv(), serviceAccount)
91+
92+
resource.Test(t, resource.TestCase{
93+
PreCheck: func() { testAccPreCheck(t) },
94+
Providers: testAccProviders,
95+
Steps: []resource.TestStep{
96+
{
97+
Config: testAccCheckGoogleServiceAccountJwt(targetServiceAccountEmail),
98+
Check: resource.ComposeTestCheckFunc(
99+
testAccCheckServiceAccountJwtValue(resourceName, targetAudience),
100+
),
101+
},
102+
},
103+
})
104+
}
105+
106+
func testAccCheckGoogleServiceAccountJwt(targetServiceAccount string) string {
107+
return fmt.Sprintf(`
108+
data "google_service_account_jwt" "default" {
109+
target_service_account = "%s"
110+
111+
payload = jsonencode({
112+
sub: "%s",
113+
foo: "%s",
114+
complexFoo: {
115+
nested: "%s"
116+
}
117+
})
118+
}
119+
`, targetServiceAccount, jwtTestSubject, jwtTestFoo, jwtTestComplexFooNested)
120+
}

google/provider.go

+1
Original file line numberDiff line numberDiff line change
@@ -840,6 +840,7 @@ func Provider() *schema.Provider {
840840
"google_service_account": dataSourceGoogleServiceAccount(),
841841
"google_service_account_access_token": dataSourceGoogleServiceAccountAccessToken(),
842842
"google_service_account_id_token": dataSourceGoogleServiceAccountIdToken(),
843+
"google_service_account_jwt": dataSourceGoogleServiceAccountJwt(),
843844
"google_service_account_key": dataSourceGoogleServiceAccountKey(),
844845
"google_sourcerepo_repository": dataSourceGoogleSourceRepoRepository(),
845846
"google_spanner_instance": dataSourceSpannerInstance(),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
---
2+
subcategory: "Cloud Platform"
3+
layout: "google"
4+
page_title: "Google: google_service_account_jwt"
5+
sidebar_current: "docs-google-service-account-jwt"
6+
description: |-
7+
Produces an arbitrary self-signed JWT for service accounts
8+
---
9+
10+
# google\_service\_account\_jwt
11+
12+
This data source provides a [self-signed JWT](https://cloud.google.com/iam/docs/create-short-lived-credentials-direct#sa-credentials-jwt). Tokens issued from this data source are typically used to call external services that accept JWTs for authentication.
13+
14+
## Example Usage
15+
16+
Note: in order to use the following, the caller must have _at least_ `roles/iam.serviceAccountTokenCreator` on the `target_service_account`.
17+
18+
```hcl
19+
data "google_service_account_jwt" "foo" {
20+
target_service_account = "[email protected]"
21+
22+
payload = jsonencode({
23+
foo: "bar",
24+
sub: "subject",
25+
})
26+
}
27+
28+
output "jwt" {
29+
value = data.google_service_account_jwt.foo.jwt
30+
}
31+
```
32+
33+
## Argument Reference
34+
35+
The following arguments are supported:
36+
37+
* `target_service_account` (Required) - The email of the service account that will sign the JWT.
38+
* `payload` (Required) - The JSON-encoded JWT claims set to include in the self-signed JWT.
39+
* `delegates` (Optional) - Delegate chain of approvals needed to perform full impersonation. Specify the fully qualified service account name.
40+
41+
## Attributes Reference
42+
43+
The following attribute is exported:
44+
45+
* `jwt` - The signed JWT containing the JWT Claims Set from the `payload`.

website/google.erb

+4
Original file line numberDiff line numberDiff line change
@@ -1313,6 +1313,10 @@
13131313
<a href="/docs/providers/google/d/service_account_id_token.html">google_service_account_id_token</a>
13141314
</li>
13151315

1316+
<li>
1317+
<a href="/docs/providers/google/d/service_account_jwt.html">google_service_account_jwt</a>
1318+
</li>
1319+
13161320
<li>
13171321
<a href="/docs/providers/google/d/service_account_key.html">google_service_account_key</a>
13181322
</li>

0 commit comments

Comments
 (0)