Skip to content

Commit b8f66ee

Browse files
jaymclaude
andauthored
Add WIF credential input to integration and export resources (#417)
* 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent e61aa86 commit b8f66ee

9 files changed

Lines changed: 384 additions & 102 deletions

docs/resources/export_bigquery.md

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,33 @@ Export data to Google BigQuery.
3636

3737
- `dataset_id` (String) Target BigQuery dataset (project-id.dataset_id).
3838
- `name` (String) A descriptive name for the integration.
39-
- `service_account_key` (String, Sensitive) Google service account JSON key content.
4039

4140
### Optional
4241

42+
- `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))
4343
- `scope_mrn` (String) The MRN of the scope (space, organization, or platform) for the export integration.
44+
- `service_account_key` (String, Sensitive) Google service account JSON key content. Mutually exclusive with `credentials.wif`.
4445
- `space_id` (String, Deprecated) Mondoo space identifier. If there is no space ID, the provider space is used.
4546

4647
### Read-Only
4748

4849
- `mrn` (String) Mondoo resource name (MRN) of the integration.
4950
- `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.
51+
52+
<a id="nestedatt--credentials"></a>
53+
### Nested Schema for `credentials`
54+
55+
Optional:
56+
57+
- `wif` (Attributes) Workload identity federation configuration. Mutually exclusive with `service_account_key`. (see [below for nested schema](#nestedatt--credentials--wif))
58+
59+
<a id="nestedatt--credentials--wif"></a>
60+
### Nested Schema for `credentials.wif`
61+
62+
Required:
63+
64+
- `audience` (String) WIF audience URL for GCP workload identity federation.
65+
66+
Optional:
67+
68+
- `service_account_email` (String) Optional GCP service account email to impersonate via workload identity federation.

docs/resources/export_gcs_bucket.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ Export data to a Google Cloud Storage bucket.
4141
### Required
4242

4343
- `bucket_name` (String) Name of the Google Cloud Storage bucket to export data to.
44-
- `credentials` (Attributes) Credentials for the Google Cloud Storage bucket. (see [below for nested schema](#nestedatt--credentials))
44+
- `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))
4545
- `name` (String) Name of the export integration.
4646

4747
### Optional
@@ -58,6 +58,18 @@ Export data to a Google Cloud Storage bucket.
5858
<a id="nestedatt--credentials"></a>
5959
### Nested Schema for `credentials`
6060

61+
Optional:
62+
63+
- `private_key` (String, Sensitive) Private key for the service account in JSON format. Mutually exclusive with `wif`.
64+
- `wif` (Attributes) Workload identity federation configuration. Mutually exclusive with `private_key`. (see [below for nested schema](#nestedatt--credentials--wif))
65+
66+
<a id="nestedatt--credentials--wif"></a>
67+
### Nested Schema for `credentials.wif`
68+
6169
Required:
6270

63-
- `private_key` (String, Sensitive) Private key for the service account in JSON format.
71+
- `audience` (String) WIF audience URL for GCP workload identity federation.
72+
73+
Optional:
74+
75+
- `service_account_email` (String) Optional GCP service account email to impersonate via workload identity federation.

docs/resources/integration_aws.md

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ resource "mondoo_integration_aws" "name" {
4747

4848
### Required
4949

50-
- `credentials` (Attributes) (see [below for nested schema](#nestedatt--credentials))
50+
- `credentials` (Attributes) Credentials for the AWS integration. Exactly one of `role`, `key`, or `wif` must be configured. (see [below for nested schema](#nestedatt--credentials))
5151
- `name` (String) Name of the integration.
5252

5353
### Optional
@@ -64,8 +64,9 @@ resource "mondoo_integration_aws" "name" {
6464

6565
Optional:
6666

67-
- `key` (Attributes) (see [below for nested schema](#nestedatt--credentials--key))
68-
- `role` (Attributes) (see [below for nested schema](#nestedatt--credentials--role))
67+
- `key` (Attributes) Static IAM access key credentials. Mutually exclusive with `role` and `wif`. (see [below for nested schema](#nestedatt--credentials--key))
68+
- `role` (Attributes) IAM role credentials. Mutually exclusive with `key` and `wif`. (see [below for nested schema](#nestedatt--credentials--role))
69+
- `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))
6970

7071
<a id="nestedatt--credentials--key"></a>
7172
### Nested Schema for `credentials.key`
@@ -87,6 +88,15 @@ Optional:
8788

8889
- `external_id` (String, Sensitive)
8990

91+
92+
<a id="nestedatt--credentials--wif"></a>
93+
### Nested Schema for `credentials.wif`
94+
95+
Required:
96+
97+
- `audience` (String) Audience value configured in the AWS IAM OIDC identity provider.
98+
- `role_arn` (String) ARN of the IAM role to assume via web identity federation.
99+
90100
## Import
91101

92102
Import is supported using the following syntax:

docs/resources/integration_gcp.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ resource "mondoo_integration_gcp" "name" {
7373

7474
### Required
7575

76-
- `credentials` (Attributes) (see [below for nested schema](#nestedatt--credentials))
76+
- `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))
7777
- `name` (String) Name of the integration.
7878

7979
### Optional
@@ -89,9 +89,21 @@ resource "mondoo_integration_gcp" "name" {
8989
<a id="nestedatt--credentials"></a>
9090
### Nested Schema for `credentials`
9191

92+
Optional:
93+
94+
- `private_key` (String, Sensitive) GCP service account JSON key. Mutually exclusive with `wif`.
95+
- `wif` (Attributes) Workload identity federation configuration. Mutually exclusive with `private_key`. (see [below for nested schema](#nestedatt--credentials--wif))
96+
97+
<a id="nestedatt--credentials--wif"></a>
98+
### Nested Schema for `credentials.wif`
99+
92100
Required:
93101

94-
- `private_key` (String, Sensitive)
102+
- `audience` (String) WIF audience URL for GCP workload identity federation.
103+
104+
Optional:
105+
106+
- `service_account_email` (String) Optional GCP service account email to impersonate via workload identity federation.
95107

96108
## Import
97109

internal/provider/export_bigquery.go

Lines changed: 76 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"context"
88
"fmt"
99

10+
"github.com/hashicorp/terraform-plugin-framework-validators/resourcevalidator"
1011
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
1112
"github.com/hashicorp/terraform-plugin-framework/path"
1213
"github.com/hashicorp/terraform-plugin-framework/resource"
@@ -21,6 +22,7 @@ import (
2122

2223
// Ensure provider defined types fully satisfy framework interfaces.
2324
var _ resource.Resource = &ExportBigQueryResource{}
25+
var _ resource.ResourceWithConfigValidators = &ExportBigQueryResource{}
2426

2527
func NewMondooExportBigQueryResource() resource.Resource {
2628
return &ExportBigQueryResource{}
@@ -42,7 +44,31 @@ type BigQueryExportResourceModel struct {
4244
WifSubject types.String `tfsdk:"wif_subject"`
4345

4446
// credentials
45-
ServiceAccountKey types.String `tfsdk:"service_account_key"`
47+
ServiceAccountKey types.String `tfsdk:"service_account_key"`
48+
Credentials *exportBigQueryCredentialsWrapper `tfsdk:"credentials"`
49+
}
50+
51+
type exportBigQueryCredentialsWrapper struct {
52+
Wif *gcpWifCredentialModel `tfsdk:"wif"`
53+
}
54+
55+
func (m BigQueryExportResourceModel) GetConfigurationOptions() *mondoov1.BigqueryConfigurationOptionsInput {
56+
opts := &mondoov1.BigqueryConfigurationOptionsInput{
57+
DatasetId: mondoov1.String(m.DatasetID.ValueString()),
58+
}
59+
60+
if !m.ServiceAccountKey.IsNull() && !m.ServiceAccountKey.IsUnknown() {
61+
opts.ServiceAccount = mondoov1.NewStringPtr(mondoov1.String(m.ServiceAccountKey.ValueString()))
62+
}
63+
64+
if m.Credentials != nil && m.Credentials.Wif != nil {
65+
opts.WifAudience = mondoov1.NewStringPtr(mondoov1.String(m.Credentials.Wif.Audience.ValueString()))
66+
if !m.Credentials.Wif.ServiceAccountEmail.IsNull() && !m.Credentials.Wif.ServiceAccountEmail.IsUnknown() {
67+
opts.WifServiceAccountEmail = mondoov1.NewStringPtr(mondoov1.String(m.Credentials.Wif.ServiceAccountEmail.ValueString()))
68+
}
69+
}
70+
71+
return opts
4672
}
4773

4874
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
109135
},
110136
},
111137
"service_account_key": schema.StringAttribute{
112-
MarkdownDescription: "Google service account JSON key content.",
113-
Required: true,
138+
MarkdownDescription: "Google service account JSON key content. Mutually exclusive with `credentials.wif`.",
139+
Optional: true,
114140
Sensitive: true,
115-
PlanModifiers: []planmodifier.String{
116-
stringplanmodifier.RequiresReplace(),
141+
},
142+
"credentials": schema.SingleNestedAttribute{
143+
MarkdownDescription: "Credentials for the BigQuery export. Provide `wif` for workload identity federation instead of the top-level `service_account_key`.",
144+
Optional: true,
145+
Attributes: map[string]schema.Attribute{
146+
"wif": schema.SingleNestedAttribute{
147+
MarkdownDescription: "Workload identity federation configuration. Mutually exclusive with `service_account_key`.",
148+
Optional: true,
149+
Attributes: map[string]schema.Attribute{
150+
"audience": schema.StringAttribute{
151+
MarkdownDescription: "WIF audience URL for GCP workload identity federation.",
152+
Required: true,
153+
},
154+
"service_account_email": schema.StringAttribute{
155+
MarkdownDescription: "Optional GCP service account email to impersonate via workload identity federation.",
156+
Optional: true,
157+
},
158+
},
159+
},
117160
},
118161
},
119162
"wif_subject": schema.StringAttribute{
@@ -127,6 +170,15 @@ func (r *ExportBigQueryResource) Schema(ctx context.Context, req resource.Schema
127170
}
128171
}
129172

173+
func (r *ExportBigQueryResource) ConfigValidators(ctx context.Context) []resource.ConfigValidator {
174+
return []resource.ConfigValidator{
175+
resourcevalidator.ExactlyOneOf(
176+
path.MatchRoot("service_account_key"),
177+
path.MatchRoot("credentials").AtName("wif"),
178+
),
179+
}
180+
}
181+
130182
func (r *ExportBigQueryResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
131183
// Prevent panic if the provider has not been configured.
132184
if req.ProviderData == nil {
@@ -157,10 +209,7 @@ func (r *ExportBigQueryResource) Create(ctx context.Context, req resource.Create
157209
}
158210

159211
configOpts := mondoov1.ClientIntegrationConfigurationInput{
160-
BigqueryConfigurationOptions: &mondoov1.BigqueryConfigurationOptionsInput{
161-
DatasetId: mondoov1.String(data.DatasetID.ValueString()),
162-
ServiceAccount: mondoov1.NewStringPtr(mondoov1.String(data.ServiceAccountKey.ValueString())),
163-
},
212+
BigqueryConfigurationOptions: data.GetConfigurationOptions(),
164213
}
165214

166215
var integration *CreateClientIntegrationPayload
@@ -248,8 +297,13 @@ func (r *ExportBigQueryResource) Read(ctx context.Context, req resource.ReadRequ
248297
}
249298

250299
// Update the state with the latest information
300+
opts := integration.ConfigurationOptions.BigqueryConfigurationOptions
251301
data.Name = types.StringValue(integration.Name)
252-
data.WifSubject = types.StringValue(integration.ConfigurationOptions.BigqueryConfigurationOptions.WifSubject)
302+
data.WifSubject = types.StringValue(opts.WifSubject)
303+
if data.Credentials != nil && data.Credentials.Wif != nil {
304+
data.Credentials.Wif.Audience = types.StringValue(opts.WifAudience)
305+
data.Credentials.Wif.ServiceAccountEmail = stringOrNull(opts.WifServiceAccountEmail)
306+
}
253307
// Note: We don't update service_account_key to avoid showing sensitive data
254308

255309
// Save updated data into Terraform state
@@ -271,10 +325,7 @@ func (r *ExportBigQueryResource) Update(ctx context.Context, req resource.Update
271325
data.Name.ValueString(),
272326
mondoov1.ClientIntegrationTypeBigquery,
273327
mondoov1.ClientIntegrationConfigurationInput{
274-
BigqueryConfigurationOptions: &mondoov1.BigqueryConfigurationOptionsInput{
275-
DatasetId: mondoov1.String(data.DatasetID.ValueString()),
276-
ServiceAccount: mondoov1.NewStringPtr(mondoov1.String(data.ServiceAccountKey.ValueString())),
277-
},
328+
BigqueryConfigurationOptions: data.GetConfigurationOptions(),
278329
})
279330

280331
if err != nil {
@@ -308,14 +359,23 @@ func (r *ExportBigQueryResource) ImportState(ctx context.Context, req resource.I
308359
return
309360
}
310361

362+
opts := integration.ConfigurationOptions.BigqueryConfigurationOptions
311363
model := BigQueryExportResourceModel{
312364
Mrn: types.StringValue(integration.Mrn),
313365
Name: types.StringValue(integration.Name),
314366
ScopeMrn: types.StringValue(integration.ScopeMRN()),
315-
DatasetID: types.StringValue(integration.ConfigurationOptions.BigqueryConfigurationOptions.DatasetId),
316-
WifSubject: types.StringValue(integration.ConfigurationOptions.BigqueryConfigurationOptions.WifSubject),
367+
DatasetID: types.StringValue(opts.DatasetId),
368+
WifSubject: types.StringValue(opts.WifSubject),
317369
ServiceAccountKey: types.StringPointerValue(nil), // Don't expose sensitive data
318370
}
371+
if opts.WifAudience != "" {
372+
model.Credentials = &exportBigQueryCredentialsWrapper{
373+
Wif: &gcpWifCredentialModel{
374+
Audience: types.StringValue(opts.WifAudience),
375+
ServiceAccountEmail: stringOrNull(opts.WifServiceAccountEmail),
376+
},
377+
}
378+
}
319379

320380
if integration.IsSpaceScoped() {
321381
model.SpaceID = types.StringValue(integration.SpaceID())

0 commit comments

Comments
 (0)