diff --git a/docs/resources/export_bigquery.md b/docs/resources/export_bigquery.md index b592e8b..6ebf7b2 100644 --- a/docs/resources/export_bigquery.md +++ b/docs/resources/export_bigquery.md @@ -36,14 +36,33 @@ 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. + +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 7376f1c..32abf3f 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,18 @@ 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. + +Optional: + +- `service_account_email` (String) Optional GCP service account email to impersonate 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..3f0267a 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,21 @@ 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. + +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 32c58d8..b36df90 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,31 @@ 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())) + if !m.Credentials.Wif.ServiceAccountEmail.IsNull() && !m.Credentials.Wif.ServiceAccountEmail.IsUnknown() { + 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 +135,28 @@ 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(), + }, + "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: "Optional GCP service account email to impersonate via workload identity federation.", + Optional: true, + }, + }, + }, }, }, "wif_subject": schema.StringAttribute{ @@ -127,6 +170,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 +209,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 +297,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 = stringOrNull(opts.WifServiceAccountEmail) + } // Note: We don't update service_account_key to avoid showing sensitive data // Save updated data into Terraform state @@ -271,10 +325,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 +359,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: stringOrNull(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..b1c6840 100644 --- a/internal/provider/export_gcs_bucket.go +++ b/internal/provider/export_gcs_bucket.go @@ -8,6 +8,7 @@ import ( "fmt" "strings" + "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 +24,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 +47,37 @@ 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())) + if !m.Credential.Wif.ServiceAccountEmail.IsNull() && !m.Credential.Wif.ServiceAccountEmail.IsUnknown() { + 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,20 +164,43 @@ 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, }, + "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: "Optional GCP service account email to impersonate via workload identity federation.", + Optional: true, + }, + }, + }, }, }, }, } } +func (r *ExportGcsBucketResource) ConfigValidators(ctx context.Context) []resource.ConfigValidator { + return []resource.ConfigValidator{ + resourcevalidator.ExactlyOneOf( + 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 +230,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 +318,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 = stringOrNull(opts.WifServiceAccountEmail) + } // Save updated data into Terraform state resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) @@ -288,23 +338,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 +377,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: stringOrNull(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..389d3f2 100644 --- a/internal/provider/integration_aws_resource.go +++ b/internal/provider/integration_aws_resource.go @@ -8,7 +8,7 @@ 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" "github.com/hashicorp/terraform-plugin-framework/resource" @@ -24,6 +24,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 +50,7 @@ type integrationAwsResourceModel struct { type integrationAwsCredentialModel struct { Role *roleCredentialModel `tfsdk:"role"` Key *accessKeyCredentialModel `tfsdk:"key"` + Wif *awsWifCredentialModel `tfsdk:"wif"` } type roleCredentialModel struct { @@ -61,6 +63,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 +91,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 +139,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, @@ -139,15 +155,10 @@ func (r *integrationAwsResource) Schema(ctx context.Context, req resource.Schema Sensitive: true, }, }, - Validators: []validator.Object{ - // Validate this attribute must not be configured with other_attr. - objectvalidator.ConflictsWith(path.Expressions{ - path.MatchRoot("credentials").AtName("key"), - }...), - }, }, "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, @@ -171,12 +182,36 @@ func (r *integrationAwsResource) Schema(ctx context.Context, req resource.Schema }, }, }, + "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, + }, + }, + }, }, }, }, } } +func (r *integrationAwsResource) ConfigValidators(ctx context.Context) []resource.ConfigValidator { + return []resource.ConfigValidator{ + resourcevalidator.ExactlyOneOf( + 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 +302,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 +373,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..398e60e 100644 --- a/internal/provider/integration_gcp_resource.go +++ b/internal/provider/integration_gcp_resource.go @@ -7,7 +7,9 @@ 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" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" @@ -21,6 +23,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 +48,40 @@ 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 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())), + 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())) + if !m.Credential.Wif.ServiceAccountEmail.IsNull() && !m.Credential.Wif.ServiceAccountEmail.IsUnknown() { + 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 +126,27 @@ 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, + }, + "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: "Optional GCP service account email to impersonate via workload identity federation.", + Optional: true, + }, + }, }, }, }, @@ -102,6 +154,15 @@ func (r *integrationGcpResource) Schema(ctx context.Context, req resource.Schema } } +func (r *integrationGcpResource) ConfigValidators(ctx context.Context) []resource.ConfigValidator { + return []resource.ConfigValidator{ + resourcevalidator.ExactlyOneOf( + 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 +208,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 +264,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 = stringOrNull(opts.WifServiceAccountEmail) + } // Save updated data into Terraform state resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) @@ -225,11 +287,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 +336,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: stringOrNull(opts.WifServiceAccountEmail), + } + } resp.State.Set(ctx, &model) }