You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cloudstack.apache.org by su...@apache.org on 2026/08/17 06:40:00 UTC

(cloudstack-terraform-provider) branch main updated: Add cloudstack_role_permission resource (#300)

This is an automated email from the ASF dual-hosted git repository.

sureshanaparti pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/cloudstack-terraform-provider.git


The following commit(s) were added to refs/heads/main by this push:
     new add8c3a  Add cloudstack_role_permission resource (#300)
add8c3a is described below

commit add8c3a4ab26f2aad2b037719e6bc752f9c81007
Author: bddvlpr <lu...@bddvlpr.com>
AuthorDate: Mon Aug 17 08:39:51 2026 +0200

    Add cloudstack_role_permission resource (#300)
    
    * Add cloudstack_role_permission resource
    
    * Fix ordering of permissions by using a list with (optionally) authoritative management
---
 cloudstack/provider.go                             |   1 +
 cloudstack/resource_cloudstack_role_permission.go  | 455 +++++++++++++++++++++
 .../resource_cloudstack_role_permission_test.go    | 378 +++++++++++++++++
 website/cloudstack.erb                             |   4 +
 website/docs/r/role_permission.html.markdown       |  67 +++
 5 files changed, 905 insertions(+)

diff --git a/cloudstack/provider.go b/cloudstack/provider.go
index aea132a..bad42dc 100644
--- a/cloudstack/provider.go
+++ b/cloudstack/provider.go
@@ -164,6 +164,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 0000000..1769f8d
--- /dev/null
+++ b/cloudstack/resource_cloudstack_role_permission.go
@@ -0,0 +1,455 @@
+//
+// 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"
+	"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,
+		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 permissions belong to.",
+			},
+			"authoritative": {
+				Type:        schema.TypeBool,
+				Optional:    true,
+				Default:     false,
+				Description: "Whether permissions not declared in this resource should be deleted.",
+			},
+			"permission": {
+				Type:        schema.TypeList,
+				Optional:    true,
+				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 {
+	roleID := d.Get("role_id").(string)
+	d.SetId(roleID)
+
+	roleLock := rolePermissionLock(roleID)
+	roleLock.Lock()
+	defer roleLock.Unlock()
+
+	if err := reconcileCloudStackRolePermissions(d, meta, nil); err != nil {
+		return err
+	}
+
+	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 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("")
+	}
+
+	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 resourceCloudStackRolePermissionDelete(d *schema.ResourceData, meta interface{}) error {
+	cs := meta.(*cloudstack.CloudStackClient)
+	roleID := d.Get("role_id").(string)
+
+	roleLock := rolePermissionLock(roleID)
+	roleLock.Lock()
+	defer roleLock.Unlock()
+
+	rolePermissions, err := listCloudStackRolePermissions(cs, roleID)
+	if err != nil {
+		return fmt.Errorf("Error listing Role Permissions: %s", err)
+	}
+
+	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
+	}
+
+	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 reconcileCloudStackRolePermissions(d *schema.ResourceData, meta interface{}, oldPermissions []rolePermissionSpec) error {
+	cs := meta.(*cloudstack.CloudStackClient)
+	roleID := d.Get("role_id").(string)
+
+	rolePermissions, err := listCloudStackRolePermissions(cs, roleID)
+	if err != nil {
+		return fmt.Errorf("Error listing Role Permissions: %s", err)
+	}
+
+	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 listing Role Permissions: %s", err)
+	}
+
+	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 listCloudStackRolePermissions(cs *cloudstack.CloudStackClient, roleID string) ([]*cloudstack.RolePermission, error) {
+	p := cs.Role.NewListRolePermissionsParams()
+	p.SetRoleid(roleID)
+
+	l, err := cs.Role.ListRolePermissions(p)
+	if err != nil {
+		return nil, err
+	}
+
+	return l.RolePermissions, nil
+}
+
+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
new file mode 100644
index 0000000..18eeee6
--- /dev/null
+++ b/cloudstack/resource_cloudstack_role_permission_test.go
@@ -0,0 +1,378 @@
+//
+// 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) {
+	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"),
+					testAccCheckCloudStackRolePermissionOrder("cloudstack_role_permission.foo", []string{"listVirtualMachines"}),
+					resource.TestCheckResourceAttr(
+						"cloudstack_role_permission.foo", "permission.0.rule", "listVirtualMachines"),
+					resource.TestCheckResourceAttr(
+						"cloudstack_role_permission.foo", "permission.0.permission", "allow"),
+					resource.TestCheckResourceAttr(
+						"cloudstack_role_permission.foo", "permission.0.description", "terraform test role permission"),
+				),
+			},
+			{
+				Config: testAccCloudStackRolePermission_update,
+				Check: resource.ComposeTestCheckFunc(
+					testAccCheckCloudStackRolePermissionExists("cloudstack_role_permission.foo"),
+					testAccCheckCloudStackRolePermissionOrder("cloudstack_role_permission.foo", []string{"listVirtualMachines"}),
+					resource.TestCheckResourceAttr(
+						"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) 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"])
+
+		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 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)
+			}
+		}
+
+		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 %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)
+
+	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"])
+
+		if _, err := cs.Role.ListRolePermissions(p); err != nil {
+			// If the parent role is already gone, the permissions are too.
+			continue
+		}
+	}
+
+	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
+
+  permission {
+    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
+
+  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/cloudstack.erb b/website/cloudstack.erb
index 1ea7f7e..06b6a7c 100644
--- a/website/cloudstack.erb
+++ b/website/cloudstack.erb
@@ -132,6 +132,10 @@
                         <li<%= sidebar_current("docs-cloudstack-resource-role") %>>
                             <a href="/docs/providers/cloudstack/r/role.html">cloudstack_role</a>
                         </li>
+
+                        <li<%= sidebar_current("docs-cloudstack-resource-role-permission") %>>
+                            <a href="/docs/providers/cloudstack/r/role_permission.html">cloudstack_role_permission</a>
+                        </li>
                     </ul>
                 </li>
             </ul>
diff --git a/website/docs/r/role_permission.html.markdown b/website/docs/r/role_permission.html.markdown
new file mode 100644
index 0000000..090d927
--- /dev/null
+++ b/website/docs/r/role_permission.html.markdown
@@ -0,0 +1,67 @@
+---
+layout: "cloudstack"
+page_title: "CloudStack: cloudstack_role_permission"
+description: |-
+  Manages ordered role permissions for a role.
+---
+
+# cloudstack_role_permission
+
+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 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
+
+```hcl
+resource "cloudstack_role" "custom" {
+  name = "custom-role"
+  type = "User"
+}
+
+resource "cloudstack_role_permission" "custom" {
+  role_id = cloudstack_role.custom.id
+
+  permission {
+    rule        = "listVirtualMachines"
+    permission  = "allow"
+    description = "Allow listing virtual machines"
+  }
+
+  permission {
+    rule       = "*"
+    permission = "deny"
+  }
+}
+```
+
+## Argument Reference
+
+The following arguments are supported:
+
+* `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 role ID.
+* `permission.*.id` - The ID of each role permission rule.