From 60cb27cede5c6cb2c71c17ae1ec642a2a075a56a Mon Sep 17 00:00:00 2001 From: Stijn Simons Date: Thu, 9 Jul 2026 07:56:53 +0000 Subject: [PATCH 1/3] 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 c401de6ac94dae6250aaf59f6db00692c291ae34 Mon Sep 17 00:00:00 2001 From: Stijn Simons Date: Fri, 10 Jul 2026 12:04:11 +0000 Subject: [PATCH 2/3] 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 } From fe2fa9413a727bef0195fc473ca041d9bf39787a Mon Sep 17 00:00:00 2001 From: Stijn Simons Date: Fri, 31 Jul 2026 11:01:30 +0200 Subject: [PATCH 3/3] Fix ordering of permissions by using a list with (optionally) authorative management --- .../resource_cloudstack_role_permission.go | 418 +++++++++++++++--- ...esource_cloudstack_role_permission_test.go | 290 ++++++++++-- website/docs/r/role_permission.html.markdown | 62 +-- 3 files changed, 648 insertions(+), 122 deletions(-) diff --git a/cloudstack/resource_cloudstack_role_permission.go b/cloudstack/resource_cloudstack_role_permission.go index f35d1bb6..1769f8d0 100644 --- a/cloudstack/resource_cloudstack_role_permission.go +++ b/cloudstack/resource_cloudstack_role_permission.go @@ -22,12 +22,22 @@ package cloudstack import ( "fmt" "log" + "sync" "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" ) +var rolePermissionLocks sync.Map + +type rolePermissionSpec struct { + ID string + Rule string + Permission string + Description string +} + func resourceCloudStackRolePermission() *schema.Resource { return &schema.Resource{ Create: resourceCloudStackRolePermissionCreate, @@ -39,119 +49,407 @@ func resourceCloudStackRolePermission() *schema.Resource { Type: schema.TypeString, Required: true, ForceNew: true, - Description: "ID of the role the permission (rule) belongs to.", + Description: "ID of the role the permissions belong to.", }, - "rule": { - Type: schema.TypeString, - Required: true, - ForceNew: true, - Description: "The API name or wildcard (e.g. 'list*') the permission applies to.", + "authoritative": { + Type: schema.TypeBool, + Optional: true, + Default: false, + Description: "Whether permissions not declared in this resource should be deleted.", }, "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, + Type: schema.TypeList, Optional: true, - ForceNew: true, - Description: "A description for the role permission.", + Description: "Ordered list of role permission rules. Rules are evaluated from top to bottom.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Computed: true, + Description: "ID of the role permission.", + }, + "rule": { + Type: schema.TypeString, + Required: 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, + 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) + d.SetId(roleID) - // Create a new parameter struct - p := cs.Role.NewCreateRolePermissionParams(permission, roleID, rule) + roleLock := rolePermissionLock(roleID) + roleLock.Lock() + defer roleLock.Unlock() - if description, ok := d.GetOk("description"); ok { - p.SetDescription(description.(string)) + if err := reconcileCloudStackRolePermissions(d, meta, nil); err != nil { + return err } - log.Printf("[DEBUG] Creating Role Permission %s (%s) for role %s", rule, permission, roleID) - r, err := cs.Role.CreateRolePermission(p) + return resourceCloudStackRolePermissionRead(d, meta) +} + +func resourceCloudStackRolePermissionRead(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + roleID := d.Get("role_id").(string) + if roleID == "" { + roleID = d.Id() + } + rolePermissions, err := listCloudStackRolePermissions(cs, roleID) if err != nil { - return fmt.Errorf("Error creating Role Permission: %s", err) + return fmt.Errorf("Error listing Role Permissions: %s", err) + } + + permissionsByID := make(map[string]*cloudstack.RolePermission) + for _, rp := range rolePermissions { + permissionsByID[rp.Id] = rp + } + + var missing bool + var readPermissions []interface{} + used := make(map[string]bool) + + for _, desired := range rolePermissionSpecs(d.Get("permission").([]interface{})) { + var rp *cloudstack.RolePermission + if desired.ID != "" { + rp = permissionsByID[desired.ID] + } + if rp == nil { + rp = findMatchingRolePermission(rolePermissions, desired, used) + } + if rp == nil { + missing = true + readPermissions = append(readPermissions, rolePermissionState(desired)) + continue + } + + used[rp.Id] = true + readPermissions = append(readPermissions, rolePermissionState(rolePermissionSpec{ + ID: rp.Id, + Rule: rp.Rule, + Permission: rp.Permission, + Description: rp.Description, + })) + } + + if err := d.Set("permission", readPermissions); err != nil { + return fmt.Errorf("Error setting Role Permissions: %s", err) + } + + if missing { + log.Printf("[DEBUG] One or more Role Permissions for role %s no longer exist", roleID) + d.SetId("") } - log.Printf("[DEBUG] Role Permission %s successfully created", rule) - d.SetId(r.Id) + return nil +} + +func resourceCloudStackRolePermissionUpdate(d *schema.ResourceData, meta interface{}) error { + roleID := d.Get("role_id").(string) + roleLock := rolePermissionLock(roleID) + roleLock.Lock() + defer roleLock.Unlock() + + var oldPermissions []rolePermissionSpec + if d.HasChange("permission") { + oldRaw, _ := d.GetChange("permission") + oldPermissions = rolePermissionSpecs(oldRaw.([]interface{})) + } + + if err := reconcileCloudStackRolePermissions(d, meta, oldPermissions); err != nil { + return err + } return resourceCloudStackRolePermissionRead(d, meta) } -func resourceCloudStackRolePermissionRead(d *schema.ResourceData, meta interface{}) error { +func resourceCloudStackRolePermissionDelete(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) + roleLock := rolePermissionLock(roleID) + roleLock.Lock() + defer roleLock.Unlock() - l, err := cs.Role.ListRolePermissions(p) + rolePermissions, err := listCloudStackRolePermissions(cs, roleID) 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 + if d.Get("authoritative").(bool) { + for _, rp := range rolePermissions { + if err := deleteCloudStackRolePermission(cs, rp.Id); err != nil { + return err + } } + return nil + } + + rolePermissionsByID := make(map[string]*cloudstack.RolePermission) + for _, rp := range rolePermissions { + rolePermissionsByID[rp.Id] = rp } - log.Printf("[DEBUG] Role Permission %s no longer exists", d.Id()) - d.SetId("") + used := make(map[string]bool) + for _, permission := range rolePermissionSpecs(d.Get("permission").([]interface{})) { + ruleID := permission.ID + if ruleID == "" { + if rp := findMatchingRolePermission(rolePermissions, permission, used); rp != nil { + ruleID = rp.Id + } + } + if ruleID == "" || rolePermissionsByID[ruleID] == nil { + continue + } + used[ruleID] = true + if err := deleteCloudStackRolePermission(cs, ruleID); err != nil { + return err + } + } return nil } -func resourceCloudStackRolePermissionUpdate(d *schema.ResourceData, meta interface{}) error { +func reconcileCloudStackRolePermissions(d *schema.ResourceData, meta interface{}, oldPermissions []rolePermissionSpec) error { cs := meta.(*cloudstack.CloudStackClient) + roleID := d.Get("role_id").(string) - // 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)) + rolePermissions, err := listCloudStackRolePermissions(cs, roleID) + if err != nil { + return fmt.Errorf("Error listing Role Permissions: %s", err) + } - log.Printf("[DEBUG] Updating Role Permission %s", d.Id()) - _, err := cs.Role.UpdateRolePermission(p) + rolePermissionsByID := make(map[string]*cloudstack.RolePermission) + for _, rp := range rolePermissions { + rolePermissionsByID[rp.Id] = rp + } + + used := make(map[string]bool) + deleted := make(map[string]bool) + managedIDs := make([]string, 0) + managedIDSet := make(map[string]bool) + + for _, desired := range rolePermissionSpecs(d.Get("permission").([]interface{})) { + rp := rolePermissionsByID[desired.ID] + if rp != nil && (rp.Rule != desired.Rule || rp.Description != desired.Description) { + if exactMatch := findMatchingRolePermission(rolePermissions, desired, used); exactMatch != nil { + rp = exactMatch + } else { + if err := deleteCloudStackRolePermission(cs, rp.Id); err != nil { + return err + } + deleted[rp.Id] = true + used[rp.Id] = true + rp = nil + } + } else if rp == nil { + rp = findMatchingRolePermission(rolePermissions, desired, used) + } + + if rp == nil { + rp, err = createCloudStackRolePermission(cs, roleID, desired) + if err != nil { + return err + } + } else if rp.Permission != desired.Permission { + if err := updateCloudStackRolePermission(cs, roleID, rp.Id, desired.Permission); err != nil { + return err + } + } + + used[rp.Id] = true + managedIDs = append(managedIDs, rp.Id) + managedIDSet[rp.Id] = true + } + oldManagedIDs := make(map[string]bool) + for _, oldPermission := range oldPermissions { + if oldPermission.ID != "" { + oldManagedIDs[oldPermission.ID] = true + } + } + + if d.Get("authoritative").(bool) { + for _, rp := range rolePermissions { + if managedIDSet[rp.Id] || deleted[rp.Id] { + continue + } + if err := deleteCloudStackRolePermission(cs, rp.Id); err != nil { + return err + } + deleted[rp.Id] = true + } + } else { + for oldID := range oldManagedIDs { + if managedIDSet[oldID] || deleted[oldID] { + continue + } + if rolePermissionsByID[oldID] == nil { + continue + } + if err := deleteCloudStackRolePermission(cs, oldID); err != nil { + return err + } + deleted[oldID] = true + } + } + + rolePermissions, err = listCloudStackRolePermissions(cs, roleID) if err != nil { - return fmt.Errorf("Error updating Role Permission: %s", err) + return fmt.Errorf("Error listing Role Permissions: %s", err) } - return resourceCloudStackRolePermissionRead(d, meta) + ruleOrder := append([]string{}, managedIDs...) + for _, rp := range rolePermissions { + if !managedIDSet[rp.Id] { + ruleOrder = append(ruleOrder, rp.Id) + } + } + + if err := orderCloudStackRolePermissions(cs, roleID, ruleOrder); err != nil { + return err + } + + return nil } -func resourceCloudStackRolePermissionDelete(d *schema.ResourceData, meta interface{}) error { - cs := meta.(*cloudstack.CloudStackClient) +func listCloudStackRolePermissions(cs *cloudstack.CloudStackClient, roleID string) ([]*cloudstack.RolePermission, error) { + p := cs.Role.NewListRolePermissionsParams() + p.SetRoleid(roleID) - // Create a new parameter struct - p := cs.Role.NewDeleteRolePermissionParams(d.Id()) + l, err := cs.Role.ListRolePermissions(p) + if err != nil { + return nil, err + } + + return l.RolePermissions, nil +} - log.Printf("[DEBUG] Deleting Role Permission %s", d.Id()) - _, err := cs.Role.DeleteRolePermission(p) +func createCloudStackRolePermission(cs *cloudstack.CloudStackClient, roleID string, permission rolePermissionSpec) (*cloudstack.RolePermission, error) { + p := cs.Role.NewCreateRolePermissionParams(permission.Permission, roleID, permission.Rule) + if permission.Description != "" { + p.SetDescription(permission.Description) + } + log.Printf("[DEBUG] Creating Role Permission %s (%s) for role %s", permission.Rule, permission.Permission, roleID) + r, err := cs.Role.CreateRolePermission(p) if err != nil { + return nil, fmt.Errorf("Error creating Role Permission: %s", err) + } + + return &cloudstack.RolePermission{ + Id: r.Id, + Roleid: roleID, + Rule: permission.Rule, + Permission: permission.Permission, + Description: permission.Description, + }, nil +} + +func updateCloudStackRolePermission(cs *cloudstack.CloudStackClient, roleID, ruleID, permission string) error { + p := cs.Role.NewUpdateRolePermissionParams(roleID) + p.SetRuleid(ruleID) + p.SetPermission(permission) + + log.Printf("[DEBUG] Updating Role Permission %s", ruleID) + if _, err := cs.Role.UpdateRolePermission(p); err != nil { + return fmt.Errorf("Error updating Role Permission: %s", err) + } + + return nil +} + +func deleteCloudStackRolePermission(cs *cloudstack.CloudStackClient, ruleID string) error { + p := cs.Role.NewDeleteRolePermissionParams(ruleID) + + log.Printf("[DEBUG] Deleting Role Permission %s", ruleID) + if _, err := cs.Role.DeleteRolePermission(p); err != nil { return fmt.Errorf("Error deleting Role Permission: %s", err) } return nil } + +func orderCloudStackRolePermissions(cs *cloudstack.CloudStackClient, roleID string, ruleIDs []string) error { + if len(ruleIDs) == 0 { + return nil + } + + p := cs.Role.NewUpdateRolePermissionParams(roleID) + p.SetRuleorder(ruleIDs) + + log.Printf("[DEBUG] Ordering Role Permissions for role %s: %v", roleID, ruleIDs) + if _, err := cs.Role.UpdateRolePermission(p); err != nil { + return fmt.Errorf("Error ordering Role Permissions: %s", err) + } + + return nil +} + +func rolePermissionSpecs(raw []interface{}) []rolePermissionSpec { + permissions := make([]rolePermissionSpec, 0, len(raw)) + for _, item := range raw { + permissionMap := item.(map[string]interface{}) + permissions = append(permissions, rolePermissionSpec{ + ID: rolePermissionString(permissionMap, "id"), + Rule: rolePermissionString(permissionMap, "rule"), + Permission: rolePermissionString(permissionMap, "permission"), + Description: rolePermissionString(permissionMap, "description"), + }) + } + + return permissions +} + +func rolePermissionState(permission rolePermissionSpec) map[string]interface{} { + return map[string]interface{}{ + "id": permission.ID, + "rule": permission.Rule, + "permission": permission.Permission, + "description": permission.Description, + } +} + +func findMatchingRolePermission(rolePermissions []*cloudstack.RolePermission, desired rolePermissionSpec, used map[string]bool) *cloudstack.RolePermission { + for _, rp := range rolePermissions { + if used[rp.Id] { + continue + } + if rp.Rule == desired.Rule && rp.Description == desired.Description { + return rp + } + } + + return nil +} + +func rolePermissionLock(roleID string) *sync.Mutex { + lock, _ := rolePermissionLocks.LoadOrStore(roleID, &sync.Mutex{}) + return lock.(*sync.Mutex) +} + +func rolePermissionString(permission map[string]interface{}, key string) string { + if value, ok := permission[key].(string); ok { + return value + } + + return "" +} diff --git a/cloudstack/resource_cloudstack_role_permission_test.go b/cloudstack/resource_cloudstack_role_permission_test.go index fae3cfde..18eeee63 100644 --- a/cloudstack/resource_cloudstack_role_permission_test.go +++ b/cloudstack/resource_cloudstack_role_permission_test.go @@ -29,8 +29,6 @@ import ( ) func TestAccCloudStackRolePermission_basic(t *testing.T) { - var rolePermission cloudstack.RolePermission - resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, @@ -39,28 +37,94 @@ func TestAccCloudStackRolePermission_basic(t *testing.T) { { Config: testAccCloudStackRolePermission_basic, Check: resource.ComposeTestCheckFunc( - testAccCheckCloudStackRolePermissionExists("cloudstack_role_permission.foo", &rolePermission), + testAccCheckCloudStackRolePermissionExists("cloudstack_role_permission.foo"), + testAccCheckCloudStackRolePermissionOrder("cloudstack_role_permission.foo", []string{"listVirtualMachines"}), resource.TestCheckResourceAttr( - "cloudstack_role_permission.foo", "rule", "listVirtualMachines"), + "cloudstack_role_permission.foo", "permission.0.rule", "listVirtualMachines"), resource.TestCheckResourceAttr( - "cloudstack_role_permission.foo", "permission", "allow"), + "cloudstack_role_permission.foo", "permission.0.permission", "allow"), resource.TestCheckResourceAttr( - "cloudstack_role_permission.foo", "description", "terraform test role permission"), + "cloudstack_role_permission.foo", "permission.0.description", "terraform test role permission"), ), }, { Config: testAccCloudStackRolePermission_update, Check: resource.ComposeTestCheckFunc( - testAccCheckCloudStackRolePermissionExists("cloudstack_role_permission.foo", &rolePermission), + testAccCheckCloudStackRolePermissionExists("cloudstack_role_permission.foo"), + testAccCheckCloudStackRolePermissionOrder("cloudstack_role_permission.foo", []string{"listVirtualMachines"}), resource.TestCheckResourceAttr( - "cloudstack_role_permission.foo", "permission", "deny"), + "cloudstack_role_permission.foo", "permission.0.permission", "deny"), + ), + }, + }, + }) +} + +func TestAccCloudStackRolePermission_orderAfterRecreate(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackRolePermissionDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCloudStackRolePermission_orderWithSpecificRule, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackRolePermissionExists("cloudstack_role_permission.foo"), + testAccCheckCloudStackRolePermissionOrder("cloudstack_role_permission.foo", []string{"listZones", "*"}), + ), + }, + { + Config: testAccCloudStackRolePermission_orderWithoutSpecificRule, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackRolePermissionExists("cloudstack_role_permission.foo"), + testAccCheckCloudStackRolePermissionOrder("cloudstack_role_permission.foo", []string{"*"}), + ), + }, + { + Config: testAccCloudStackRolePermission_orderWithSpecificRule, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackRolePermissionExists("cloudstack_role_permission.foo"), + testAccCheckCloudStackRolePermissionOrder("cloudstack_role_permission.foo", []string{"listZones", "*"}), + ), + }, + }, + }) +} + +func TestAccCloudStackRolePermission_authoritative(t *testing.T) { + var externalRuleID string + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackRolePermissionDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCloudStackRolePermission_subset, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackRolePermissionExists("cloudstack_role_permission.foo"), + testAccCreateCloudStackRolePermission("cloudstack_role_permission.foo", "listVirtualMachines", "allow", "external role permission", &externalRuleID), + ), + }, + { + Config: testAccCloudStackRolePermission_subset, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackRolePermissionExists("cloudstack_role_permission.foo"), + testAccCheckCloudStackRolePermissionRuleExists("cloudstack_role_permission.foo", &externalRuleID), + ), + }, + { + Config: testAccCloudStackRolePermission_authoritative, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackRolePermissionExists("cloudstack_role_permission.foo"), + testAccCheckCloudStackRolePermissionRuleMissing("cloudstack_role_permission.foo", &externalRuleID), ), }, }, }) } -func testAccCheckCloudStackRolePermissionExists(n string, rolePermission *cloudstack.RolePermission) resource.TestCheckFunc { +func testAccCheckCloudStackRolePermissionExists(n string) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[n] if !ok { @@ -76,26 +140,115 @@ func testAccCheckCloudStackRolePermissionExists(n string, rolePermission *clouds p := cs.Role.NewListRolePermissionsParams() p.SetRoleid(rs.Primary.Attributes["role_id"]) - l, err := cs.Role.ListRolePermissions(p) + if _, err := cs.Role.ListRolePermissions(p); err != nil { + return err + } + + return nil + } +} + +func testAccCheckCloudStackRolePermissionOrder(n string, rules []string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rolePermissions, err := testAccListCloudStackRolePermissions(s, n) if err != nil { return err } + if len(rolePermissions) < len(rules) { + return fmt.Errorf("Expected at least %d Role Permissions, got %d", len(rules), len(rolePermissions)) + } - 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) - } + for i, rule := range rules { + if rolePermissions[i].Rule != rule { + return fmt.Errorf("Expected Role Permission rule %d to be %q, got %q", i, rule, rolePermissions[i].Rule) + } + } - *rolePermission = *rp + return nil + } +} + +func testAccCreateCloudStackRolePermission(n, rule, permission, description string, ruleID *string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + cs := testAccProvider.Meta().(*cloudstack.CloudStackClient) + p := cs.Role.NewCreateRolePermissionParams(permission, rs.Primary.Attributes["role_id"], rule) + p.SetDescription(description) + + r, err := cs.Role.CreateRolePermission(p) + if err != nil { + return err + } + + *ruleID = r.Id + return nil + } +} + +func testAccCheckCloudStackRolePermissionRuleExists(n string, ruleID *string) resource.TestCheckFunc { + return func(s *terraform.State) error { + if *ruleID == "" { + return fmt.Errorf("No external Role Permission ID is set") + } + + rolePermissions, err := testAccListCloudStackRolePermissions(s, n) + if err != nil { + return err + } + + for _, rp := range rolePermissions { + if rp.Id == *ruleID { return nil } } - return fmt.Errorf("Role Permission not found") + return fmt.Errorf("Role Permission %s not found", *ruleID) } } +func testAccCheckCloudStackRolePermissionRuleMissing(n string, ruleID *string) resource.TestCheckFunc { + return func(s *terraform.State) error { + if *ruleID == "" { + return fmt.Errorf("No external Role Permission ID is set") + } + + rolePermissions, err := testAccListCloudStackRolePermissions(s, n) + if err != nil { + return err + } + + for _, rp := range rolePermissions { + if rp.Id == *ruleID { + return fmt.Errorf("Role Permission %s still exists", *ruleID) + } + } + + return nil + } +} + +func testAccListCloudStackRolePermissions(s *terraform.State, n string) ([]*cloudstack.RolePermission, error) { + rs, ok := s.RootModule().Resources[n] + if !ok { + return nil, fmt.Errorf("Not found: %s", n) + } + + 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 nil, err + } + + return l.RolePermissions, nil +} + func testAccCheckCloudStackRolePermissionDestroy(s *terraform.State) error { cs := testAccProvider.Meta().(*cloudstack.CloudStackClient) @@ -111,17 +264,10 @@ func testAccCheckCloudStackRolePermissionDestroy(s *terraform.State) error { 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. + if _, err := cs.Role.ListRolePermissions(p); err != nil { + // If the parent role is already gone, the permissions are 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 @@ -134,10 +280,13 @@ resource "cloudstack_role" "foo" { } resource "cloudstack_role_permission" "foo" { - role_id = cloudstack_role.foo.id - rule = "listVirtualMachines" - permission = "allow" - description = "terraform test role permission" + role_id = cloudstack_role.foo.id + + permission { + rule = "listVirtualMachines" + permission = "allow" + description = "terraform test role permission" + } } ` @@ -148,9 +297,82 @@ resource "cloudstack_role" "foo" { } resource "cloudstack_role_permission" "foo" { - role_id = cloudstack_role.foo.id - rule = "listVirtualMachines" - permission = "deny" - description = "terraform test role permission" + role_id = cloudstack_role.foo.id + + permission { + rule = "listVirtualMachines" + permission = "deny" + description = "terraform test role permission" + } +} +` + +const testAccCloudStackRolePermission_orderWithSpecificRule = ` +resource "cloudstack_role" "foo" { + name = "terraform-role" + type = "User" +} + +resource "cloudstack_role_permission" "foo" { + role_id = cloudstack_role.foo.id + + permission { + rule = "listZones" + permission = "allow" + } + + permission { + rule = "*" + permission = "deny" + } +} +` + +const testAccCloudStackRolePermission_orderWithoutSpecificRule = ` +resource "cloudstack_role" "foo" { + name = "terraform-role" + type = "User" +} + +resource "cloudstack_role_permission" "foo" { + role_id = cloudstack_role.foo.id + + permission { + rule = "*" + permission = "deny" + } +} +` + +const testAccCloudStackRolePermission_subset = ` +resource "cloudstack_role" "foo" { + name = "terraform-role" + type = "User" +} + +resource "cloudstack_role_permission" "foo" { + role_id = cloudstack_role.foo.id + + permission { + rule = "listZones" + permission = "allow" + } +} +` + +const testAccCloudStackRolePermission_authoritative = ` +resource "cloudstack_role" "foo" { + name = "terraform-role" + type = "User" +} + +resource "cloudstack_role_permission" "foo" { + role_id = cloudstack_role.foo.id + authoritative = true + + permission { + rule = "listZones" + permission = "allow" + } } ` diff --git a/website/docs/r/role_permission.html.markdown b/website/docs/r/role_permission.html.markdown index 59eb78f0..090d9270 100644 --- a/website/docs/r/role_permission.html.markdown +++ b/website/docs/r/role_permission.html.markdown @@ -2,18 +2,22 @@ layout: "cloudstack" page_title: "CloudStack: cloudstack_role_permission" description: |- - Creates a role permission (rule) for a role. + Manages ordered role permissions 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). +Manages an ordered list of role permissions for a role. A role permission is a +single rule that allows or denies access to an API or 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. +Rules belonging to the same role are evaluated in order, and the first matching +rule wins. This resource stores that order explicitly and reapplies it after +rules are added, removed, or recreated. + +By default, only the permissions declared in this resource are managed. +Undeclared permissions on the role are preserved and ordered after the declared +permissions. Set `authoritative = true` to delete undeclared permissions and +make the CloudStack role permission list exactly match this resource. ## Example Usage @@ -23,21 +27,19 @@ resource "cloudstack_role" "custom" { 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" -} +resource "cloudstack_role_permission" "custom" { + role_id = cloudstack_role.custom.id -# Deny every other API using a wildcard -resource "cloudstack_role_permission" "deny_all" { - role_id = cloudstack_role.custom.id - rule = "*" - permission = "deny" + permission { + rule = "listVirtualMachines" + permission = "allow" + description = "Allow listing virtual machines" + } - depends_on = [cloudstack_role_permission.list_vms] + permission { + rule = "*" + permission = "deny" + } } ``` @@ -45,17 +47,21 @@ resource "cloudstack_role_permission" "deny_all" { 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 +* `role_id` - (Required) ID of the role the permissions belong to. Changing this forces a new resource to be created. +* `authoritative` - (Optional) Whether permissions not declared in this resource + should be deleted. Defaults to `false`. +* `permission` - (Optional) Ordered list of role permission rules. Each block + supports the following: + * `rule` - (Required) The API name or a wildcard (e.g. `list*` or `*`) the + rule applies to. + * `permission` - (Required) Whether the rule is allowed or denied. Valid + options are: `allow`, `deny`. + * `description` - (Optional) A description for the role permission. ## Attributes Reference The following attributes are exported: -* `id` - The ID of the role permission. +* `id` - The role ID. +* `permission.*.id` - The ID of each role permission rule.