Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions docs/resources/policy_assignment.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ description: |-
## Example Usage

```terraform
# Assign policies to a space (using provider-configured space)
provider "mondoo" {
space = "hungry-poet-123456"
}
Expand All @@ -22,16 +23,26 @@ resource "mondoo_policy_assignment" "space" {
"//policy.api.mondoo.app/policies/mondoo-aws-security",
]
}

# Assign policies to an organization using scope_mrn
resource "mondoo_policy_assignment" "org" {
scope_mrn = "//captain.api.mondoo.app/organizations/your-org-id"

policies = [
"//policy.api.mondoo.app/policies/mondoo-aws-security",
]
}
```

<!-- schema generated by tfplugindocs -->
## Schema

### Required

- `policies` (List of String) Policies to assign to the space.
- `policies` (List of String) Policies to assign to the scope.

### Optional

- `space_id` (String) Mondoo space identifier. If there is no space ID, the provider space is used.
- `scope_mrn` (String) The MRN of the scope (space, organization, or platform) to assign policies to.
- `space_id` (String, Deprecated) Mondoo space identifier. If there is no space ID, the provider space is used.
- `state` (String) Policy assignment state (preview, enabled, or disabled).
10 changes: 10 additions & 0 deletions examples/resources/mondoo_policy_assignment/resource.tf
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# Assign policies to a space (using provider-configured space)
provider "mondoo" {
space = "hungry-poet-123456"
}
Expand All @@ -7,3 +8,12 @@ resource "mondoo_policy_assignment" "space" {
"//policy.api.mondoo.app/policies/mondoo-aws-security",
]
}

