Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New resource: azuread_directory_role_assignment #826

Merged
merged 1 commit into from
Jun 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions docs/resources/directory_role_assignment.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
---
subcategory: "Directory Roles"
---

# Resource: azuread_directory_role_assignment

Manages a single directory role assignment within Azure Active Directory.

## API Permissions

The following API permissions are required in order to use this resource.

When authenticated with a service principal, this resource requires one of the following application roles: `RoleManagement.ReadWrite.Directory` or `Directory.ReadWrite.All`

When authenticated with a user principal, this resource requires one of the following directory roles: `Privileged Role Administrator` or `Global Administrator`

## Example Usage

*Assignment for a built-in role*

```terraform
data "azuread_user" "example" {
user_principal_name = "[email protected]"
}

resource "azuread_directory_role" "example" {
display_name = "Security administrator"
}

resource "azuread_directory_role_assignment" "example" {
role_id = azuread_directory_role.example.template_id
principal_object_id = data.azuread_user.example.object_id
}
```

~> Note the use of the `template_id` attribute when referencing built-in roles.

*Assignment for a custom role*

```terraform
data "azuread_user" "example" {
user_principal_name = "[email protected]"
}

resource "azuread_custom_directory_role" "example" {
display_name = "My Custom Role"
enabled = true
version = "1.0"

permissions {
allowed_resource_actions = [
"microsoft.directory/applications/basic/update",
"microsoft.directory/applications/standard/read",
]
}
}

resource "azuread_directory_role_assignment" "example" {
role_id = azuread_custom_directory_role.example.object_id
principal_object_id = data.azuread_user.example.object_id
}
```

## Argument Reference

The following arguments are supported:

* `app_scope_object_id` - (Optional) Identifier of the app-specific scope when the assignment scope is app-specific. Cannot be used with `directory_scope_object_id`. Changing this forces a new resource to be created.
* `directory_scope_object_id` - (Optional) The object ID of a directory object representing the scope of the assignment. Cannot be used with `app_scope_object_id`. Changing this forces a new resource to be created.
* `principal_object_id` - (Required) The object ID of the principal for you want to create a role assignment. Supported object types are Users, Groups or Service Principals. Changing this forces a new resource to be created.
* `role_id` - (Required) The template ID (in the case of built-in roles) or object ID (in the case of custom roles) of the directory role you want to assign. Changing this forces a new resource to be created.

## Attributes Reference

In addition to all arguments above, the following attributes are exported:

*No additional attributes are exported*

## Import

Directory role assignments can be imported using the ID of the assignment, e.g.

```shell
terraform import azuread_directory_role_assignment.test ePROZI_iKE653D_d6aoLHyr-lKgHI8ZGiIdz8CLVcng-1
```
2 changes: 2 additions & 0 deletions docs/resources/directory_role_member.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ subcategory: "Directory Roles"

Manages a single directory role membership (assignment) within Azure Active Directory.

