Skip to content

Commit

Permalink
Merge pull request #573 from hashicorp/feature/directory-role-members
Browse files Browse the repository at this point in the history
New resources: directory_role and directory_role_member
  • Loading branch information
manicminer authored Sep 16, 2021
2 parents 84f91f7 + 8c11716 commit bcb7b3f
Show file tree
Hide file tree
Showing 24 changed files with 1,044 additions and 43 deletions.
1 change: 1 addition & 0 deletions .teamcity/components/project.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const val providerName = "azuread"
var services = mapOf(
"applications" to "Applications",
"conditionalaccess" to "Conditional Access",
"directoryroles" to "Directory Roles",
"domains" to "Domains",
"groups" to "Groups",
"invitations" to "Invitations",
Expand Down
55 changes: 55 additions & 0 deletions docs/resources/directory_role.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
---
subcategory: "Directory Roles"
---

# Resource: azuread_directory_role

Manages a Directory Role within Azure Active Directory. Directory Roles are also known as Administrator Roles.

Directory Roles are built-in to Azure Active Directory and are immutable. However, by default they are not activated in a tenant (except for the Global Administrator role). This resource ensures a directory role is activated from its associated role template, and exports the object ID of the role, so that role assignments can be made for it.

## 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

*Activate a directory role by its template ID*

```terraform
resource "azuread_directory_role" "example" {
template_id = "00000000-0000-0000-0000-000000000000"
}
```

*Activate a directory role by display name*

```terraform
resource "azuread_directory_role" "example" {
display_name = "Printer administrator"
}
```

## Argument Reference

The following arguments are supported:

* `display_name` - (Optional) The display name of the directory role to activate. Changing this forces a new resource to be created.
* `template_id` - (Optional) The object ID of the role template from which to activate the directory role. Changing this forces a new resource to be created.

~> Either `display_name` or `template_id` must be specified.

## Attributes Reference

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

* `description` - The description of the directory role.
* `object_id` - The object ID of the directory role.

## Import

This resource does not support importing.
56 changes: 56 additions & 0 deletions docs/resources/directory_role_member.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
---
subcategory: "Directory Roles"
---

# Resource: azuread_directory_role_member

Manages a single directory role membership (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

```terraform
data "azuread_user" "example" {
user_principal_name = "[email protected]"
}
resource "azuread_directory_role" "example" {
display_name = "Security administrator"
}
resource "azuread_directory_role_member" "example" {
directory_role_object_id = azuread_directory_role.example.object_id
member_object_id = data.azuread_user.example.object_id
}
```

## Argument Reference

The following arguments are supported:

* `directory_role_object_id` - (Required) The object ID of the directory role you want to add the member to. Changing this forces a new resource to be created.
* `member_object_id` - (Required) The object ID of the principal you want to add as a member to the directory role. Supported object types are Users or Service Principals. 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 members can be imported using the object ID of the role and the object ID of the member, e.g.

```shell
terraform import azuread_directory_role_member.test 00000000-0000-0000-0000-000000000000/member/11111111-1111-1111-1111-111111111111
```

-> This ID format is unique to Terraform and is composed of the Directory Role Object ID and the target Member Object ID in the format `{GroupObjectID}/member/{MemberObjectID}`.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ require (
github.com/hashicorp/terraform-plugin-sdk/v2 v2.7.0
github.com/hashicorp/yamux v0.0.0-20210316155119-a95892c5f864 // indirect
github.com/klauspost/compress v1.12.2 // indirect
github.com/manicminer/hamilton v0.28.2
github.com/manicminer/hamilton v0.29.0
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
github.com/mitchellh/mapstructure v1.4.1 // indirect
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -285,8 +285,8 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/manicminer/hamilton v0.28.2 h1:W+ZRftPTOgWAa9zr3bUW1CzU1yOMFz1ZumHcA5kXqes=
github.com/manicminer/hamilton v0.28.2/go.mod h1:QryxpD/4+cdKuXNi0UjLDvgxYdP0LLmYz7dYU7DAX4U=
github.com/manicminer/hamilton v0.29.0 h1:SmKNUMbuk7Crp9mJUWJTNRKnsFzdIxyIhlvvJ0HjCGk=
github.com/manicminer/hamilton v0.29.0/go.mod h1:QryxpD/4+cdKuXNi0UjLDvgxYdP0LLmYz7dYU7DAX4U=
github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
Expand Down
3 changes: 3 additions & 0 deletions internal/clients/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/hashicorp/terraform-provider-azuread/internal/common"
applications "github.com/hashicorp/terraform-provider-azuread/internal/services/applications/client"
conditionalaccess "github.com/hashicorp/terraform-provider-azuread/internal/services/conditionalaccess/client"
directoryroles "github.com/hashicorp/terraform-provider-azuread/internal/services/directoryroles/client"
domains "github.com/hashicorp/terraform-provider-azuread/internal/services/domains/client"
groups "github.com/hashicorp/terraform-provider-azuread/internal/services/groups/client"
invitations "github.com/hashicorp/terraform-provider-azuread/internal/services/invitations/client"
Expand All @@ -30,6 +31,7 @@ type Client struct {

Applications *applications.Client
ConditionalAccess *conditionalaccess.Client
DirectoryRoles *directoryroles.Client
Domains *domains.Client
Groups *groups.Client
Invitations *invitations.Client
Expand All @@ -43,6 +45,7 @@ func (client *Client) build(ctx context.Context, o *common.ClientOptions) error
client.Applications = applications.NewClient(o)
client.Domains = domains.NewClient(o)
client.ConditionalAccess = conditionalaccess.NewClient(o)
client.DirectoryRoles = directoryroles.NewClient(o)
client.Groups = groups.NewClient(o)
client.Invitations = invitations.NewClient(o)
client.ServicePrincipals = serviceprincipals.NewClient(o)
Expand Down
2 changes: 2 additions & 0 deletions internal/provider/services.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package provider
import (
"github.com/hashicorp/terraform-provider-azuread/internal/services/applications"
"github.com/hashicorp/terraform-provider-azuread/internal/services/conditionalaccess"
"github.com/hashicorp/terraform-provider-azuread/internal/services/directoryroles"
"github.com/hashicorp/terraform-provider-azuread/internal/services/domains"
"github.com/hashicorp/terraform-provider-azuread/internal/services/groups"
"github.com/hashicorp/terraform-provider-azuread/internal/services/invitations"
Expand All @@ -14,6 +15,7 @@ func SupportedServices() []ServiceRegistration {
return []ServiceRegistration{
applications.Registration{},
conditionalaccess.Registration{},
directoryroles.Registration{},
domains.Registration{},
groups.Registration{},
invitations.Registration{},
Expand Down
30 changes: 30 additions & 0 deletions internal/services/directoryroles/client/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package client

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

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

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

func NewClient(o *common.ClientOptions) *Client {
directoryObjectsClient := msgraph.NewDirectoryObjectsClient(o.TenantID)
o.ConfigureClient(&directoryObjectsClient.BaseClient)

directoryRolesClient := msgraph.NewDirectoryRolesClient(o.TenantID)
o.ConfigureClient(&directoryRolesClient.BaseClient)

directoryRoleTemplatesClient := msgraph.NewDirectoryRoleTemplatesClient(o.TenantID)
o.ConfigureClient(&directoryRoleTemplatesClient.BaseClient)

return &Client{
DirectoryObjectsClient: directoryObjectsClient,
DirectoryRolesClient: directoryRolesClient,
DirectoryRoleTemplatesClient: directoryRoleTemplatesClient,
}
}
173 changes: 173 additions & 0 deletions internal/services/directoryroles/directory_role_member_resource.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
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/manicminer/hamilton/msgraph"
"github.com/manicminer/hamilton/odata"

"github.com/hashicorp/terraform-provider-azuread/internal/clients"
"github.com/hashicorp/terraform-provider-azuread/internal/services/directoryroles/parse"
"github.com/hashicorp/terraform-provider-azuread/internal/tf"
"github.com/hashicorp/terraform-provider-azuread/internal/validate"
)

const directoryRoleMemberResourceName = "azuread_directory_role_member"

func directoryRoleMemberResource() *schema.Resource {
return &schema.Resource{
CreateContext: directoryRoleMemberResourceCreate,
ReadContext: directoryRoleMemberResourceRead,
DeleteContext: directoryRoleMemberResourceDelete,

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 {
_, err := parse.DirectoryRoleMemberID(id)
return err
}),

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

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

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

id := parse.NewDirectoryRoleMemberID(d.Get("role_object_id").(string), d.Get("member_object_id").(string))

tf.LockByName(directoryRoleMemberResourceName, id.DirectoryRoleId)
defer tf.UnlockByName(directoryRoleMemberResourceName, id.DirectoryRoleId)

role, status, err := client.Get(ctx, id.DirectoryRoleId)
if err != nil {
if status == http.StatusNotFound {
return tf.ErrorDiagPathF(nil, "object_id", "Directory role with object ID %q was not found", id.DirectoryRoleId)
}
return tf.ErrorDiagPathF(err, "object_id", "Retrieving directory role with object ID: %q", id.DirectoryRoleId)
}

if _, status, err = client.GetMember(ctx, id.DirectoryRoleId, id.MemberId); err == nil {
return tf.ImportAsExistsDiag("azuread_directory_role_member", id.String())
} else if status != http.StatusNotFound {
return tf.ErrorDiagF(err, "Checking for existing membership of member %q for directory role with object ID: %q", id.MemberId, id.DirectoryRoleId)
}

memberObject, _, err := directoryObjectsClient.Get(ctx, id.MemberId, odata.Query{})
if err != nil {
return tf.ErrorDiagF(err, "Could not retrieve member principal object %q", id.MemberId)
}
if memberObject == nil {
return tf.ErrorDiagF(errors.New("returned memberObject was nil"), "Could not retrieve member principal object %q", id.MemberId)
}
if memberObject.ODataId == nil {
return tf.ErrorDiagF(errors.New("ODataId was nil"), "Could not retrieve member principal object %q", id.MemberId)
}
role.Members = &msgraph.Members{*memberObject}

if _, err := client.AddMembers(ctx, role); err != nil {
return tf.ErrorDiagF(err, "Adding role member %q to directory role %q", id.MemberId, id.DirectoryRoleId)
}

// 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", id.MemberId, id.DirectoryRoleId)
}
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.GetMember(ctx, id.DirectoryRoleId, id.MemberId)
if err != nil {
if status == http.StatusNotFound {
return "stub", "Waiting", nil
}
return nil, "Error", fmt.Errorf("retrieving role member")
}
return "stub", "Done", nil
},
}).WaitForStateContext(ctx)
if err != nil {
return tf.ErrorDiagF(err, "Waiting for role member %q to reflect for directory role %q", id.MemberId, id.DirectoryRoleId)
}

d.SetId(id.String())

return directoryRoleMemberResourceRead(ctx, d, meta)
}

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

id, err := parse.DirectoryRoleMemberID(d.Id())
if err != nil {
return tf.ErrorDiagPathF(err, "id", "Parsing Directory Role Member ID %q", d.Id())
}

if _, status, err := client.GetMember(ctx, id.DirectoryRoleId, id.MemberId); err != nil {
if status == http.StatusNotFound {
log.Printf("[DEBUG] Member with ID %q was not found in directory role %q - removing from state", id.MemberId, id.DirectoryRoleId)
d.SetId("")
return nil
}
return tf.ErrorDiagF(err, "Retrieving role member %q for directory role with object ID: %q", id.MemberId, id.DirectoryRoleId)
}

tf.Set(d, "role_object_id", id.DirectoryRoleId)
tf.Set(d, "member_object_id", id.MemberId)

return nil
}

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

id, err := parse.DirectoryRoleMemberID(d.Id())
if err != nil {
return tf.ErrorDiagPathF(err, "id", "Parsing Directory Role Member ID %q", d.Id())
}

tf.LockByName(directoryRoleMemberResourceName, id.DirectoryRoleId)
defer tf.UnlockByName(directoryRoleMemberResourceName, id.DirectoryRoleId)

if _, err := client.RemoveMembers(ctx, id.DirectoryRoleId, &[]string{id.MemberId}); err != nil {
return tf.ErrorDiagF(err, "Removing member %q from directory role with object ID: %q", id.MemberId, id.DirectoryRoleId)
}

return nil
}
Loading

0 comments on commit bcb7b3f

Please sign in to comment.