Skip to content

Commit b1c6428

Browse files
BBBmauSarahFrench
andauthored
ephemeral: add ephemeral_google_service_account_jwt (#12142)
Co-authored-by: Sarah French <[email protected]>
1 parent 89b1a01 commit b1c6428

File tree

4 files changed

+291
-0
lines changed

4 files changed

+291
-0
lines changed

mmv1/third_party/terraform/fwprovider/framework_provider.go.tmpl

+1
Original file line numberDiff line numberDiff line change
@@ -311,5 +311,6 @@ func (p *FrameworkProvider) EphemeralResources(_ context.Context) []func() ephem
311311
return []func() ephemeral.EphemeralResource{
312312
resourcemanager.GoogleEphemeralServiceAccountAccessToken,
313313
resourcemanager.GoogleEphemeralServiceAccountIdToken,
314+
resourcemanager.GoogleEphemeralServiceAccountJwt,
314315
}
315316
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
package resourcemanager
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"time"
8+
9+
"github.com/hashicorp/terraform-plugin-framework-validators/int64validator"
10+
"github.com/hashicorp/terraform-plugin-framework-validators/setvalidator"
11+
"github.com/hashicorp/terraform-plugin-framework/ephemeral"
12+
"github.com/hashicorp/terraform-plugin-framework/ephemeral/schema"
13+
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
14+
"github.com/hashicorp/terraform-plugin-framework/types"
15+
"github.com/hashicorp/terraform-provider-google/google/fwtransport"
16+
"github.com/hashicorp/terraform-provider-google/google/fwutils"
17+
"github.com/hashicorp/terraform-provider-google/google/fwvalidators"
18+
"google.golang.org/api/iamcredentials/v1"
19+
)
20+
21+
var _ ephemeral.EphemeralResource = &googleEphemeralServiceAccountJwt{}
22+
23+
func GoogleEphemeralServiceAccountJwt() ephemeral.EphemeralResource {
24+
return &googleEphemeralServiceAccountJwt{}
25+
}
26+
27+
type googleEphemeralServiceAccountJwt struct {
28+
providerConfig *fwtransport.FrameworkProviderConfig
29+
}
30+
31+
func (p *googleEphemeralServiceAccountJwt) Metadata(ctx context.Context, req ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) {
32+
resp.TypeName = req.ProviderTypeName + "_service_account_jwt"
33+
}
34+
35+
type ephemeralServiceAccountJwtModel struct {
36+
Payload types.String `tfsdk:"payload"`
37+
ExpiresIn types.Int64 `tfsdk:"expires_in"`
38+
TargetServiceAccount types.String `tfsdk:"target_service_account"`
39+
Delegates types.Set `tfsdk:"delegates"`
40+
Jwt types.String `tfsdk:"jwt"`
41+
}
42+
43+
func (p *googleEphemeralServiceAccountJwt) Schema(ctx context.Context, req ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) {
44+
resp.Schema = schema.Schema{
45+
Description: "Produces an arbitrary self-signed JWT for service accounts.",
46+
Attributes: map[string]schema.Attribute{
47+
"payload": schema.StringAttribute{
48+
Required: true,
49+
Description: `A JSON-encoded JWT claims set that will be included in the signed JWT.`,
50+
},
51+
"expires_in": schema.Int64Attribute{
52+
Optional: true,
53+
Description: "Number of seconds until the JWT expires. If set and non-zero an `exp` claim will be added to the payload derived from the current timestamp plus expires_in seconds.",
54+
Validators: []validator.Int64{
55+
int64validator.AtLeast(1), // Must be greater than 0
56+
},
57+
},
58+
"target_service_account": schema.StringAttribute{
59+
Description: "The email of the service account that will sign the JWT.",
60+
Required: true,
61+
Validators: []validator.String{
62+
fwvalidators.ServiceAccountEmailValidator{},
63+
},
64+
},
65+
"delegates": schema.SetAttribute{
66+
Description: "Delegate chain of approvals needed to perform full impersonation. Specify the fully qualified service account name.",
67+
Optional: true,
68+
ElementType: types.StringType,
69+
Validators: []validator.Set{
70+
setvalidator.ValueStringsAre(fwvalidators.ServiceAccountEmailValidator{}),
71+
},
72+
},
73+
"jwt": schema.StringAttribute{
74+
Description: "The signed JWT containing the JWT Claims Set from the `payload`.",
75+
Computed: true,
76+
Sensitive: true,
77+
},
78+
},
79+
}
80+
}
81+
82+
func (p *googleEphemeralServiceAccountJwt) Configure(ctx context.Context, req ephemeral.ConfigureRequest, resp *ephemeral.ConfigureResponse) {
83+
if req.ProviderData == nil {
84+
return
85+
}
86+
87+
pd, ok := req.ProviderData.(*fwtransport.FrameworkProviderConfig)
88+
if !ok {
89+
resp.Diagnostics.AddError(
90+
"Unexpected Data Source Configure Type",
91+
fmt.Sprintf("Expected *fwtransport.FrameworkProviderConfig, got: %T. Please report this issue to the provider developers.", req.ProviderData),
92+
)
93+
return
94+
}
95+
96+
p.providerConfig = pd
97+
}
98+
99+
func (p *googleEphemeralServiceAccountJwt) Open(ctx context.Context, req ephemeral.OpenRequest, resp *ephemeral.OpenResponse) {
100+
var data ephemeralServiceAccountJwtModel
101+
102+
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
103+
104+
payload := data.Payload.ValueString()
105+
106+
if !data.ExpiresIn.IsNull() {
107+
expiresIn := data.ExpiresIn.ValueInt64()
108+
var decoded map[string]interface{}
109+
if err := json.Unmarshal([]byte(payload), &decoded); err != nil {
110+
resp.Diagnostics.AddError("Error decoding payload", err.Error())
111+
return
112+
}
113+
114+
decoded["exp"] = time.Now().Add(time.Duration(expiresIn) * time.Second).Unix()
115+
116+
payloadBytesWithExp, err := json.Marshal(decoded)
117+
if err != nil {
118+
resp.Diagnostics.AddError("Error re-encoding payload", err.Error())
119+
return
120+
}
121+
122+
payload = string(payloadBytesWithExp)
123+
124+
}
125+
126+
name := fmt.Sprintf("projects/-/serviceAccounts/%s", data.TargetServiceAccount.ValueString())
127+
128+
service := p.providerConfig.NewIamCredentialsClient(p.providerConfig.UserAgent)
129+
jwtRequest := &iamcredentials.SignJwtRequest{
130+
Payload: payload,
131+
Delegates: fwutils.StringSet(data.Delegates),
132+
}
133+
134+
jwtResponse, err := service.Projects.ServiceAccounts.SignJwt(name, jwtRequest).Do()
135+
if err != nil {
136+
resp.Diagnostics.AddError("Error calling iamcredentials.SignJwt", err.Error())
137+
return
138+
}
139+
140+
data.Jwt = types.StringValue(jwtResponse.SignedJwt)
141+
142+
resp.Diagnostics.Append(resp.Result.Set(ctx, data)...)
143+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package resourcemanager_test
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
7+
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
8+
"github.com/hashicorp/terraform-provider-google/google/acctest"
9+
"github.com/hashicorp/terraform-provider-google/google/envvar"
10+
)
11+
12+
func TestAccEphemeralServiceAccountJwt_basic(t *testing.T) {
13+
t.Parallel()
14+
15+
serviceAccount := envvar.GetTestServiceAccountFromEnv(t)
16+
targetServiceAccountEmail := acctest.BootstrapServiceAccount(t, "jwt-basic", serviceAccount)
17+
18+
resource.Test(t, resource.TestCase{
19+
PreCheck: func() { acctest.AccTestPreCheck(t) },
20+
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t),
21+
Steps: []resource.TestStep{
22+
{
23+
Config: testAccEphemeralServiceAccountJwt_basic(targetServiceAccountEmail),
24+
},
25+
},
26+
})
27+
}
28+
29+
func TestAccEphemeralServiceAccountJwt_withDelegates(t *testing.T) {
30+
t.Parallel()
31+
32+
initialServiceAccount := envvar.GetTestServiceAccountFromEnv(t)
33+
delegateServiceAccountEmailOne := acctest.BootstrapServiceAccount(t, "jwt-delegate1", initialServiceAccount) // SA_2
34+
delegateServiceAccountEmailTwo := acctest.BootstrapServiceAccount(t, "jwt-delegate2", delegateServiceAccountEmailOne) // SA_3
35+
targetServiceAccountEmail := acctest.BootstrapServiceAccount(t, "jwt-target", delegateServiceAccountEmailTwo) // SA_4
36+
37+
resource.Test(t, resource.TestCase{
38+
PreCheck: func() { acctest.AccTestPreCheck(t) },
39+
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t),
40+
Steps: []resource.TestStep{
41+
{
42+
Config: testAccEphemeralServiceAccountJwt_withDelegates(delegateServiceAccountEmailOne, delegateServiceAccountEmailTwo, targetServiceAccountEmail),
43+
},
44+
},
45+
})
46+
}
47+
48+
func TestAccEphemeralServiceAccountJwt_withExpiresIn(t *testing.T) {
49+
t.Parallel()
50+
51+
serviceAccount := envvar.GetTestServiceAccountFromEnv(t)
52+
targetServiceAccountEmail := acctest.BootstrapServiceAccount(t, "expiry", serviceAccount)
53+
54+
resource.Test(t, resource.TestCase{
55+
PreCheck: func() { acctest.AccTestPreCheck(t) },
56+
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t),
57+
Steps: []resource.TestStep{
58+
{
59+
Config: testAccEphemeralServiceAccountJwt_withExpiresIn(targetServiceAccountEmail),
60+
},
61+
},
62+
})
63+
}
64+
65+
func testAccEphemeralServiceAccountJwt_basic(serviceAccountEmail string) string {
66+
return fmt.Sprintf(`
67+
ephemeral "google_service_account_jwt" "jwt" {
68+
target_service_account = "%s"
69+
payload = jsonencode({
70+
"sub": "%[1]s",
71+
"aud": "https://example.com"
72+
})
73+
}
74+
`, serviceAccountEmail)
75+
}
76+
77+
func testAccEphemeralServiceAccountJwt_withDelegates(delegateServiceAccountEmailOne, delegateServiceAccountEmailTwo, targetServiceAccountEmail string) string {
78+
return fmt.Sprintf(`
79+
ephemeral "google_service_account_jwt" "jwt" {
80+
target_service_account = "%s"
81+
delegates = [
82+
"%s",
83+
"%s",
84+
]
85+
payload = jsonencode({
86+
"sub": "%[1]s",
87+
"aud": "https://example.com"
88+
})
89+
}
90+
# The delegation chain is:
91+
# SA_1 (initialServiceAccountEmail) -> SA_2 (delegateServiceAccountEmailOne) -> SA_3 (delegateServiceAccountEmailTwo) -> SA_4 (targetServiceAccountEmail)
92+
`, targetServiceAccountEmail, delegateServiceAccountEmailOne, delegateServiceAccountEmailTwo)
93+
}
94+
95+
func testAccEphemeralServiceAccountJwt_withExpiresIn(serviceAccountEmail string) string {
96+
return fmt.Sprintf(`
97+
ephemeral "google_service_account_jwt" "jwt" {
98+
target_service_account = "%s"
99+
expires_in = 3600
100+
payload = jsonencode({
101+
"sub": "%[1]s",
102+
"aud": "https://example.com"
103+
})
104+
}
105+
`, serviceAccountEmail)
106+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
---
2+
subcategory: "Cloud Platform"
3+
description: |-
4+
Produces an arbitrary self-signed JWT for service accounts
5+
---
6+
7+
# google_service_account_jwt
8+
9+
This ephemeral resource provides a [self-signed JWT](https://cloud.google.com/iam/docs/create-short-lived-credentials-direct#sa-credentials-jwt). Tokens issued from this ephemeral resource are typically used to call external services that accept JWTs for authentication.
10+
11+
## Example Usage
12+
13+
Note: in order to use the following, the caller must have _at least_ `roles/iam.serviceAccountTokenCreator` on the `target_service_account`.
14+
15+
```hcl
16+
ephemeral "google_service_account_jwt" "foo" {
17+
target_service_account = "[email protected]"
18+
19+
payload = jsonencode({
20+
foo: "bar",
21+
sub: "subject",
22+
})
23+
24+
expires_in = 60
25+
}
26+
```
27+
28+
## Argument Reference
29+
30+
The following arguments are supported:
31+
32+
* `target_service_account` (Required) - The email of the service account that will sign the JWT.
33+
* `payload` (Required) - The JSON-encoded JWT claims set to include in the self-signed JWT.
34+
* `expires_in` (Optional) - Number of seconds until the JWT expires. If set and non-zero an `exp` claim will be added to the payload derived from the current timestamp plus expires_in seconds.
35+
* `delegates` (Optional) - Delegate chain of approvals needed to perform full impersonation. Specify the fully qualified service account name.
36+
37+
## Attributes Reference
38+
39+
The following attribute is exported:
40+
41+
* `jwt` - The signed JWT containing the JWT Claims Set from the `payload`.

0 commit comments

Comments
 (0)