From a89725c2259b612773e26e2d1aa149bc1e5b94eb Mon Sep 17 00:00:00 2001 From: Stijn Simons Date: Thu, 9 Jul 2026 07:56:53 +0000 Subject: [PATCH 1/2] Add cloudstack_role_permission resource --- cloudstack/provider.go | 1 + .../resource_cloudstack_role_permission.go | 157 ++++++++++++++++++ ...esource_cloudstack_role_permission_test.go | 152 +++++++++++++++++ website/cloudstack.erb | 4 + website/docs/r/role_permission.html.markdown | 61 +++++++ 5 files changed, 375 insertions(+) create mode 100644 cloudstack/resource_cloudstack_role_permission.go create mode 100644 cloudstack/resource_cloudstack_role_permission_test.go create mode 100644 website/docs/r/role_permission.html.markdown diff --git a/cloudstack/provider.go b/cloudstack/provider.go index 72090147..cf2b0150 100644 --- a/cloudstack/provider.go +++ b/cloudstack/provider.go @@ -162,6 +162,7 @@ func Provider() *schema.Provider { "cloudstack_domain": resourceCloudStackDomain(), "cloudstack_network_service_provider": resourceCloudStackNetworkServiceProvider(), "cloudstack_role": resourceCloudStackRole(), + "cloudstack_role_permission": resourceCloudStackRolePermission(), "cloudstack_limits": resourceCloudStackLimits(), "cloudstack_snapshot_policy": resourceCloudStackSnapshotPolicy(), "cloudstack_quota_tariff": resourceCloudStackQuotaTariff(), diff --git a/cloudstack/resource_cloudstack_role_permission.go b/cloudstack/resource_cloudstack_role_permission.go new file mode 100644 index 00000000..f35d1bb6 --- /dev/null +++ b/cloudstack/resource_cloudstack_role_permission.go @@ -0,0 +1,157 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +package cloudstack + +import ( + "fmt" + "log" + + "github.com/apache/cloudstack-go/v2/cloudstack" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +func resourceCloudStackRolePermission() *schema.Resource { + return &schema.Resource{ + Create: resourceCloudStackRolePermissionCreate, + Read: resourceCloudStackRolePermissionRead, + Update: resourceCloudStackRolePermissionUpdate, + Delete: resourceCloudStackRolePermissionDelete, + Schema: map[string]*schema.Schema{ + "role_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "ID of the role the permission (rule) belongs to.", + }, + "rule": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "The API name or wildcard (e.g. 'list*') the permission applies to.", + }, + "permission": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice([]string{"allow", "deny"}, false), + Description: "Whether the rule is allowed or denied. Valid options are: allow, deny.", + }, + "description": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: "A description for the role permission.", + }, + }, + } +} + +func resourceCloudStackRolePermissionCreate(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + roleID := d.Get("role_id").(string) + rule := d.Get("rule").(string) + permission := d.Get("permission").(string) + + // Create a new parameter struct + p := cs.Role.NewCreateRolePermissionParams(permission, roleID, rule) + + if description, ok := d.GetOk("description"); ok { + p.SetDescription(description.(string)) + } + + log.Printf("[DEBUG] Creating Role Permission %s (%s) for role %s", rule, permission, roleID) + r, err := cs.Role.CreateRolePermission(p) + + if err != nil { + return fmt.Errorf("Error creating Role Permission: %s", err) + } + + log.Printf("[DEBUG] Role Permission %s successfully created", rule) + d.SetId(r.Id) + + return resourceCloudStackRolePermissionRead(d, meta) +} + +func resourceCloudStackRolePermissionRead(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + roleID := d.Get("role_id").(string) + + // The API only supports listing permissions by role, so fetch them all + // and locate the one matching this resource's ID. + p := cs.Role.NewListRolePermissionsParams() + p.SetRoleid(roleID) + + l, err := cs.Role.ListRolePermissions(p) + if err != nil { + return fmt.Errorf("Error listing Role Permissions: %s", err) + } + + for _, rp := range l.RolePermissions { + if rp.Id == d.Id() { + d.Set("role_id", rp.Roleid) + d.Set("rule", rp.Rule) + d.Set("permission", rp.Permission) + d.Set("description", rp.Description) + return nil + } + } + + log.Printf("[DEBUG] Role Permission %s no longer exists", d.Id()) + d.SetId("") + + return nil +} + +func resourceCloudStackRolePermissionUpdate(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + // Only the permission (allow/deny) can be changed in place; the role_id, + // rule and description are all ForceNew. + p := cs.Role.NewUpdateRolePermissionParams(d.Get("role_id").(string)) + p.SetRuleid(d.Id()) + p.SetPermission(d.Get("permission").(string)) + + log.Printf("[DEBUG] Updating Role Permission %s", d.Id()) + _, err := cs.Role.UpdateRolePermission(p) + + if err != nil { + return fmt.Errorf("Error updating Role Permission: %s", err) + } + + return resourceCloudStackRolePermissionRead(d, meta) +} + +func resourceCloudStackRolePermissionDelete(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + // Create a new parameter struct + p := cs.Role.NewDeleteRolePermissionParams(d.Id()) + + log.Printf("[DEBUG] Deleting Role Permission %s", d.Id()) + _, err := cs.Role.DeleteRolePermission(p) + + if err != nil { + return fmt.Errorf("Error deleting Role Permission: %s", err) + } + + return nil +} diff --git a/cloudstack/resource_cloudstack_role_permission_test.go b/cloudstack/resource_cloudstack_role_permission_test.go new file mode 100644 index 00000000..545c0571 --- /dev/null +++ b/cloudstack/resource_cloudstack_role_permission_test.go @@ -0,0 +1,152 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +package cloudstack + +import ( + "fmt" + "testing" + + "github.com/apache/cloudstack-go/v2/cloudstack" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" +) + +func TestAccCloudStackRolePermission_basic(t *testing.T) { + var rolePermission cloudstack.RolePermission + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackRolePermissionDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCloudStackRolePermission_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackRolePermissionExists("cloudstack_role_permission.foo", &rolePermission), + resource.TestCheckResourceAttr( + "cloudstack_role_permission.foo", "rule", "listVirtualMachines"), + resource.TestCheckResourceAttr( + "cloudstack_role_permission.foo", "permission", "allow"), + resource.TestCheckResourceAttr( + "cloudstack_role_permission.foo", "description", "terraform test role permission"), + ), + }, + { + Config: testAccCloudStackRolePermission_update, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackRolePermissionExists("cloudstack_role_permission.foo", &rolePermission), + resource.TestCheckResourceAttr( + "cloudstack_role_permission.foo", "permission", "deny"), + ), + }, + }, + }) +} + +func testAccCheckCloudStackRolePermissionExists(n string, rolePermission *cloudstack.RolePermission) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No Role Permission ID is set") + } + + cs := testAccProvider.Meta().(*cloudstack.CloudStackClient) + + p := cs.Role.NewListRolePermissionsParams() + p.SetRoleid(rs.Primary.Attributes["role_id"]) + + l, err := cs.Role.ListRolePermissions(p) + if err != nil { + return err + } + + for _, rp := range l.RolePermissions { + if rp.Id == rs.Primary.ID { + *rolePermission = *rp + return nil + } + } + + return fmt.Errorf("Role Permission not found") + } +} + +func testAccCheckCloudStackRolePermissionDestroy(s *terraform.State) error { + cs := testAccProvider.Meta().(*cloudstack.CloudStackClient) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "cloudstack_role_permission" { + continue + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No Role Permission ID is set") + } + + p := cs.Role.NewListRolePermissionsParams() + p.SetRoleid(rs.Primary.Attributes["role_id"]) + + l, err := cs.Role.ListRolePermissions(p) + if err != nil { + // If the parent role is already gone, the permission is too. + continue + } + + for _, rp := range l.RolePermissions { + if rp.Id == rs.Primary.ID { + return fmt.Errorf("Role Permission %s still exists", rs.Primary.ID) + } + } + } + + return nil +} + +const testAccCloudStackRolePermission_basic = ` +resource "cloudstack_role" "foo" { + name = "terraform-role" + type = "User" +} + +resource "cloudstack_role_permission" "foo" { + role_id = cloudstack_role.foo.id + rule = "listVirtualMachines" + permission = "allow" + description = "terraform test role permission" +} +` + +const testAccCloudStackRolePermission_update = ` +resource "cloudstack_role" "foo" { + name = "terraform-role" + type = "User" +} + +resource "cloudstack_role_permission" "foo" { + role_id = cloudstack_role.foo.id + rule = "listVirtualMachines" + permission = "deny" + description = "terraform test role permission" +} +` diff --git a/website/cloudstack.erb b/website/cloudstack.erb index 1ea7f7ec..06b6a7c3 100644 --- a/website/cloudstack.erb +++ b/website/cloudstack.erb @@ -132,6 +132,10 @@ > cloudstack_role + + > + cloudstack_role_permission + diff --git a/website/docs/r/role_permission.html.markdown b/website/docs/r/role_permission.html.markdown new file mode 100644 index 00000000..59eb78f0 --- /dev/null +++ b/website/docs/r/role_permission.html.markdown @@ -0,0 +1,61 @@ +--- +layout: "cloudstack" +page_title: "CloudStack: cloudstack_role_permission" +description: |- + Creates a role permission (rule) for a role. +--- + +# cloudstack_role_permission + +Creates a role permission. A role permission is a single rule that allows or +denies a role access to an API (or a wildcard set of APIs). + +Rules belonging to the same role are evaluated in the order in which they are +created, and the first matching rule wins. Order the corresponding +`cloudstack_role_permission` resources accordingly (for example with +`depends_on`) when precedence matters. + +## Example Usage + +```hcl +resource "cloudstack_role" "custom" { + name = "custom-role" + type = "User" +} + +# Allow listing virtual machines +resource "cloudstack_role_permission" "list_vms" { + role_id = cloudstack_role.custom.id + rule = "listVirtualMachines" + permission = "allow" + description = "Allow listing virtual machines" +} + +# Deny every other API using a wildcard +resource "cloudstack_role_permission" "deny_all" { + role_id = cloudstack_role.custom.id + rule = "*" + permission = "deny" + + depends_on = [cloudstack_role_permission.list_vms] +} +``` + +## Argument Reference + +The following arguments are supported: + +* `role_id` - (Required) ID of the role the permission belongs to. Changing this + forces a new resource to be created. +* `rule` - (Required) The API name or a wildcard (e.g. `list*` or `*`) the rule + applies to. Changing this forces a new resource to be created. +* `permission` - (Required) Whether the rule is allowed or denied. Valid options + are: `allow`, `deny`. +* `description` - (Optional) A description for the role permission. Changing this + forces a new resource to be created. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The ID of the role permission. From d6abfc8c04e946e5535a3b719650e6cb6f994c13 Mon Sep 17 00:00:00 2001 From: Stijn Simons Date: Fri, 10 Jul 2026 12:04:11 +0000 Subject: [PATCH 2/2] Assert role permission update is in-place --- cloudstack/resource_cloudstack_role_permission_test.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cloudstack/resource_cloudstack_role_permission_test.go b/cloudstack/resource_cloudstack_role_permission_test.go index 545c0571..fae3cfde 100644 --- a/cloudstack/resource_cloudstack_role_permission_test.go +++ b/cloudstack/resource_cloudstack_role_permission_test.go @@ -83,6 +83,10 @@ func testAccCheckCloudStackRolePermissionExists(n string, rolePermission *clouds for _, rp := range l.RolePermissions { if rp.Id == rs.Primary.ID { + if rolePermission.Id != "" && rolePermission.Id != rp.Id { + return fmt.Errorf("Role Permission was recreated (old ID: %s, new ID: %s)", rolePermission.Id, rp.Id) + } + *rolePermission = *rp return nil }