diff --git a/docs/resources/iam_workload_identity_binding.md b/docs/resources/iam_workload_identity_binding.md new file mode 100644 index 0000000..ff4a971 --- /dev/null +++ b/docs/resources/iam_workload_identity_binding.md @@ -0,0 +1,48 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "mondoo_iam_workload_identity_binding Resource - terraform-provider-mondoo" +subcategory: "" +description: |- + Allows management of a Mondoo Workload Identity Federation bindings. +--- + +# mondoo_iam_workload_identity_binding (Resource) + +Allows management of a Mondoo Workload Identity Federation bindings. + +## Example Usage + +```terraform +provider "mondoo" { + space = "hungry-poet-123456" +} + +resource "mondoo_iam_workload_identity_binding" "example" { + name = "GitHub binding example" + issuer_uri = "https://token.actions.githubusercontent.com" + subject = "repo:mondoohq/server:ref:refs/heads/main" + expiration = 3600 +} +``` + + +## Schema + +### Required + +- `issuer_uri` (String) URI for the token issuer, e.g. https://accounts.google.com. +- `name` (String) Name of the binding. +- `subject` (String) Unique identifier to confirm. + +### Optional + +- `allowed_audiences` (List of String) List of allowed audiences. +- `description` (String) Description of the binding. +- `expiration` (Number) Expiration in seconds associated with the binding. +- `mappings` (Map of String) List of additional configurations to confirm. +- `roles` (List of String) List of roles associated with the binding (e.g. agent mrn). +- `space_id` (String) Mondoo space identifier. If there is no ID, the provider space is used. + +### Read-Only + +- `mrn` (String) The Mondoo resource name (MRN) of the created binding. diff --git a/examples/resources/mondoo_iam_workload_identity_binding/main.tf b/examples/resources/mondoo_iam_workload_identity_binding/main.tf new file mode 100644 index 0000000..6aeddc8 --- /dev/null +++ b/examples/resources/mondoo_iam_workload_identity_binding/main.tf @@ -0,0 +1,8 @@ +terraform { + required_providers { + mondoo = { + source = "mondoohq/mondoo" + version = ">= 0.19" + } + } +} diff --git a/examples/resources/mondoo_iam_workload_identity_binding/resource.tf b/examples/resources/mondoo_iam_workload_identity_binding/resource.tf new file mode 100644 index 0000000..078aae0 --- /dev/null +++ b/examples/resources/mondoo_iam_workload_identity_binding/resource.tf @@ -0,0 +1,10 @@ +provider "mondoo" { + space = "hungry-poet-123456" +} + +resource "mondoo_iam_workload_identity_binding" "example" { + name = "GitHub binding example" + issuer_uri = "https://token.actions.githubusercontent.com" + subject = "repo:mondoohq/server:ref:refs/heads/main" + expiration = 3600 +} diff --git a/go.mod b/go.mod index 6408980..b71dcf3 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/hashicorp/terraform-plugin-testing v1.11.0 github.com/stretchr/testify v1.10.0 go.mondoo.com/cnquery/v11 v11.37.1 - go.mondoo.com/mondoo-go v0.0.0-20250108144440-673a4fac8289 + go.mondoo.com/mondoo-go v0.0.0-20250129071639-c3de624e0c5a gopkg.in/yaml.v2 v2.4.0 ) diff --git a/go.sum b/go.sum index d256df5..978cf37 100644 --- a/go.sum +++ b/go.sum @@ -610,8 +610,10 @@ go.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3 go.etcd.io/etcd/client/v3 v3.5.4/go.mod h1:ZaRkVgBZC+L+dLCjTcF1hRXpgZXQPOvnA/Ak/gq3kiY= go.mondoo.com/cnquery/v11 v11.37.1 h1:bzM4o7+k/WGrqFHaY0t1aUZjVG+ufOL8BwEphoFiL6w= go.mondoo.com/cnquery/v11 v11.37.1/go.mod h1:Fy0e1XJzZgtQyRAuPzoEapfxB2G5DjwWagJAPqKT/Ks= -go.mondoo.com/mondoo-go v0.0.0-20250108144440-673a4fac8289 h1:D47xahKosrO4gjRtjnBte3tlHbtDAGYkEWyPXheRaac= -go.mondoo.com/mondoo-go v0.0.0-20250108144440-673a4fac8289/go.mod h1:dGj5d8BoLzVppdYI2k0Oay9pcg7bqsCYbyiBH9uhKGc= +go.mondoo.com/mondoo-go v0.0.0-20250127074240-22a812af6d20 h1:RkZ6b/BOuVVWn4vS+0e4Tv0G9MP0L4hZLvgEs+2ESmg= +go.mondoo.com/mondoo-go v0.0.0-20250127074240-22a812af6d20/go.mod h1:0HMHhLaS0V1himFIJQxABmvqEAdWv1NUScXpSjrhxqo= +go.mondoo.com/mondoo-go v0.0.0-20250129071639-c3de624e0c5a h1:DtwCDuKcXUVJZyKni8TlkxxlFdutPVK6JFCldIuq8cw= +go.mondoo.com/mondoo-go v0.0.0-20250129071639-c3de624e0c5a/go.mod h1:0HMHhLaS0V1himFIJQxABmvqEAdWv1NUScXpSjrhxqo= go.mondoo.com/ranger-rpc v0.6.5 h1:KKoeTGPonJI3T6lrT9oxdH9eNlZC6pdqYvsuWZWyB6w= go.mondoo.com/ranger-rpc v0.6.5/go.mod h1:kwPJSYj32vZJjWoQSKEao5YoUO/ZRcjVGxBOL4tApf0= go.mongodb.org/mongo-driver v1.10.0 h1:UtV6N5k14upNp4LTduX0QCufG124fSu25Wz9tu94GLg= diff --git a/internal/provider/gql.go b/internal/provider/gql.go index 1ab887a..eef3ebf 100644 --- a/internal/provider/gql.go +++ b/internal/provider/gql.go @@ -223,40 +223,6 @@ type SpaceReportPayload struct { SpaceReport SpaceReport } -func (c *ExtendedGqlClient) GetPolicySpaceReport(ctx context.Context, spaceMrn string) (*[]Policy, error) { - // Define the query struct according to the provided query - var spaceReportQuery struct { - Report struct { - SpaceReport SpaceReport `graphql:"... on SpaceReport"` - } `graphql:"spaceReport(input: $input)"` - } - // Define the input variable according to the provided query - input := mondoov1.SpaceReportInput{ - SpaceMrn: mondoov1.String(spaceMrn), - } - - variables := map[string]interface{}{ - "input": input, - } - - tflog.Trace(ctx, "GetSpaceReportInput", map[string]interface{}{ - "input": fmt.Sprintf("%+v", input), - }) - - // Execute the query - err := c.Query(ctx, &spaceReportQuery, variables) - if err != nil { - return nil, err - } - - var policies []Policy - for _, edges := range spaceReportQuery.Report.SpaceReport.PolicyReportSummaries.Edges { - policies = append(policies, edges.Node.Policy) - } - - return &policies, nil -} - type ContentInput struct { ScopeMrn string CatalogType string @@ -467,7 +433,7 @@ func (c *ExtendedGqlClient) CreateIntegration(ctx context.Context, spaceMrn, nam ConfigurationOptions: opts, } - tflog.Trace(ctx, "CreateSpaceInput", map[string]interface{}{ + tflog.Trace(ctx, "CreateClientIntegrationInput", map[string]interface{}{ "input": fmt.Sprintf("%+v", createInput), }) diff --git a/internal/provider/iam_workload_identity_binding.go b/internal/provider/iam_workload_identity_binding.go new file mode 100644 index 0000000..e5b50c6 --- /dev/null +++ b/internal/provider/iam_workload_identity_binding.go @@ -0,0 +1,390 @@ +// Copyright (c) Mondoo, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package provider + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/mapplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + mondoov1 "go.mondoo.com/mondoo-go" +) + +var _ resource.Resource = (*IAMWorkloadIdentityBindingResource)(nil) + +func NewIAMWorkloadIdentityBindingResource() resource.Resource { + return &IAMWorkloadIdentityBindingResource{} +} + +// IAMWorkloadIdentityBindingResource defines the resource implementation. +type IAMWorkloadIdentityBindingResource struct { + client *ExtendedGqlClient +} + +// IAMWorkloadIdentityBindingResourceModel describes the resource data model. +type IAMWorkloadIdentityBindingResourceModel struct { + // scope + SpaceID types.String `tfsdk:"space_id"` + + // Binding details + + // Mondoo resource name + Mrn types.String `tfsdk:"mrn"` + // User selected name. (Required.) + Name types.String `tfsdk:"name"` + // URI for the token issuer, e.g. https://accounts.google.com. (Required.) + IssuerURI types.String `tfsdk:"issuer_uri"` + // Optional description. (Optional.) + Description types.String `tfsdk:"description"` + // List of roles associated with the binding (e.g. agent mrn). (Optional.) + Roles types.List `tfsdk:"roles"` + // Unique identifier to confirm. (Required.) + Subject types.String `tfsdk:"subject"` + // Expiration in seconds associated with the binding. (Optional.) + Expiration types.Int32 `tfsdk:"expiration"` + // List of allowed audiences. (Optional.) + AllowedAudiences types.List `tfsdk:"allowed_audiences"` + // List of additional configurations to confirm. (Optional.) + Mappings types.Map `tfsdk:"mappings"` +} + +func (r *IAMWorkloadIdentityBindingResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_iam_workload_identity_binding" +} + +func (r *IAMWorkloadIdentityBindingResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + // This description is used by the documentation generator and the language server. + MarkdownDescription: `Allows management of a Mondoo Workload Identity Federation bindings.`, + + Attributes: map[string]schema.Attribute{ + "space_id": schema.StringAttribute{ + MarkdownDescription: "Mondoo space identifier. If there is no ID, the provider space is used.", + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "mrn": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "The Mondoo resource name (MRN) of the created binding.", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "name": schema.StringAttribute{ + MarkdownDescription: "Name of the binding.", + Required: true, + }, + "description": schema.StringAttribute{ + MarkdownDescription: "Description of the binding.", + Optional: true, + Computed: true, + Default: stringdefault.StaticString("Created by Terraform"), + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "roles": schema.ListAttribute{ + MarkdownDescription: "List of roles associated with the binding (e.g. agent mrn).", + ElementType: types.StringType, + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.List{ + listplanmodifier.UseStateForUnknown(), + }, + }, + "issuer_uri": schema.StringAttribute{ + MarkdownDescription: "URI for the token issuer, e.g. https://accounts.google.com.", + Required: true, + }, + "subject": schema.StringAttribute{ + MarkdownDescription: "Unique identifier to confirm.", + Required: true, + }, + "expiration": schema.Int32Attribute{ + MarkdownDescription: "Expiration in seconds associated with the binding.", + Optional: true, + }, + "allowed_audiences": schema.ListAttribute{ + MarkdownDescription: " List of allowed audiences.", + ElementType: types.StringType, + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.List{ + listplanmodifier.UseStateForUnknown(), + }, + }, + "mappings": schema.MapAttribute{ + MarkdownDescription: "List of additional configurations to confirm.", + Optional: true, + ElementType: types.StringType, + PlanModifiers: []planmodifier.Map{ + mapplanmodifier.UseStateForUnknown(), + }, + }, + }, + } +} + +func (r *IAMWorkloadIdentityBindingResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(*ExtendedGqlClient) + + if !ok { + resp.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Expected *http.Client. Got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + + return + } + + r.client = client +} + +type WIFAuthBinding struct { + Mrn string + Name string + Description string + Scope string + Roles []string + Expiration int32 + IssuerURI string + Subject string + Mappings []KeyValue + AllowedAudiences []string +} + +type WIFExternalAuthConfig struct { + UniverseDomain string + Type string + Audience string + SubjectTokenType string + Scopes []string + IssuerURI string +} + +func (r *IAMWorkloadIdentityBindingResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data IAMWorkloadIdentityBindingResourceModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + // Compute and validate the space + space, err := r.client.ComputeSpace(data.SpaceID) + if err != nil { + resp.Diagnostics.AddError("Invalid Configuration", err.Error()) + return + } + ctx = tflog.SetField(ctx, "space_mrn", space.MRN()) + + // Do GraphQL request to API to create the resource + var ( + roles = ConvertSliceStrings(data.Roles) + allowedAudiences = ConvertSliceStrings(data.AllowedAudiences) + ) + + var mappings []mondoov1.KeyValueInput + mappingsMap, d := data.Mappings.ToMapValue(context.Background()) + resp.Diagnostics.Append(d...) + if resp.Diagnostics.HasError() { + return + } + for key, value := range mappingsMap.Elements() { + mappings = append(mappings, mondoov1.KeyValueInput{ + Key: mondoov1.String(key), + Value: mondoov1.NewStringPtr(mondoov1.String(value.String())), + }) + } + + // We can't do this until we fix some inconsistencies + createInput := mondoov1.CreateWIFAuthBindingInput{ + ScopeMrn: mondoov1.String(space.MRN()), + Name: mondoov1.String(data.Name.ValueString()), + Description: mondoov1.NewStringPtr(mondoov1.String(data.Description.ValueString())), + Roles: &roles, + IssuerURI: mondoov1.String(data.IssuerURI.ValueString()), + Subject: mondoov1.String(data.SpaceID.ValueString()), + Expiration: mondoov1.NewIntPtr(mondoov1.Int(data.Expiration.ValueInt32())), + AllowedAudiences: &allowedAudiences, + Mappings: &mappings, + } + + tflog.Debug(ctx, "CreateWIFAuthBindingInput", map[string]interface{}{ + "input": fmt.Sprintf("%+v", createInput), + }) + + var createMutation struct { + CreateIAMWorkloadIdentityBinding struct { + Binding WIFAuthBinding + Config WIFExternalAuthConfig + } `graphql:"createWIFAuthBinding(input: $input)"` + } + + err = r.client.Mutate(ctx, &createMutation, createInput, nil) + if err != nil { + resp.Diagnostics. + AddError("Client Error", + fmt.Sprintf("Unable to create binding. Got error: %s", err), + ) + return + } + + // Write logs using the tflog package + tflog.Debug(ctx, "created a b2nding resource", map[string]interface{}{ + "input": fmt.Sprintf("%+v", createMutation), + }) + // Save space mrn into the Terraform state. + data.Mrn = types.StringValue(createMutation.CreateIAMWorkloadIdentityBinding.Binding.Mrn) + data.Description = types.StringValue(createMutation.CreateIAMWorkloadIdentityBinding.Binding.Description) + data.Roles = ConvertListValue(createMutation.CreateIAMWorkloadIdentityBinding.Binding.Roles) + data.AllowedAudiences = ConvertListValue(createMutation.CreateIAMWorkloadIdentityBinding.Binding.AllowedAudiences) + data.SpaceID = types.StringValue(space.ID()) + if len(createMutation.CreateIAMWorkloadIdentityBinding.Binding.Mappings) != 0 { + newMappings, _ := types.MapValueFrom(context.Background(), types.StringType, createMutation.CreateIAMWorkloadIdentityBinding.Binding.Mappings) + data.Mappings = newMappings + } + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *IAMWorkloadIdentityBindingResource) readIAMWorkloadIdentityBinding(ctx context.Context, mrn string) (IAMWorkloadIdentityBindingResourceModel, error) { + var q struct { + IAMWorkloadIdentityBinding struct { + Binding WIFAuthBinding + Config WIFExternalAuthConfig + } `graphql:"getWIFAuthBinding(mrn: $mrn)"` + } + variables := map[string]interface{}{ + "mrn": mondoov1.String(mrn), + } + + tflog.Debug(ctx, "getWIFAuthBindingInput", map[string]interface{}{ + "input": fmt.Sprintf("%+v", variables), + }) + + err := r.client.Query(ctx, &q, variables) + if err != nil { + return IAMWorkloadIdentityBindingResourceModel{}, err + } + + tflog.Debug(ctx, "getWIFAuthBindingPayload", map[string]interface{}{ + "payload": fmt.Sprintf("%+v", q), + }) + + return IAMWorkloadIdentityBindingResourceModel{ + SpaceID: types.StringValue(q.IAMWorkloadIdentityBinding.Binding.Scope), + Mrn: types.StringValue(q.IAMWorkloadIdentityBinding.Binding.Mrn), + Name: types.StringValue(q.IAMWorkloadIdentityBinding.Binding.Name), + Description: types.StringValue(q.IAMWorkloadIdentityBinding.Binding.Description), + IssuerURI: types.StringValue(q.IAMWorkloadIdentityBinding.Binding.IssuerURI), + Subject: types.StringValue(q.IAMWorkloadIdentityBinding.Binding.Subject), + Expiration: types.Int32Value(q.IAMWorkloadIdentityBinding.Binding.Expiration), + Roles: ConvertListValue(q.IAMWorkloadIdentityBinding.Binding.Roles), + AllowedAudiences: ConvertListValue(q.IAMWorkloadIdentityBinding.Binding.AllowedAudiences), + Mappings: types.MapNull(types.StringType), + }, nil +} + +func (r *IAMWorkloadIdentityBindingResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data IAMWorkloadIdentityBindingResourceModel + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + m, err := r.readIAMWorkloadIdentityBinding(ctx, data.Mrn.ValueString()) + if err != nil { + resp.Diagnostics.AddError( + "Client Error", + fmt.Sprintf("Unable to read binding. Got error: %s", err), + ) + return + } + + // Save updated data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &m)...) +} + +// Update is not allowed by design. We only read and exist. +func (r *IAMWorkloadIdentityBindingResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var data IAMWorkloadIdentityBindingResourceModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) +} + +type DeletePayload struct { + Mrn mondoov1.String +} + +func (r *IAMWorkloadIdentityBindingResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var data IAMWorkloadIdentityBindingResourceModel + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + // Do GraphQL request to API to delete the resource. + var deleteMutation struct { + RemoveWIFAuthBinding DeletePayload `graphql:"removeWIFAuthBinding(mrn: $mrn)"` + } + + variables := map[string]interface{}{ + "mrn": mondoov1.String(data.Mrn.String()), + } + tflog.Debug(ctx, "RemoveWIFAuthBindingVariables", map[string]interface{}{ + "input": fmt.Sprintf("%+v", variables), + }) + err := r.client.Mutate(ctx, &deleteMutation, nil, variables) + if err != nil { + resp.Diagnostics.AddError( + "Client Error", + fmt.Sprintf("Unable to delete binding. Got error: %s", err), + ) + } +} + +func (r *IAMWorkloadIdentityBindingResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + mrn := req.ID + + m, err := r.readIAMWorkloadIdentityBinding(ctx, mrn) + if err != nil { + resp. + Diagnostics. + AddError("Client Error", + fmt.Sprintf( + "Unable to import binding. Got error: %s", err, + ), + ) + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &m)...) +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 207e0c6..ba90773 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -212,6 +212,7 @@ func (p *MondooProvider) Resources(_ context.Context) []func() resource.Resource NewIntegrationMsDefenderResource, NewIntegrationCrowdstrikeResource, NewIntegrationSentinelOneResource, + NewIAMWorkloadIdentityBindingResource, } }