From 1c7a3c0e74358a058455d938ccc8cd8540f5e29e Mon Sep 17 00:00:00 2001 From: Jay Mundrawala Date: Fri, 17 Apr 2026 09:15:52 -0500 Subject: [PATCH 1/3] Add WIF credential input to integration and export resources Commit bd71f95 exposed the server-computed `wif_subject` as a read-only attribute. This commit makes WIF usable end-to-end by allowing customers to configure WIF credentials in Terraform instead of static service account JSON / IAM access keys. - mondoo_integration_gcp, mondoo_export_gcs_bucket: `credentials.private_key` is now optional; adds `credentials.wif { audience, service_account_email }` as an alternative. ConflictsWith + AtLeastOneOf enforce exactly one auth method. - mondoo_integration_aws: adds `credentials.wif { audience, role_arn }` alongside the existing `role` and `key` options. - mondoo_export_bigquery: `service_account_key` is now optional with RequiresReplace dropped; adds `credentials.wif { audience, service_account_email }` gated by ExactlyOneOf so users can flip between static creds and WIF without recreating the export. Read and ImportState round-trip the new WIF fields (wifAudience / wifServiceAccountEmail for GCP-family, wifAudience / wifRoleArn for AWS) from the server response. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/resources/export_bigquery.md | 18 ++- docs/resources/export_gcs_bucket.md | 13 +- docs/resources/integration_aws.md | 16 ++- docs/resources/integration_gcp.md | 13 +- internal/provider/export_bigquery.go | 95 +++++++++++--- internal/provider/export_gcs_bucket.go | 118 +++++++++++++----- internal/provider/gql.go | 24 ++-- internal/provider/integration_aws_resource.go | 104 ++++++++++++--- internal/provider/integration_gcp_resource.go | 101 ++++++++++++--- 9 files changed, 404 insertions(+), 98 deletions(-) diff --git a/docs/resources/export_bigquery.md b/docs/resources/export_bigquery.md index b592e8b..91752da 100644 --- a/docs/resources/export_bigquery.md +++ b/docs/resources/export_bigquery.md @@ -36,14 +36,30 @@ Export data to Google BigQuery. - `dataset_id` (String) Target BigQuery dataset (project-id.dataset_id). - `name` (String) A descriptive name for the integration. -- `service_account_key` (String, Sensitive) Google service account JSON key content. ### Optional +- `credentials` (Attributes) Credentials for the BigQuery export. Provide `wif` for workload identity federation instead of the top-level `service_account_key`. (see [below for nested schema](#nestedatt--credentials)) - `scope_mrn` (String) The MRN of the scope (space, organization, or platform) for the export integration. +- `service_account_key` (String, Sensitive) Google service account JSON key content. Mutually exclusive with `credentials.wif`. - `space_id` (String, Deprecated) Mondoo space identifier. If there is no space ID, the provider space is used. ### Read-Only - `mrn` (String) Mondoo resource name (MRN) of the integration. - `wif_subject` (String) Computed OIDC subject used when Mondoo requests a WIF token for this integration. Configure your cloud provider's trust policy to accept this subject. + + +### Nested Schema for `credentials` + +Optional: + +- `wif` (Attributes) Workload identity federation configuration. Mutually exclusive with `service_account_key`. (see [below for nested schema](#nestedatt--credentials--wif)) + + +### Nested Schema for `credentials.wif` + +Required: + +- `audience` (String) WIF audience URL for GCP workload identity federation. +- `service_account_email` (String) GCP service account email impersonated via workload identity federation. diff --git a/docs/resources/export_gcs_bucket.md b/docs/resources/export_gcs_bucket.md index 7376f1c..a64951a 100644 --- a/docs/resources/export_gcs_bucket.md +++ b/docs/resources/export_gcs_bucket.md @@ -41,7 +41,7 @@ Export data to a Google Cloud Storage bucket. ### Required - `bucket_name` (String) Name of the Google Cloud Storage bucket to export data to. -- `credentials` (Attributes) Credentials for the Google Cloud Storage bucket. (see [below for nested schema](#nestedatt--credentials)) +- `credentials` (Attributes) Credentials for the Google Cloud Storage bucket. Provide either a static service account `private_key` or a `wif` block for workload identity federation. (see [below for nested schema](#nestedatt--credentials)) - `name` (String) Name of the export integration. ### Optional @@ -58,6 +58,15 @@ Export data to a Google Cloud Storage bucket. ### Nested Schema for `credentials` +Optional: + +- `private_key` (String, Sensitive) Private key for the service account in JSON format. Mutually exclusive with `wif`. +- `wif` (Attributes) Workload identity federation configuration. Mutually exclusive with `private_key`. (see [below for nested schema](#nestedatt--credentials--wif)) + + +### Nested Schema for `credentials.wif` + Required: -- `private_key` (String, Sensitive) Private key for the service account in JSON format. +- `audience` (String) WIF audience URL for GCP workload identity federation. +- `service_account_email` (String) GCP service account email impersonated via workload identity federation. diff --git a/docs/resources/integration_aws.md b/docs/resources/integration_aws.md index 7a63ef9..f888427 100644 --- a/docs/resources/integration_aws.md +++ b/docs/resources/integration_aws.md @@ -47,7 +47,7 @@ resource "mondoo_integration_aws" "name" { ### Required -- `credentials` (Attributes) (see [below for nested schema](#nestedatt--credentials)) +- `credentials` (Attributes) Credentials for the AWS integration. Exactly one of `role`, `key`, or `wif` must be configured. (see [below for nested schema](#nestedatt--credentials)) - `name` (String) Name of the integration. ### Optional @@ -64,8 +64,9 @@ resource "mondoo_integration_aws" "name" { Optional: -- `key` (Attributes) (see [below for nested schema](#nestedatt--credentials--key)) -- `role` (Attributes) (see [below for nested schema](#nestedatt--credentials--role)) +- `key` (Attributes) Static IAM access key credentials. Mutually exclusive with `role` and `wif`. (see [below for nested schema](#nestedatt--credentials--key)) +- `role` (Attributes) IAM role credentials. Mutually exclusive with `key` and `wif`. (see [below for nested schema](#nestedatt--credentials--role)) +- `wif` (Attributes) Workload identity federation credentials. Uses Mondoo as an OIDC identity provider to assume an IAM role via web identity. Mutually exclusive with `role` and `key`. (see [below for nested schema](#nestedatt--credentials--wif)) ### Nested Schema for `credentials.key` @@ -87,6 +88,15 @@ Optional: - `external_id` (String, Sensitive) + + +### Nested Schema for `credentials.wif` + +Required: + +- `audience` (String) Audience value configured in the AWS IAM OIDC identity provider. +- `role_arn` (String) ARN of the IAM role to assume via web identity federation. + ## Import Import is supported using the following syntax: diff --git a/docs/resources/integration_gcp.md b/docs/resources/integration_gcp.md index 49c550f..70b02fd 100644 --- a/docs/resources/integration_gcp.md +++ b/docs/resources/integration_gcp.md @@ -73,7 +73,7 @@ resource "mondoo_integration_gcp" "name" { ### Required -- `credentials` (Attributes) (see [below for nested schema](#nestedatt--credentials)) +- `credentials` (Attributes) Credentials for the GCP integration. Provide either a static service account `private_key` or a `wif` block for workload identity federation. (see [below for nested schema](#nestedatt--credentials)) - `name` (String) Name of the integration. ### Optional @@ -89,9 +89,18 @@ resource "mondoo_integration_gcp" "name" { ### Nested Schema for `credentials` +Optional: + +- `private_key` (String, Sensitive) GCP service account JSON key. Mutually exclusive with `wif`. +- `wif` (Attributes) Workload identity federation configuration. Mutually exclusive with `private_key`. (see [below for nested schema](#nestedatt--credentials--wif)) + + +### Nested Schema for `credentials.wif` + Required: -- `private_key` (String, Sensitive) +- `audience` (String) WIF audience URL for GCP workload identity federation. +- `service_account_email` (String) GCP service account email impersonated via workload identity federation. ## Import diff --git a/internal/provider/export_bigquery.go b/internal/provider/export_bigquery.go index 32c58d8..4b6d92f 100644 --- a/internal/provider/export_bigquery.go +++ b/internal/provider/export_bigquery.go @@ -7,6 +7,7 @@ import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework-validators/resourcevalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" @@ -21,6 +22,7 @@ import ( // Ensure provider defined types fully satisfy framework interfaces. var _ resource.Resource = &ExportBigQueryResource{} +var _ resource.ResourceWithConfigValidators = &ExportBigQueryResource{} func NewMondooExportBigQueryResource() resource.Resource { return &ExportBigQueryResource{} @@ -42,7 +44,29 @@ type BigQueryExportResourceModel struct { WifSubject types.String `tfsdk:"wif_subject"` // credentials - ServiceAccountKey types.String `tfsdk:"service_account_key"` + ServiceAccountKey types.String `tfsdk:"service_account_key"` + Credentials *exportBigQueryCredentialsWrapper `tfsdk:"credentials"` +} + +type exportBigQueryCredentialsWrapper struct { + Wif *gcpWifCredentialModel `tfsdk:"wif"` +} + +func (m BigQueryExportResourceModel) GetConfigurationOptions() *mondoov1.BigqueryConfigurationOptionsInput { + opts := &mondoov1.BigqueryConfigurationOptionsInput{ + DatasetId: mondoov1.String(m.DatasetID.ValueString()), + } + + if !m.ServiceAccountKey.IsNull() && !m.ServiceAccountKey.IsUnknown() { + opts.ServiceAccount = mondoov1.NewStringPtr(mondoov1.String(m.ServiceAccountKey.ValueString())) + } + + if m.Credentials != nil && m.Credentials.Wif != nil { + opts.WifAudience = mondoov1.NewStringPtr(mondoov1.String(m.Credentials.Wif.Audience.ValueString())) + opts.WifServiceAccountEmail = mondoov1.NewStringPtr(mondoov1.String(m.Credentials.Wif.ServiceAccountEmail.ValueString())) + } + + return opts } func (r *ExportBigQueryResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { @@ -109,11 +133,33 @@ func (r *ExportBigQueryResource) Schema(ctx context.Context, req resource.Schema }, }, "service_account_key": schema.StringAttribute{ - MarkdownDescription: "Google service account JSON key content.", - Required: true, + MarkdownDescription: "Google service account JSON key content. Mutually exclusive with `credentials.wif`.", + Optional: true, Sensitive: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), + Validators: []validator.String{ + stringvalidator.ConflictsWith( + path.MatchRoot("credentials").AtName("wif"), + ), + }, + }, + "credentials": schema.SingleNestedAttribute{ + MarkdownDescription: "Credentials for the BigQuery export. Provide `wif` for workload identity federation instead of the top-level `service_account_key`.", + Optional: true, + Attributes: map[string]schema.Attribute{ + "wif": schema.SingleNestedAttribute{ + MarkdownDescription: "Workload identity federation configuration. Mutually exclusive with `service_account_key`.", + Optional: true, + Attributes: map[string]schema.Attribute{ + "audience": schema.StringAttribute{ + MarkdownDescription: "WIF audience URL for GCP workload identity federation.", + Required: true, + }, + "service_account_email": schema.StringAttribute{ + MarkdownDescription: "GCP service account email impersonated via workload identity federation.", + Required: true, + }, + }, + }, }, }, "wif_subject": schema.StringAttribute{ @@ -127,6 +173,15 @@ func (r *ExportBigQueryResource) Schema(ctx context.Context, req resource.Schema } } +func (r *ExportBigQueryResource) ConfigValidators(ctx context.Context) []resource.ConfigValidator { + return []resource.ConfigValidator{ + resourcevalidator.ExactlyOneOf( + path.MatchRoot("service_account_key"), + path.MatchRoot("credentials").AtName("wif"), + ), + } +} + func (r *ExportBigQueryResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { // Prevent panic if the provider has not been configured. if req.ProviderData == nil { @@ -157,10 +212,7 @@ func (r *ExportBigQueryResource) Create(ctx context.Context, req resource.Create } configOpts := mondoov1.ClientIntegrationConfigurationInput{ - BigqueryConfigurationOptions: &mondoov1.BigqueryConfigurationOptionsInput{ - DatasetId: mondoov1.String(data.DatasetID.ValueString()), - ServiceAccount: mondoov1.NewStringPtr(mondoov1.String(data.ServiceAccountKey.ValueString())), - }, + BigqueryConfigurationOptions: data.GetConfigurationOptions(), } var integration *CreateClientIntegrationPayload @@ -248,8 +300,13 @@ func (r *ExportBigQueryResource) Read(ctx context.Context, req resource.ReadRequ } // Update the state with the latest information + opts := integration.ConfigurationOptions.BigqueryConfigurationOptions data.Name = types.StringValue(integration.Name) - data.WifSubject = types.StringValue(integration.ConfigurationOptions.BigqueryConfigurationOptions.WifSubject) + data.WifSubject = types.StringValue(opts.WifSubject) + if data.Credentials != nil && data.Credentials.Wif != nil { + data.Credentials.Wif.Audience = types.StringValue(opts.WifAudience) + data.Credentials.Wif.ServiceAccountEmail = types.StringValue(opts.WifServiceAccountEmail) + } // Note: We don't update service_account_key to avoid showing sensitive data // Save updated data into Terraform state @@ -271,10 +328,7 @@ func (r *ExportBigQueryResource) Update(ctx context.Context, req resource.Update data.Name.ValueString(), mondoov1.ClientIntegrationTypeBigquery, mondoov1.ClientIntegrationConfigurationInput{ - BigqueryConfigurationOptions: &mondoov1.BigqueryConfigurationOptionsInput{ - DatasetId: mondoov1.String(data.DatasetID.ValueString()), - ServiceAccount: mondoov1.NewStringPtr(mondoov1.String(data.ServiceAccountKey.ValueString())), - }, + BigqueryConfigurationOptions: data.GetConfigurationOptions(), }) if err != nil { @@ -308,14 +362,23 @@ func (r *ExportBigQueryResource) ImportState(ctx context.Context, req resource.I return } + opts := integration.ConfigurationOptions.BigqueryConfigurationOptions model := BigQueryExportResourceModel{ Mrn: types.StringValue(integration.Mrn), Name: types.StringValue(integration.Name), ScopeMrn: types.StringValue(integration.ScopeMRN()), - DatasetID: types.StringValue(integration.ConfigurationOptions.BigqueryConfigurationOptions.DatasetId), - WifSubject: types.StringValue(integration.ConfigurationOptions.BigqueryConfigurationOptions.WifSubject), + DatasetID: types.StringValue(opts.DatasetId), + WifSubject: types.StringValue(opts.WifSubject), ServiceAccountKey: types.StringPointerValue(nil), // Don't expose sensitive data } + if opts.WifAudience != "" { + model.Credentials = &exportBigQueryCredentialsWrapper{ + Wif: &gcpWifCredentialModel{ + Audience: types.StringValue(opts.WifAudience), + ServiceAccountEmail: types.StringValue(opts.WifServiceAccountEmail), + }, + } + } if integration.IsSpaceScoped() { model.SpaceID = types.StringValue(integration.SpaceID()) diff --git a/internal/provider/export_gcs_bucket.go b/internal/provider/export_gcs_bucket.go index c95625e..e0efe2d 100644 --- a/internal/provider/export_gcs_bucket.go +++ b/internal/provider/export_gcs_bucket.go @@ -8,6 +8,8 @@ import ( "fmt" "strings" + "github.com/hashicorp/terraform-plugin-framework-validators/objectvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/resourcevalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" @@ -23,6 +25,7 @@ import ( // Ensure provider defined types fully satisfy framework interfaces. var _ resource.Resource = &ExportGcsBucketResource{} +var _ resource.ResourceWithConfigValidators = &ExportGcsBucketResource{} func NewMondooExportGSCBucketResource() resource.Resource { return &ExportGcsBucketResource{} @@ -45,7 +48,35 @@ type ExportGcsBucketResourceModel struct { WifSubject types.String `tfsdk:"wif_subject"` // credentials - Credential gcsBucketExportCredentialModel `tfsdk:"credentials"` + Credential exportGcsBucketCredentialModel `tfsdk:"credentials"` +} + +type exportGcsBucketCredentialModel struct { + PrivateKey types.String `tfsdk:"private_key"` + Wif *gcpWifCredentialModel `tfsdk:"wif"` +} + +func (m ExportGcsBucketResourceModel) GetConfigurationOptions() *mondoov1.GcsBucketConfigurationOptionsInput { + outputFormat := mondoov1.BucketOutputTypeJsonl + if strings.ToLower(m.ExportFormat.ValueString()) == "csv" { + outputFormat = mondoov1.BucketOutputTypeCsv + } + + opts := &mondoov1.GcsBucketConfigurationOptionsInput{ + Output: outputFormat, + Bucket: mondoov1.String(m.BucketName.ValueString()), + } + + if !m.Credential.PrivateKey.IsNull() && !m.Credential.PrivateKey.IsUnknown() { + opts.ServiceAccount = mondoov1.NewStringPtr(mondoov1.String(m.Credential.PrivateKey.ValueString())) + } + + if m.Credential.Wif != nil { + opts.WifAudience = mondoov1.NewStringPtr(mondoov1.String(m.Credential.Wif.Audience.ValueString())) + opts.WifServiceAccountEmail = mondoov1.NewStringPtr(mondoov1.String(m.Credential.Wif.ServiceAccountEmail.ValueString())) + } + + return opts } func (r *ExportGcsBucketResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { @@ -132,13 +163,37 @@ func (r *ExportGcsBucketResource) Schema(ctx context.Context, req resource.Schem }, }, "credentials": schema.SingleNestedAttribute{ - MarkdownDescription: "Credentials for the Google Cloud Storage bucket.", + MarkdownDescription: "Credentials for the Google Cloud Storage bucket. Provide either a static service account `private_key` or a `wif` block for workload identity federation.", Required: true, Attributes: map[string]schema.Attribute{ "private_key": schema.StringAttribute{ - MarkdownDescription: "Private key for the service account in JSON format.", - Required: true, + MarkdownDescription: "Private key for the service account in JSON format. Mutually exclusive with `wif`.", + Optional: true, Sensitive: true, + Validators: []validator.String{ + stringvalidator.ConflictsWith( + path.MatchRoot("credentials").AtName("wif"), + ), + }, + }, + "wif": schema.SingleNestedAttribute{ + MarkdownDescription: "Workload identity federation configuration. Mutually exclusive with `private_key`.", + Optional: true, + Attributes: map[string]schema.Attribute{ + "audience": schema.StringAttribute{ + MarkdownDescription: "WIF audience URL for GCP workload identity federation.", + Required: true, + }, + "service_account_email": schema.StringAttribute{ + MarkdownDescription: "GCP service account email impersonated via workload identity federation.", + Required: true, + }, + }, + Validators: []validator.Object{ + objectvalidator.ConflictsWith( + path.MatchRoot("credentials").AtName("private_key"), + ), + }, }, }, }, @@ -146,6 +201,15 @@ func (r *ExportGcsBucketResource) Schema(ctx context.Context, req resource.Schem } } +func (r *ExportGcsBucketResource) ConfigValidators(ctx context.Context) []resource.ConfigValidator { + return []resource.ConfigValidator{ + resourcevalidator.AtLeastOneOf( + path.MatchRoot("credentials").AtName("private_key"), + path.MatchRoot("credentials").AtName("wif"), + ), + } +} + func (r *ExportGcsBucketResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { // Prevent panic if the provider has not been configured. if req.ProviderData == nil { @@ -175,18 +239,8 @@ func (r *ExportGcsBucketResource) Create(ctx context.Context, req resource.Creat return } - // Determine output format - outputFormat := mondoov1.BucketOutputTypeJsonl - if strings.ToLower(data.ExportFormat.ValueString()) == "csv" { - outputFormat = mondoov1.BucketOutputTypeCsv - } - configOpts := mondoov1.ClientIntegrationConfigurationInput{ - GcsBucketConfigurationOptions: &mondoov1.GcsBucketConfigurationOptionsInput{ - Output: outputFormat, - Bucket: mondoov1.String(data.BucketName.ValueString()), - ServiceAccount: mondoov1.NewStringPtr(mondoov1.String(data.Credential.PrivateKey.ValueString())), - }, + GcsBucketConfigurationOptions: data.GetConfigurationOptions(), } var integration *CreateClientIntegrationPayload @@ -273,7 +327,12 @@ func (r *ExportGcsBucketResource) Read(ctx context.Context, req resource.ReadReq resp.Diagnostics.AddError("Error reading GCS bucket export integration", err.Error()) return } - data.WifSubject = types.StringValue(integration.ConfigurationOptions.GcsBucketConfigurationOptions.WifSubject) + opts := integration.ConfigurationOptions.GcsBucketConfigurationOptions + data.WifSubject = types.StringValue(opts.WifSubject) + if data.Credential.Wif != nil { + data.Credential.Wif.Audience = types.StringValue(opts.WifAudience) + data.Credential.Wif.ServiceAccountEmail = types.StringValue(opts.WifServiceAccountEmail) + } // Save updated data into Terraform state resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) @@ -288,23 +347,13 @@ func (r *ExportGcsBucketResource) Update(ctx context.Context, req resource.Updat return } - // Determine output format - outputFormat := mondoov1.BucketOutputTypeJsonl - if strings.ToLower(data.ExportFormat.ValueString()) == "csv" { - outputFormat = mondoov1.BucketOutputTypeCsv - } - // Do GraphQL request to API to update the resource. _, err := r.client.UpdateIntegration(ctx, data.Mrn.ValueString(), data.Name.ValueString(), mondoov1.ClientIntegrationTypeGcsBucket, mondoov1.ClientIntegrationConfigurationInput{ - GcsBucketConfigurationOptions: &mondoov1.GcsBucketConfigurationOptionsInput{ - Output: outputFormat, - Bucket: mondoov1.String(data.BucketName.ValueString()), - ServiceAccount: mondoov1.NewStringPtr(mondoov1.String(data.Credential.PrivateKey.ValueString())), - }, + GcsBucketConfigurationOptions: data.GetConfigurationOptions(), }) if err != nil { @@ -337,18 +386,25 @@ func (r *ExportGcsBucketResource) ImportState(ctx context.Context, req resource. return } + opts := integration.ConfigurationOptions.GcsBucketConfigurationOptions model := ExportGcsBucketResourceModel{ Mrn: types.StringValue(integration.Mrn), Name: types.StringValue(integration.Name), ScopeMrn: types.StringValue(integration.ScopeMRN()), - BucketName: types.StringValue(integration.ConfigurationOptions.GcsBucketConfigurationOptions.Bucket), - ExportFormat: types.StringValue(integration.ConfigurationOptions.GcsBucketConfigurationOptions.Output), - WifSubject: types.StringValue(integration.ConfigurationOptions.GcsBucketConfigurationOptions.WifSubject), + BucketName: types.StringValue(opts.Bucket), + ExportFormat: types.StringValue(opts.Output), + WifSubject: types.StringValue(opts.WifSubject), - Credential: gcsBucketExportCredentialModel{ + Credential: exportGcsBucketCredentialModel{ PrivateKey: types.StringPointerValue(nil), }, } + if opts.WifAudience != "" { + model.Credential.Wif = &gcpWifCredentialModel{ + Audience: types.StringValue(opts.WifAudience), + ServiceAccountEmail: types.StringValue(opts.WifServiceAccountEmail), + } + } if integration.IsSpaceScoped() { model.SpaceID = types.StringValue(integration.SpaceID()) diff --git a/internal/provider/gql.go b/internal/provider/gql.go index 4ba9151..f105b2a 100644 --- a/internal/provider/gql.go +++ b/internal/provider/gql.go @@ -839,9 +839,11 @@ type GithubConfigurationOptions struct { } type GcsBucketConfigurationOptions struct { - Bucket string - Output string - WifSubject string + Bucket string + Output string + WifAudience string + WifServiceAccountEmail string + WifSubject string } type AwsS3ConfigurationOptions struct { @@ -851,8 +853,10 @@ type AwsS3ConfigurationOptions struct { } type BigqueryConfigurationOptions struct { - DatasetId string - WifSubject string + DatasetId string + WifAudience string + WifServiceAccountEmail string + WifSubject string } type GitlabConfigurationOptions struct { @@ -877,13 +881,17 @@ type MsIntuneConfigurationOptions struct { type HostedAwsConfigurationOptions struct { AccessKeyId string Role string + WifAudience string + WifRoleArn string WifSubject string } type GcpConfigurationOptions struct { - ProjectId string - DiscoverAll bool - WifSubject string + ProjectId string + DiscoverAll bool + WifAudience string + WifServiceAccountEmail string + WifSubject string } type ShodanConfigurationOptions struct { diff --git a/internal/provider/integration_aws_resource.go b/internal/provider/integration_aws_resource.go index c3a1a6c..d989ee5 100644 --- a/internal/provider/integration_aws_resource.go +++ b/internal/provider/integration_aws_resource.go @@ -9,6 +9,7 @@ import ( "regexp" "github.com/hashicorp/terraform-plugin-framework-validators/objectvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/resourcevalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" @@ -24,6 +25,7 @@ import ( // Ensure provider defined types fully satisfy framework interfaces. var _ resource.Resource = (*integrationAwsResource)(nil) var _ resource.ResourceWithImportState = (*integrationAwsResource)(nil) +var _ resource.ResourceWithConfigValidators = (*integrationAwsResource)(nil) func NewIntegrationAwsResource() resource.Resource { return &integrationAwsResource{} @@ -49,6 +51,7 @@ type integrationAwsResourceModel struct { type integrationAwsCredentialModel struct { Role *roleCredentialModel `tfsdk:"role"` Key *accessKeyCredentialModel `tfsdk:"key"` + Wif *awsWifCredentialModel `tfsdk:"wif"` } type roleCredentialModel struct { @@ -61,6 +64,11 @@ type accessKeyCredentialModel struct { SecretKey types.String `tfsdk:"secret_key"` } +type awsWifCredentialModel struct { + Audience types.String `tfsdk:"audience"` + RoleArn types.String `tfsdk:"role_arn"` +} + func (m integrationAwsResourceModel) GetConfigurationOptions() *mondoov1.HostedAwsConfigurationOptionsInput { opts := &mondoov1.HostedAwsConfigurationOptionsInput{} @@ -84,6 +92,13 @@ func (m integrationAwsResourceModel) GetConfigurationOptions() *mondoov1.HostedA } } + if m.Credential.Wif != nil { + opts.WifCredential = &mondoov1.AWSWifCredential{ + Audience: mondoov1.String(m.Credential.Wif.Audience.ValueString()), + RoleArn: mondoov1.String(m.Credential.Wif.RoleArn.ValueString()), + } + } + return opts } @@ -125,10 +140,12 @@ func (r *integrationAwsResource) Schema(ctx context.Context, req resource.Schema }, }, "credentials": schema.SingleNestedAttribute{ - Required: true, + MarkdownDescription: "Credentials for the AWS integration. Exactly one of `role`, `key`, or `wif` must be configured.", + Required: true, Attributes: map[string]schema.Attribute{ "role": schema.SingleNestedAttribute{ - Optional: true, + MarkdownDescription: "IAM role credentials. Mutually exclusive with `key` and `wif`.", + Optional: true, Attributes: map[string]schema.Attribute{ "role_arn": schema.StringAttribute{ Required: true, @@ -140,14 +157,15 @@ func (r *integrationAwsResource) Schema(ctx context.Context, req resource.Schema }, }, Validators: []validator.Object{ - // Validate this attribute must not be configured with other_attr. - objectvalidator.ConflictsWith(path.Expressions{ + objectvalidator.ConflictsWith( path.MatchRoot("credentials").AtName("key"), - }...), + path.MatchRoot("credentials").AtName("wif"), + ), }, }, "key": schema.SingleNestedAttribute{ - Optional: true, + MarkdownDescription: "Static IAM access key credentials. Mutually exclusive with `role` and `wif`.", + Optional: true, Attributes: map[string]schema.Attribute{ "access_key": schema.StringAttribute{ Required: true, @@ -170,6 +188,32 @@ func (r *integrationAwsResource) Schema(ctx context.Context, req resource.Schema }, }, }, + Validators: []validator.Object{ + objectvalidator.ConflictsWith( + path.MatchRoot("credentials").AtName("role"), + path.MatchRoot("credentials").AtName("wif"), + ), + }, + }, + "wif": schema.SingleNestedAttribute{ + MarkdownDescription: "Workload identity federation credentials. Uses Mondoo as an OIDC identity provider to assume an IAM role via web identity. Mutually exclusive with `role` and `key`.", + Optional: true, + Attributes: map[string]schema.Attribute{ + "audience": schema.StringAttribute{ + MarkdownDescription: "Audience value configured in the AWS IAM OIDC identity provider.", + Required: true, + }, + "role_arn": schema.StringAttribute{ + MarkdownDescription: "ARN of the IAM role to assume via web identity federation.", + Required: true, + }, + }, + Validators: []validator.Object{ + objectvalidator.ConflictsWith( + path.MatchRoot("credentials").AtName("role"), + path.MatchRoot("credentials").AtName("key"), + ), + }, }, }, }, @@ -177,6 +221,16 @@ func (r *integrationAwsResource) Schema(ctx context.Context, req resource.Schema } } +func (r *integrationAwsResource) ConfigValidators(ctx context.Context) []resource.ConfigValidator { + return []resource.ConfigValidator{ + resourcevalidator.AtLeastOneOf( + path.MatchRoot("credentials").AtName("role"), + path.MatchRoot("credentials").AtName("key"), + path.MatchRoot("credentials").AtName("wif"), + ), + } +} + func (r *integrationAwsResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { // Prevent panic if the provider has not been configured. if req.ProviderData == nil { @@ -267,7 +321,12 @@ func (r *integrationAwsResource) Read(ctx context.Context, req resource.ReadRequ resp.Diagnostics.AddError("Error reading AWS integration", err.Error()) return } - data.WifSubject = types.StringValue(integration.ConfigurationOptions.HostedAwsConfigurationOptions.WifSubject) + opts := integration.ConfigurationOptions.HostedAwsConfigurationOptions + data.WifSubject = types.StringValue(opts.WifSubject) + if data.Credential.Wif != nil { + data.Credential.Wif.Audience = types.StringValue(opts.WifAudience) + data.Credential.Wif.RoleArn = types.StringValue(opts.WifRoleArn) + } // Save updated data into Terraform state resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) @@ -333,21 +392,30 @@ func (r *integrationAwsResource) ImportState(ctx context.Context, req resource.I return } + opts := integration.ConfigurationOptions.HostedAwsConfigurationOptions model := integrationAwsResourceModel{ SpaceID: types.StringValue(integration.SpaceID()), Mrn: types.StringValue(integration.Mrn), Name: types.StringValue(integration.Name), - WifSubject: types.StringValue(integration.ConfigurationOptions.HostedAwsConfigurationOptions.WifSubject), - Credential: integrationAwsCredentialModel{ - Role: &roleCredentialModel{ - RoleArn: types.StringValue(integration.ConfigurationOptions.HostedAwsConfigurationOptions.Role), - ExternalId: types.StringPointerValue(nil), // cannot be imported - }, - Key: &accessKeyCredentialModel{ - AccessKey: types.StringValue(integration.ConfigurationOptions.HostedAwsConfigurationOptions.AccessKeyId), - SecretKey: types.StringPointerValue(nil), // cannot be imported - }, - }, + WifSubject: types.StringValue(opts.WifSubject), + } + + switch { + case opts.WifAudience != "" || opts.WifRoleArn != "": + model.Credential.Wif = &awsWifCredentialModel{ + Audience: types.StringValue(opts.WifAudience), + RoleArn: types.StringValue(opts.WifRoleArn), + } + case opts.AccessKeyId != "": + model.Credential.Key = &accessKeyCredentialModel{ + AccessKey: types.StringValue(opts.AccessKeyId), + SecretKey: types.StringPointerValue(nil), // cannot be imported + } + case opts.Role != "": + model.Credential.Role = &roleCredentialModel{ + RoleArn: types.StringValue(opts.Role), + ExternalId: types.StringPointerValue(nil), // cannot be imported + } } resp.State.Set(ctx, &model) diff --git a/internal/provider/integration_gcp_resource.go b/internal/provider/integration_gcp_resource.go index 1d71b7f..ae36b27 100644 --- a/internal/provider/integration_gcp_resource.go +++ b/internal/provider/integration_gcp_resource.go @@ -7,7 +7,10 @@ import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework-validators/objectvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/resourcevalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" @@ -21,6 +24,7 @@ import ( // Ensure provider defined types fully satisfy framework interfaces. var _ resource.Resource = (*integrationGcpResource)(nil) var _ resource.ResourceWithImportState = (*integrationGcpResource)(nil) +var _ resource.ResourceWithConfigValidators = (*integrationGcpResource)(nil) func NewIntegrationGcpResource() resource.Resource { return &integrationGcpResource{} @@ -45,7 +49,31 @@ type integrationGcpResourceModel struct { } type integrationGcpCredentialModel struct { - PrivateKey types.String `tfsdk:"private_key"` + PrivateKey types.String `tfsdk:"private_key"` + Wif *gcpWifCredentialModel `tfsdk:"wif"` +} + +type gcpWifCredentialModel struct { + Audience types.String `tfsdk:"audience"` + ServiceAccountEmail types.String `tfsdk:"service_account_email"` +} + +func (m integrationGcpResourceModel) GetConfigurationOptions() *mondoov1.GcpConfigurationOptionsInput { + opts := &mondoov1.GcpConfigurationOptionsInput{ + ProjectId: mondoov1.NewStringPtr(mondoov1.String(m.ProjectId.ValueString())), + DiscoverAll: mondoov1.NewBooleanPtr(mondoov1.Boolean(true)), + } + + if !m.Credential.PrivateKey.IsNull() && !m.Credential.PrivateKey.IsUnknown() { + opts.ServiceAccount = mondoov1.NewStringPtr(mondoov1.String(m.Credential.PrivateKey.ValueString())) + } + + if m.Credential.Wif != nil { + opts.WifAudience = mondoov1.NewStringPtr(mondoov1.String(m.Credential.Wif.Audience.ValueString())) + opts.WifServiceAccountEmail = mondoov1.NewStringPtr(mondoov1.String(m.Credential.Wif.ServiceAccountEmail.ValueString())) + } + + return opts } func (r *integrationGcpResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { @@ -90,11 +118,37 @@ func (r *integrationGcpResource) Schema(ctx context.Context, req resource.Schema }, }, "credentials": schema.SingleNestedAttribute{ - Required: true, + MarkdownDescription: "Credentials for the GCP integration. Provide either a static service account `private_key` or a `wif` block for workload identity federation.", + Required: true, Attributes: map[string]schema.Attribute{ "private_key": schema.StringAttribute{ - Required: true, - Sensitive: true, + MarkdownDescription: "GCP service account JSON key. Mutually exclusive with `wif`.", + Optional: true, + Sensitive: true, + Validators: []validator.String{ + stringvalidator.ConflictsWith( + path.MatchRoot("credentials").AtName("wif"), + ), + }, + }, + "wif": schema.SingleNestedAttribute{ + MarkdownDescription: "Workload identity federation configuration. Mutually exclusive with `private_key`.", + Optional: true, + Attributes: map[string]schema.Attribute{ + "audience": schema.StringAttribute{ + MarkdownDescription: "WIF audience URL for GCP workload identity federation.", + Required: true, + }, + "service_account_email": schema.StringAttribute{ + MarkdownDescription: "GCP service account email impersonated via workload identity federation.", + Required: true, + }, + }, + Validators: []validator.Object{ + objectvalidator.ConflictsWith( + path.MatchRoot("credentials").AtName("private_key"), + ), + }, }, }, }, @@ -102,6 +156,15 @@ func (r *integrationGcpResource) Schema(ctx context.Context, req resource.Schema } } +func (r *integrationGcpResource) ConfigValidators(ctx context.Context) []resource.ConfigValidator { + return []resource.ConfigValidator{ + resourcevalidator.AtLeastOneOf( + path.MatchRoot("credentials").AtName("private_key"), + path.MatchRoot("credentials").AtName("wif"), + ), + } +} + func (r *integrationGcpResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { // Prevent panic if the provider has not been configured. if req.ProviderData == nil { @@ -147,11 +210,7 @@ func (r *integrationGcpResource) Create(ctx context.Context, req resource.Create data.Name.ValueString(), mondoov1.ClientIntegrationTypeGcp, mondoov1.ClientIntegrationConfigurationInput{ - GcpConfigurationOptions: &mondoov1.GcpConfigurationOptionsInput{ - ProjectId: mondoov1.NewStringPtr(mondoov1.String(data.ProjectId.ValueString())), - ServiceAccount: mondoov1.NewStringPtr(mondoov1.String(data.Credential.PrivateKey.ValueString())), - DiscoverAll: mondoov1.NewBooleanPtr(mondoov1.Boolean(true)), - }, + GcpConfigurationOptions: data.GetConfigurationOptions(), }) if err != nil { resp.Diagnostics. @@ -207,7 +266,12 @@ func (r *integrationGcpResource) Read(ctx context.Context, req resource.ReadRequ resp.Diagnostics.AddError("Error reading GCP integration", err.Error()) return } - data.WifSubject = types.StringValue(integration.ConfigurationOptions.GcpConfigurationOptions.WifSubject) + opts := integration.ConfigurationOptions.GcpConfigurationOptions + data.WifSubject = types.StringValue(opts.WifSubject) + if data.Credential.Wif != nil { + data.Credential.Wif.Audience = types.StringValue(opts.WifAudience) + data.Credential.Wif.ServiceAccountEmail = types.StringValue(opts.WifServiceAccountEmail) + } // Save updated data into Terraform state resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) @@ -225,11 +289,7 @@ func (r *integrationGcpResource) Update(ctx context.Context, req resource.Update // Do GraphQL request to API to update the resource. opts := mondoov1.ClientIntegrationConfigurationInput{ - GcpConfigurationOptions: &mondoov1.GcpConfigurationOptionsInput{ - ProjectId: mondoov1.NewStringPtr(mondoov1.String(data.ProjectId.ValueString())), - ServiceAccount: mondoov1.NewStringPtr(mondoov1.String(data.Credential.PrivateKey.ValueString())), - DiscoverAll: mondoov1.NewBooleanPtr(mondoov1.Boolean(true)), - }, + GcpConfigurationOptions: data.GetConfigurationOptions(), } _, err := r.client.UpdateIntegration(ctx, @@ -278,16 +338,23 @@ func (r *integrationGcpResource) ImportState(ctx context.Context, req resource.I return } + opts := integration.ConfigurationOptions.GcpConfigurationOptions model := integrationGcpResourceModel{ Mrn: types.StringValue(integration.Mrn), Name: types.StringValue(integration.Name), SpaceID: types.StringValue(integration.SpaceID()), - ProjectId: types.StringValue(integration.ConfigurationOptions.GcpConfigurationOptions.ProjectId), - WifSubject: types.StringValue(integration.ConfigurationOptions.GcpConfigurationOptions.WifSubject), + ProjectId: types.StringValue(opts.ProjectId), + WifSubject: types.StringValue(opts.WifSubject), Credential: integrationGcpCredentialModel{ PrivateKey: types.StringPointerValue(nil), }, } + if opts.WifAudience != "" { + model.Credential.Wif = &gcpWifCredentialModel{ + Audience: types.StringValue(opts.WifAudience), + ServiceAccountEmail: types.StringValue(opts.WifServiceAccountEmail), + } + } resp.State.Set(ctx, &model) } From 0bf25529c8ff3d69625c642d713d756c9452d44d Mon Sep 17 00:00:00 2001 From: Jay Mundrawala Date: Fri, 17 Apr 2026 09:31:55 -0500 Subject: [PATCH 2/3] Make wif.service_account_email optional for GCP-family resources Service account impersonation is an optional step for GCP workload identity federation - customers can also grant the identity pool's principal direct access to the resource. Only send the field to the server when it is set, and map an empty server response back to null in state so an unset value does not churn in subsequent plans. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/resources/export_bigquery.md | 5 ++++- docs/resources/export_gcs_bucket.md | 5 ++++- docs/resources/integration_gcp.md | 5 ++++- internal/provider/export_bigquery.go | 12 +++++++----- internal/provider/export_gcs_bucket.go | 12 +++++++----- internal/provider/integration_gcp_resource.go | 19 ++++++++++++++----- 6 files changed, 40 insertions(+), 18 deletions(-) diff --git a/docs/resources/export_bigquery.md b/docs/resources/export_bigquery.md index 91752da..6ebf7b2 100644 --- a/docs/resources/export_bigquery.md +++ b/docs/resources/export_bigquery.md @@ -62,4 +62,7 @@ Optional: Required: - `audience` (String) WIF audience URL for GCP workload identity federation. -- `service_account_email` (String) GCP service account email impersonated via workload identity federation. + +Optional: + +- `service_account_email` (String) Optional GCP service account email to impersonate via workload identity federation. diff --git a/docs/resources/export_gcs_bucket.md b/docs/resources/export_gcs_bucket.md index a64951a..32abf3f 100644 --- a/docs/resources/export_gcs_bucket.md +++ b/docs/resources/export_gcs_bucket.md @@ -69,4 +69,7 @@ Optional: Required: - `audience` (String) WIF audience URL for GCP workload identity federation. -- `service_account_email` (String) GCP service account email impersonated via workload identity federation. + +Optional: + +- `service_account_email` (String) Optional GCP service account email to impersonate via workload identity federation. diff --git a/docs/resources/integration_gcp.md b/docs/resources/integration_gcp.md index 70b02fd..3f0267a 100644 --- a/docs/resources/integration_gcp.md +++ b/docs/resources/integration_gcp.md @@ -100,7 +100,10 @@ Optional: Required: - `audience` (String) WIF audience URL for GCP workload identity federation. -- `service_account_email` (String) GCP service account email impersonated via workload identity federation. + +Optional: + +- `service_account_email` (String) Optional GCP service account email to impersonate via workload identity federation. ## Import diff --git a/internal/provider/export_bigquery.go b/internal/provider/export_bigquery.go index 4b6d92f..ed0dc3c 100644 --- a/internal/provider/export_bigquery.go +++ b/internal/provider/export_bigquery.go @@ -63,7 +63,9 @@ func (m BigQueryExportResourceModel) GetConfigurationOptions() *mondoov1.Bigquer if m.Credentials != nil && m.Credentials.Wif != nil { opts.WifAudience = mondoov1.NewStringPtr(mondoov1.String(m.Credentials.Wif.Audience.ValueString())) - opts.WifServiceAccountEmail = mondoov1.NewStringPtr(mondoov1.String(m.Credentials.Wif.ServiceAccountEmail.ValueString())) + if !m.Credentials.Wif.ServiceAccountEmail.IsNull() && !m.Credentials.Wif.ServiceAccountEmail.IsUnknown() { + opts.WifServiceAccountEmail = mondoov1.NewStringPtr(mondoov1.String(m.Credentials.Wif.ServiceAccountEmail.ValueString())) + } } return opts @@ -155,8 +157,8 @@ func (r *ExportBigQueryResource) Schema(ctx context.Context, req resource.Schema Required: true, }, "service_account_email": schema.StringAttribute{ - MarkdownDescription: "GCP service account email impersonated via workload identity federation.", - Required: true, + MarkdownDescription: "Optional GCP service account email to impersonate via workload identity federation.", + Optional: true, }, }, }, @@ -305,7 +307,7 @@ func (r *ExportBigQueryResource) Read(ctx context.Context, req resource.ReadRequ data.WifSubject = types.StringValue(opts.WifSubject) if data.Credentials != nil && data.Credentials.Wif != nil { data.Credentials.Wif.Audience = types.StringValue(opts.WifAudience) - data.Credentials.Wif.ServiceAccountEmail = types.StringValue(opts.WifServiceAccountEmail) + data.Credentials.Wif.ServiceAccountEmail = stringOrNull(opts.WifServiceAccountEmail) } // Note: We don't update service_account_key to avoid showing sensitive data @@ -375,7 +377,7 @@ func (r *ExportBigQueryResource) ImportState(ctx context.Context, req resource.I model.Credentials = &exportBigQueryCredentialsWrapper{ Wif: &gcpWifCredentialModel{ Audience: types.StringValue(opts.WifAudience), - ServiceAccountEmail: types.StringValue(opts.WifServiceAccountEmail), + ServiceAccountEmail: stringOrNull(opts.WifServiceAccountEmail), }, } } diff --git a/internal/provider/export_gcs_bucket.go b/internal/provider/export_gcs_bucket.go index e0efe2d..bfe5626 100644 --- a/internal/provider/export_gcs_bucket.go +++ b/internal/provider/export_gcs_bucket.go @@ -73,7 +73,9 @@ func (m ExportGcsBucketResourceModel) GetConfigurationOptions() *mondoov1.GcsBuc if m.Credential.Wif != nil { opts.WifAudience = mondoov1.NewStringPtr(mondoov1.String(m.Credential.Wif.Audience.ValueString())) - opts.WifServiceAccountEmail = mondoov1.NewStringPtr(mondoov1.String(m.Credential.Wif.ServiceAccountEmail.ValueString())) + if !m.Credential.Wif.ServiceAccountEmail.IsNull() && !m.Credential.Wif.ServiceAccountEmail.IsUnknown() { + opts.WifServiceAccountEmail = mondoov1.NewStringPtr(mondoov1.String(m.Credential.Wif.ServiceAccountEmail.ValueString())) + } } return opts @@ -185,8 +187,8 @@ func (r *ExportGcsBucketResource) Schema(ctx context.Context, req resource.Schem Required: true, }, "service_account_email": schema.StringAttribute{ - MarkdownDescription: "GCP service account email impersonated via workload identity federation.", - Required: true, + MarkdownDescription: "Optional GCP service account email to impersonate via workload identity federation.", + Optional: true, }, }, Validators: []validator.Object{ @@ -331,7 +333,7 @@ func (r *ExportGcsBucketResource) Read(ctx context.Context, req resource.ReadReq data.WifSubject = types.StringValue(opts.WifSubject) if data.Credential.Wif != nil { data.Credential.Wif.Audience = types.StringValue(opts.WifAudience) - data.Credential.Wif.ServiceAccountEmail = types.StringValue(opts.WifServiceAccountEmail) + data.Credential.Wif.ServiceAccountEmail = stringOrNull(opts.WifServiceAccountEmail) } // Save updated data into Terraform state @@ -402,7 +404,7 @@ func (r *ExportGcsBucketResource) ImportState(ctx context.Context, req resource. if opts.WifAudience != "" { model.Credential.Wif = &gcpWifCredentialModel{ Audience: types.StringValue(opts.WifAudience), - ServiceAccountEmail: types.StringValue(opts.WifServiceAccountEmail), + ServiceAccountEmail: stringOrNull(opts.WifServiceAccountEmail), } } diff --git a/internal/provider/integration_gcp_resource.go b/internal/provider/integration_gcp_resource.go index ae36b27..4ca76b5 100644 --- a/internal/provider/integration_gcp_resource.go +++ b/internal/provider/integration_gcp_resource.go @@ -58,6 +58,13 @@ type gcpWifCredentialModel struct { ServiceAccountEmail types.String `tfsdk:"service_account_email"` } +func stringOrNull(s string) types.String { + if s == "" { + return types.StringNull() + } + return types.StringValue(s) +} + func (m integrationGcpResourceModel) GetConfigurationOptions() *mondoov1.GcpConfigurationOptionsInput { opts := &mondoov1.GcpConfigurationOptionsInput{ ProjectId: mondoov1.NewStringPtr(mondoov1.String(m.ProjectId.ValueString())), @@ -70,7 +77,9 @@ func (m integrationGcpResourceModel) GetConfigurationOptions() *mondoov1.GcpConf if m.Credential.Wif != nil { opts.WifAudience = mondoov1.NewStringPtr(mondoov1.String(m.Credential.Wif.Audience.ValueString())) - opts.WifServiceAccountEmail = mondoov1.NewStringPtr(mondoov1.String(m.Credential.Wif.ServiceAccountEmail.ValueString())) + if !m.Credential.Wif.ServiceAccountEmail.IsNull() && !m.Credential.Wif.ServiceAccountEmail.IsUnknown() { + opts.WifServiceAccountEmail = mondoov1.NewStringPtr(mondoov1.String(m.Credential.Wif.ServiceAccountEmail.ValueString())) + } } return opts @@ -140,8 +149,8 @@ func (r *integrationGcpResource) Schema(ctx context.Context, req resource.Schema Required: true, }, "service_account_email": schema.StringAttribute{ - MarkdownDescription: "GCP service account email impersonated via workload identity federation.", - Required: true, + MarkdownDescription: "Optional GCP service account email to impersonate via workload identity federation.", + Optional: true, }, }, Validators: []validator.Object{ @@ -270,7 +279,7 @@ func (r *integrationGcpResource) Read(ctx context.Context, req resource.ReadRequ data.WifSubject = types.StringValue(opts.WifSubject) if data.Credential.Wif != nil { data.Credential.Wif.Audience = types.StringValue(opts.WifAudience) - data.Credential.Wif.ServiceAccountEmail = types.StringValue(opts.WifServiceAccountEmail) + data.Credential.Wif.ServiceAccountEmail = stringOrNull(opts.WifServiceAccountEmail) } // Save updated data into Terraform state @@ -352,7 +361,7 @@ func (r *integrationGcpResource) ImportState(ctx context.Context, req resource.I if opts.WifAudience != "" { model.Credential.Wif = &gcpWifCredentialModel{ Audience: types.StringValue(opts.WifAudience), - ServiceAccountEmail: types.StringValue(opts.WifServiceAccountEmail), + ServiceAccountEmail: stringOrNull(opts.WifServiceAccountEmail), } } From c29e12d4b689c5a6bfd3f537ff36c061c42d4511 Mon Sep 17 00:00:00 2001 From: Jay Mundrawala Date: Tue, 21 Apr 2026 06:45:59 -0500 Subject: [PATCH 3/3] Simplify WIF credential validators and harden AWS import - Replace the per-attribute ConflictsWith + AtLeastOneOf pairing with a single ExactlyOneOf ConfigValidator on the GCP integration, GCS bucket export, and AWS integration. BigQuery already used ExactlyOneOf; drop the redundant string-level ConflictsWith on its service_account_key. - Require both WifAudience and WifRoleArn to be non-empty before importing a wif credential block on the AWS integration so a partial server response cannot write a state that fails validation on the next plan. Co-Authored-By: Claude Opus 4.7 (1M context) --- internal/provider/export_bigquery.go | 5 ---- internal/provider/export_gcs_bucket.go | 13 +---------- internal/provider/integration_aws_resource.go | 23 ++----------------- internal/provider/integration_gcp_resource.go | 13 +---------- 4 files changed, 4 insertions(+), 50 deletions(-) diff --git a/internal/provider/export_bigquery.go b/internal/provider/export_bigquery.go index ed0dc3c..b36df90 100644 --- a/internal/provider/export_bigquery.go +++ b/internal/provider/export_bigquery.go @@ -138,11 +138,6 @@ func (r *ExportBigQueryResource) Schema(ctx context.Context, req resource.Schema MarkdownDescription: "Google service account JSON key content. Mutually exclusive with `credentials.wif`.", Optional: true, Sensitive: true, - Validators: []validator.String{ - stringvalidator.ConflictsWith( - path.MatchRoot("credentials").AtName("wif"), - ), - }, }, "credentials": schema.SingleNestedAttribute{ MarkdownDescription: "Credentials for the BigQuery export. Provide `wif` for workload identity federation instead of the top-level `service_account_key`.", diff --git a/internal/provider/export_gcs_bucket.go b/internal/provider/export_gcs_bucket.go index bfe5626..b1c6840 100644 --- a/internal/provider/export_gcs_bucket.go +++ b/internal/provider/export_gcs_bucket.go @@ -8,7 +8,6 @@ import ( "fmt" "strings" - "github.com/hashicorp/terraform-plugin-framework-validators/objectvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/resourcevalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/path" @@ -172,11 +171,6 @@ func (r *ExportGcsBucketResource) Schema(ctx context.Context, req resource.Schem MarkdownDescription: "Private key for the service account in JSON format. Mutually exclusive with `wif`.", Optional: true, Sensitive: true, - Validators: []validator.String{ - stringvalidator.ConflictsWith( - path.MatchRoot("credentials").AtName("wif"), - ), - }, }, "wif": schema.SingleNestedAttribute{ MarkdownDescription: "Workload identity federation configuration. Mutually exclusive with `private_key`.", @@ -191,11 +185,6 @@ func (r *ExportGcsBucketResource) Schema(ctx context.Context, req resource.Schem Optional: true, }, }, - Validators: []validator.Object{ - objectvalidator.ConflictsWith( - path.MatchRoot("credentials").AtName("private_key"), - ), - }, }, }, }, @@ -205,7 +194,7 @@ func (r *ExportGcsBucketResource) Schema(ctx context.Context, req resource.Schem func (r *ExportGcsBucketResource) ConfigValidators(ctx context.Context) []resource.ConfigValidator { return []resource.ConfigValidator{ - resourcevalidator.AtLeastOneOf( + resourcevalidator.ExactlyOneOf( path.MatchRoot("credentials").AtName("private_key"), path.MatchRoot("credentials").AtName("wif"), ), diff --git a/internal/provider/integration_aws_resource.go b/internal/provider/integration_aws_resource.go index d989ee5..389d3f2 100644 --- a/internal/provider/integration_aws_resource.go +++ b/internal/provider/integration_aws_resource.go @@ -8,7 +8,6 @@ import ( "fmt" "regexp" - "github.com/hashicorp/terraform-plugin-framework-validators/objectvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/resourcevalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/path" @@ -156,12 +155,6 @@ func (r *integrationAwsResource) Schema(ctx context.Context, req resource.Schema Sensitive: true, }, }, - Validators: []validator.Object{ - objectvalidator.ConflictsWith( - path.MatchRoot("credentials").AtName("key"), - path.MatchRoot("credentials").AtName("wif"), - ), - }, }, "key": schema.SingleNestedAttribute{ MarkdownDescription: "Static IAM access key credentials. Mutually exclusive with `role` and `wif`.", @@ -188,12 +181,6 @@ func (r *integrationAwsResource) Schema(ctx context.Context, req resource.Schema }, }, }, - Validators: []validator.Object{ - objectvalidator.ConflictsWith( - path.MatchRoot("credentials").AtName("role"), - path.MatchRoot("credentials").AtName("wif"), - ), - }, }, "wif": schema.SingleNestedAttribute{ MarkdownDescription: "Workload identity federation credentials. Uses Mondoo as an OIDC identity provider to assume an IAM role via web identity. Mutually exclusive with `role` and `key`.", @@ -208,12 +195,6 @@ func (r *integrationAwsResource) Schema(ctx context.Context, req resource.Schema Required: true, }, }, - Validators: []validator.Object{ - objectvalidator.ConflictsWith( - path.MatchRoot("credentials").AtName("role"), - path.MatchRoot("credentials").AtName("key"), - ), - }, }, }, }, @@ -223,7 +204,7 @@ func (r *integrationAwsResource) Schema(ctx context.Context, req resource.Schema func (r *integrationAwsResource) ConfigValidators(ctx context.Context) []resource.ConfigValidator { return []resource.ConfigValidator{ - resourcevalidator.AtLeastOneOf( + resourcevalidator.ExactlyOneOf( path.MatchRoot("credentials").AtName("role"), path.MatchRoot("credentials").AtName("key"), path.MatchRoot("credentials").AtName("wif"), @@ -401,7 +382,7 @@ func (r *integrationAwsResource) ImportState(ctx context.Context, req resource.I } switch { - case opts.WifAudience != "" || opts.WifRoleArn != "": + case opts.WifAudience != "" && opts.WifRoleArn != "": model.Credential.Wif = &awsWifCredentialModel{ Audience: types.StringValue(opts.WifAudience), RoleArn: types.StringValue(opts.WifRoleArn), diff --git a/internal/provider/integration_gcp_resource.go b/internal/provider/integration_gcp_resource.go index 4ca76b5..398e60e 100644 --- a/internal/provider/integration_gcp_resource.go +++ b/internal/provider/integration_gcp_resource.go @@ -7,7 +7,6 @@ import ( "context" "fmt" - "github.com/hashicorp/terraform-plugin-framework-validators/objectvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/resourcevalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/path" @@ -134,11 +133,6 @@ func (r *integrationGcpResource) Schema(ctx context.Context, req resource.Schema MarkdownDescription: "GCP service account JSON key. Mutually exclusive with `wif`.", Optional: true, Sensitive: true, - Validators: []validator.String{ - stringvalidator.ConflictsWith( - path.MatchRoot("credentials").AtName("wif"), - ), - }, }, "wif": schema.SingleNestedAttribute{ MarkdownDescription: "Workload identity federation configuration. Mutually exclusive with `private_key`.", @@ -153,11 +147,6 @@ func (r *integrationGcpResource) Schema(ctx context.Context, req resource.Schema Optional: true, }, }, - Validators: []validator.Object{ - objectvalidator.ConflictsWith( - path.MatchRoot("credentials").AtName("private_key"), - ), - }, }, }, }, @@ -167,7 +156,7 @@ func (r *integrationGcpResource) Schema(ctx context.Context, req resource.Schema func (r *integrationGcpResource) ConfigValidators(ctx context.Context) []resource.ConfigValidator { return []resource.ConfigValidator{ - resourcevalidator.AtLeastOneOf( + resourcevalidator.ExactlyOneOf( path.MatchRoot("credentials").AtName("private_key"), path.MatchRoot("credentials").AtName("wif"), ),