From 7b01fddbb74d0f1ed613b55b4e41c57bcd2420cc Mon Sep 17 00:00:00 2001 From: BBBmau Date: Wed, 6 Nov 2024 16:55:13 -0800 Subject: [PATCH 01/23] move framework_validators to fwvalidators package --- mmv1/provider/terraform.go | 2 +- mmv1/provider/terraform/common~copy.yaml | 7 + .../fwprovider/framework_validators.go | 117 ------- .../fwvalidators/framework_validators.go | 285 ++++++++++++++++++ .../framework_validators_test.go | 2 +- 5 files changed, 294 insertions(+), 119 deletions(-) delete mode 100644 mmv1/third_party/terraform/fwprovider/framework_validators.go create mode 100644 mmv1/third_party/terraform/fwvalidators/framework_validators.go rename mmv1/third_party/terraform/{fwprovider => fwvalidators}/framework_validators_test.go (98%) diff --git a/mmv1/provider/terraform.go b/mmv1/provider/terraform.go index 14aa6ddfaef9..80a2a7d4faa8 100644 --- a/mmv1/provider/terraform.go +++ b/mmv1/provider/terraform.go @@ -313,7 +313,7 @@ func (t Terraform) getCommonCopyFiles(versionName string, generateCode, generate // save the folder name to foldersCopiedToGoogleDir var foldersCopiedToGoogleDir []string if generateCode { - foldersCopiedToGoogleDir = []string{"third_party/terraform/services", "third_party/terraform/acctest", "third_party/terraform/sweeper", "third_party/terraform/provider", "third_party/terraform/tpgdclresource", "third_party/terraform/tpgiamresource", "third_party/terraform/tpgresource", "third_party/terraform/transport", "third_party/terraform/fwmodels", "third_party/terraform/fwprovider", "third_party/terraform/fwtransport", "third_party/terraform/fwresource", "third_party/terraform/verify", "third_party/terraform/envvar", "third_party/terraform/functions", "third_party/terraform/test-fixtures"} + foldersCopiedToGoogleDir = []string{"third_party/terraform/services", "third_party/terraform/acctest", "third_party/terraform/sweeper", "third_party/terraform/provider", "third_party/terraform/tpgdclresource", "third_party/terraform/tpgiamresource", "third_party/terraform/tpgresource", "third_party/terraform/transport", "third_party/terraform/fwmodels", "third_party/terraform/fwprovider", "third_party/terraform/fwvalidators", "third_party/terraform/fwtransport", "third_party/terraform/fwresource", "third_party/terraform/verify", "third_party/terraform/envvar", "third_party/terraform/functions", "third_party/terraform/test-fixtures"} } googleDir := "google" if versionName != "ga" { diff --git a/mmv1/provider/terraform/common~copy.yaml b/mmv1/provider/terraform/common~copy.yaml index b9ad0c850979..21df28c2925a 100644 --- a/mmv1/provider/terraform/common~copy.yaml +++ b/mmv1/provider/terraform/common~copy.yaml @@ -101,6 +101,13 @@ '<%= dir -%>/fwprovider/<%= fname -%>': 'third_party/terraform/fwprovider/<%= fname -%>' <% end -%> +<% + Dir["third_party/terraform/fwvalidators/*.go"].each do |file_path| + fname = file_path.split('/')[-1] +-%> +'<%= dir -%>/fwvalidators/<%= fname -%>': 'third_party/terraform/fwvalidators/<%= fname -%>' +<% end -%> + <% Dir["third_party/terraform/fwtransport/*.go"].each do |file_path| fname = file_path.split('/')[-1] diff --git a/mmv1/third_party/terraform/fwprovider/framework_validators.go b/mmv1/third_party/terraform/fwprovider/framework_validators.go deleted file mode 100644 index 3af98fd9db92..000000000000 --- a/mmv1/third_party/terraform/fwprovider/framework_validators.go +++ /dev/null @@ -1,117 +0,0 @@ -package fwprovider - -import ( - "context" - "fmt" - "os" - "time" - - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - - googleoauth "golang.org/x/oauth2/google" -) - -// Credentials Validator -var _ validator.String = credentialsValidator{} - -// credentialsValidator validates that a string Attribute's is valid JSON credentials. -type credentialsValidator struct { -} - -// Description describes the validation in plain text formatting. -func (v credentialsValidator) Description(_ context.Context) string { - return "value must be a path to valid JSON credentials or valid, raw, JSON credentials" -} - -// MarkdownDescription describes the validation in Markdown formatting. -func (v credentialsValidator) MarkdownDescription(ctx context.Context) string { - return v.Description(ctx) -} - -// ValidateString performs the validation. -func (v credentialsValidator) ValidateString(ctx context.Context, request validator.StringRequest, response *validator.StringResponse) { - if request.ConfigValue.IsNull() || request.ConfigValue.IsUnknown() { - return - } - - value := request.ConfigValue.ValueString() - - // if this is a path and we can stat it, assume it's ok - if _, err := os.Stat(value); err == nil { - return - } - if _, err := googleoauth.CredentialsFromJSON(context.Background(), []byte(value)); err != nil { - response.Diagnostics.AddError("JSON credentials are not valid", err.Error()) - } -} - -func CredentialsValidator() validator.String { - return credentialsValidator{} -} - -// Non Negative Duration Validator -type nonnegativedurationValidator struct { -} - -// Description describes the validation in plain text formatting. -func (v nonnegativedurationValidator) Description(_ context.Context) string { - return "value expected to be a string representing a non-negative duration" -} - -// MarkdownDescription describes the validation in Markdown formatting. -func (v nonnegativedurationValidator) MarkdownDescription(ctx context.Context) string { - return v.Description(ctx) -} - -// ValidateString performs the validation. -func (v nonnegativedurationValidator) ValidateString(ctx context.Context, request validator.StringRequest, response *validator.StringResponse) { - if request.ConfigValue.IsNull() || request.ConfigValue.IsUnknown() { - return - } - - value := request.ConfigValue.ValueString() - dur, err := time.ParseDuration(value) - if err != nil { - response.Diagnostics.AddError(fmt.Sprintf("expected %s to be a duration", value), err.Error()) - return - } - - if dur < 0 { - response.Diagnostics.AddError("duration must be non-negative", fmt.Sprintf("duration provided: %d", dur)) - } -} - -func NonNegativeDurationValidator() validator.String { - return nonnegativedurationValidator{} -} - -// Non Empty String Validator -type nonEmptyStringValidator struct { -} - -// Description describes the validation in plain text formatting. -func (v nonEmptyStringValidator) Description(_ context.Context) string { - return "value expected to be a string that isn't an empty string" -} - -// MarkdownDescription describes the validation in Markdown formatting. -func (v nonEmptyStringValidator) MarkdownDescription(ctx context.Context) string { - return v.Description(ctx) -} - -// ValidateString performs the validation. -func (v nonEmptyStringValidator) ValidateString(ctx context.Context, request validator.StringRequest, response *validator.StringResponse) { - if request.ConfigValue.IsNull() || request.ConfigValue.IsUnknown() { - return - } - - value := request.ConfigValue.ValueString() - - if value == "" { - response.Diagnostics.AddError("expected a non-empty string", fmt.Sprintf("%s was set to `%s`", request.Path, value)) - } -} - -func NonEmptyStringValidator() validator.String { - return nonEmptyStringValidator{} -} diff --git a/mmv1/third_party/terraform/fwvalidators/framework_validators.go b/mmv1/third_party/terraform/fwvalidators/framework_validators.go new file mode 100644 index 000000000000..0539e1e30ac0 --- /dev/null +++ b/mmv1/third_party/terraform/fwvalidators/framework_validators.go @@ -0,0 +1,285 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 +package fwvalidators + +import ( + "context" + "fmt" + "os" + "regexp" + "time" + + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + + googleoauth "golang.org/x/oauth2/google" +) + +// Credentials Validator +var _ validator.String = credentialsValidator{} + +// credentialsValidator validates that a string Attribute's is valid JSON credentials. +type credentialsValidator struct { +} + +// Description describes the validation in plain text formatting. +func (v credentialsValidator) Description(_ context.Context) string { + return "value must be a path to valid JSON credentials or valid, raw, JSON credentials" +} + +// MarkdownDescription describes the validation in Markdown formatting. +func (v credentialsValidator) MarkdownDescription(ctx context.Context) string { + return v.Description(ctx) +} + +// ValidateString performs the validation. +func (v credentialsValidator) ValidateString(ctx context.Context, request validator.StringRequest, response *validator.StringResponse) { + if request.ConfigValue.IsNull() || request.ConfigValue.IsUnknown() { + return + } + + value := request.ConfigValue.ValueString() + + // if this is a path and we can stat it, assume it's ok + if _, err := os.Stat(value); err == nil { + return + } + if _, err := googleoauth.CredentialsFromJSON(context.Background(), []byte(value)); err != nil { + response.Diagnostics.AddError("JSON credentials are not valid", err.Error()) + } +} + +func CredentialsValidator() validator.String { + return credentialsValidator{} +} + +// Non Negative Duration Validator +type nonnegativedurationValidator struct { +} + +// Description describes the validation in plain text formatting. +func (v nonnegativedurationValidator) Description(_ context.Context) string { + return "value expected to be a string representing a non-negative duration" +} + +// MarkdownDescription describes the validation in Markdown formatting. +func (v nonnegativedurationValidator) MarkdownDescription(ctx context.Context) string { + return v.Description(ctx) +} + +// ValidateString performs the validation. +func (v nonnegativedurationValidator) ValidateString(ctx context.Context, request validator.StringRequest, response *validator.StringResponse) { + if request.ConfigValue.IsNull() || request.ConfigValue.IsUnknown() { + return + } + + value := request.ConfigValue.ValueString() + dur, err := time.ParseDuration(value) + if err != nil { + response.Diagnostics.AddError(fmt.Sprintf("expected %s to be a duration", value), err.Error()) + return + } + + if dur < 0 { + response.Diagnostics.AddError("duration must be non-negative", fmt.Sprintf("duration provided: %d", dur)) + } +} + +func NonNegativeDurationValidator() validator.String { + return nonnegativedurationValidator{} +} + +// Non Empty String Validator +type nonEmptyStringValidator struct { +} + +// Description describes the validation in plain text formatting. +func (v nonEmptyStringValidator) Description(_ context.Context) string { + return "value expected to be a string that isn't an empty string" +} + +// MarkdownDescription describes the validation in Markdown formatting. +func (v nonEmptyStringValidator) MarkdownDescription(ctx context.Context) string { + return v.Description(ctx) +} + +// ValidateString performs the validation. +func (v nonEmptyStringValidator) ValidateString(ctx context.Context, request validator.StringRequest, response *validator.StringResponse) { + if request.ConfigValue.IsNull() || request.ConfigValue.IsUnknown() { + return + } + + value := request.ConfigValue.ValueString() + + if value == "" { + response.Diagnostics.AddError("expected a non-empty string", fmt.Sprintf("%s was set to `%s`", request.Path, value)) + } +} + +func NonEmptyStringValidator() validator.String { + return nonEmptyStringValidator{} +} + +func StringSet(d basetypes.SetValue) []string { + + StringSlice := make([]string, 0) + for _, v := range d.Elements() { + StringSlice = append(StringSlice, v.(basetypes.StringValue).ValueString()) + } + return StringSlice +} + +// Define the possible service account name patterns +var serviceAccountNamePatterns = []string{ + `^.+@.+\.iam\.gserviceaccount\.com$`, // Standard IAM service account + `^.+@developer\.gserviceaccount\.com$`, // Legacy developer service account + `^.+@appspot\.gserviceaccount\.com$`, // App Engine service account + `^.+@cloudservices\.gserviceaccount\.com$`, // Google Cloud services service account + `^.+@cloudbuild\.gserviceaccount\.com$`, // Cloud Build service account + `^service-[0-9]+@.+-compute\.iam\.gserviceaccount\.com$`, // Compute Engine service account +} + +// Create a custom validator for service account names +type ServiceAccountNameValidator struct{} + +func (v ServiceAccountNameValidator) Description(ctx context.Context) string { + return "value must be a valid service account email address" +} + +func (v ServiceAccountNameValidator) MarkdownDescription(ctx context.Context) string { + return v.Description(ctx) +} + +func (v ServiceAccountNameValidator) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { + if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { + return + } + + value := req.ConfigValue.ValueString() + valid := false + for _, pattern := range serviceAccountNamePatterns { + if matched, _ := regexp.MatchString(pattern, value); matched { + valid = true + break + } + } + + if !valid { + resp.Diagnostics.AddAttributeError( + req.Path, + "Invalid Service Account Name", + "Service account name must match one of the expected patterns for Google service accounts", + ) + } +} + +// Create a custom validator for duration +type DurationValidator struct { + MaxDuration time.Duration +} + +func (v DurationValidator) Description(ctx context.Context) string { + return fmt.Sprintf("value must be a valid duration string less than or equal to %v", v.MaxDuration) +} + +func (v DurationValidator) MarkdownDescription(ctx context.Context) string { + return v.Description(ctx) +} + +func (v DurationValidator) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { + if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { + return + } + + value := req.ConfigValue.ValueString() + duration, err := time.ParseDuration(value) + if err != nil { + resp.Diagnostics.AddAttributeError( + req.Path, + "Invalid Duration Format", + "Duration must be a valid duration string (e.g., '3600s', '1h')", + ) + return + } + + if duration > v.MaxDuration { + resp.Diagnostics.AddAttributeError( + req.Path, + "Duration Too Long", + fmt.Sprintf("Duration must be less than or equal to %v", v.MaxDuration), + ) + } +} + +// ServiceScopeValidator validates that a service scope is in canonical form +var _ validator.String = &ServiceScopeValidator{} + +// ServiceScopeValidator validates service scope strings +type ServiceScopeValidator struct { +} + +// Description returns a plain text description of the validator's behavior +func (v ServiceScopeValidator) Description(ctx context.Context) string { + return "service scope must be in canonical form" +} + +// MarkdownDescription returns a markdown formatted description of the validator's behavior +func (v ServiceScopeValidator) MarkdownDescription(ctx context.Context) string { + return v.Description(ctx) +} + +// ValidateString performs the validation +func (v ServiceScopeValidator) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { + if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { + return + } + + canonicalized := CanonicalizeServiceScope(req.ConfigValue.ValueString()) + if req.ConfigValue.ValueString() != canonicalized { + resp.Diagnostics.AddAttributeWarning( + req.Path, + "Non-canonical service scope", + fmt.Sprintf("Service scope %q will be canonicalized to %q", + req.ConfigValue.ValueString(), + canonicalized, + ), + ) + } +} + +func CanonicalizeServiceScope(scope string) string { + // This is a convenience map of short names used by the gcloud tool + // to the GCE auth endpoints they alias to. + scopeMap := map[string]string{ + "bigquery": "https://www.googleapis.com/auth/bigquery", + "cloud-platform": "https://www.googleapis.com/auth/cloud-platform", + "cloud-source-repos": "https://www.googleapis.com/auth/source.full_control", + "cloud-source-repos-ro": "https://www.googleapis.com/auth/source.read_only", + "compute-ro": "https://www.googleapis.com/auth/compute.readonly", + "compute-rw": "https://www.googleapis.com/auth/compute", + "datastore": "https://www.googleapis.com/auth/datastore", + "logging-write": "https://www.googleapis.com/auth/logging.write", + "monitoring": "https://www.googleapis.com/auth/monitoring", + "monitoring-read": "https://www.googleapis.com/auth/monitoring.read", + "monitoring-write": "https://www.googleapis.com/auth/monitoring.write", + "pubsub": "https://www.googleapis.com/auth/pubsub", + "service-control": "https://www.googleapis.com/auth/servicecontrol", + "service-management": "https://www.googleapis.com/auth/service.management.readonly", + "sql": "https://www.googleapis.com/auth/sqlservice", + "sql-admin": "https://www.googleapis.com/auth/sqlservice.admin", + "storage-full": "https://www.googleapis.com/auth/devstorage.full_control", + "storage-ro": "https://www.googleapis.com/auth/devstorage.read_only", + "storage-rw": "https://www.googleapis.com/auth/devstorage.read_write", + "taskqueue": "https://www.googleapis.com/auth/taskqueue", + "trace": "https://www.googleapis.com/auth/trace.append", + "useraccounts-ro": "https://www.googleapis.com/auth/cloud.useraccounts.readonly", + "useraccounts-rw": "https://www.googleapis.com/auth/cloud.useraccounts", + "userinfo-email": "https://www.googleapis.com/auth/userinfo.email", + } + + if matchedURL, ok := scopeMap[scope]; ok { + return matchedURL + } + + return scope +} diff --git a/mmv1/third_party/terraform/fwprovider/framework_validators_test.go b/mmv1/third_party/terraform/fwvalidators/framework_validators_test.go similarity index 98% rename from mmv1/third_party/terraform/fwprovider/framework_validators_test.go rename to mmv1/third_party/terraform/fwvalidators/framework_validators_test.go index 6462f3af43b8..b561264e7faf 100644 --- a/mmv1/third_party/terraform/fwprovider/framework_validators_test.go +++ b/mmv1/third_party/terraform/fwvalidators/framework_validators_test.go @@ -1,4 +1,4 @@ -package fwprovider_test +package fwvalidators_test import ( "context" From e7f003afe8b31e9dc93fb0534626bf40ea9e3f75 Mon Sep 17 00:00:00 2001 From: BBBmau Date: Thu, 7 Nov 2024 08:39:46 -0800 Subject: [PATCH 02/23] reference fwvalidators in framework_provider --- .../fwprovider/framework_provider.go.tmpl | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/mmv1/third_party/terraform/fwprovider/framework_provider.go.tmpl b/mmv1/third_party/terraform/fwprovider/framework_provider.go.tmpl index 01bc572f6388..a2c836e0ba5c 100644 --- a/mmv1/third_party/terraform/fwprovider/framework_provider.go.tmpl +++ b/mmv1/third_party/terraform/fwprovider/framework_provider.go.tmpl @@ -15,6 +15,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-provider-google/google/fwvalidators" "github.com/hashicorp/terraform-provider-google/google/functions" "github.com/hashicorp/terraform-provider-google/google/fwmodels" "github.com/hashicorp/terraform-provider-google/google/fwtransport" @@ -77,8 +78,8 @@ func (p *FrameworkProvider) Schema(_ context.Context, _ provider.SchemaRequest, stringvalidator.ConflictsWith(path.Expressions{ path.MatchRoot("access_token"), }...), - CredentialsValidator(), - NonEmptyStringValidator(), + fwvalidators.CredentialsValidator(), + fwvalidators.NonEmptyStringValidator(), }, }, "access_token": schema.StringAttribute{ @@ -87,13 +88,13 @@ func (p *FrameworkProvider) Schema(_ context.Context, _ provider.SchemaRequest, stringvalidator.ConflictsWith(path.Expressions{ path.MatchRoot("credentials"), }...), - NonEmptyStringValidator(), + fwvalidators.NonEmptyStringValidator(), }, }, "impersonate_service_account": schema.StringAttribute{ Optional: true, Validators: []validator.String{ - NonEmptyStringValidator(), + fwvalidators.NonEmptyStringValidator(), }, }, "impersonate_service_account_delegates": schema.ListAttribute{ @@ -103,25 +104,25 @@ func (p *FrameworkProvider) Schema(_ context.Context, _ provider.SchemaRequest, "project": schema.StringAttribute{ Optional: true, Validators: []validator.String{ - NonEmptyStringValidator(), + fwvalidators.NonEmptyStringValidator(), }, }, "billing_project": schema.StringAttribute{ Optional: true, Validators: []validator.String{ - NonEmptyStringValidator(), + fwvalidators.NonEmptyStringValidator(), }, }, "region": schema.StringAttribute{ Optional: true, Validators: []validator.String{ - NonEmptyStringValidator(), + fwvalidators.NonEmptyStringValidator(), }, }, "zone": schema.StringAttribute{ Optional: true, Validators: []validator.String{ - NonEmptyStringValidator(), + fwvalidators.NonEmptyStringValidator(), }, }, "scopes": schema.ListAttribute{ From 03e66639f2ef66f9a57ee6b977ce3d9bff4eff14 Mon Sep 17 00:00:00 2001 From: Sarah French <15078782+SarahFrench@users.noreply.github.com> Date: Thu, 7 Nov 2024 19:14:35 +0000 Subject: [PATCH 03/23] Fix reference to `NonNegativeDurationValidator` --- .../terraform/fwprovider/framework_provider.go.tmpl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mmv1/third_party/terraform/fwprovider/framework_provider.go.tmpl b/mmv1/third_party/terraform/fwprovider/framework_provider.go.tmpl index a2c836e0ba5c..bb85cdeb1fb6 100644 --- a/mmv1/third_party/terraform/fwprovider/framework_provider.go.tmpl +++ b/mmv1/third_party/terraform/fwprovider/framework_provider.go.tmpl @@ -234,7 +234,7 @@ func (p *FrameworkProvider) Schema(_ context.Context, _ provider.SchemaRequest, "send_after": schema.StringAttribute{ Optional: true, Validators: []validator.String{ - NonNegativeDurationValidator(), + fwvalidators.NonNegativeDurationValidator(), }, }, "enable_batching": schema.BoolAttribute{ @@ -311,4 +311,4 @@ func (p *FrameworkProvider) EphemeralResources(_ context.Context) []func() ephem return []func() ephemeral.EphemeralResource{ // TODO! } -} \ No newline at end of file +} From 46af2ac7df8ae626d56ef33bd6d7b8b731d2ffcf Mon Sep 17 00:00:00 2001 From: Sarah French <15078782+SarahFrench@users.noreply.github.com> Date: Thu, 7 Nov 2024 19:54:43 +0000 Subject: [PATCH 04/23] Fix reference to `CredentialsValidator` in test --- .../terraform/fwvalidators/framework_validators_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mmv1/third_party/terraform/fwvalidators/framework_validators_test.go b/mmv1/third_party/terraform/fwvalidators/framework_validators_test.go index b561264e7faf..fd12e9fa498f 100644 --- a/mmv1/third_party/terraform/fwvalidators/framework_validators_test.go +++ b/mmv1/third_party/terraform/fwvalidators/framework_validators_test.go @@ -9,7 +9,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-provider-google/google/acctest" - "github.com/hashicorp/terraform-provider-google/google/fwprovider" + "github.com/hashicorp/terraform-provider-google/google/fwvalidators" transport_tpg "github.com/hashicorp/terraform-provider-google/google/transport" ) @@ -50,7 +50,7 @@ func TestFrameworkProvider_CredentialsValidator(t *testing.T) { Diagnostics: diag.Diagnostics{}, } - cv := fwprovider.CredentialsValidator() + cv := fwvalidators.CredentialsValidator() // Act cv.ValidateString(context.Background(), req, &resp) From 04adbfee54651d004ef35c662490d3dbe178ab3f Mon Sep 17 00:00:00 2001 From: Sarah French <15078782+SarahFrench@users.noreply.github.com> Date: Thu, 7 Nov 2024 19:55:03 +0000 Subject: [PATCH 05/23] Update mmv1/third_party/terraform/fwvalidators/framework_validators.go --- mmv1/third_party/terraform/fwvalidators/framework_validators.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/mmv1/third_party/terraform/fwvalidators/framework_validators.go b/mmv1/third_party/terraform/fwvalidators/framework_validators.go index 0539e1e30ac0..a6654323702f 100644 --- a/mmv1/third_party/terraform/fwvalidators/framework_validators.go +++ b/mmv1/third_party/terraform/fwvalidators/framework_validators.go @@ -1,5 +1,3 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 package fwvalidators import ( From 8c0375c3b2d785ca2cc7f0f4be5c2d3001662fc8 Mon Sep 17 00:00:00 2001 From: BBBmau Date: Thu, 7 Nov 2024 14:43:39 -0800 Subject: [PATCH 06/23] update validator with simpler regex and tests --- .../fwvalidators/framework_validators.go | 39 ++-- .../fwvalidators/framework_validators_test.go | 213 +++++++++++++++++- 2 files changed, 227 insertions(+), 25 deletions(-) diff --git a/mmv1/third_party/terraform/fwvalidators/framework_validators.go b/mmv1/third_party/terraform/fwvalidators/framework_validators.go index a6654323702f..e5270635c940 100644 --- a/mmv1/third_party/terraform/fwvalidators/framework_validators.go +++ b/mmv1/third_party/terraform/fwvalidators/framework_validators.go @@ -127,16 +127,6 @@ func StringSet(d basetypes.SetValue) []string { return StringSlice } -// Define the possible service account name patterns -var serviceAccountNamePatterns = []string{ - `^.+@.+\.iam\.gserviceaccount\.com$`, // Standard IAM service account - `^.+@developer\.gserviceaccount\.com$`, // Legacy developer service account - `^.+@appspot\.gserviceaccount\.com$`, // App Engine service account - `^.+@cloudservices\.gserviceaccount\.com$`, // Google Cloud services service account - `^.+@cloudbuild\.gserviceaccount\.com$`, // Cloud Build service account - `^service-[0-9]+@.+-compute\.iam\.gserviceaccount\.com$`, // Compute Engine service account -} - // Create a custom validator for service account names type ServiceAccountNameValidator struct{} @@ -154,30 +144,31 @@ func (v ServiceAccountNameValidator) ValidateString(ctx context.Context, req val } value := req.ConfigValue.ValueString() - valid := false - for _, pattern := range serviceAccountNamePatterns { - if matched, _ := regexp.MatchString(pattern, value); matched { - valid = true - break - } + + fmt.Printf("value in ValidateString: %q\n", value) + // Check for empty string + if value == "" { + resp.Diagnostics.AddError("Invalid Service Account Name", "Service account name must not be empty") + return } - if !valid { - resp.Diagnostics.AddAttributeError( - req.Path, + // Define the possible service account name patterns + serviceAccountNamePattern := `^.+@.+\.iam\.gserviceaccount\.com$` // Standard IAM service account + + if matched, _ := regexp.MatchString(serviceAccountNamePattern, value); !matched { + resp.Diagnostics.AddError( "Invalid Service Account Name", - "Service account name must match one of the expected patterns for Google service accounts", + "Service account name must be in the format: name@project.iam.gserviceaccount.com", ) } } // Create a custom validator for duration type DurationValidator struct { - MaxDuration time.Duration } func (v DurationValidator) Description(ctx context.Context) string { - return fmt.Sprintf("value must be a valid duration string less than or equal to %v", v.MaxDuration) + return "value must be a valid duration string less than or equal to 1 hour" } func (v DurationValidator) MarkdownDescription(ctx context.Context) string { @@ -200,11 +191,11 @@ func (v DurationValidator) ValidateString(ctx context.Context, req validator.Str return } - if duration > v.MaxDuration { + if duration > 3600*time.Second { resp.Diagnostics.AddAttributeError( req.Path, "Duration Too Long", - fmt.Sprintf("Duration must be less than or equal to %v", v.MaxDuration), + "Duration must be less than or equal to 1 hour", ) } } diff --git a/mmv1/third_party/terraform/fwvalidators/framework_validators_test.go b/mmv1/third_party/terraform/fwvalidators/framework_validators_test.go index fd12e9fa498f..eb138dced609 100644 --- a/mmv1/third_party/terraform/fwvalidators/framework_validators_test.go +++ b/mmv1/third_party/terraform/fwvalidators/framework_validators_test.go @@ -5,11 +5,12 @@ import ( "testing" "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-provider-google/fwvalidators" "github.com/hashicorp/terraform-provider-google/google/acctest" - "github.com/hashicorp/terraform-provider-google/google/fwvalidators" transport_tpg "github.com/hashicorp/terraform-provider-google/google/transport" ) @@ -65,3 +66,213 @@ func TestFrameworkProvider_CredentialsValidator(t *testing.T) { }) } } + +func TestServiceAccountNameValidator(t *testing.T) { + t.Parallel() + + type testCase struct { + value types.String + expectError bool + errorContains string + } + + tests := map[string]testCase{ + "correct service account name": { + value: types.StringValue("test@test.iam.gserviceaccount.com"), + expectError: false, + }, + "incorrect service account name": { + value: types.StringValue("test"), + expectError: true, + errorContains: "Service account name must be in the format: name@project.iam.gserviceaccount.com", + }, + "empty string": { + value: types.StringValue(""), + expectError: true, + errorContains: "Service account name must not be empty", + }, + "null value": { + value: types.StringNull(), + expectError: false, + }, + "unknown value": { + value: types.StringUnknown(), + expectError: false, + }, + } + + for name, test := range tests { + name, test := name, test + t.Run(name, func(t *testing.T) { + t.Parallel() + + request := validator.StringRequest{ + Path: path.Root("test"), + PathExpression: path.MatchRoot("test"), + ConfigValue: test.value, + } + response := validator.StringResponse{} + validator := fwvalidators.ServiceAccountNameValidator{} + + validator.ValidateString(context.Background(), request, &response) + + if test.expectError && !response.Diagnostics.HasError() { + t.Errorf("expected error, got none") + } + + if !test.expectError && response.Diagnostics.HasError() { + t.Errorf("got unexpected error: %s", response.Diagnostics.Errors()) + } + + if test.errorContains != "" { + foundError := false + for _, err := range response.Diagnostics.Errors() { + if err.Detail() == test.errorContains { + foundError = true + break + } + } + if !foundError { + t.Errorf("expected error with summary %q, got none", test.errorContains) + } + } + }) + } +} + +func TestDurationValidator(t *testing.T) { + t.Parallel() + + type testCase struct { + value types.String + expectError bool + errorContains string + } + + tests := map[string]testCase{ + "valid duration under max": { + value: types.StringValue("1800s"), + expectError: false, + }, + "valid duration at max": { + value: types.StringValue("3600s"), + expectError: false, + }, + "valid duration with different unit": { + value: types.StringValue("1h"), + expectError: false, + }, + "duration exceeds max - seconds": { + value: types.StringValue("7200s"), + expectError: true, + errorContains: "Duration Too Long", + }, + "duration exceeds max - minutes": { + value: types.StringValue("120m"), + expectError: true, + errorContains: "Duration Too Long", + }, + "duration exceeds max - hours": { + value: types.StringValue("2h"), + expectError: true, + errorContains: "Duration Too Long", + }, + "invalid duration format": { + value: types.StringValue("invalid"), + expectError: true, + errorContains: "Invalid Duration Format", + }, + "empty string": { + value: types.StringValue(""), + expectError: true, + errorContains: "Invalid Duration Format", + }, + } + + for name, test := range tests { + name, test := name, test + t.Run(name, func(t *testing.T) { + t.Parallel() + + request := validator.StringRequest{ + Path: path.Root("test"), + PathExpression: path.MatchRoot("test"), + ConfigValue: test.value, + } + response := validator.StringResponse{} + validator := fwvalidators.DurationValidator{} + + validator.ValidateString(context.Background(), request, &response) + + if test.expectError && !response.Diagnostics.HasError() { + t.Errorf("expected error, got none") + } + + if !test.expectError && response.Diagnostics.HasError() { + t.Errorf("got unexpected error: %s", response.Diagnostics.Errors()) + } + + if test.errorContains != "" { + foundError := false + for _, err := range response.Diagnostics.Errors() { + if err.Summary() == test.errorContains { + foundError = true + break + } + } + if !foundError { + t.Errorf("expected error with summary %q, got none", test.errorContains) + } + } + }) + } +} + +func TestServiceScopeValidator(t *testing.T) { + t.Parallel() + + type testCase struct { + value types.String + expectError bool + } + + tests := map[string]testCase{ + "canonical form": { + value: types.StringValue("https://www.googleapis.com/auth/cloud-platform"), + expectError: false, + }, + "non-canonical form": { + value: types.StringValue("cloud-platform"), + expectError: false, + }, + "empty string": { + value: types.StringValue(""), + expectError: false, + }, + } + + for name, test := range tests { + name, test := name, test + t.Run(name, func(t *testing.T) { + t.Parallel() + + request := validator.StringRequest{ + Path: path.Root("test"), + PathExpression: path.MatchRoot("test"), + ConfigValue: test.value, + } + response := validator.StringResponse{} + validator := fwvalidators.ServiceScopeValidator{} + + validator.ValidateString(context.Background(), request, &response) + + if test.expectError && !response.Diagnostics.HasError() { + t.Errorf("expected error, got none") + } + + if !test.expectError && response.Diagnostics.HasError() { + t.Errorf("got unexpected error: %s", response.Diagnostics.Errors()) + } + }) + } +} From 9c9e0358cd7e352e73040097a926c65a8e0a1851 Mon Sep 17 00:00:00 2001 From: BBBmau Date: Fri, 8 Nov 2024 01:21:54 -0800 Subject: [PATCH 07/23] revert single service account type check --- .../fwvalidators/framework_validators.go | 28 ++++++++++++++----- .../fwvalidators/framework_validators_test.go | 2 +- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/mmv1/third_party/terraform/fwvalidators/framework_validators.go b/mmv1/third_party/terraform/fwvalidators/framework_validators.go index e5270635c940..b4ec1a0ec8f3 100644 --- a/mmv1/third_party/terraform/fwvalidators/framework_validators.go +++ b/mmv1/third_party/terraform/fwvalidators/framework_validators.go @@ -127,6 +127,16 @@ func StringSet(d basetypes.SetValue) []string { return StringSlice } +// Define the possible service account name patterns +var serviceAccountNamePatterns = []string{ + `^.+@.+\.iam\.gserviceaccount\.com$`, // Standard IAM service account + `^.+@developer\.gserviceaccount\.com$`, // Legacy developer service account + `^.+@appspot\.gserviceaccount\.com$`, // App Engine service account + `^.+@cloudservices\.gserviceaccount\.com$`, // Google Cloud services service account + `^.+@cloudbuild\.gserviceaccount\.com$`, // Cloud Build service account + `^service-[0-9]+@.+-compute\.iam\.gserviceaccount\.com$`, // Compute Engine service account +} + // Create a custom validator for service account names type ServiceAccountNameValidator struct{} @@ -144,21 +154,25 @@ func (v ServiceAccountNameValidator) ValidateString(ctx context.Context, req val } value := req.ConfigValue.ValueString() + valid := false + for _, pattern := range serviceAccountNamePatterns { + if matched, _ := regexp.MatchString(pattern, value); matched { + valid = true + break + } + } - fmt.Printf("value in ValidateString: %q\n", value) // Check for empty string if value == "" { resp.Diagnostics.AddError("Invalid Service Account Name", "Service account name must not be empty") return } - // Define the possible service account name patterns - serviceAccountNamePattern := `^.+@.+\.iam\.gserviceaccount\.com$` // Standard IAM service account - - if matched, _ := regexp.MatchString(serviceAccountNamePattern, value); !matched { - resp.Diagnostics.AddError( + if !valid { + resp.Diagnostics.AddAttributeError( + req.Path, "Invalid Service Account Name", - "Service account name must be in the format: name@project.iam.gserviceaccount.com", + "Service account name must match one of the expected patterns for Google service accounts", ) } } diff --git a/mmv1/third_party/terraform/fwvalidators/framework_validators_test.go b/mmv1/third_party/terraform/fwvalidators/framework_validators_test.go index eb138dced609..826d4f4d4d30 100644 --- a/mmv1/third_party/terraform/fwvalidators/framework_validators_test.go +++ b/mmv1/third_party/terraform/fwvalidators/framework_validators_test.go @@ -84,7 +84,7 @@ func TestServiceAccountNameValidator(t *testing.T) { "incorrect service account name": { value: types.StringValue("test"), expectError: true, - errorContains: "Service account name must be in the format: name@project.iam.gserviceaccount.com", + errorContains: "Service account name must match one of the expected patterns for Google service accounts", }, "empty string": { value: types.StringValue(""), From 8774e0ecc13bb6b680c3e1642a0478bb209af629 Mon Sep 17 00:00:00 2001 From: Sarah French <15078782+SarahFrench@users.noreply.github.com> Date: Fri, 8 Nov 2024 11:07:39 +0000 Subject: [PATCH 08/23] Fix mistake I made in an import path --- .../terraform/fwvalidators/framework_validators_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mmv1/third_party/terraform/fwvalidators/framework_validators_test.go b/mmv1/third_party/terraform/fwvalidators/framework_validators_test.go index 826d4f4d4d30..3d82f893667f 100644 --- a/mmv1/third_party/terraform/fwvalidators/framework_validators_test.go +++ b/mmv1/third_party/terraform/fwvalidators/framework_validators_test.go @@ -9,7 +9,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-provider-google/fwvalidators" + "github.com/hashicorp/terraform-provider-google/google/fwvalidators" "github.com/hashicorp/terraform-provider-google/google/acctest" transport_tpg "github.com/hashicorp/terraform-provider-google/google/transport" From cfa4f06321ab1bba5b772f028ab2ae122345be8f Mon Sep 17 00:00:00 2001 From: Sarah French <15078782+SarahFrench@users.noreply.github.com> Date: Fri, 8 Nov 2024 12:24:47 +0000 Subject: [PATCH 09/23] go fmt --- .../terraform/fwvalidators/framework_validators_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mmv1/third_party/terraform/fwvalidators/framework_validators_test.go b/mmv1/third_party/terraform/fwvalidators/framework_validators_test.go index 3d82f893667f..3fc40ca5f12b 100644 --- a/mmv1/third_party/terraform/fwvalidators/framework_validators_test.go +++ b/mmv1/third_party/terraform/fwvalidators/framework_validators_test.go @@ -9,8 +9,8 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-provider-google/google/fwvalidators" "github.com/hashicorp/terraform-provider-google/google/acctest" + "github.com/hashicorp/terraform-provider-google/google/fwvalidators" transport_tpg "github.com/hashicorp/terraform-provider-google/google/transport" ) From 3798f66e91b9a2dab9a332745479151e6341be21 Mon Sep 17 00:00:00 2001 From: BBBmau Date: Fri, 8 Nov 2024 10:54:09 -0800 Subject: [PATCH 10/23] add min/max duration in durationValidator --- .../fwvalidators/framework_validators.go | 16 ++++-- .../fwvalidators/framework_validators_test.go | 51 ++++++++++++++++++- 2 files changed, 62 insertions(+), 5 deletions(-) diff --git a/mmv1/third_party/terraform/fwvalidators/framework_validators.go b/mmv1/third_party/terraform/fwvalidators/framework_validators.go index b4ec1a0ec8f3..71953003d21c 100644 --- a/mmv1/third_party/terraform/fwvalidators/framework_validators.go +++ b/mmv1/third_party/terraform/fwvalidators/framework_validators.go @@ -179,10 +179,12 @@ func (v ServiceAccountNameValidator) ValidateString(ctx context.Context, req val // Create a custom validator for duration type DurationValidator struct { + MinDuration time.Duration + MaxDuration time.Duration } func (v DurationValidator) Description(ctx context.Context) string { - return "value must be a valid duration string less than or equal to 1 hour" + return fmt.Sprintf("value must be a valid duration string between %v and %v", v.MinDuration, v.MaxDuration) } func (v DurationValidator) MarkdownDescription(ctx context.Context) string { @@ -205,11 +207,19 @@ func (v DurationValidator) ValidateString(ctx context.Context, req validator.Str return } - if duration > 3600*time.Second { + if v.MinDuration != 0 && duration < v.MinDuration { + resp.Diagnostics.AddAttributeError( + req.Path, + "Duration Too Short", + fmt.Sprintf("Duration must be greater than or equal to %v", v.MinDuration), + ) + } + + if v.MaxDuration != 0 && duration > v.MaxDuration { resp.Diagnostics.AddAttributeError( req.Path, "Duration Too Long", - "Duration must be less than or equal to 1 hour", + fmt.Sprintf("Duration must be less than or equal to %v", v.MaxDuration), ) } } diff --git a/mmv1/third_party/terraform/fwvalidators/framework_validators_test.go b/mmv1/third_party/terraform/fwvalidators/framework_validators_test.go index 3fc40ca5f12b..7e514edfa0e5 100644 --- a/mmv1/third_party/terraform/fwvalidators/framework_validators_test.go +++ b/mmv1/third_party/terraform/fwvalidators/framework_validators_test.go @@ -3,6 +3,7 @@ package fwvalidators_test import ( "context" "testing" + "time" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/path" @@ -145,48 +146,91 @@ func TestDurationValidator(t *testing.T) { type testCase struct { value types.String + minDuration time.Duration + maxDuration time.Duration expectError bool errorContains string } tests := map[string]testCase{ - "valid duration under max": { + "valid duration between min and max": { value: types.StringValue("1800s"), + minDuration: time.Hour / 2, + maxDuration: time.Hour, + expectError: false, + }, + "valid duration at min": { + value: types.StringValue("1800s"), + minDuration: 30 * time.Minute, + maxDuration: time.Hour, expectError: false, }, "valid duration at max": { value: types.StringValue("3600s"), + minDuration: time.Hour / 2, + maxDuration: time.Hour, expectError: false, }, "valid duration with different unit": { value: types.StringValue("1h"), + minDuration: 30 * time.Minute, + maxDuration: 2 * time.Hour, expectError: false, }, + "duration below min": { + value: types.StringValue("900s"), + minDuration: 30 * time.Minute, + maxDuration: time.Hour, + expectError: true, + errorContains: "Duration Too Short", + }, "duration exceeds max - seconds": { value: types.StringValue("7200s"), + minDuration: 30 * time.Minute, + maxDuration: time.Hour, expectError: true, errorContains: "Duration Too Long", }, "duration exceeds max - minutes": { value: types.StringValue("120m"), + minDuration: 30 * time.Minute, + maxDuration: time.Hour, expectError: true, errorContains: "Duration Too Long", }, "duration exceeds max - hours": { value: types.StringValue("2h"), + minDuration: 30 * time.Minute, + maxDuration: time.Hour, expectError: true, errorContains: "Duration Too Long", }, "invalid duration format": { value: types.StringValue("invalid"), + minDuration: 30 * time.Minute, + maxDuration: time.Hour, expectError: true, errorContains: "Invalid Duration Format", }, "empty string": { value: types.StringValue(""), + minDuration: 30 * time.Minute, + maxDuration: time.Hour, expectError: true, errorContains: "Invalid Duration Format", }, + "null value": { + value: types.StringNull(), + minDuration: 30 * time.Minute, + maxDuration: time.Hour, + expectError: false, + }, + "unknown value": { + value: types.StringUnknown(), + minDuration: 30 * time.Minute, + maxDuration: time.Hour, + expectError: false, + }, } for name, test := range tests { @@ -200,7 +244,10 @@ func TestDurationValidator(t *testing.T) { ConfigValue: test.value, } response := validator.StringResponse{} - validator := fwvalidators.DurationValidator{} + validator := fwvalidators.DurationValidator{ + MinDuration: test.minDuration, + MaxDuration: test.maxDuration, + } validator.ValidateString(context.Background(), request, &response) From 604e19ea47271fb60af16253eeb52279d17c18b3 Mon Sep 17 00:00:00 2001 From: BBBmau Date: Fri, 8 Nov 2024 11:02:36 -0800 Subject: [PATCH 11/23] add more testcases for service name validator --- .../fwvalidators/framework_validators_test.go | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/mmv1/third_party/terraform/fwvalidators/framework_validators_test.go b/mmv1/third_party/terraform/fwvalidators/framework_validators_test.go index 7e514edfa0e5..a14368825181 100644 --- a/mmv1/third_party/terraform/fwvalidators/framework_validators_test.go +++ b/mmv1/third_party/terraform/fwvalidators/framework_validators_test.go @@ -82,6 +82,26 @@ func TestServiceAccountNameValidator(t *testing.T) { value: types.StringValue("test@test.iam.gserviceaccount.com"), expectError: false, }, + "developer service account": { + value: types.StringValue("test@developer.gserviceaccount.com"), + expectError: false, + }, + "app engine service account": { + value: types.StringValue("test@appspot.gserviceaccount.com"), + expectError: false, + }, + "cloud services service account": { + value: types.StringValue("test@cloudservices.gserviceaccount.com"), + expectError: false, + }, + "cloud build service account": { + value: types.StringValue("test@cloudbuild.gserviceaccount.com"), + expectError: false, + }, + "compute engine service account": { + value: types.StringValue("service-123456@compute-system.iam.gserviceaccount.com"), + expectError: false, + }, "incorrect service account name": { value: types.StringValue("test"), expectError: true, From 2c0ef5b615ef4f586c4e168038d78400ca0b1068 Mon Sep 17 00:00:00 2001 From: BBBmau Date: Fri, 8 Nov 2024 11:45:57 -0800 Subject: [PATCH 12/23] remove ServiceScopevalidator --- .../fwvalidators/framework_validators.go | 73 ------------------- .../fwvalidators/framework_validators_test.go | 49 ------------- 2 files changed, 122 deletions(-) diff --git a/mmv1/third_party/terraform/fwvalidators/framework_validators.go b/mmv1/third_party/terraform/fwvalidators/framework_validators.go index 71953003d21c..25a2bed83def 100644 --- a/mmv1/third_party/terraform/fwvalidators/framework_validators.go +++ b/mmv1/third_party/terraform/fwvalidators/framework_validators.go @@ -223,76 +223,3 @@ func (v DurationValidator) ValidateString(ctx context.Context, req validator.Str ) } } - -// ServiceScopeValidator validates that a service scope is in canonical form -var _ validator.String = &ServiceScopeValidator{} - -// ServiceScopeValidator validates service scope strings -type ServiceScopeValidator struct { -} - -// Description returns a plain text description of the validator's behavior -func (v ServiceScopeValidator) Description(ctx context.Context) string { - return "service scope must be in canonical form" -} - -// MarkdownDescription returns a markdown formatted description of the validator's behavior -func (v ServiceScopeValidator) MarkdownDescription(ctx context.Context) string { - return v.Description(ctx) -} - -// ValidateString performs the validation -func (v ServiceScopeValidator) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { - if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { - return - } - - canonicalized := CanonicalizeServiceScope(req.ConfigValue.ValueString()) - if req.ConfigValue.ValueString() != canonicalized { - resp.Diagnostics.AddAttributeWarning( - req.Path, - "Non-canonical service scope", - fmt.Sprintf("Service scope %q will be canonicalized to %q", - req.ConfigValue.ValueString(), - canonicalized, - ), - ) - } -} - -func CanonicalizeServiceScope(scope string) string { - // This is a convenience map of short names used by the gcloud tool - // to the GCE auth endpoints they alias to. - scopeMap := map[string]string{ - "bigquery": "https://www.googleapis.com/auth/bigquery", - "cloud-platform": "https://www.googleapis.com/auth/cloud-platform", - "cloud-source-repos": "https://www.googleapis.com/auth/source.full_control", - "cloud-source-repos-ro": "https://www.googleapis.com/auth/source.read_only", - "compute-ro": "https://www.googleapis.com/auth/compute.readonly", - "compute-rw": "https://www.googleapis.com/auth/compute", - "datastore": "https://www.googleapis.com/auth/datastore", - "logging-write": "https://www.googleapis.com/auth/logging.write", - "monitoring": "https://www.googleapis.com/auth/monitoring", - "monitoring-read": "https://www.googleapis.com/auth/monitoring.read", - "monitoring-write": "https://www.googleapis.com/auth/monitoring.write", - "pubsub": "https://www.googleapis.com/auth/pubsub", - "service-control": "https://www.googleapis.com/auth/servicecontrol", - "service-management": "https://www.googleapis.com/auth/service.management.readonly", - "sql": "https://www.googleapis.com/auth/sqlservice", - "sql-admin": "https://www.googleapis.com/auth/sqlservice.admin", - "storage-full": "https://www.googleapis.com/auth/devstorage.full_control", - "storage-ro": "https://www.googleapis.com/auth/devstorage.read_only", - "storage-rw": "https://www.googleapis.com/auth/devstorage.read_write", - "taskqueue": "https://www.googleapis.com/auth/taskqueue", - "trace": "https://www.googleapis.com/auth/trace.append", - "useraccounts-ro": "https://www.googleapis.com/auth/cloud.useraccounts.readonly", - "useraccounts-rw": "https://www.googleapis.com/auth/cloud.useraccounts", - "userinfo-email": "https://www.googleapis.com/auth/userinfo.email", - } - - if matchedURL, ok := scopeMap[scope]; ok { - return matchedURL - } - - return scope -} diff --git a/mmv1/third_party/terraform/fwvalidators/framework_validators_test.go b/mmv1/third_party/terraform/fwvalidators/framework_validators_test.go index a14368825181..69374cbc69ba 100644 --- a/mmv1/third_party/terraform/fwvalidators/framework_validators_test.go +++ b/mmv1/third_party/terraform/fwvalidators/framework_validators_test.go @@ -294,52 +294,3 @@ func TestDurationValidator(t *testing.T) { }) } } - -func TestServiceScopeValidator(t *testing.T) { - t.Parallel() - - type testCase struct { - value types.String - expectError bool - } - - tests := map[string]testCase{ - "canonical form": { - value: types.StringValue("https://www.googleapis.com/auth/cloud-platform"), - expectError: false, - }, - "non-canonical form": { - value: types.StringValue("cloud-platform"), - expectError: false, - }, - "empty string": { - value: types.StringValue(""), - expectError: false, - }, - } - - for name, test := range tests { - name, test := name, test - t.Run(name, func(t *testing.T) { - t.Parallel() - - request := validator.StringRequest{ - Path: path.Root("test"), - PathExpression: path.MatchRoot("test"), - ConfigValue: test.value, - } - response := validator.StringResponse{} - validator := fwvalidators.ServiceScopeValidator{} - - validator.ValidateString(context.Background(), request, &response) - - if test.expectError && !response.Diagnostics.HasError() { - t.Errorf("expected error, got none") - } - - if !test.expectError && response.Diagnostics.HasError() { - t.Errorf("got unexpected error: %s", response.Diagnostics.Errors()) - } - }) - } -} From db1a870b0235215360e5d3ae64e765abc85cc6d9 Mon Sep 17 00:00:00 2001 From: BBBmau Date: Mon, 11 Nov 2024 06:09:34 -0800 Subject: [PATCH 13/23] add fwutils package --- mmv1/third_party/terraform/fwutils/utils.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 mmv1/third_party/terraform/fwutils/utils.go diff --git a/mmv1/third_party/terraform/fwutils/utils.go b/mmv1/third_party/terraform/fwutils/utils.go new file mode 100644 index 000000000000..eee57edde3c8 --- /dev/null +++ b/mmv1/third_party/terraform/fwutils/utils.go @@ -0,0 +1,12 @@ +package fwutils + +import "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + +func StringSet(d basetypes.SetValue) []string { + + StringSlice := make([]string, 0) + for _, v := range d.Elements() { + StringSlice = append(StringSlice, v.(basetypes.StringValue).ValueString()) + } + return StringSlice +} From 14799247f4aacb86a831cbd902dc1dcb1a709e63 Mon Sep 17 00:00:00 2001 From: BBBmau Date: Mon, 11 Nov 2024 07:01:32 -0800 Subject: [PATCH 14/23] address some refactors and extra tests --- .../fwvalidators/framework_validators.go | 69 +++++++------------ .../fwvalidators/framework_validators_test.go | 21 ++++-- 2 files changed, 43 insertions(+), 47 deletions(-) diff --git a/mmv1/third_party/terraform/fwvalidators/framework_validators.go b/mmv1/third_party/terraform/fwvalidators/framework_validators.go index 25a2bed83def..018440c20a74 100644 --- a/mmv1/third_party/terraform/fwvalidators/framework_validators.go +++ b/mmv1/third_party/terraform/fwvalidators/framework_validators.go @@ -8,7 +8,6 @@ import ( "time" "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types/basetypes" googleoauth "golang.org/x/oauth2/google" ) @@ -52,21 +51,21 @@ func CredentialsValidator() validator.String { } // Non Negative Duration Validator -type nonnegativedurationValidator struct { +type nonnegativeBoundedDuration struct { } // Description describes the validation in plain text formatting. -func (v nonnegativedurationValidator) Description(_ context.Context) string { +func (v nonnegativeBoundedDuration) Description(_ context.Context) string { return "value expected to be a string representing a non-negative duration" } // MarkdownDescription describes the validation in Markdown formatting. -func (v nonnegativedurationValidator) MarkdownDescription(ctx context.Context) string { +func (v nonnegativeBoundedDuration) MarkdownDescription(ctx context.Context) string { return v.Description(ctx) } // ValidateString performs the validation. -func (v nonnegativedurationValidator) ValidateString(ctx context.Context, request validator.StringRequest, response *validator.StringResponse) { +func (v nonnegativeBoundedDuration) ValidateString(ctx context.Context, request validator.StringRequest, response *validator.StringResponse) { if request.ConfigValue.IsNull() || request.ConfigValue.IsUnknown() { return } @@ -83,8 +82,8 @@ func (v nonnegativedurationValidator) ValidateString(ctx context.Context, reques } } -func NonNegativeDurationValidator() validator.String { - return nonnegativedurationValidator{} +func NonNegativeBoundedDuration() validator.String { + return nonnegativeBoundedDuration{} } // Non Empty String Validator @@ -118,17 +117,8 @@ func NonEmptyStringValidator() validator.String { return nonEmptyStringValidator{} } -func StringSet(d basetypes.SetValue) []string { - - StringSlice := make([]string, 0) - for _, v := range d.Elements() { - StringSlice = append(StringSlice, v.(basetypes.StringValue).ValueString()) - } - return StringSlice -} - // Define the possible service account name patterns -var serviceAccountNamePatterns = []string{ +var ServiceAccountEmailPatterns = []string{ `^.+@.+\.iam\.gserviceaccount\.com$`, // Standard IAM service account `^.+@developer\.gserviceaccount\.com$`, // Legacy developer service account `^.+@appspot\.gserviceaccount\.com$`, // App Engine service account @@ -138,29 +128,22 @@ var serviceAccountNamePatterns = []string{ } // Create a custom validator for service account names -type ServiceAccountNameValidator struct{} +type ServiceAccountEmailValidator struct{} -func (v ServiceAccountNameValidator) Description(ctx context.Context) string { +func (v ServiceAccountEmailValidator) Description(ctx context.Context) string { return "value must be a valid service account email address" } -func (v ServiceAccountNameValidator) MarkdownDescription(ctx context.Context) string { +func (v ServiceAccountEmailValidator) MarkdownDescription(ctx context.Context) string { return v.Description(ctx) } -func (v ServiceAccountNameValidator) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { +func (v ServiceAccountEmailValidator) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { return } value := req.ConfigValue.ValueString() - valid := false - for _, pattern := range serviceAccountNamePatterns { - if matched, _ := regexp.MatchString(pattern, value); matched { - valid = true - break - } - } // Check for empty string if value == "" { @@ -168,6 +151,14 @@ func (v ServiceAccountNameValidator) ValidateString(ctx context.Context, req val return } + valid := false + for _, pattern := range ServiceAccountEmailPatterns { + if matched, _ := regexp.MatchString(pattern, value); matched { + valid = true + break + } + } + if !valid { resp.Diagnostics.AddAttributeError( req.Path, @@ -178,20 +169,20 @@ func (v ServiceAccountNameValidator) ValidateString(ctx context.Context, req val } // Create a custom validator for duration -type DurationValidator struct { +type BoundedDuration struct { MinDuration time.Duration MaxDuration time.Duration } -func (v DurationValidator) Description(ctx context.Context) string { +func (v BoundedDuration) Description(ctx context.Context) string { return fmt.Sprintf("value must be a valid duration string between %v and %v", v.MinDuration, v.MaxDuration) } -func (v DurationValidator) MarkdownDescription(ctx context.Context) string { +func (v BoundedDuration) MarkdownDescription(ctx context.Context) string { return v.Description(ctx) } -func (v DurationValidator) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { +func (v BoundedDuration) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { return } @@ -207,19 +198,11 @@ func (v DurationValidator) ValidateString(ctx context.Context, req validator.Str return } - if v.MinDuration != 0 && duration < v.MinDuration { - resp.Diagnostics.AddAttributeError( - req.Path, - "Duration Too Short", - fmt.Sprintf("Duration must be greater than or equal to %v", v.MinDuration), - ) - } - - if v.MaxDuration != 0 && duration > v.MaxDuration { + if duration < v.MinDuration || duration > v.MaxDuration { resp.Diagnostics.AddAttributeError( req.Path, - "Duration Too Long", - fmt.Sprintf("Duration must be less than or equal to %v", v.MaxDuration), + "Invalid Duration", + fmt.Sprintf("Duration must be between %v and %v", v.MinDuration, v.MaxDuration), ) } } diff --git a/mmv1/third_party/terraform/fwvalidators/framework_validators_test.go b/mmv1/third_party/terraform/fwvalidators/framework_validators_test.go index 69374cbc69ba..06455829298e 100644 --- a/mmv1/third_party/terraform/fwvalidators/framework_validators_test.go +++ b/mmv1/third_party/terraform/fwvalidators/framework_validators_test.go @@ -68,7 +68,7 @@ func TestFrameworkProvider_CredentialsValidator(t *testing.T) { } } -func TestServiceAccountNameValidator(t *testing.T) { +func TestServiceAccountEmailValidator(t *testing.T) { t.Parallel() type testCase struct { @@ -133,7 +133,7 @@ func TestServiceAccountNameValidator(t *testing.T) { ConfigValue: test.value, } response := validator.StringResponse{} - validator := fwvalidators.ServiceAccountNameValidator{} + validator := fwvalidators.ServiceAccountEmailValidator{} validator.ValidateString(context.Background(), request, &response) @@ -161,7 +161,7 @@ func TestServiceAccountNameValidator(t *testing.T) { } } -func TestDurationValidator(t *testing.T) { +func TestBoundedDuration(t *testing.T) { t.Parallel() type testCase struct { @@ -232,6 +232,19 @@ func TestDurationValidator(t *testing.T) { expectError: true, errorContains: "Invalid Duration Format", }, + "setting min to 0": { + value: types.StringValue("10s"), + minDuration: 0, + maxDuration: time.Hour, + expectError: false, + }, + "setting max to be less than min": { + value: types.StringValue("10s"), + minDuration: 30 * time.Minute, + maxDuration: 10 * time.Second, + expectError: true, + errorContains: "Invalid Duration", + }, "empty string": { value: types.StringValue(""), minDuration: 30 * time.Minute, @@ -264,7 +277,7 @@ func TestDurationValidator(t *testing.T) { ConfigValue: test.value, } response := validator.StringResponse{} - validator := fwvalidators.DurationValidator{ + validator := fwvalidators.BoundedDuration{ MinDuration: test.minDuration, MaxDuration: test.maxDuration, } From e42d381214a32bbc0ca21a3895d405d2c5fbcf2a Mon Sep 17 00:00:00 2001 From: BBBmau Date: Mon, 11 Nov 2024 09:44:33 -0800 Subject: [PATCH 15/23] revert NonNegativeDurationValidator --- .../terraform/fwvalidators/framework_validators.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/mmv1/third_party/terraform/fwvalidators/framework_validators.go b/mmv1/third_party/terraform/fwvalidators/framework_validators.go index 018440c20a74..f0c273df7e96 100644 --- a/mmv1/third_party/terraform/fwvalidators/framework_validators.go +++ b/mmv1/third_party/terraform/fwvalidators/framework_validators.go @@ -51,21 +51,21 @@ func CredentialsValidator() validator.String { } // Non Negative Duration Validator -type nonnegativeBoundedDuration struct { +type nonnegativedurationValidator struct { } // Description describes the validation in plain text formatting. -func (v nonnegativeBoundedDuration) Description(_ context.Context) string { +func (v nonnegativedurationValidator) Description(_ context.Context) string { return "value expected to be a string representing a non-negative duration" } // MarkdownDescription describes the validation in Markdown formatting. -func (v nonnegativeBoundedDuration) MarkdownDescription(ctx context.Context) string { +func (v nonnegativedurationValidator) MarkdownDescription(ctx context.Context) string { return v.Description(ctx) } // ValidateString performs the validation. -func (v nonnegativeBoundedDuration) ValidateString(ctx context.Context, request validator.StringRequest, response *validator.StringResponse) { +func (v nonnegativedurationValidator) ValidateString(ctx context.Context, request validator.StringRequest, response *validator.StringResponse) { if request.ConfigValue.IsNull() || request.ConfigValue.IsUnknown() { return } @@ -82,8 +82,8 @@ func (v nonnegativeBoundedDuration) ValidateString(ctx context.Context, request } } -func NonNegativeBoundedDuration() validator.String { - return nonnegativeBoundedDuration{} +func NonNegativeDurationValidator() validator.String { + return nonnegativedurationValidator{} } // Non Empty String Validator From f072c5e1812e5b1dda349274e94c353fee66a97b Mon Sep 17 00:00:00 2001 From: Mauricio Alvarez Leon <65101411+BBBmau@users.noreply.github.com> Date: Mon, 11 Nov 2024 11:30:51 -0800 Subject: [PATCH 16/23] Update mmv1/third_party/terraform/fwvalidators/framework_validators_test.go Co-authored-by: Sarah French <15078782+SarahFrench@users.noreply.github.com> --- .../terraform/fwvalidators/framework_validators_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mmv1/third_party/terraform/fwvalidators/framework_validators_test.go b/mmv1/third_party/terraform/fwvalidators/framework_validators_test.go index 06455829298e..6d782bb76df6 100644 --- a/mmv1/third_party/terraform/fwvalidators/framework_validators_test.go +++ b/mmv1/third_party/terraform/fwvalidators/framework_validators_test.go @@ -216,7 +216,7 @@ func TestBoundedDuration(t *testing.T) { minDuration: 30 * time.Minute, maxDuration: time.Hour, expectError: true, - errorContains: "Duration Too Long", + errorContains: "Invalid Duration", }, "duration exceeds max - hours": { value: types.StringValue("2h"), From a04206613564b3da50b4413bbf4b20e9d10fc371 Mon Sep 17 00:00:00 2001 From: Mauricio Alvarez Leon <65101411+BBBmau@users.noreply.github.com> Date: Mon, 11 Nov 2024 11:31:00 -0800 Subject: [PATCH 17/23] Update mmv1/third_party/terraform/fwvalidators/framework_validators_test.go Co-authored-by: Sarah French <15078782+SarahFrench@users.noreply.github.com> --- .../terraform/fwvalidators/framework_validators_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mmv1/third_party/terraform/fwvalidators/framework_validators_test.go b/mmv1/third_party/terraform/fwvalidators/framework_validators_test.go index 6d782bb76df6..408747025632 100644 --- a/mmv1/third_party/terraform/fwvalidators/framework_validators_test.go +++ b/mmv1/third_party/terraform/fwvalidators/framework_validators_test.go @@ -223,7 +223,7 @@ func TestBoundedDuration(t *testing.T) { minDuration: 30 * time.Minute, maxDuration: time.Hour, expectError: true, - errorContains: "Duration Too Long", + errorContains: "Invalid Duration", }, "invalid duration format": { value: types.StringValue("invalid"), From 6bbcef9bc39698b640ad5f5144b21b78c8d5b4ad Mon Sep 17 00:00:00 2001 From: Mauricio Alvarez Leon <65101411+BBBmau@users.noreply.github.com> Date: Mon, 11 Nov 2024 11:31:07 -0800 Subject: [PATCH 18/23] Update mmv1/third_party/terraform/fwvalidators/framework_validators_test.go Co-authored-by: Sarah French <15078782+SarahFrench@users.noreply.github.com> --- .../terraform/fwvalidators/framework_validators_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mmv1/third_party/terraform/fwvalidators/framework_validators_test.go b/mmv1/third_party/terraform/fwvalidators/framework_validators_test.go index 408747025632..03a87082bdda 100644 --- a/mmv1/third_party/terraform/fwvalidators/framework_validators_test.go +++ b/mmv1/third_party/terraform/fwvalidators/framework_validators_test.go @@ -202,7 +202,7 @@ func TestBoundedDuration(t *testing.T) { minDuration: 30 * time.Minute, maxDuration: time.Hour, expectError: true, - errorContains: "Duration Too Short", + errorContains: "Invalid Duration", }, "duration exceeds max - seconds": { value: types.StringValue("7200s"), From 9f526f586d627dfc8b377cdb4da6d8118d81dbdf Mon Sep 17 00:00:00 2001 From: Mauricio Alvarez Leon <65101411+BBBmau@users.noreply.github.com> Date: Mon, 11 Nov 2024 11:31:15 -0800 Subject: [PATCH 19/23] Update mmv1/third_party/terraform/fwvalidators/framework_validators_test.go Co-authored-by: Sarah French <15078782+SarahFrench@users.noreply.github.com> --- .../terraform/fwvalidators/framework_validators_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mmv1/third_party/terraform/fwvalidators/framework_validators_test.go b/mmv1/third_party/terraform/fwvalidators/framework_validators_test.go index 03a87082bdda..6aa1a0c558a3 100644 --- a/mmv1/third_party/terraform/fwvalidators/framework_validators_test.go +++ b/mmv1/third_party/terraform/fwvalidators/framework_validators_test.go @@ -209,7 +209,7 @@ func TestBoundedDuration(t *testing.T) { minDuration: 30 * time.Minute, maxDuration: time.Hour, expectError: true, - errorContains: "Duration Too Long", + errorContains: "Invalid Duration", }, "duration exceeds max - minutes": { value: types.StringValue("120m"), From 0683149c2e6e74d8d509fb5756bd07c66e5c2841 Mon Sep 17 00:00:00 2001 From: BBBmau Date: Tue, 12 Nov 2024 02:32:42 -0800 Subject: [PATCH 20/23] add generation files for fwutils --- mmv1/provider/terraform.go | 2 +- mmv1/provider/terraform/common~copy.yaml | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/mmv1/provider/terraform.go b/mmv1/provider/terraform.go index 14aa6ddfaef9..d5ef5ea3518c 100644 --- a/mmv1/provider/terraform.go +++ b/mmv1/provider/terraform.go @@ -313,7 +313,7 @@ func (t Terraform) getCommonCopyFiles(versionName string, generateCode, generate // save the folder name to foldersCopiedToGoogleDir var foldersCopiedToGoogleDir []string if generateCode { - foldersCopiedToGoogleDir = []string{"third_party/terraform/services", "third_party/terraform/acctest", "third_party/terraform/sweeper", "third_party/terraform/provider", "third_party/terraform/tpgdclresource", "third_party/terraform/tpgiamresource", "third_party/terraform/tpgresource", "third_party/terraform/transport", "third_party/terraform/fwmodels", "third_party/terraform/fwprovider", "third_party/terraform/fwtransport", "third_party/terraform/fwresource", "third_party/terraform/verify", "third_party/terraform/envvar", "third_party/terraform/functions", "third_party/terraform/test-fixtures"} + foldersCopiedToGoogleDir = []string{"third_party/terraform/services", "third_party/terraform/acctest", "third_party/terraform/sweeper", "third_party/terraform/provider", "third_party/terraform/tpgdclresource", "third_party/terraform/tpgiamresource", "third_party/terraform/tpgresource", "third_party/terraform/transport", "third_party/terraform/fwmodels", "third_party/terraform/fwprovider", "third_party/terraform/fwtransport", "third_party/terraform/fwresource", "third_party/terraform/fwutils", "third_party/terraform/verify", "third_party/terraform/envvar", "third_party/terraform/functions", "third_party/terraform/test-fixtures"} } googleDir := "google" if versionName != "ga" { diff --git a/mmv1/provider/terraform/common~copy.yaml b/mmv1/provider/terraform/common~copy.yaml index b9ad0c850979..4ba1800202b7 100644 --- a/mmv1/provider/terraform/common~copy.yaml +++ b/mmv1/provider/terraform/common~copy.yaml @@ -101,6 +101,13 @@ '<%= dir -%>/fwprovider/<%= fname -%>': 'third_party/terraform/fwprovider/<%= fname -%>' <% end -%> +<% + Dir["third_party/terraform/fwutils/*.go"].each do |file_path| + fname = file_path.split('/')[-1] +-%> +'<%= dir -%>/fwutils/<%= fname -%>': 'third_party/terraform/fwutils/<%= fname -%>' +<% end -%> + <% Dir["third_party/terraform/fwtransport/*.go"].each do |file_path| fname = file_path.split('/')[-1] From f907caad56677d1b1ef6e9e03cdbe09e60ef5dba Mon Sep 17 00:00:00 2001 From: Mauricio Alvarez Leon <65101411+BBBmau@users.noreply.github.com> Date: Mon, 18 Nov 2024 09:30:26 -0800 Subject: [PATCH 21/23] `ephemeral`: add `ephemeral_google_service_account_access_token` (#12140) Co-authored-by: Sarah French <15078782+SarahFrench@users.noreply.github.com> --- .../fwprovider/framework_provider.go.tmpl | 2 +- ...ral_google_service_account_access_token.go | 152 ++++++++++++++++++ ...oogle_service_account_access_token_test.go | 109 +++++++++++++ ...service_account_access_token.html.markdown | 78 +++++++++ 4 files changed, 340 insertions(+), 1 deletion(-) create mode 100644 mmv1/third_party/terraform/services/resourcemanager/ephemeral_google_service_account_access_token.go create mode 100644 mmv1/third_party/terraform/services/resourcemanager/ephemeral_google_service_account_access_token_test.go create mode 100644 mmv1/third_party/terraform/website/docs/ephemeral-resources/service_account_access_token.html.markdown diff --git a/mmv1/third_party/terraform/fwprovider/framework_provider.go.tmpl b/mmv1/third_party/terraform/fwprovider/framework_provider.go.tmpl index bb85cdeb1fb6..8084ff6a4c49 100644 --- a/mmv1/third_party/terraform/fwprovider/framework_provider.go.tmpl +++ b/mmv1/third_party/terraform/fwprovider/framework_provider.go.tmpl @@ -309,6 +309,6 @@ func (p *FrameworkProvider) Functions(_ context.Context) []func() function.Funct // EphemeralResources defines the resources that are of ephemeral type implemented in the provider. func (p *FrameworkProvider) EphemeralResources(_ context.Context) []func() ephemeral.EphemeralResource { return []func() ephemeral.EphemeralResource{ - // TODO! + resourcemanager.GoogleEphemeralServiceAccountAccessToken, } } diff --git a/mmv1/third_party/terraform/services/resourcemanager/ephemeral_google_service_account_access_token.go b/mmv1/third_party/terraform/services/resourcemanager/ephemeral_google_service_account_access_token.go new file mode 100644 index 000000000000..8b7b5e7c3290 --- /dev/null +++ b/mmv1/third_party/terraform/services/resourcemanager/ephemeral_google_service_account_access_token.go @@ -0,0 +1,152 @@ +package resourcemanager + +import ( + "context" + "fmt" + "time" + + "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-provider-google/google/fwtransport" + "github.com/hashicorp/terraform-provider-google/google/fwutils" + "github.com/hashicorp/terraform-provider-google/google/fwvalidators" + "github.com/hashicorp/terraform-provider-google/google/tpgresource" + "google.golang.org/api/iamcredentials/v1" +) + +var _ ephemeral.EphemeralResource = &googleEphemeralServiceAccountAccessToken{} + +func GoogleEphemeralServiceAccountAccessToken() ephemeral.EphemeralResource { + return &googleEphemeralServiceAccountAccessToken{} +} + +type googleEphemeralServiceAccountAccessToken struct { + providerConfig *fwtransport.FrameworkProviderConfig +} + +func (p *googleEphemeralServiceAccountAccessToken) Metadata(ctx context.Context, req ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_service_account_access_token" +} + +type ephemeralServiceAccountAccessTokenModel struct { + TargetServiceAccount types.String `tfsdk:"target_service_account"` + AccessToken types.String `tfsdk:"access_token"` + Scopes types.Set `tfsdk:"scopes"` + Delegates types.Set `tfsdk:"delegates"` + Lifetime types.String `tfsdk:"lifetime"` +} + +func (p *googleEphemeralServiceAccountAccessToken) Schema(ctx context.Context, req ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema.Description = "This ephemeral resource provides a google oauth2 access_token for a different service account than the one initially running the script." + resp.Schema.MarkdownDescription = "This ephemeral resource provides a google oauth2 access_token for a different service account than the one initially running the script." + + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "target_service_account": schema.StringAttribute{ + Description: "The service account to impersonate (e.g. `service_B@your-project-id.iam.gserviceaccount.com`)", + Required: true, + Validators: []validator.String{ + fwvalidators.ServiceAccountEmailValidator{}, + }, + }, + "access_token": schema.StringAttribute{ + Description: "The `access_token` representing the new generated identity.", + Sensitive: true, + Computed: true, + }, + "lifetime": schema.StringAttribute{ + Description: "Lifetime of the impersonated token (defaults to its max: `3600s`)", + Optional: true, + Computed: true, + Validators: []validator.String{ + fwvalidators.BoundedDuration{ + MinDuration: 0, + MaxDuration: 3600 * time.Second, + }, + }, + }, + "scopes": schema.SetAttribute{ + Description: "The scopes the new credential should have (e.g. `['cloud-platform']`)", + Required: true, + ElementType: types.StringType, + }, + "delegates": schema.SetAttribute{ + Description: "Delegate chain of approvals needed to perform full impersonation. Specify the fully qualified service account name. (e.g. `['projects/-/serviceAccounts/delegate-svc-account@project-id.iam.gserviceaccount.com']`)", + Optional: true, + ElementType: types.StringType, + Validators: []validator.Set{ + setvalidator.ValueStringsAre(fwvalidators.ServiceAccountEmailValidator{}), + }, + }, + }, + } +} + +func (p *googleEphemeralServiceAccountAccessToken) Configure(ctx context.Context, req ephemeral.ConfigureRequest, resp *ephemeral.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + if req.ProviderData == nil { + return + } + + pd, ok := req.ProviderData.(*fwtransport.FrameworkProviderConfig) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Data Source Configure Type", + fmt.Sprintf("Expected *fwtransport.FrameworkProviderConfig, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + + // Required for accessing userAgent and passing as an argument into a util function + p.providerConfig = pd +} + +func (p *googleEphemeralServiceAccountAccessToken) Open(ctx context.Context, req ephemeral.OpenRequest, resp *ephemeral.OpenResponse) { + var data ephemeralServiceAccountAccessTokenModel + + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + // This is the default value for the lifetime of the access token + // Both ephemeral resources and data sources do not allow you to set a value for this attribute in the schema + if data.Lifetime.IsNull() { + data.Lifetime = types.StringValue("3600s") + } + + service := p.providerConfig.NewIamCredentialsClient(p.providerConfig.UserAgent) + name := fmt.Sprintf("projects/-/serviceAccounts/%s", data.TargetServiceAccount.ValueString()) + + ScopesSetValue, diags := data.Scopes.ToSetValue(ctx) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + var delegates []string + if !data.Delegates.IsNull() { + delegates = fwutils.StringSet(data.Delegates) + } + + tokenRequest := &iamcredentials.GenerateAccessTokenRequest{ + Lifetime: data.Lifetime.ValueString(), + Delegates: delegates, + Scope: tpgresource.CanonicalizeServiceScopes(fwutils.StringSet(ScopesSetValue)), + } + + at, err := service.Projects.ServiceAccounts.GenerateAccessToken(name, tokenRequest).Do() + if err != nil { + resp.Diagnostics.AddError( + "Error generating access token", + fmt.Sprintf("Error generating access token: %s", err), + ) + return + } + + data.AccessToken = types.StringValue(at.AccessToken) + resp.Diagnostics.Append(resp.Result.Set(ctx, data)...) +} diff --git a/mmv1/third_party/terraform/services/resourcemanager/ephemeral_google_service_account_access_token_test.go b/mmv1/third_party/terraform/services/resourcemanager/ephemeral_google_service_account_access_token_test.go new file mode 100644 index 000000000000..27ffe2af58b8 --- /dev/null +++ b/mmv1/third_party/terraform/services/resourcemanager/ephemeral_google_service_account_access_token_test.go @@ -0,0 +1,109 @@ +package resourcemanager_test + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-provider-google/google/acctest" + "github.com/hashicorp/terraform-provider-google/google/envvar" +) + +func TestAccEphemeralServiceAccountToken_basic(t *testing.T) { + t.Parallel() + + serviceAccount := envvar.GetTestServiceAccountFromEnv(t) + targetServiceAccountEmail := acctest.BootstrapServiceAccount(t, "basic", serviceAccount) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ExternalProviders: map[string]resource.ExternalProvider{ + "time": {}, + }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + Steps: []resource.TestStep{ + { + Config: testAccEphemeralServiceAccountToken_basic(targetServiceAccountEmail), + }, + }, + }) +} + +func TestAccEphemeralServiceAccountToken_withDelegates(t *testing.T) { + t.Parallel() + + project := envvar.GetTestProjectFromEnv() + initialServiceAccount := envvar.GetTestServiceAccountFromEnv(t) + delegateServiceAccountEmailOne := acctest.BootstrapServiceAccount(t, "delegate1", initialServiceAccount) // SA_2 + delegateServiceAccountEmailTwo := acctest.BootstrapServiceAccount(t, "delegate2", delegateServiceAccountEmailOne) // SA_3 + targetServiceAccountEmail := acctest.BootstrapServiceAccount(t, "target", delegateServiceAccountEmailTwo) // SA_4 + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ExternalProviders: map[string]resource.ExternalProvider{ + "time": {}, + }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + Steps: []resource.TestStep{ + { + Config: testAccEphemeralServiceAccountToken_withDelegates(initialServiceAccount, delegateServiceAccountEmailOne, delegateServiceAccountEmailTwo, targetServiceAccountEmail, project), + }, + }, + }) +} + +func TestAccEphemeralServiceAccountToken_withCustomLifetime(t *testing.T) { + t.Parallel() + + serviceAccount := envvar.GetTestServiceAccountFromEnv(t) + targetServiceAccountEmail := acctest.BootstrapServiceAccount(t, "lifetime", serviceAccount) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ExternalProviders: map[string]resource.ExternalProvider{ + "time": {}, + }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + Steps: []resource.TestStep{ + { + Config: testAccEphemeralServiceAccountToken_withCustomLifetime(targetServiceAccountEmail), + }, + }, + }) +} + +func testAccEphemeralServiceAccountToken_basic(serviceAccountEmail string) string { + return fmt.Sprintf(` +ephemeral "google_service_account_access_token" "token" { + target_service_account = "%s" + scopes = ["https://www.googleapis.com/auth/cloud-platform"] +} +`, serviceAccountEmail) +} + +func testAccEphemeralServiceAccountToken_withDelegates(initialServiceAccountEmail, delegateServiceAccountEmailOne, delegateServiceAccountEmailTwo, targetServiceAccountEmail, project string) string { + return fmt.Sprintf(` +ephemeral "google_service_account_access_token" "test" { + target_service_account = "%s" + delegates = [ + "%s", + "%s", + ] + scopes = ["https://www.googleapis.com/auth/cloud-platform"] + lifetime = "3600s" +} + +# The delegation chain is: +# SA_1 (initialServiceAccountEmail) -> SA_2 (delegateServiceAccountEmailOne) -> SA_3 (delegateServiceAccountEmailTwo) -> SA_4 (targetServiceAccountEmail) +`, targetServiceAccountEmail, delegateServiceAccountEmailOne, delegateServiceAccountEmailTwo) +} + +func testAccEphemeralServiceAccountToken_withCustomLifetime(serviceAccountEmail string) string { + return fmt.Sprintf(` +ephemeral "google_service_account_access_token" "token" { + target_service_account = "%s" + scopes = ["https://www.googleapis.com/auth/cloud-platform"] + lifetime = "3600s" +} +`, serviceAccountEmail) +} diff --git a/mmv1/third_party/terraform/website/docs/ephemeral-resources/service_account_access_token.html.markdown b/mmv1/third_party/terraform/website/docs/ephemeral-resources/service_account_access_token.html.markdown new file mode 100644 index 000000000000..c51a08df48e0 --- /dev/null +++ b/mmv1/third_party/terraform/website/docs/ephemeral-resources/service_account_access_token.html.markdown @@ -0,0 +1,78 @@ +--- +subcategory: "Cloud Platform" +description: |- + Produces access_token for impersonated service accounts +--- + +# google_service_account_access_token + +This ephemeral resource provides a google `oauth2` `access_token` for a different service account than the one initially running the script. + +For more information see +[the official documentation](https://cloud.google.com/iam/docs/creating-short-lived-service-account-credentials) as well as [iamcredentials.generateAccessToken()](https://cloud.google.com/iam/credentials/reference/rest/v1/projects.serviceAccounts/generateAccessToken) + +## Example Usage + +To allow `service_A` to impersonate `service_B`, grant the [Service Account Token Creator](https://cloud.google.com/iam/docs/service-accounts#the_service_account_token_creator_role) on B to A. + +In the IAM policy below, `service_A` is given the Token Creator role impersonate `service_B` + +```hcl +resource "google_service_account_iam_binding" "token-creator-iam" { + service_account_id = "projects/-/serviceAccounts/service_B@projectB.iam.gserviceaccount.com" + role = "roles/iam.serviceAccountTokenCreator" + members = [ + "serviceAccount:service_A@projectA.iam.gserviceaccount.com", + ] +} +``` + +Once the IAM permissions are set, you can apply the new token to a provider bootstrapped with it. Any resources that references the aliased provider will run as the new identity. + +In the example below, `google_project` will run as `service_B`. + +```hcl +provider "google" { +} + +data "google_client_config" "default" { + provider = google +} + +ephemeral "google_service_account_access_token" "default" { + provider = google + target_service_account = "service_B@projectB.iam.gserviceaccount.com" + scopes = ["userinfo-email", "cloud-platform"] + lifetime = "300s" +} + +provider "google" { + alias = "impersonated" + access_token = ephemeral.google_service_account_access_token.default.access_token +} + +data "google_client_openid_userinfo" "me" { + provider = google.impersonated +} + +output "target-email" { + value = data.google_client_openid_userinfo.me.email +} +``` + +> *Note*: the generated token is non-refreshable and can have a maximum `lifetime` of `3600` seconds. + +## Argument Reference + +The following arguments are supported: + +* `target_service_account` (Required) - The service account _to_ impersonate (e.g. `service_B@your-project-id.iam.gserviceaccount.com`) +* `scopes` (Required) - The scopes the new credential should have (e.g. `["cloud-platform"]`) +* `delegates` (Optional) - Delegate chain of approvals needed to perform full impersonation. Specify the fully qualified service account name. (e.g. `["projects/-/serviceAccounts/delegate-svc-account@project-id.iam.gserviceaccount.com"]`) +* `lifetime` (Optional) Lifetime of the impersonated token (defaults to its max: `3600s`). + +## Attributes Reference + +The following attribute is exported: + +* `access_token` - The `access_token` representing the new generated identity. From 89b1a0143545508effaf0040fd04dd56806f8624 Mon Sep 17 00:00:00 2001 From: Mauricio Alvarez Leon <65101411+BBBmau@users.noreply.github.com> Date: Wed, 20 Nov 2024 09:47:48 -0800 Subject: [PATCH 22/23] `ephemeral`: add `google_service_account_id_token` (#12141) Co-authored-by: Sarah French <15078782+SarahFrench@users.noreply.github.com> --- .../fwprovider/framework_provider.go.tmpl | 1 + ...hemeral_google_service_account_id_token.go | 171 ++++++++++++++++++ ...al_google_service_account_id_token_test.go | 107 +++++++++++ .../service_account_id_token.html.markdown | 89 +++++++++ 4 files changed, 368 insertions(+) create mode 100644 mmv1/third_party/terraform/services/resourcemanager/ephemeral_google_service_account_id_token.go create mode 100644 mmv1/third_party/terraform/services/resourcemanager/ephemeral_google_service_account_id_token_test.go create mode 100644 mmv1/third_party/terraform/website/docs/ephemeral-resources/service_account_id_token.html.markdown diff --git a/mmv1/third_party/terraform/fwprovider/framework_provider.go.tmpl b/mmv1/third_party/terraform/fwprovider/framework_provider.go.tmpl index 8084ff6a4c49..e1d28ee3b670 100644 --- a/mmv1/third_party/terraform/fwprovider/framework_provider.go.tmpl +++ b/mmv1/third_party/terraform/fwprovider/framework_provider.go.tmpl @@ -310,5 +310,6 @@ func (p *FrameworkProvider) Functions(_ context.Context) []func() function.Funct func (p *FrameworkProvider) EphemeralResources(_ context.Context) []func() ephemeral.EphemeralResource { return []func() ephemeral.EphemeralResource{ resourcemanager.GoogleEphemeralServiceAccountAccessToken, + resourcemanager.GoogleEphemeralServiceAccountIdToken, } } diff --git a/mmv1/third_party/terraform/services/resourcemanager/ephemeral_google_service_account_id_token.go b/mmv1/third_party/terraform/services/resourcemanager/ephemeral_google_service_account_id_token.go new file mode 100644 index 000000000000..312856c4d14d --- /dev/null +++ b/mmv1/third_party/terraform/services/resourcemanager/ephemeral_google_service_account_id_token.go @@ -0,0 +1,171 @@ +package resourcemanager + +import ( + "context" + "fmt" + + "google.golang.org/api/idtoken" + "google.golang.org/api/option" + + "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-provider-google/google/fwmodels" + "github.com/hashicorp/terraform-provider-google/google/fwtransport" + "github.com/hashicorp/terraform-provider-google/google/fwutils" + "github.com/hashicorp/terraform-provider-google/google/fwvalidators" + "google.golang.org/api/iamcredentials/v1" +) + +var _ ephemeral.EphemeralResource = &googleEphemeralServiceAccountIdToken{} + +func GoogleEphemeralServiceAccountIdToken() ephemeral.EphemeralResource { + return &googleEphemeralServiceAccountIdToken{} +} + +type googleEphemeralServiceAccountIdToken struct { + providerConfig *fwtransport.FrameworkProviderConfig +} + +func (p *googleEphemeralServiceAccountIdToken) Metadata(ctx context.Context, req ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_service_account_id_token" +} + +type ephemeralServiceAccountIdTokenModel struct { + TargetAudience types.String `tfsdk:"target_audience"` + TargetServiceAccount types.String `tfsdk:"target_service_account"` + Delegates types.Set `tfsdk:"delegates"` + IncludeEmail types.Bool `tfsdk:"include_email"` + IdToken types.String `tfsdk:"id_token"` +} + +func (p *googleEphemeralServiceAccountIdToken) Schema(ctx context.Context, req ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema.Description = "This ephemeral resource provides a Google OpenID Connect (oidc) id_token." + resp.Schema.MarkdownDescription = "This ephemeral resource provides a Google OpenID Connect (oidc) id_token." + + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "target_audience": schema.StringAttribute{ + Description: "The audience claim for the `id_token`.", + Required: true, + }, + "target_service_account": schema.StringAttribute{ + Description: "The email of the service account being impersonated. Used only when using impersonation mode.", + Optional: true, + Validators: []validator.String{ + fwvalidators.ServiceAccountEmailValidator{}, + }, + }, + "delegates": schema.SetAttribute{ + Description: "Delegate chain of approvals needed to perform full impersonation. Specify the fully qualified service account name. Used only when using impersonation mode.", + Optional: true, + ElementType: types.StringType, + Validators: []validator.Set{ + setvalidator.ValueStringsAre(fwvalidators.ServiceAccountEmailValidator{}), + }, + }, + "include_email": schema.BoolAttribute{ + Description: "Include the verified email in the claim. Used only when using impersonation mode.", + Optional: true, // Defaults to false when not set (Null / Unknown) + }, + "id_token": schema.StringAttribute{ + Description: "The `id_token` representing the new generated identity.", + Computed: true, + Sensitive: true, + }, + }, + } +} + +func (p *googleEphemeralServiceAccountIdToken) Configure(ctx context.Context, req ephemeral.ConfigureRequest, resp *ephemeral.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + if req.ProviderData == nil { + return + } + + pd, ok := req.ProviderData.(*fwtransport.FrameworkProviderConfig) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Data Source Configure Type", + fmt.Sprintf("Expected *fwtransport.FrameworkProviderConfig, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + + p.providerConfig = pd +} + +func (p *googleEphemeralServiceAccountIdToken) Open(ctx context.Context, req ephemeral.OpenRequest, resp *ephemeral.OpenResponse) { + var data ephemeralServiceAccountIdTokenModel + + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + + targetAudience := data.TargetAudience.ValueString() + + // TODO: This is a temporary solution to get the credentials from the provider config. + // we'll address this once muxing issues are resolved. + model := fwmodels.ProviderModel{ + Credentials: p.providerConfig.Credentials, + AccessToken: p.providerConfig.AccessToken, + ImpersonateServiceAccount: p.providerConfig.ImpersonateServiceAccount, + ImpersonateServiceAccountDelegates: p.providerConfig.ImpersonateServiceAccountDelegates, + Project: p.providerConfig.Project, + BillingProject: p.providerConfig.BillingProject, + Scopes: p.providerConfig.Scopes, + UniverseDomain: p.providerConfig.UniverseDomain, + } + creds := fwtransport.GetCredentials(ctx, model, false, &resp.Diagnostics) + targetServiceAccount := data.TargetServiceAccount + // If a target service account is provided, use the API to generate the idToken + if !targetServiceAccount.IsNull() && !targetServiceAccount.IsUnknown() { + service := p.providerConfig.NewIamCredentialsClient(p.providerConfig.UserAgent) + name := fmt.Sprintf("projects/-/serviceAccounts/%s", targetServiceAccount.ValueString()) + + tokenRequest := &iamcredentials.GenerateIdTokenRequest{ + Audience: targetAudience, + IncludeEmail: data.IncludeEmail.ValueBool(), + Delegates: fwutils.StringSet(data.Delegates), + } + at, err := service.Projects.ServiceAccounts.GenerateIdToken(name, tokenRequest).Do() + if err != nil { + resp.Diagnostics.AddError( + "Error calling iamcredentials.GenerateIdToken", + err.Error(), + ) + return + } + + data.IdToken = types.StringValue(at.Token) + resp.Diagnostics.Append(resp.Result.Set(ctx, data)...) + return + } + + // If no target service account, use the default credentials + ctx = context.Background() + co := []option.ClientOption{} + if creds.JSON != nil { + co = append(co, idtoken.WithCredentialsJSON(creds.JSON)) + } + + idTokenSource, err := idtoken.NewTokenSource(ctx, targetAudience, co...) + if err != nil { + resp.Diagnostics.AddError( + "Unable to retrieve TokenSource", + err.Error(), + ) + return + } + idToken, err := idTokenSource.Token() + if err != nil { + resp.Diagnostics.AddError( + "Unable to retrieve Token", + err.Error(), + ) + return + } + + data.IdToken = types.StringValue(idToken.AccessToken) + resp.Diagnostics.Append(resp.Result.Set(ctx, data)...) +} diff --git a/mmv1/third_party/terraform/services/resourcemanager/ephemeral_google_service_account_id_token_test.go b/mmv1/third_party/terraform/services/resourcemanager/ephemeral_google_service_account_id_token_test.go new file mode 100644 index 000000000000..b35af47cf56e --- /dev/null +++ b/mmv1/third_party/terraform/services/resourcemanager/ephemeral_google_service_account_id_token_test.go @@ -0,0 +1,107 @@ +package resourcemanager_test + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-provider-google/google/acctest" + "github.com/hashicorp/terraform-provider-google/google/envvar" +) + +func TestAccEphemeralServiceAccountIdToken_basic(t *testing.T) { + t.Parallel() + + serviceAccount := envvar.GetTestServiceAccountFromEnv(t) + targetServiceAccountEmail := acctest.BootstrapServiceAccount(t, "idtoken", serviceAccount) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + ExternalProviders: map[string]resource.ExternalProvider{ + "time": {}, + }, + Steps: []resource.TestStep{ + { + Config: testAccEphemeralServiceAccountIdToken_basic(targetServiceAccountEmail), + }, + }, + }) +} + +func TestAccEphemeralServiceAccountIdToken_withDelegates(t *testing.T) { + t.Parallel() + + initialServiceAccount := envvar.GetTestServiceAccountFromEnv(t) + delegateServiceAccountEmailOne := acctest.BootstrapServiceAccount(t, "id-delegate1", initialServiceAccount) // SA_2 + delegateServiceAccountEmailTwo := acctest.BootstrapServiceAccount(t, "id-delegate2", delegateServiceAccountEmailOne) // SA_3 + targetServiceAccountEmail := acctest.BootstrapServiceAccount(t, "id-target", delegateServiceAccountEmailTwo) // SA_4 + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + ExternalProviders: map[string]resource.ExternalProvider{ + "time": {}, + }, + Steps: []resource.TestStep{ + { + Config: testAccEphemeralServiceAccountIdToken_withDelegates(delegateServiceAccountEmailOne, delegateServiceAccountEmailTwo, targetServiceAccountEmail), + }, + }, + }) +} + +func TestAccEphemeralServiceAccountIdToken_withIncludeEmail(t *testing.T) { + t.Parallel() + + serviceAccount := envvar.GetTestServiceAccountFromEnv(t) + targetServiceAccountEmail := acctest.BootstrapServiceAccount(t, "idtoken-email", serviceAccount) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + ExternalProviders: map[string]resource.ExternalProvider{ + "time": {}, + }, + Steps: []resource.TestStep{ + { + Config: testAccEphemeralServiceAccountIdToken_withIncludeEmail(targetServiceAccountEmail), + }, + }, + }) +} + +func testAccEphemeralServiceAccountIdToken_basic(serviceAccountEmail string) string { + return fmt.Sprintf(` +ephemeral "google_service_account_id_token" "token" { + target_service_account = "%s" + target_audience = "https://example.com" +} +`, serviceAccountEmail) +} + +func testAccEphemeralServiceAccountIdToken_withDelegates(delegateServiceAccountEmailOne, delegateServiceAccountEmailTwo, targetServiceAccountEmail string) string { + return fmt.Sprintf(` +ephemeral "google_service_account_id_token" "token" { + target_service_account = "%s" + delegates = [ + "%s", + "%s", + ] + target_audience = "https://example.com" +} + +# The delegation chain is: +# SA_1 (initialServiceAccountEmail) -> SA_2 (delegateServiceAccountEmailOne) -> SA_3 (delegateServiceAccountEmailTwo) -> SA_4 (targetServiceAccountEmail) +`, targetServiceAccountEmail, delegateServiceAccountEmailOne, delegateServiceAccountEmailTwo) +} + +func testAccEphemeralServiceAccountIdToken_withIncludeEmail(serviceAccountEmail string) string { + return fmt.Sprintf(` +ephemeral "google_service_account_id_token" "token" { + target_service_account = "%s" + target_audience = "https://example.com" + include_email = true +} +`, serviceAccountEmail) +} diff --git a/mmv1/third_party/terraform/website/docs/ephemeral-resources/service_account_id_token.html.markdown b/mmv1/third_party/terraform/website/docs/ephemeral-resources/service_account_id_token.html.markdown new file mode 100644 index 000000000000..00cd9c30b0e4 --- /dev/null +++ b/mmv1/third_party/terraform/website/docs/ephemeral-resources/service_account_id_token.html.markdown @@ -0,0 +1,89 @@ +--- +subcategory: "Cloud Platform" +description: |- + Produces OpenID Connect token for service accounts +--- + +# google_service_account_id_token + +This ephemeral resource provides a Google OpenID Connect (`oidc`) `id_token`. Tokens issued from this ephemeral resource are typically used to call external services that accept OIDC tokens for authentication (e.g. [Google Cloud Run](https://cloud.google.com/run/docs/authenticating/service-to-service)). + +For more information see +[OpenID Connect](https://openid.net/specs/openid-connect-core-1_0.html#IDToken). + +## Example Usage - ServiceAccount JSON credential file. + `google_service_account_id_token` will use the configured [provider credentials](https://registry.terraform.io/providers/hashicorp/google/latest/docs/guides/provider_reference#credentials-1) + + ```hcl + ephemeral "google_service_account_id_token" "oidc" { + target_audience = "https://foo.bar/" + } + ``` + +## Example Usage - Service Account Impersonation. + `google_service_account_access_token` will use background impersonated credentials provided by [google_service_account_access_token](https://registry.terraform.io/providers/hashicorp/google/latest/docs/data-sources/service_account_access_token). + + Note: to use the following, you must grant `target_service_account` the + `roles/iam.serviceAccountTokenCreator` role on itself. + + ```hcl + data "google_service_account_access_token" "impersonated" { + provider = google + target_service_account = "impersonated-account@project.iam.gserviceaccount.com" + delegates = [] + scopes = ["userinfo-email", "cloud-platform"] + lifetime = "300s" + } + + provider "google" { + alias = "impersonated" + access_token = data.google_service_account_access_token.impersonated.access_token + } + + ephemeral "google_service_account_id_token" "oidc" { + provider = google.impersonated + target_service_account = "impersonated-account@project.iam.gserviceaccount.com" + delegates = [] + include_email = true + target_audience = "https://foo.bar/" + } + + ``` + +## Example Usage - Invoking Cloud Run Endpoint + + The following configuration will invoke [Cloud Run](https://cloud.google.com/run/docs/authenticating/service-to-service) endpoint where the service account for Terraform has been granted `roles/run.invoker` role previously. + +```hcl + +ephemeral "google_service_account_id_token" "oidc" { + target_audience = "https://your.cloud.run.app/" +} + +data "http" "cloudrun" { + url = "https://your.cloud.run.app/" + request_headers = { + Authorization = "Bearer ${ephemeral.google_service_account_id_token.oidc.id_token}" + } +} + + +output "cloud_run_response" { + value = data.http.cloudrun.body +} +``` + +## Argument Reference + +The following arguments are supported: + +* `target_audience` (Required) - The audience claim for the `id_token`. +* `target_service_account` (Optional) - The email of the service account being impersonated. Used only when using impersonation mode. +* `delegates` (Optional) - Delegate chain of approvals needed to perform full impersonation. Specify the fully qualified service account name. Used only when using impersonation mode. +* `include_email` (Optional) Include the verified email in the claim. Used only when using impersonation mode. + +## Attributes Reference + +The following attribute is exported: + +* `id_token` - The `id_token` representing the new generated identity. From b1c6428403350845a49fdc116ad38472674b1ad3 Mon Sep 17 00:00:00 2001 From: Mauricio Alvarez Leon <65101411+BBBmau@users.noreply.github.com> Date: Thu, 21 Nov 2024 09:04:33 -0800 Subject: [PATCH 23/23] `ephemeral`: add `ephemeral_google_service_account_jwt` (#12142) Co-authored-by: Sarah French <15078782+SarahFrench@users.noreply.github.com> --- .../fwprovider/framework_provider.go.tmpl | 1 + .../ephemeral_google_service_account_jwt.go | 143 ++++++++++++++++++ ...hemeral_google_service_account_jwt_test.go | 106 +++++++++++++ .../service_account_jwt.html.markdown | 41 +++++ 4 files changed, 291 insertions(+) create mode 100644 mmv1/third_party/terraform/services/resourcemanager/ephemeral_google_service_account_jwt.go create mode 100644 mmv1/third_party/terraform/services/resourcemanager/ephemeral_google_service_account_jwt_test.go create mode 100644 mmv1/third_party/terraform/website/docs/ephemeral-resources/service_account_jwt.html.markdown diff --git a/mmv1/third_party/terraform/fwprovider/framework_provider.go.tmpl b/mmv1/third_party/terraform/fwprovider/framework_provider.go.tmpl index e1d28ee3b670..b03e20310f64 100644 --- a/mmv1/third_party/terraform/fwprovider/framework_provider.go.tmpl +++ b/mmv1/third_party/terraform/fwprovider/framework_provider.go.tmpl @@ -311,5 +311,6 @@ func (p *FrameworkProvider) EphemeralResources(_ context.Context) []func() ephem return []func() ephemeral.EphemeralResource{ resourcemanager.GoogleEphemeralServiceAccountAccessToken, resourcemanager.GoogleEphemeralServiceAccountIdToken, + resourcemanager.GoogleEphemeralServiceAccountJwt, } } diff --git a/mmv1/third_party/terraform/services/resourcemanager/ephemeral_google_service_account_jwt.go b/mmv1/third_party/terraform/services/resourcemanager/ephemeral_google_service_account_jwt.go new file mode 100644 index 000000000000..7d1117f2b1d4 --- /dev/null +++ b/mmv1/third_party/terraform/services/resourcemanager/ephemeral_google_service_account_jwt.go @@ -0,0 +1,143 @@ +package resourcemanager + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" + "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-provider-google/google/fwtransport" + "github.com/hashicorp/terraform-provider-google/google/fwutils" + "github.com/hashicorp/terraform-provider-google/google/fwvalidators" + "google.golang.org/api/iamcredentials/v1" +) + +var _ ephemeral.EphemeralResource = &googleEphemeralServiceAccountJwt{} + +func GoogleEphemeralServiceAccountJwt() ephemeral.EphemeralResource { + return &googleEphemeralServiceAccountJwt{} +} + +type googleEphemeralServiceAccountJwt struct { + providerConfig *fwtransport.FrameworkProviderConfig +} + +func (p *googleEphemeralServiceAccountJwt) Metadata(ctx context.Context, req ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_service_account_jwt" +} + +type ephemeralServiceAccountJwtModel struct { + Payload types.String `tfsdk:"payload"` + ExpiresIn types.Int64 `tfsdk:"expires_in"` + TargetServiceAccount types.String `tfsdk:"target_service_account"` + Delegates types.Set `tfsdk:"delegates"` + Jwt types.String `tfsdk:"jwt"` +} + +func (p *googleEphemeralServiceAccountJwt) Schema(ctx context.Context, req ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "Produces an arbitrary self-signed JWT for service accounts.", + Attributes: map[string]schema.Attribute{ + "payload": schema.StringAttribute{ + Required: true, + Description: `A JSON-encoded JWT claims set that will be included in the signed JWT.`, + }, + "expires_in": schema.Int64Attribute{ + Optional: true, + 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.", + Validators: []validator.Int64{ + int64validator.AtLeast(1), // Must be greater than 0 + }, + }, + "target_service_account": schema.StringAttribute{ + Description: "The email of the service account that will sign the JWT.", + Required: true, + Validators: []validator.String{ + fwvalidators.ServiceAccountEmailValidator{}, + }, + }, + "delegates": schema.SetAttribute{ + Description: "Delegate chain of approvals needed to perform full impersonation. Specify the fully qualified service account name.", + Optional: true, + ElementType: types.StringType, + Validators: []validator.Set{ + setvalidator.ValueStringsAre(fwvalidators.ServiceAccountEmailValidator{}), + }, + }, + "jwt": schema.StringAttribute{ + Description: "The signed JWT containing the JWT Claims Set from the `payload`.", + Computed: true, + Sensitive: true, + }, + }, + } +} + +func (p *googleEphemeralServiceAccountJwt) Configure(ctx context.Context, req ephemeral.ConfigureRequest, resp *ephemeral.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + pd, ok := req.ProviderData.(*fwtransport.FrameworkProviderConfig) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Data Source Configure Type", + fmt.Sprintf("Expected *fwtransport.FrameworkProviderConfig, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + + p.providerConfig = pd +} + +func (p *googleEphemeralServiceAccountJwt) Open(ctx context.Context, req ephemeral.OpenRequest, resp *ephemeral.OpenResponse) { + var data ephemeralServiceAccountJwtModel + + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + + payload := data.Payload.ValueString() + + if !data.ExpiresIn.IsNull() { + expiresIn := data.ExpiresIn.ValueInt64() + var decoded map[string]interface{} + if err := json.Unmarshal([]byte(payload), &decoded); err != nil { + resp.Diagnostics.AddError("Error decoding payload", err.Error()) + return + } + + decoded["exp"] = time.Now().Add(time.Duration(expiresIn) * time.Second).Unix() + + payloadBytesWithExp, err := json.Marshal(decoded) + if err != nil { + resp.Diagnostics.AddError("Error re-encoding payload", err.Error()) + return + } + + payload = string(payloadBytesWithExp) + + } + + name := fmt.Sprintf("projects/-/serviceAccounts/%s", data.TargetServiceAccount.ValueString()) + + service := p.providerConfig.NewIamCredentialsClient(p.providerConfig.UserAgent) + jwtRequest := &iamcredentials.SignJwtRequest{ + Payload: payload, + Delegates: fwutils.StringSet(data.Delegates), + } + + jwtResponse, err := service.Projects.ServiceAccounts.SignJwt(name, jwtRequest).Do() + if err != nil { + resp.Diagnostics.AddError("Error calling iamcredentials.SignJwt", err.Error()) + return + } + + data.Jwt = types.StringValue(jwtResponse.SignedJwt) + + resp.Diagnostics.Append(resp.Result.Set(ctx, data)...) +} diff --git a/mmv1/third_party/terraform/services/resourcemanager/ephemeral_google_service_account_jwt_test.go b/mmv1/third_party/terraform/services/resourcemanager/ephemeral_google_service_account_jwt_test.go new file mode 100644 index 000000000000..89091c887bfd --- /dev/null +++ b/mmv1/third_party/terraform/services/resourcemanager/ephemeral_google_service_account_jwt_test.go @@ -0,0 +1,106 @@ +package resourcemanager_test + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-provider-google/google/acctest" + "github.com/hashicorp/terraform-provider-google/google/envvar" +) + +func TestAccEphemeralServiceAccountJwt_basic(t *testing.T) { + t.Parallel() + + serviceAccount := envvar.GetTestServiceAccountFromEnv(t) + targetServiceAccountEmail := acctest.BootstrapServiceAccount(t, "jwt-basic", serviceAccount) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + Steps: []resource.TestStep{ + { + Config: testAccEphemeralServiceAccountJwt_basic(targetServiceAccountEmail), + }, + }, + }) +} + +func TestAccEphemeralServiceAccountJwt_withDelegates(t *testing.T) { + t.Parallel() + + initialServiceAccount := envvar.GetTestServiceAccountFromEnv(t) + delegateServiceAccountEmailOne := acctest.BootstrapServiceAccount(t, "jwt-delegate1", initialServiceAccount) // SA_2 + delegateServiceAccountEmailTwo := acctest.BootstrapServiceAccount(t, "jwt-delegate2", delegateServiceAccountEmailOne) // SA_3 + targetServiceAccountEmail := acctest.BootstrapServiceAccount(t, "jwt-target", delegateServiceAccountEmailTwo) // SA_4 + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + Steps: []resource.TestStep{ + { + Config: testAccEphemeralServiceAccountJwt_withDelegates(delegateServiceAccountEmailOne, delegateServiceAccountEmailTwo, targetServiceAccountEmail), + }, + }, + }) +} + +func TestAccEphemeralServiceAccountJwt_withExpiresIn(t *testing.T) { + t.Parallel() + + serviceAccount := envvar.GetTestServiceAccountFromEnv(t) + targetServiceAccountEmail := acctest.BootstrapServiceAccount(t, "expiry", serviceAccount) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + Steps: []resource.TestStep{ + { + Config: testAccEphemeralServiceAccountJwt_withExpiresIn(targetServiceAccountEmail), + }, + }, + }) +} + +func testAccEphemeralServiceAccountJwt_basic(serviceAccountEmail string) string { + return fmt.Sprintf(` +ephemeral "google_service_account_jwt" "jwt" { + target_service_account = "%s" + payload = jsonencode({ + "sub": "%[1]s", + "aud": "https://example.com" + }) +} +`, serviceAccountEmail) +} + +func testAccEphemeralServiceAccountJwt_withDelegates(delegateServiceAccountEmailOne, delegateServiceAccountEmailTwo, targetServiceAccountEmail string) string { + return fmt.Sprintf(` +ephemeral "google_service_account_jwt" "jwt" { + target_service_account = "%s" + delegates = [ + "%s", + "%s", + ] + payload = jsonencode({ + "sub": "%[1]s", + "aud": "https://example.com" + }) +} +# The delegation chain is: +# SA_1 (initialServiceAccountEmail) -> SA_2 (delegateServiceAccountEmailOne) -> SA_3 (delegateServiceAccountEmailTwo) -> SA_4 (targetServiceAccountEmail) +`, targetServiceAccountEmail, delegateServiceAccountEmailOne, delegateServiceAccountEmailTwo) +} + +func testAccEphemeralServiceAccountJwt_withExpiresIn(serviceAccountEmail string) string { + return fmt.Sprintf(` +ephemeral "google_service_account_jwt" "jwt" { + target_service_account = "%s" + expires_in = 3600 + payload = jsonencode({ + "sub": "%[1]s", + "aud": "https://example.com" + }) +} +`, serviceAccountEmail) +} diff --git a/mmv1/third_party/terraform/website/docs/ephemeral-resources/service_account_jwt.html.markdown b/mmv1/third_party/terraform/website/docs/ephemeral-resources/service_account_jwt.html.markdown new file mode 100644 index 000000000000..ea9d1413cf3d --- /dev/null +++ b/mmv1/third_party/terraform/website/docs/ephemeral-resources/service_account_jwt.html.markdown @@ -0,0 +1,41 @@ +--- +subcategory: "Cloud Platform" +description: |- + Produces an arbitrary self-signed JWT for service accounts +--- + +# google_service_account_jwt + +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. + +## Example Usage + +Note: in order to use the following, the caller must have _at least_ `roles/iam.serviceAccountTokenCreator` on the `target_service_account`. + +```hcl +ephemeral "google_service_account_jwt" "foo" { + target_service_account = "impersonated-account@project.iam.gserviceaccount.com" + + payload = jsonencode({ + foo: "bar", + sub: "subject", + }) + + expires_in = 60 +} +``` + +## Argument Reference + +The following arguments are supported: + +* `target_service_account` (Required) - The email of the service account that will sign the JWT. +* `payload` (Required) - The JSON-encoded JWT claims set to include in the self-signed JWT. +* `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. +* `delegates` (Optional) - Delegate chain of approvals needed to perform full impersonation. Specify the fully qualified service account name. + +## Attributes Reference + +The following attribute is exported: + +* `jwt` - The signed JWT containing the JWT Claims Set from the `payload`.