# Assign policies to an organization using scope_mrn
resource "mondoo_policy_assignment" "org" {
scope_mrn = "//captain.api.mondoo.app/organizations/your-org-id"

policies = [
"//policy.api.mondoo.app/policies/mondoo-aws-security",
]
}
83 changes: 56 additions & 27 deletions internal/provider/policy_assignment_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ import (

"github.com/hashicorp/terraform-plugin-framework-validators/listvalidator"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/path"
"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/schema/validator"
"github.com/hashicorp/terraform-plugin-log/tflog"
mondoov1 "go.mondoo.com/mondoo-go"
Expand All @@ -31,7 +34,8 @@ type policyAssignmentResource struct {

type policyAssignmentsResourceModel struct {
// scope
SpaceID types.String `tfsdk:"space_id"`
SpaceID types.String `tfsdk:"space_id"`
ScopeMrn types.String `tfsdk:"scope_mrn"`

// assigned policies
PolicyMrns types.List `tfsdk:"policies"`
Expand All @@ -50,9 +54,23 @@ func (r *policyAssignmentResource) Schema(_ context.Context, req resource.Schema
"space_id": schema.StringAttribute{
MarkdownDescription: "Mondoo space identifier. If there is no space ID, the provider space is used.",
Optional: true,
DeprecationMessage: "Use `scope_mrn` instead.",
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"scope_mrn": schema.StringAttribute{
MarkdownDescription: "The MRN of the scope (space, organization, or platform) to assign policies to.",
Optional: true,
Validators: []validator.String{
stringvalidator.ConflictsWith(path.MatchRoot("space_id")),
},
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"policies": schema.ListAttribute{
MarkdownDescription: "Policies to assign to the space.",
MarkdownDescription: "Policies to assign to the scope.",
ElementType: types.StringType,
Required: true,
Validators: []validator.List{listvalidator.SizeAtLeast(1)},
Expand Down Expand Up @@ -90,6 +108,17 @@ func (r *policyAssignmentResource) Configure(ctx context.Context, req resource.C
r.client = client
}

func (r *policyAssignmentResource) getScope(data *policyAssignmentsResourceModel) (string, error) {
if !data.ScopeMrn.IsNull() && data.ScopeMrn.ValueString() != "" {
return data.ScopeMrn.ValueString(), nil
}
space, err := r.client.ComputeSpace(data.SpaceID)
if err != nil {
return "", err
Comment thread
jaym marked this conversation as resolved.
}
return space.MRN(), nil
}

func (r *policyAssignmentResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data policyAssignmentsResourceModel

Expand All @@ -100,13 +129,13 @@ func (r *policyAssignmentResource) Create(ctx context.Context, req resource.Crea
return
}

// Compute and validate the space
space, err := r.client.ComputeSpace(data.SpaceID)
// Resolve the scope MRN
scopeMrn, err := r.getScope(&data)
if err != nil {
resp.Diagnostics.AddError("Invalid Configuration", err.Error())
return
}
ctx = tflog.SetField(ctx, "space_mrn", space.MRN())
ctx = tflog.SetField(ctx, "scope_mrn", scopeMrn)

// Do GraphQL request to API to create the resource
policyMrns := []string{}
Expand All @@ -118,12 +147,12 @@ func (r *policyAssignmentResource) Create(ctx context.Context, req resource.Crea
switch state {
case "", "enabled":
action := mondoov1.PolicyActionActive
err = r.client.AssignPolicy(ctx, space.MRN(), action, policyMrns)
err = r.client.AssignPolicy(ctx, scopeMrn, action, policyMrns)
case "preview":
action := mondoov1.PolicyActionIgnore
err = r.client.AssignPolicy(ctx, space.MRN(), action, policyMrns)
err = r.client.AssignPolicy(ctx, scopeMrn, action, policyMrns)
case "disabled":
err = r.client.UnassignPolicy(ctx, space.MRN(), policyMrns)
err = r.client.UnassignPolicy(ctx, scopeMrn, policyMrns)
default:
resp.Diagnostics.AddError(
"Invalid state: "+state,
Expand Down Expand Up @@ -154,16 +183,16 @@ func (r *policyAssignmentResource) Read(ctx context.Context, req resource.ReadRe
return
}

// Compute and validate the space
space, err := r.client.ComputeSpace(data.SpaceID)
// Resolve the scope MRN
scopeMrn, err := r.getScope(&data)
if err != nil {
resp.Diagnostics.AddError("Invalid Configuration", err.Error())
return
}
ctx = tflog.SetField(ctx, "space_mrn", space.MRN())
ctx = tflog.SetField(ctx, "scope_mrn", scopeMrn)

// Fetch active policies from API
activePolicies, err := r.client.GetActivePolicies(ctx, space.MRN())
activePolicies, err := r.client.GetActivePolicies(ctx, scopeMrn)
if err != nil {
resp.Diagnostics.AddError("Failed to fetch active policies", err.Error())
return
Expand All @@ -172,7 +201,7 @@ func (r *policyAssignmentResource) Read(ctx context.Context, req resource.ReadRe
// Build lookup map: policyMrn -> action, filtered by assignedScope
policyActions := make(map[string]string)
for _, p := range activePolicies {
if string(p.AssignedScope) == space.MRN() {
if string(p.AssignedScope) == scopeMrn {
policyActions[string(p.Mrn)] = string(p.Action)
}
}
Expand Down Expand Up @@ -225,15 +254,15 @@ func (r *policyAssignmentResource) Update(ctx context.Context, req resource.Upda
return
}

// Compute and validate the space
space, err := r.client.ComputeSpace(data.SpaceID)
// Resolve the scope MRN
scopeMrn, err := r.getScope(&data)
if err != nil {
resp.Diagnostics.AddError("Invalid Configuration", err.Error())
return
}
ctx = tflog.SetField(ctx, "space_mrn", space.MRN())
ctx = tflog.SetField(ctx, "scope_mrn", scopeMrn)

// Do GraphQL request to API to create the resource
// Do GraphQL request to API to update the resource
policyMrns := []string{}
data.PolicyMrns.ElementsAs(ctx, &policyMrns, false)

Expand All @@ -243,12 +272,12 @@ func (r *policyAssignmentResource) Update(ctx context.Context, req resource.Upda
switch state {
case "", "enabled":
action := mondoov1.PolicyActionActive
err = r.client.AssignPolicy(ctx, space.MRN(), action, policyMrns)
err = r.client.AssignPolicy(ctx, scopeMrn, action, policyMrns)
case "preview":
action := mondoov1.PolicyActionIgnore
err = r.client.AssignPolicy(ctx, space.MRN(), action, policyMrns)
err = r.client.AssignPolicy(ctx, scopeMrn, action, policyMrns)
case "disabled":
err = r.client.UnassignPolicy(ctx, space.MRN(), policyMrns)
err = r.client.UnassignPolicy(ctx, scopeMrn, policyMrns)
default:
resp.Diagnostics.AddError(
"Invalid state: "+state,
Expand Down Expand Up @@ -279,25 +308,25 @@ func (r *policyAssignmentResource) Delete(ctx context.Context, req resource.Dele
return
}

// Compute and validate the space
space, err := r.client.ComputeSpace(data.SpaceID)
// Resolve the scope MRN
scopeMrn, err := r.getScope(&data)
if err != nil {
resp.Diagnostics.AddError("Invalid Configuration", err.Error())
return
}
ctx = tflog.SetField(ctx, "space_mrn", space.MRN())
ctx = tflog.SetField(ctx, "scope_mrn", scopeMrn)

// Do GraphQL request to API to create the resource
// Do GraphQL request to API to delete the resource
policyMrns := []string{}
data.PolicyMrns.ElementsAs(ctx, &policyMrns, false)

tflog.Debug(ctx, "Deleting policy assignment")
// no matter the state, we unassign the policies
Comment thread
mondoo-code-review[bot] marked this conversation as resolved.
err = r.client.UnassignPolicy(ctx, space.MRN(), policyMrns)
err = r.client.UnassignPolicy(ctx, scopeMrn, policyMrns)
if err != nil {
resp.Diagnostics.AddError(
"Error creating policy assignment",
fmt.Sprintf("Error creating policy assignment: %s", err),
"Error deleting policy assignment",
fmt.Sprintf("Error deleting policy assignment: %s", err),
)
return
}
Expand Down
44 changes: 43 additions & 1 deletion internal/provider/policy_assignment_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ resource "mondoo_policy_assignment" "space" {
policies = [
"//policy.api.mondoo.app/policies/mondoo-aws-security",
]

state = %[2]q

depends_on = [
Expand All @@ -68,3 +68,45 @@ resource "mondoo_policy_assignment" "space" {
}
`, resourceOrgID, state)
}

func TestAccPolicyAssignmentResourceWithScopeMrn(t *testing.T) {
orgID, err := getOrgId()
if err != nil {
t.Fatal(err)
}
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
// Create and Read testing
{
Config: testAccPolicyAssignmentResourceWithScopeMrnConfig(orgID, "enabled"),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("mondoo_policy_assignment.org", "state", "enabled"),
),
},
// Update and Read testing
{
Config: testAccPolicyAssignmentResourceWithScopeMrnConfig(orgID, "disabled"),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("mondoo_policy_assignment.org", "state", "disabled"),
),
},
// Delete testing automatically occurs in TestCase
},
})
}

func testAccPolicyAssignmentResourceWithScopeMrnConfig(orgID string, state string) string {
return fmt.Sprintf(`
resource "mondoo_policy_assignment" "org" {
scope_mrn = "//captain.api.mondoo.app/organizations/%[1]s"

policies = [
"//policy.api.mondoo.app/policies/mondoo-aws-security",
]

state = %[2]q
}
Comment thread
jaym marked this conversation as resolved.
`, orgID, state)
}
Loading