-> **Deprecation Warning:** This resource has been superseded by the [azuread_directory_role_assignment](https://registry.terraform.io/providers/hashicorp/azuread/latest/docs/resources/directory_role_assignment) resource and will be removed in version 3.0 of the AzureAD provider

## API Permissions

The following API permissions are required in order to use this resource.
Expand Down
8 changes: 6 additions & 2 deletions internal/services/directoryroles/client/client.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
package client

import (
"github.com/manicminer/hamilton/msgraph"

"github.com/hashicorp/terraform-provider-azuread/internal/common"
"github.com/manicminer/hamilton/msgraph"
)

type Client struct {
DirectoryObjectsClient *msgraph.DirectoryObjectsClient
DirectoryRolesClient *msgraph.DirectoryRolesClient
DirectoryRoleTemplatesClient *msgraph.DirectoryRoleTemplatesClient
RoleAssignmentsClient *msgraph.RoleAssignmentsClient
RoleDefinitionsClient *msgraph.RoleDefinitionsClient
}

Expand All @@ -23,13 +23,17 @@ func NewClient(o *common.ClientOptions) *Client {
directoryRoleTemplatesClient := msgraph.NewDirectoryRoleTemplatesClient(o.TenantID)
o.ConfigureClient(&directoryRoleTemplatesClient.BaseClient)

roleAssignmentsClient := msgraph.NewRoleAssignmentsClient(o.TenantID)
o.ConfigureClient(&roleAssignmentsClient.BaseClient)

roleDefinitionsClient := msgraph.NewRoleDefinitionsClient(o.TenantID)
o.ConfigureClient(&roleDefinitionsClient.BaseClient)

return &Client{
DirectoryObjectsClient: directoryObjectsClient,
DirectoryRolesClient: directoryRolesClient,
DirectoryRoleTemplatesClient: directoryRoleTemplatesClient,
RoleAssignmentsClient: roleAssignmentsClient,
RoleDefinitionsClient: roleDefinitionsClient,
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
package directoryroles

import (
"context"
"errors"
"fmt"
"log"
"net/http"
"time"

"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-provider-azuread/internal/clients"
"github.com/hashicorp/terraform-provider-azuread/internal/tf"
"github.com/hashicorp/terraform-provider-azuread/internal/utils"
"github.com/hashicorp/terraform-provider-azuread/internal/validate"
"github.com/manicminer/hamilton/msgraph"
"github.com/manicminer/hamilton/odata"
)

func directoryRoleAssignmentResource() *schema.Resource {
return &schema.Resource{
CreateContext: directoryRoleAssignmentResourceCreate,
ReadContext: directoryRoleAssignmentResourceRead,
DeleteContext: directoryRoleAssignmentResourceDelete,

Timeouts: &schema.ResourceTimeout{
Create: schema.DefaultTimeout(5 * time.Minute),
Read: schema.DefaultTimeout(5 * time.Minute),
Update: schema.DefaultTimeout(5 * time.Minute),
Delete: schema.DefaultTimeout(5 * time.Minute),
},

Importer: tf.ValidateResourceIDPriorToImport(func(id string) error {
if id == "" {
return errors.New("id was empty")
}
return nil
}),

Schema: map[string]*schema.Schema{
"role_id": {
Description: "The object ID of the directory role for this assignment",
Type: schema.TypeString,
Optional: true,
ForceNew: true,
ValidateDiagFunc: validate.UUID,
},

"principal_object_id": {
Description: "The object ID of the member principal",
Type: schema.TypeString,
Optional: true,
ForceNew: true,
ValidateDiagFunc: validate.UUID,
},

"directory_scope_object_id": {
Description: "The object ID of a directory object representing the scope of the assignment",
Type: schema.TypeString,
Optional: true,
ForceNew: true,
ConflictsWith: []string{"app_scope_object_id"},
ValidateDiagFunc: validate.UUID,
},

"app_scope_object_id": {
Description: "Identifier of the app-specific scope when the assignment scope is app-specific",
Type: schema.TypeString,
Optional: true,
ForceNew: true,
ConflictsWith: []string{"directory_scope_object_id"},
ValidateDiagFunc: validate.UUID,
},
},
}
}

func directoryRoleAssignmentResourceCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*clients.Client).DirectoryRoles.RoleAssignmentsClient

roleId := d.Get("role_id").(string)
memberId := d.Get("principal_object_id").(string)

directoryScopeId := d.Get("directory_scope_object_id").(string)
if directoryScopeId == "" {
directoryScopeId = "/"
}

properties := msgraph.UnifiedRoleAssignment{
DirectoryScopeId: &directoryScopeId,
PrincipalId: &memberId,
RoleDefinitionId: &roleId,
}

appScopeId := d.Get("app_scope_object_id").(string)
if appScopeId != "" {
properties.AppScopeId = &appScopeId
}

assignment, status, err := client.Create(ctx, properties)
if err != nil {
return tf.ErrorDiagF(err, "Adding role member %q to directory role %q, received %d with error: %+v", memberId, roleId, status, err)
}
if assignment == nil || assignment.ID == nil {
return tf.ErrorDiagF(errors.New("returned role assignment ID was nil"), "API Error")
}

d.SetId(*assignment.ID)

// Wait for role membership to reflect
deadline, ok := ctx.Deadline()
if !ok {
return tf.ErrorDiagF(errors.New("context has no deadline"), "Waiting for role member %q to reflect for directory role %q", memberId, roleId)
}
timeout := time.Until(deadline)
_, err = (&resource.StateChangeConf{
Pending: []string{"Waiting"},
Target: []string{"Done"},
Timeout: timeout,
MinTimeout: 1 * time.Second,
ContinuousTargetOccurence: 3,
Refresh: func() (interface{}, string, error) {
_, status, err := client.Get(ctx, *assignment.ID, odata.Query{})
if err != nil {
if status == http.StatusNotFound {
return "stub", "Waiting", nil
}
return nil, "Error", fmt.Errorf("retrieving role assignment")
}
return "stub", "Done", nil
},
}).WaitForStateContext(ctx)
if err != nil {
return tf.ErrorDiagF(err, "Waiting for role assignment for %q to reflect in directory role %q", memberId, roleId)
}

return directoryRoleAssignmentResourceRead(ctx, d, meta)
}

func directoryRoleAssignmentResourceRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*clients.Client).DirectoryRoles.RoleAssignmentsClient

id := d.Id()
assignment, status, err := client.Get(ctx, id, odata.Query{})
if err != nil {
if status == http.StatusNotFound {
log.Printf("[DEBUG] Assignment with ID %q was not found - removing from state", id)
d.SetId("")
return nil
}
return tf.ErrorDiagF(err, "Retrieving role assignment %q", id)
}

directoryScopeId := assignment.DirectoryScopeId
if directoryScopeId == nil || *directoryScopeId == "/" {
directoryScopeId = utils.String("")
}

appScopeId := assignment.DirectoryScopeId
if appScopeId == nil || *appScopeId == "/" {
appScopeId = utils.String("")
}

tf.Set(d, "app_scope_object_id", appScopeId)
tf.Set(d, "directory_scope_object_id", directoryScopeId)
tf.Set(d, "principal_object_id", assignment.PrincipalId)
tf.Set(d, "role_id", assignment.RoleDefinitionId)

return nil
}

func directoryRoleAssignmentResourceDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*clients.Client).DirectoryRoles.RoleAssignmentsClient

if _, err := client.Delete(ctx, d.Id()); err != nil {
return tf.ErrorDiagF(err, "Deleting role assignment %q: %+v", d.Id(), err)
}
return nil
}
Loading