diff --git a/internal/services/authorization/parse/role_definition.go b/internal/services/authorization/parse/role_definition.go index 4cfc2e022da7..f364b8566e79 100644 --- a/internal/services/authorization/parse/role_definition.go +++ b/internal/services/authorization/parse/role_definition.go @@ -6,6 +6,8 @@ package parse import ( "fmt" "strings" + + "github.com/hashicorp/go-azure-helpers/resourcemanager/resourceids" ) type RoleDefinitionID struct { @@ -14,6 +16,31 @@ type RoleDefinitionID struct { RoleID string } +var _ resourceids.ResourceId = RoleDefinitionID{} + +func (r RoleDefinitionID) ID() string { + return fmt.Sprintf("%s|%s", r.ResourceID, r.Scope) +} + +func (r RoleDefinitionID) String() string { + components := []string{ + fmt.Sprintf("Resource ID: %q", r.ResourceID), + fmt.Sprintf("Scope: %q", r.Scope), + fmt.Sprintf("Role Definition: %q", r.RoleID), + } + return fmt.Sprintf("Role Definition (%s)", strings.Join(components, "\n")) +} + +func (r RoleDefinitionID) Segments() []resourceids.Segment { + return []resourceids.Segment{ + resourceids.ScopeSegment("scope", "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/some-resource-group"), + resourceids.StaticSegment("staticProviders", "providers", "providers"), + resourceids.ResourceProviderSegment("staticMicrosoftAuthorization", "Microsoft.Authorization", "Microsoft.Authorization"), + resourceids.StaticSegment("staticRoleDefinitions", "roleDefinitions", "roleDefinitions"), + resourceids.UserSpecifiedSegment("roleDefinitionId", "roleDefinitionIdValue"), + } +} + // RoleDefinitionId is a pseudo ID for storing Scope parameter as this it not retrievable from API // It is formed of the Azure Resource ID for the Role and the Scope it is created against func RoleDefinitionId(input string) (*RoleDefinitionID, error) { diff --git a/internal/services/authorization/registration.go b/internal/services/authorization/registration.go index 00c694b6a520..d849dffdbba9 100644 --- a/internal/services/authorization/registration.go +++ b/internal/services/authorization/registration.go @@ -8,11 +8,12 @@ import ( "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" ) -type Registration struct { -} +type Registration struct{} -var _ sdk.TypedServiceRegistrationWithAGitHubLabel = Registration{} -var _ sdk.UntypedServiceRegistrationWithAGitHubLabel = Registration{} +var ( + _ sdk.TypedServiceRegistrationWithAGitHubLabel = Registration{} + _ sdk.UntypedServiceRegistrationWithAGitHubLabel = Registration{} +) func (r Registration) AssociatedGitHubLabel() string { return "service/authorization" @@ -33,8 +34,7 @@ func (r Registration) WebsiteCategories() []string { // SupportedDataSources returns the supported Data Sources supported by this Service func (r Registration) SupportedDataSources() map[string]*pluginsdk.Resource { return map[string]*pluginsdk.Resource{ - "azurerm_client_config": dataSourceArmClientConfig(), - "azurerm_role_definition": dataSourceArmRoleDefinition(), + "azurerm_client_config": dataSourceArmClientConfig(), } } @@ -42,12 +42,13 @@ func (r Registration) SupportedDataSources() map[string]*pluginsdk.Resource { func (r Registration) SupportedResources() map[string]*pluginsdk.Resource { return map[string]*pluginsdk.Resource{ "azurerm_role_assignment": resourceArmRoleAssignment(), - "azurerm_role_definition": resourceArmRoleDefinition(), } } func (r Registration) DataSources() []sdk.DataSource { - return []sdk.DataSource{} + return []sdk.DataSource{ + RoleDefinitionDataSource{}, + } } func (r Registration) Resources() []sdk.Resource { @@ -55,6 +56,7 @@ func (r Registration) Resources() []sdk.Resource { PimActiveRoleAssignmentResource{}, PimEligibleRoleAssignmentResource{}, RoleAssignmentMarketplaceResource{}, + RoleDefinitionResource{}, } return resources } diff --git a/internal/services/authorization/role_definition_data_source.go b/internal/services/authorization/role_definition_data_source.go index e254fe4641e9..b9dc771ba3a5 100644 --- a/internal/services/authorization/role_definition_data_source.go +++ b/internal/services/authorization/role_definition_data_source.go @@ -4,178 +4,227 @@ package authorization import ( + "context" "fmt" "time" "github.com/Azure/azure-sdk-for-go/services/preview/authorization/mgmt/2020-04-01-preview/authorization" // nolint: staticcheck - "github.com/hashicorp/terraform-provider-azurerm/internal/clients" + "github.com/hashicorp/go-azure-helpers/lang/pointer" + "github.com/hashicorp/go-azure-helpers/resourcemanager/commonids" + "github.com/hashicorp/terraform-provider-azurerm/internal/sdk" + "github.com/hashicorp/terraform-provider-azurerm/internal/services/authorization/parse" "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" "github.com/hashicorp/terraform-provider-azurerm/internal/tf/validation" - "github.com/hashicorp/terraform-provider-azurerm/internal/timeouts" ) -func dataSourceArmRoleDefinition() *pluginsdk.Resource { - return &pluginsdk.Resource{ - Read: dataSourceArmRoleDefinitionRead, +type RoleDefinitionDataSource struct{} - Timeouts: &pluginsdk.ResourceTimeout{ - Read: pluginsdk.DefaultTimeout(5 * time.Minute), - }, +var _ sdk.DataSource = RoleDefinitionDataSource{} - Schema: map[string]*pluginsdk.Schema{ - "name": { - Type: pluginsdk.TypeString, - Optional: true, - Computed: true, - ConflictsWith: []string{"role_definition_id"}, - }, +type RoleDefinitionDataSourceModel struct { + Name string `tfschema:"name"` + RoleDefinitionId string `tfschema:"role_definition_id"` + Scope string `tfschema:"scope"` + Description string `tfschema:"description"` + Type string `tfschema:"type"` + Permissions []PermissionDataSourceModel `tfschema:"permissions"` + AssignableScopes []string `tfschema:"assignable_scopes"` +} + +type PermissionDataSourceModel struct { + Actions []string `tfschema:"actions"` + NotActions []string `tfschema:"not_actions"` + DataActions []string `tfschema:"data_actions"` + NotDataActions []string `tfschema:"not_data_actions"` +} - "role_definition_id": { - Type: pluginsdk.TypeString, - Optional: true, - Computed: true, - ConflictsWith: []string{"name"}, - ValidateFunc: validation.Any(validation.IsUUID, validation.StringIsEmpty), +func (a RoleDefinitionDataSource) Arguments() map[string]*pluginsdk.Schema { + return map[string]*pluginsdk.Schema{ + "name": { + Type: pluginsdk.TypeString, + Optional: true, + Computed: true, + ExactlyOneOf: []string{ + "name", + "role_definition_id", }, + ValidateFunc: validation.StringIsNotEmpty, + }, - "scope": { - Type: pluginsdk.TypeString, - Optional: true, + "role_definition_id": { + Type: pluginsdk.TypeString, + Optional: true, + Computed: true, + ExactlyOneOf: []string{ + "name", + "role_definition_id", }, + ValidateFunc: validation.IsUUID, + }, - // Computed + "scope": { + Type: pluginsdk.TypeString, + Optional: true, + ValidateFunc: commonids.ValidateScopeID, + }, + } +} - "description": { - Type: pluginsdk.TypeString, - Computed: true, - }, +func (a RoleDefinitionDataSource) Attributes() map[string]*pluginsdk.Schema { + return map[string]*pluginsdk.Schema{ + "description": { + Type: pluginsdk.TypeString, + Computed: true, + }, - "type": { - Type: pluginsdk.TypeString, - Computed: true, - }, + "type": { + Type: pluginsdk.TypeString, + Computed: true, + }, - "permissions": { - Type: pluginsdk.TypeList, - Computed: true, - Elem: &pluginsdk.Resource{ - Schema: map[string]*pluginsdk.Schema{ - "actions": { - Type: pluginsdk.TypeList, - Computed: true, - Elem: &pluginsdk.Schema{ - Type: pluginsdk.TypeString, - }, + "permissions": { + Type: pluginsdk.TypeList, + Computed: true, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "actions": { + Type: pluginsdk.TypeList, + Computed: true, + Elem: &pluginsdk.Schema{ + Type: pluginsdk.TypeString, }, + }, - "not_actions": { - Type: pluginsdk.TypeList, - Computed: true, - Elem: &pluginsdk.Schema{ - Type: pluginsdk.TypeString, - }, + "not_actions": { + Type: pluginsdk.TypeList, + Computed: true, + Elem: &pluginsdk.Schema{ + Type: pluginsdk.TypeString, }, + }, - "data_actions": { - Type: pluginsdk.TypeSet, - Optional: true, - Elem: &pluginsdk.Schema{ - Type: pluginsdk.TypeString, - }, - Set: pluginsdk.HashString, + "data_actions": { + Type: pluginsdk.TypeSet, + Optional: true, + Elem: &pluginsdk.Schema{ + Type: pluginsdk.TypeString, }, + Set: pluginsdk.HashString, + }, - "not_data_actions": { - Type: pluginsdk.TypeSet, - Optional: true, - Elem: &pluginsdk.Schema{ - Type: pluginsdk.TypeString, - }, - Set: pluginsdk.HashString, + "not_data_actions": { + Type: pluginsdk.TypeSet, + Optional: true, + Elem: &pluginsdk.Schema{ + Type: pluginsdk.TypeString, }, + Set: pluginsdk.HashString, }, }, }, + }, - "assignable_scopes": { - Type: pluginsdk.TypeList, - Computed: true, - Elem: &pluginsdk.Schema{ - Type: pluginsdk.TypeString, - }, + "assignable_scopes": { + Type: pluginsdk.TypeList, + Computed: true, + Elem: &pluginsdk.Schema{ + Type: pluginsdk.TypeString, }, }, } } -func dataSourceArmRoleDefinitionRead(d *pluginsdk.ResourceData, meta interface{}) error { - client := meta.(*clients.Client).Authorization.RoleDefinitionsClient - ctx, cancel := timeouts.ForRead(meta.(*clients.Client).StopContext, d) - defer cancel() +func (a RoleDefinitionDataSource) ModelObject() interface{} { + return &RoleDefinitionDataSourceModel{} +} - name := d.Get("name").(string) - defId := d.Get("role_definition_id").(string) - scope := d.Get("scope").(string) +func (a RoleDefinitionDataSource) ResourceType() string { + return "azurerm_role_definition" +} - if name == "" && defId == "" { - return fmt.Errorf("one of `name` or `role_definition_id` must be specified") - } +func (a RoleDefinitionDataSource) Read() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 5 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + client := metadata.Client.Authorization.RoleDefinitionsClient - // search by name - var role authorization.RoleDefinition - if name != "" { - // Accounting for eventual consistency - err := pluginsdk.Retry(d.Timeout(pluginsdk.TimeoutRead), func() *pluginsdk.RetryError { - roleDefinitions, err := client.List(ctx, scope, fmt.Sprintf("roleName eq '%s'", name)) - if err != nil { - return pluginsdk.NonRetryableError(fmt.Errorf("loading Role Definition List: %+v", err)) + var config RoleDefinitionDataSourceModel + if err := metadata.Decode(&config); err != nil { + return err } - if len(roleDefinitions.Values()) != 1 { - return pluginsdk.RetryableError(fmt.Errorf("loading Role Definition List: could not find role '%s'", name)) - } - if roleDefinitions.Values()[0].ID == nil { - return pluginsdk.NonRetryableError(fmt.Errorf("loading Role Definition List: values[0].ID is nil '%s'", name)) + + defId := config.RoleDefinitionId + + // search by name + var id parse.RoleDefinitionID + var role authorization.RoleDefinition + if config.Name != "" { + // Accounting for eventual consistency + deadline, ok := ctx.Deadline() + if !ok { + return fmt.Errorf("internal error: context had no deadline") + } + err := pluginsdk.Retry(time.Until(deadline), func() *pluginsdk.RetryError { + roleDefinitions, err := client.List(ctx, config.Scope, fmt.Sprintf("roleName eq '%s'", config.Name)) + if err != nil { + return pluginsdk.NonRetryableError(fmt.Errorf("loading Role Definition List: %+v", err)) + } + if len(roleDefinitions.Values()) != 1 { + return pluginsdk.RetryableError(fmt.Errorf("loading Role Definition List: could not find role '%s'", config.Name)) + } + if roleDefinitions.Values()[0].ID == nil { + return pluginsdk.NonRetryableError(fmt.Errorf("loading Role Definition List: values[0].ID is nil '%s'", config.Name)) + } + + defId = *roleDefinitions.Values()[0].ID + role, err = client.GetByID(ctx, defId) + if err != nil { + return pluginsdk.NonRetryableError(fmt.Errorf("getting Role Definition by ID %s: %+v", defId, err)) + } + return nil + }) + if err != nil { + return err + } + } else { + var err error + role, err = client.Get(ctx, config.Scope, defId) + if err != nil { + return fmt.Errorf("loading Role Definition: %+v", err) + } } - defId = *roleDefinitions.Values()[0].ID - role, err = client.GetByID(ctx, defId) - if err != nil { - return pluginsdk.NonRetryableError(fmt.Errorf("getting Role Definition by ID %s: %+v", defId, err)) + state := RoleDefinitionDataSourceModel{ + Scope: id.Scope, + RoleDefinitionId: id.RoleID, } - return nil - }) - if err != nil { - return err - } - } else { - var err error - role, err = client.Get(ctx, scope, defId) - if err != nil { - return fmt.Errorf("loading Role Definition: %+v", err) - } + + state.Name = pointer.From(role.RoleName) + state.Type = pointer.From(role.Type) + state.Description = pointer.From(role.Description) + state.Permissions = flattenDataSourceRoleDefinitionPermissions(role.Permissions) + state.AssignableScopes = pointer.From(role.AssignableScopes) + + metadata.ResourceData.SetId(*role.ID) + return metadata.Encode(&state) + }, } +} - if role.ID == nil { - return fmt.Errorf("returned role had a nil ID (id %q, scope %q, name %q)", defId, scope, name) +func flattenDataSourceRoleDefinitionPermissions(input *[]authorization.Permission) []PermissionDataSourceModel { + permissions := make([]PermissionDataSourceModel, 0) + if input == nil { + return permissions } - d.SetId(*role.ID) - - if props := role.RoleDefinitionProperties; props != nil { - d.Set("name", props.RoleName) - d.Set("role_definition_id", defId) - d.Set("description", props.Description) - d.Set("type", props.RoleType) - - permissions := flattenRoleDefinitionPermissions(props.Permissions) - if err := d.Set("permissions", permissions); err != nil { - return err - } - - assignableScopes := flattenRoleDefinitionAssignableScopes(props.AssignableScopes) - if err := d.Set("assignable_scopes", assignableScopes); err != nil { - return err - } + + for _, permission := range *input { + permissions = append(permissions, PermissionDataSourceModel{ + Actions: pointer.From(permission.Actions), + DataActions: pointer.From(permission.DataActions), + NotActions: pointer.From(permission.NotActions), + NotDataActions: pointer.From(permission.NotDataActions), + }) } - return nil + return permissions } diff --git a/internal/services/authorization/role_definition_resource.go b/internal/services/authorization/role_definition_resource.go index 628f6b6db188..a32613a1ea24 100644 --- a/internal/services/authorization/role_definition_resource.go +++ b/internal/services/authorization/role_definition_resource.go @@ -10,350 +10,413 @@ import ( "time" "github.com/Azure/azure-sdk-for-go/services/preview/authorization/mgmt/2020-04-01-preview/authorization" // nolint: staticcheck + "github.com/hashicorp/go-azure-helpers/lang/pointer" + "github.com/hashicorp/go-azure-helpers/resourcemanager/commonids" "github.com/hashicorp/go-uuid" - "github.com/hashicorp/terraform-provider-azurerm/helpers/tf" - "github.com/hashicorp/terraform-provider-azurerm/internal/clients" + "github.com/hashicorp/terraform-provider-azurerm/internal/sdk" "github.com/hashicorp/terraform-provider-azurerm/internal/services/authorization/azuresdkhacks" "github.com/hashicorp/terraform-provider-azurerm/internal/services/authorization/migration" "github.com/hashicorp/terraform-provider-azurerm/internal/services/authorization/parse" "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" "github.com/hashicorp/terraform-provider-azurerm/internal/tf/validation" - "github.com/hashicorp/terraform-provider-azurerm/internal/timeouts" "github.com/hashicorp/terraform-provider-azurerm/utils" ) -func resourceArmRoleDefinition() *pluginsdk.Resource { - return &pluginsdk.Resource{ - Create: resourceArmRoleDefinitionCreate, - Read: resourceArmRoleDefinitionRead, - Update: resourceArmRoleDefinitionUpdate, - Delete: resourceArmRoleDefinitionDelete, +type RoleDefinitionResource struct{} - Importer: pluginsdk.ImporterValidatingResourceId(func(id string) error { - _, err := parse.RoleDefinitionId(id) - return err - }), +var ( + _ sdk.ResourceWithUpdate = RoleDefinitionResource{} + _ sdk.ResourceWithStateMigration = RoleDefinitionResource{} +) - SchemaVersion: 1, - StateUpgraders: pluginsdk.StateUpgrades(map[int]pluginsdk.StateUpgrade{ - 0: migration.RoleDefinitionV0ToV1{}, - }), +type RoleDefinitionModel struct { + RoleDefinitionId string `tfschema:"role_definition_id"` + Name string `tfschema:"name"` + Scope string `tfschema:"scope"` + Description string `tfschema:"description"` + Permissions []PermissionModel `tfschema:"permissions"` + AssignableScopes []string `tfschema:"assignable_scopes"` + RoleDefinitionResourceId string `tfschema:"role_definition_resource_id"` +} - Timeouts: &pluginsdk.ResourceTimeout{ - Create: pluginsdk.DefaultTimeout(30 * time.Minute), - Read: pluginsdk.DefaultTimeout(5 * time.Minute), - Update: pluginsdk.DefaultTimeout(60 * time.Minute), - Delete: pluginsdk.DefaultTimeout(30 * time.Minute), - }, +type PermissionModel struct { + Actions []string `tfschema:"actions"` + NotActions []string `tfschema:"not_actions"` + DataActions []string `tfschema:"data_actions"` + NotDataActions []string `tfschema:"not_data_actions"` +} - Schema: map[string]*pluginsdk.Schema{ - "role_definition_id": { - Type: pluginsdk.TypeString, - Optional: true, - Computed: true, - ForceNew: true, - }, +func (r RoleDefinitionResource) Arguments() map[string]*pluginsdk.Schema { + return map[string]*pluginsdk.Schema{ + "role_definition_id": { + Type: pluginsdk.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + ValidateFunc: validation.IsUUID, + }, - "name": { - Type: pluginsdk.TypeString, - Required: true, - }, + "name": { + Type: pluginsdk.TypeString, + Required: true, + ValidateFunc: validation.StringIsNotEmpty, + }, - "scope": { - Type: pluginsdk.TypeString, - Required: true, - ForceNew: true, - ValidateFunc: validation.StringStartsWithOneOf("/subscriptions/", "/providers/Microsoft.Management/managementGroups/"), - }, + "scope": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringStartsWithOneOf("/subscriptions/", "/providers/Microsoft.Management/managementGroups/"), + }, - "description": { - Type: pluginsdk.TypeString, - Optional: true, - }, + "description": { + Type: pluginsdk.TypeString, + Optional: true, + ValidateFunc: validation.StringIsNotEmpty, + }, - // lintignore:XS003 - "permissions": { - Type: pluginsdk.TypeList, - Optional: true, - Elem: &pluginsdk.Resource{ - Schema: map[string]*pluginsdk.Schema{ - "actions": { - Type: pluginsdk.TypeList, - Optional: true, - Elem: &pluginsdk.Schema{ - Type: pluginsdk.TypeString, - }, + // lintignore:XS003 + "permissions": { + Type: pluginsdk.TypeList, + Optional: true, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "actions": { + Type: pluginsdk.TypeList, + Optional: true, + Elem: &pluginsdk.Schema{ + Type: pluginsdk.TypeString, }, - "not_actions": { - Type: pluginsdk.TypeList, - Optional: true, - Elem: &pluginsdk.Schema{ - Type: pluginsdk.TypeString, - }, + }, + "not_actions": { + Type: pluginsdk.TypeList, + Optional: true, + Elem: &pluginsdk.Schema{ + Type: pluginsdk.TypeString, }, - "data_actions": { - Type: pluginsdk.TypeSet, - Optional: true, - Elem: &pluginsdk.Schema{ - Type: pluginsdk.TypeString, - }, - Set: pluginsdk.HashString, + }, + "data_actions": { + Type: pluginsdk.TypeSet, + Optional: true, + Elem: &pluginsdk.Schema{ + Type: pluginsdk.TypeString, }, - "not_data_actions": { - Type: pluginsdk.TypeSet, - Optional: true, - Elem: &pluginsdk.Schema{ - Type: pluginsdk.TypeString, - }, - Set: pluginsdk.HashString, + Set: pluginsdk.HashString, + }, + "not_data_actions": { + Type: pluginsdk.TypeSet, + Optional: true, + Elem: &pluginsdk.Schema{ + Type: pluginsdk.TypeString, }, + Set: pluginsdk.HashString, }, }, }, + }, - "assignable_scopes": { - Type: pluginsdk.TypeList, - Optional: true, - Computed: true, - Elem: &pluginsdk.Schema{ - Type: pluginsdk.TypeString, - }, + "assignable_scopes": { + Type: pluginsdk.TypeList, + Optional: true, + Computed: true, + Elem: &pluginsdk.Schema{ + Type: pluginsdk.TypeString, + ValidateFunc: commonids.ValidateScopeID, }, + }, + } +} - "role_definition_resource_id": { - Type: pluginsdk.TypeString, - Computed: true, - }, +func (r RoleDefinitionResource) Attributes() map[string]*pluginsdk.Schema { + return map[string]*pluginsdk.Schema{ + "role_definition_resource_id": { + Type: pluginsdk.TypeString, + Computed: true, }, } } -func resourceArmRoleDefinitionCreate(d *pluginsdk.ResourceData, meta interface{}) error { - client := meta.(*clients.Client).Authorization.RoleDefinitionsClient - ctx, cancel := timeouts.ForCreate(meta.(*clients.Client).StopContext, d) - defer cancel() +func (r RoleDefinitionResource) ResourceType() string { + return "azurerm_role_definition" +} - roleDefinitionId := d.Get("role_definition_id").(string) - if roleDefinitionId == "" { - uuid, err := uuid.GenerateUUID() - if err != nil { - return fmt.Errorf("generating UUID for Role Assignment: %+v", err) +func (r RoleDefinitionResource) ModelObject() interface{} { + return &RoleDefinitionModel{} +} + +func (r RoleDefinitionResource) IDValidationFunc() pluginsdk.SchemaValidateFunc { + return func(input interface{}, key string) (warnings []string, errors []error) { + v, ok := input.(string) + if !ok { + errors = append(errors, fmt.Errorf("expected %q to be a string", key)) + return } - roleDefinitionId = uuid + if _, err := parse.RoleDefinitionId(v); err != nil { + errors = append(errors, err) + } + + return } +} - name := d.Get("name").(string) - scope := d.Get("scope").(string) - description := d.Get("description").(string) - roleType := "CustomRole" +func (r RoleDefinitionResource) Create() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 30 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + client := metadata.Client.Authorization.RoleDefinitionsClient - permissionsRaw := d.Get("permissions").([]interface{}) - permissions := expandRoleDefinitionPermissions(permissionsRaw) - assignableScopes := expandRoleDefinitionAssignableScopes(d) + var config RoleDefinitionModel + if err := metadata.Decode(&config); err != nil { + return fmt.Errorf("decoding %+v", err) + } - if d.IsNewResource() { - existing, err := client.Get(ctx, scope, roleDefinitionId) - if err != nil { + roleDefinitionId := config.RoleDefinitionId + if roleDefinitionId == "" { + uuid, err := uuid.GenerateUUID() + if err != nil { + return fmt.Errorf("generating UUID for Role Assignment: %+v", err) + } + roleDefinitionId = uuid + } + + existing, err := client.Get(ctx, config.Scope, roleDefinitionId) + if err != nil && !utils.ResponseWasNotFound(existing.Response) { + return fmt.Errorf("checking for presence of existing Role Definition ID for %q (Scope %q)", config.Name, config.Scope) + } if !utils.ResponseWasNotFound(existing.Response) { - return fmt.Errorf("checking for presence of existing Role Definition ID for %q (Scope %q)", name, scope) + importID := parse.RoleDefinitionID{ + RoleID: roleDefinitionId, + Scope: config.Scope, + } + return metadata.ResourceRequiresImport(r.ResourceType(), importID) } - } - if existing.ID != nil && *existing.ID != "" { - importID := fmt.Sprintf("%s|%s", *existing.ID, scope) - return tf.ImportAsExistsError("azurerm_role_definition", importID) - } - } + properties := authorization.RoleDefinition{ + RoleDefinitionProperties: &authorization.RoleDefinitionProperties{ + RoleName: &config.Name, + Description: &config.Description, + RoleType: pointer.To("CustomRole"), + Permissions: pointer.To(expandRoleDefinitionPermissions(config.Permissions)), + AssignableScopes: pointer.To(expandRoleDefinitionAssignableScopes(config)), + }, + } - properties := authorization.RoleDefinition{ - RoleDefinitionProperties: &authorization.RoleDefinitionProperties{ - RoleName: utils.String(name), - Description: utils.String(description), - RoleType: utils.String(roleType), - Permissions: &permissions, - AssignableScopes: &assignableScopes, - }, - } + if _, err := client.CreateOrUpdate(ctx, config.Scope, roleDefinitionId, properties); err != nil { + return err + } + + read, err := client.Get(ctx, config.Scope, roleDefinitionId) + if err != nil { + return err + } + if read.ID == nil || *read.ID == "" { + return fmt.Errorf("cannot read Role Definition ID for %q (Scope %q)", config.Name, config.Scope) + } - if _, err := client.CreateOrUpdate(ctx, scope, roleDefinitionId, properties); err != nil { - return err + parsedId := parse.RoleDefinitionID{ + RoleID: roleDefinitionId, + Scope: config.Scope, + ResourceID: *read.ID, + } + metadata.SetID(parsedId) + return nil + }, } +} - // (@jackofallops) - Updates are subject to eventual consistency, and could be read as stale data - if !d.IsNewResource() { - id, err := parse.RoleDefinitionId(d.Id()) - if err != nil { - return err - } - stateConf := &pluginsdk.StateChangeConf{ - Pending: []string{ - "Pending", - }, - Target: []string{ - "OK", - }, - Refresh: roleDefinitionUpdateStateRefreshFunc(ctx, client, id.ResourceID), - MinTimeout: 10 * time.Second, - ContinuousTargetOccurence: 12, - Timeout: d.Timeout(pluginsdk.TimeoutUpdate), - } +func (r RoleDefinitionResource) Read() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 5 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + client := metadata.Client.Authorization.RoleDefinitionsClient - if _, err := stateConf.WaitForStateContext(ctx); err != nil { - return fmt.Errorf("waiting for update to Role Definition %q to finish replicating", name) - } - } + id, err := parse.RoleDefinitionId(metadata.ResourceData.Id()) + if err != nil { + return err + } - read, err := client.Get(ctx, scope, roleDefinitionId) - if err != nil { - return err - } - if read.ID == nil || *read.ID == "" { - return fmt.Errorf("Cannot read Role Definition ID for %q (Scope %q)", name, scope) - } + resp, err := client.Get(ctx, id.Scope, id.RoleID) + if err != nil { + if utils.ResponseWasNotFound(resp.Response) { + return metadata.MarkAsGone(id) + } - d.SetId(fmt.Sprintf("%s|%s", *read.ID, scope)) - return resourceArmRoleDefinitionRead(d, meta) -} + return fmt.Errorf("retrieving %s: %+v", id, err) + } -func resourceArmRoleDefinitionUpdate(d *pluginsdk.ResourceData, meta interface{}) error { - sdkClient := meta.(*clients.Client).Authorization.RoleDefinitionsClient - client := azuresdkhacks.NewRoleDefinitionsWorkaroundClient(sdkClient) - ctx, cancel := timeouts.ForUpdate(meta.(*clients.Client).StopContext, d) - defer cancel() + state := RoleDefinitionModel{ + Scope: id.Scope, + RoleDefinitionId: id.RoleID, + } - roleDefinitionId, err := parse.RoleDefinitionId(d.Id()) - if err != nil { - return err - } + // The Azure resource id of Role Definition is not as same as the one we used to create it. + // So we read from the response. + state.RoleDefinitionResourceId = pointer.From(resp.ID) + state.Name = pointer.From(resp.RoleName) + state.Description = pointer.From(resp.Description) + state.Permissions = flattenRoleDefinitionPermissions(resp.Permissions) + state.AssignableScopes = pointer.From(resp.AssignableScopes) - name := d.Get("name").(string) - description := d.Get("description").(string) - roleType := "CustomRole" - - permissionsRaw := d.Get("permissions").([]interface{}) - permissions := expandRoleDefinitionPermissions(permissionsRaw) - assignableScopes := expandRoleDefinitionAssignableScopes(d) - - properties := authorization.RoleDefinition{ - RoleDefinitionProperties: &authorization.RoleDefinitionProperties{ - RoleName: utils.String(name), - Description: utils.String(description), - RoleType: utils.String(roleType), - Permissions: &permissions, - AssignableScopes: &assignableScopes, + return metadata.Encode(&state) }, } +} - resp, err := client.CreateOrUpdate(ctx, roleDefinitionId.Scope, roleDefinitionId.RoleID, properties) - if err != nil { - return fmt.Errorf("updating Role Definition %q (Scope %q): %+v", roleDefinitionId.RoleID, roleDefinitionId.Scope, err) - } - if resp.RoleDefinitionProperties == nil { - return fmt.Errorf("updating Role Definition %q (Scope %q): `properties` was nil", roleDefinitionId.RoleID, roleDefinitionId.Scope) - } - updatedOn := resp.RoleDefinitionProperties.UpdatedOn - if updatedOn == nil { - return fmt.Errorf("updating Role Definition %q (Scope %q): `properties.UpdatedOn` was nil", roleDefinitionId.RoleID, roleDefinitionId.Scope) - } +func (r RoleDefinitionResource) Update() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 60 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + sdkClient := metadata.Client.Authorization.RoleDefinitionsClient + client := azuresdkhacks.NewRoleDefinitionsWorkaroundClient(sdkClient) - // "Updating" a role definition actually creates a new one and these get consolidated a few seconds later - // where the "create date" and "update date" match for the newly created record - // but eventually switch to being the old create date and the new update date - // ergo we can can for the old create date and the new updated date - log.Printf("[DEBUG] Waiting for Role Definition %q (Scope %q) to settle down..", roleDefinitionId.RoleID, roleDefinitionId.Scope) - stateConf := &pluginsdk.StateChangeConf{ - ContinuousTargetOccurence: 12, - Delay: 60 * time.Second, - MinTimeout: 10 * time.Second, - Pending: []string{"Pending"}, - Target: []string{"Updated"}, - Refresh: roleDefinitionEventualConsistencyUpdate(ctx, client, *roleDefinitionId, *updatedOn), - Timeout: d.Timeout(pluginsdk.TimeoutUpdate), - } - if _, err := stateConf.WaitForStateContext(ctx); err != nil { - return fmt.Errorf("waiting for Role Definition %q (Scope %q) to settle down: %+v", roleDefinitionId.RoleID, roleDefinitionId.Scope, err) - } + id, err := parse.RoleDefinitionId(metadata.ResourceData.Id()) + if err != nil { + return err + } - return resourceArmRoleDefinitionRead(d, meta) -} + var config RoleDefinitionModel + if err := metadata.Decode(&config); err != nil { + return err + } + + exisiting, err := client.Get(ctx, id.Scope, id.RoleID) + if err != nil { + return fmt.Errorf("retrieving %s: %+v", id, err) + } -func resourceArmRoleDefinitionRead(d *pluginsdk.ResourceData, meta interface{}) error { - client := meta.(*clients.Client).Authorization.RoleDefinitionsClient - ctx, cancel := timeouts.ForRead(meta.(*clients.Client).StopContext, d) - defer cancel() + permissions := []authorization.Permission{} + if config.Permissions != nil { + for _, permission := range *exisiting.Permissions { + permissions = append(permissions, authorization.Permission{ + Actions: permission.Actions, + DataActions: permission.DataActions, + NotActions: permission.NotActions, + NotDataActions: permission.NotDataActions, + }) + } + } - roleDefinitionId, err := parse.RoleDefinitionId(d.Id()) - if err != nil { - return err - } + update := authorization.RoleDefinition{ + RoleDefinitionProperties: &authorization.RoleDefinitionProperties{ + RoleName: exisiting.RoleName, + Description: exisiting.Description, + RoleType: exisiting.RoleType, + Permissions: &permissions, + AssignableScopes: exisiting.AssignableScopes, + }, + } - d.Set("scope", roleDefinitionId.Scope) - d.Set("role_definition_id", roleDefinitionId.RoleID) - d.Set("role_definition_resource_id", roleDefinitionId.ResourceID) + if metadata.ResourceData.HasChange("name") { + update.RoleDefinitionProperties.RoleName = &config.Name + } - resp, err := client.Get(ctx, roleDefinitionId.Scope, roleDefinitionId.RoleID) - if err != nil { - if utils.ResponseWasNotFound(resp.Response) { - log.Printf("[DEBUG] Role Definition %q was not found - removing from state", d.Id()) - d.SetId("") - return nil - } + if metadata.ResourceData.HasChange("description") { + update.RoleDefinitionProperties.Description = &config.Description + } - return fmt.Errorf("loading Role Definition %q: %+v", d.Id(), err) - } + if metadata.ResourceData.HasChange("permissions") { + update.RoleDefinitionProperties.Permissions = pointer.To(expandRoleDefinitionPermissions(config.Permissions)) + } - if props := resp.RoleDefinitionProperties; props != nil { - d.Set("name", props.RoleName) - d.Set("description", props.Description) + if metadata.ResourceData.HasChange("assignable_scopes") { + update.RoleDefinitionProperties.AssignableScopes = pointer.To(expandRoleDefinitionAssignableScopes(config)) + } - permissions := flattenRoleDefinitionPermissions(props.Permissions) - if err := d.Set("permissions", permissions); err != nil { - return err - } + resp, err := client.CreateOrUpdate(ctx, id.Scope, id.RoleID, update) + if err != nil { + return fmt.Errorf("updating %s: %+v", id, err) + } - assignableScopes := flattenRoleDefinitionAssignableScopes(props.AssignableScopes) - if err := d.Set("assignable_scopes", assignableScopes); err != nil { - return err - } - } + updatedOn := resp.RoleDefinitionProperties.UpdatedOn + if updatedOn == nil { + return fmt.Errorf("updating Role Definition %q (Scope %q): `properties.UpdatedOn` was nil", id.RoleID, id.Scope) + } + if updatedOn == nil { + return fmt.Errorf("updating %s: `properties.UpdatedOn` was nil", id) + } + + // "Updating" a role definition actually creates a new one and these get consolidated a few seconds later + // where the "create date" and "update date" match for the newly created record + // but eventually switch to being the old create date and the new update date + // ergo we can can for the old create date and the new updated date + log.Printf("[DEBUG] Waiting for %s to settle down..", id) + deadline, ok := ctx.Deadline() + if !ok { + return fmt.Errorf("internal error: context had no deadline") + } + stateConf := &pluginsdk.StateChangeConf{ + ContinuousTargetOccurence: 12, + Delay: 60 * time.Second, + MinTimeout: 10 * time.Second, + Pending: []string{"Pending"}, + Target: []string{"Updated"}, + Refresh: roleDefinitionEventualConsistencyUpdate(ctx, client, *id, *updatedOn), + Timeout: time.Until(deadline), + } + if _, err := stateConf.WaitForStateContext(ctx); err != nil { + return fmt.Errorf("waiting for %s to settle down: %+v", id, err) + } - return nil + return nil + }, + } } -func resourceArmRoleDefinitionDelete(d *pluginsdk.ResourceData, meta interface{}) error { - client := meta.(*clients.Client).Authorization.RoleDefinitionsClient - ctx, cancel := timeouts.ForDelete(meta.(*clients.Client).StopContext, d) - defer cancel() +func (r RoleDefinitionResource) Delete() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 30 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + client := metadata.Client.Authorization.RoleDefinitionsClient - id, _ := parse.RoleDefinitionId(d.Id()) + id, err := parse.RoleDefinitionId(metadata.ResourceData.Id()) + if err != nil { + return err + } - resp, err := client.Delete(ctx, id.Scope, id.RoleID) - if err != nil { - if !utils.ResponseWasNotFound(resp.Response) { - return fmt.Errorf("deleting Role Definition %q at Scope %q: %+v", id.RoleID, id.Scope, err) - } - } - // Deletes are not instant and can take time to propagate - stateConf := &pluginsdk.StateChangeConf{ - Pending: []string{ - "Pending", - }, - Target: []string{ - "Deleted", - "NotFound", + resp, err := client.Delete(ctx, id.Scope, id.RoleID) + if err != nil { + if utils.ResponseWasNotFound(resp.Response) { + return nil + } + return fmt.Errorf("deleting %s: %+v", id, err) + } + + // Deletes are not instant and can take time to propagate + deadline, ok := ctx.Deadline() + if !ok { + return fmt.Errorf("internal error: context had no deadline") + } + stateConf := &pluginsdk.StateChangeConf{ + Pending: []string{ + "Pending", + }, + Target: []string{ + "Deleted", + "NotFound", + }, + Refresh: roleDefinitionDeleteStateRefreshFunc(ctx, client, *id), + MinTimeout: 10 * time.Second, + ContinuousTargetOccurence: 20, + Timeout: time.Until(deadline), + } + + if _, err := stateConf.WaitForStateContext(ctx); err != nil { + return fmt.Errorf("waiting for delete on Role Definition %s to complete", id) + } + + return nil }, - Refresh: roleDefinitionDeleteStateRefreshFunc(ctx, client, id.ResourceID), - MinTimeout: 10 * time.Second, - ContinuousTargetOccurence: 20, - Timeout: d.Timeout(pluginsdk.TimeoutDelete), } +} - if _, err := stateConf.WaitForStateContext(ctx); err != nil { - return fmt.Errorf("waiting for delete on Role Definition %q to complete", id.RoleID) +func (RoleDefinitionResource) StateUpgraders() sdk.StateUpgradeData { + return sdk.StateUpgradeData{ + SchemaVersion: 1, + Upgraders: map[int]pluginsdk.StateUpgrade{ + 0: migration.RoleDefinitionV0ToV1{}, + }, } - - return nil } func roleDefinitionEventualConsistencyUpdate(ctx context.Context, client azuresdkhacks.RoleDefinitionsWorkaroundClient, id parse.RoleDefinitionID, updateRequestDate string) pluginsdk.StateRefreshFunc { @@ -402,59 +465,19 @@ func roleDefinitionEventualConsistencyUpdate(ctx context.Context, client azuresd } } -func expandRoleDefinitionPermissions(input []interface{}) []authorization.Permission { +func expandRoleDefinitionPermissions(input []PermissionModel) []authorization.Permission { output := make([]authorization.Permission, 0) if len(input) == 0 { return output } for _, v := range input { - if v == nil { - continue - } - - raw := v.(map[string]interface{}) permission := authorization.Permission{} - actionsOutput := make([]string, 0) - actions := raw["actions"].([]interface{}) - for _, a := range actions { - if a == nil { - continue - } - actionsOutput = append(actionsOutput, a.(string)) - } - permission.Actions = &actionsOutput - - dataActionsOutput := make([]string, 0) - dataActions := raw["data_actions"].(*pluginsdk.Set) - for _, a := range dataActions.List() { - if a == nil { - continue - } - dataActionsOutput = append(dataActionsOutput, a.(string)) - } - permission.DataActions = &dataActionsOutput - - notActionsOutput := make([]string, 0) - notActions := raw["not_actions"].([]interface{}) - for _, a := range notActions { - if a == nil { - continue - } - notActionsOutput = append(notActionsOutput, a.(string)) - } - permission.NotActions = ¬ActionsOutput - - notDataActionsOutput := make([]string, 0) - notDataActions := raw["not_data_actions"].(*pluginsdk.Set) - for _, a := range notDataActions.List() { - if a == nil { - continue - } - notDataActionsOutput = append(notDataActionsOutput, a.(string)) - } - permission.NotDataActions = ¬DataActionsOutput + permission.Actions = &v.Actions + permission.DataActions = &v.DataActions + permission.NotActions = &v.NotActions + permission.NotDataActions = &v.NotDataActions output = append(output, permission) } @@ -462,71 +485,39 @@ func expandRoleDefinitionPermissions(input []interface{}) []authorization.Permis return output } -func expandRoleDefinitionAssignableScopes(d *pluginsdk.ResourceData) []string { +func expandRoleDefinitionAssignableScopes(config RoleDefinitionModel) []string { scopes := make([]string, 0) - assignableScopes := d.Get("assignable_scopes").([]interface{}) - if len(assignableScopes) == 0 { - assignedScope := d.Get("scope").(string) - scopes = append(scopes, assignedScope) + if len(config.AssignableScopes) == 0 { + scopes = append(scopes, config.Scope) } else { - for _, scope := range assignableScopes { - if s, ok := scope.(string); ok { - scopes = append(scopes, s) - } - } + scopes = append(scopes, config.AssignableScopes...) } return scopes } -func flattenRoleDefinitionPermissions(input *[]authorization.Permission) []interface{} { - permissions := make([]interface{}, 0) +func flattenRoleDefinitionPermissions(input *[]authorization.Permission) []PermissionModel { + permissions := make([]PermissionModel, 0) if input == nil { return permissions } for _, permission := range *input { - permissions = append(permissions, map[string]interface{}{ - "actions": utils.FlattenStringSlice(permission.Actions), - "data_actions": pluginsdk.NewSet(pluginsdk.HashString, utils.FlattenStringSlice(permission.DataActions)), - "not_actions": utils.FlattenStringSlice(permission.NotActions), - "not_data_actions": pluginsdk.NewSet(pluginsdk.HashString, utils.FlattenStringSlice(permission.NotDataActions)), + permissions = append(permissions, PermissionModel{ + Actions: pointer.From(permission.Actions), + DataActions: pointer.From(permission.DataActions), + NotActions: pointer.From(permission.NotActions), + NotDataActions: pointer.From(permission.NotDataActions), }) } return permissions } -func flattenRoleDefinitionAssignableScopes(input *[]string) []interface{} { - scopes := make([]interface{}, 0) - if input == nil { - return scopes - } - - for _, scope := range *input { - scopes = append(scopes, scope) - } - - return scopes -} - -func roleDefinitionUpdateStateRefreshFunc(ctx context.Context, client *authorization.RoleDefinitionsClient, roleDefinitionId string) pluginsdk.StateRefreshFunc { +func roleDefinitionDeleteStateRefreshFunc(ctx context.Context, client *authorization.RoleDefinitionsClient, id parse.RoleDefinitionID) pluginsdk.StateRefreshFunc { return func() (interface{}, string, error) { - resp, err := client.GetByID(ctx, roleDefinitionId) - if err != nil { - if utils.ResponseWasNotFound(resp.Response) { - return resp, "NotFound", err - } - return resp, "Error", err - } - return "OK", "OK", nil - } -} - -func roleDefinitionDeleteStateRefreshFunc(ctx context.Context, client *authorization.RoleDefinitionsClient, roleDefinitionId string) pluginsdk.StateRefreshFunc { - return func() (interface{}, string, error) { - resp, err := client.GetByID(ctx, roleDefinitionId) + resp, err := client.Get(ctx, id.Scope, id.RoleID) if err != nil { if utils.ResponseWasNotFound(resp.Response) { return resp, "NotFound", nil