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

azuread_group - support for the owners property #62

Merged
merged 7 commits into from
Jul 24, 2019
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
25 changes: 25 additions & 0 deletions azuread/data_group.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,18 @@ func dataGroup() *schema.Resource {
ValidateFunc: validate.NoEmptyStrings,
ConflictsWith: []string{"object_id"},
},

"members": {
Type: schema.TypeList,
Computed: true,
Elem: &schema.Schema{Type: schema.TypeString},
},

"owners": {
Type: schema.TypeList,
Computed: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
},
}
}
Expand Down Expand Up @@ -73,5 +85,18 @@ func dataSourceActiveDirectoryGroupRead(d *schema.ResourceData, meta interface{}

d.Set("object_id", group.ObjectID)
d.Set("name", group.DisplayName)

members, err := graph.GroupAllMembers(client, ctx, d.Id())
if err != nil {
return err
}
d.Set("members", members)

owners, err := graph.GroupAllOwners(client, ctx, d.Id())
if err != nil {
return err
}
d.Set("owners", owners)

return nil
}
69 changes: 64 additions & 5 deletions azuread/data_group_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"testing"

"github.com/hashicorp/terraform/helper/acctest"
"github.com/hashicorp/terraform/helper/resource"
"github.com/terraform-providers/terraform-provider-azuread/azuread/helpers/tf"
)
Expand All @@ -17,9 +18,6 @@ func TestAccDataSourceAzureADGroup_byName(t *testing.T) {
Providers: testAccProviders,
CheckDestroy: testCheckAzureADGroupDestroy,
Steps: []resource.TestStep{
{
Config: testAccAzureADGroup_basic(id),
},
{
Config: testAccDataSourceAzureADGroup_name(id),
Check: resource.ComposeTestCheckFunc(
Expand All @@ -41,13 +39,54 @@ func TestAccDataSourceAzureADGroup_byObjectId(t *testing.T) {
CheckDestroy: testCheckAzureADGroupDestroy,
Steps: []resource.TestStep{
{
Config: testAccAzureADGroup_basic(id),
Config: testAccDataSourceAzureADGroup_objectId(id),
Check: resource.ComposeTestCheckFunc(
testCheckAzureADGroupExists(dsn),
resource.TestCheckResourceAttr(dsn, "name", fmt.Sprintf("acctestGroup-%d", id)),
),
},
},
})
}

func TestAccDataSourceAzureADGroup_members(t *testing.T) {
dsn := "data.azuread_group.test"
id := tf.AccRandTimeInt()
pw := "p@$$wR2" + acctest.RandStringFromCharSet(7, acctest.CharSetAlphaNum)

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testCheckAzureADGroupDestroy,
Steps: []resource.TestStep{
{
Config: testAccDataSourceAzureADGroup_objectId(id),
Config: testAccDataSourceAzureADGroup_members(id, pw),
Check: resource.ComposeTestCheckFunc(
testCheckAzureADGroupExists(dsn),
resource.TestCheckResourceAttr(dsn, "name", fmt.Sprintf("acctestGroup-%d", id)),
resource.TestCheckResourceAttr(dsn, "members.#", "3"),
),
},
},
})
}

func TestAccDataSourceAzureADGroup_owners(t *testing.T) {
dsn := "data.azuread_group.test"
id := tf.AccRandTimeInt()
pw := "p@$$wR2" + acctest.RandStringFromCharSet(7, acctest.CharSetAlphaNum)

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testCheckAzureADGroupDestroy,
Steps: []resource.TestStep{
{
Config: testAccDataSourceAzureADGroup_owners(id, pw),
Check: resource.ComposeTestCheckFunc(
testCheckAzureADGroupExists(dsn),
resource.TestCheckResourceAttr(dsn, "name", fmt.Sprintf("acctestGroup-%d", id)),
resource.TestCheckResourceAttr(dsn, "owners.#", "3"),
),
},
},
Expand All @@ -73,3 +112,23 @@ data "azuread_group" "test" {
}
`, testAccAzureADGroup_basic(id))
}

func testAccDataSourceAzureADGroup_members(id int, password string) string {
return fmt.Sprintf(`
%s

data "azuread_group" "test" {
object_id = "${azuread_group.test.object_id}"
}
`, testAccAzureADGroupWithThreeMembers(id, password))
}

func testAccDataSourceAzureADGroup_owners(id int, password string) string {
return fmt.Sprintf(`
%s

data "azuread_group" "test" {
object_id = "${azuread_group.test.object_id}"
}
`, testAccAzureADGroupWithThreeOwners(id, password))
}
4 changes: 2 additions & 2 deletions azuread/data_users_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ func testAccAzureADUsersDataSource_byUserPrincipalNames(id int, password string)
data "azuread_users" "test" {
user_principal_names = ["${azuread_user.testA.user_principal_name}", "${azuread_user.testB.user_principal_name}"]
}
`, testAccADUser_multiple(id, password))
`, testAccADUser_threeUsersABC(id, password))
}

func testAccAzureADUsersDataSource_byObjectIds(id int, password string) string {
Expand All @@ -67,5 +67,5 @@ func testAccAzureADUsersDataSource_byObjectIds(id int, password string) string {
data "azuread_users" "test" {
object_ids = ["${azuread_user.testA.object_id}", "${azuread_user.testB.object_id}"]
}
`, testAccADUser_multiple(id, password))
`, testAccADUser_threeUsersABC(id, password))
}
4 changes: 2 additions & 2 deletions azuread/helpers/ar/response.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (

//todo move to azure helpers
func ResponseWasNotFound(resp autorest.Response) bool {
return responseWasStatusCode(resp, http.StatusNotFound)
return ResponseWasStatusCode(resp, http.StatusNotFound)
}

func ResponseErrorIsRetryable(err error) bool {
Expand All @@ -27,7 +27,7 @@ func ResponseErrorIsRetryable(err error) bool {
return false
}

func responseWasStatusCode(resp autorest.Response, statusCode int) bool { // nolint: unparam
func ResponseWasStatusCode(resp autorest.Response, statusCode int) bool { // nolint: unparam
if r := resp.Response; r != nil {
if r.StatusCode == statusCode {
return true
Expand Down
2 changes: 1 addition & 1 deletion azuread/helpers/graph/credentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ func ParsePasswordCredentialId(id string) (PasswordCredentialId, error) {
}

if _, err := uuid.ParseUUID(parts[1]); err != nil {
return PasswordCredentialId{}, fmt.Errorf("Object ID isn't a valid UUID (%q): %+v", id[1], err)
return PasswordCredentialId{}, fmt.Errorf("Credential ID isn't a valid UUID (%q): %+v", id[1], err)
}

return PasswordCredentialId{
Expand Down
116 changes: 97 additions & 19 deletions azuread/helpers/graph/group.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,33 @@ import (
"github.com/Azure/azure-sdk-for-go/services/graphrbac/1.6/graphrbac"
)

type GroupMemberId struct {
ObjectSubResourceId
GroupId string
MemberId string
}

func GroupMemberIdFrom(groupId, memberId string) GroupMemberId {
return GroupMemberId{
ObjectSubResourceId: ObjectSubResourceIdFrom(groupId, "member", memberId),
GroupId: groupId,
MemberId: memberId,
}
}

func ParseGroupMemberId(idString string) (GroupMemberId, error) {
id, err := ParseObjectSubResourceId(idString, "member")
if err != nil {
return GroupMemberId{}, fmt.Errorf("Unable to parse Member ID: %v", err)
}

return GroupMemberId{
ObjectSubResourceId: id,
GroupId: id.objectId,
MemberId: id.subId,
}, nil
}

func GroupGetByDisplayName(client *graphrbac.GroupsClient, ctx context.Context, displayName string) (*graphrbac.ADGroup, error) {

filter := fmt.Sprintf("displayName eq '%s'", displayName)
Expand Down Expand Up @@ -39,41 +66,49 @@ func GroupGetByDisplayName(client *graphrbac.GroupsClient, ctx context.Context,
return &group, nil
}

func GroupAllMembers(client graphrbac.GroupsClient, ctx context.Context, groupId string) ([]string, error) {
it, err := client.GetGroupMembersComplete(ctx, groupId)
func DirectoryObjectListToIDs(objects graphrbac.DirectoryObjectListResultIterator, ctx context.Context) ([]string, error) {
ids := make([]string, 0)
for objects.NotDone() {
v := objects.Value()

if err != nil {
return nil, fmt.Errorf("Error listing existing group members from Azure AD Group with ID %q: %+v", groupId, err)
}

existingMembers := make([]string, 0)

var memberObjectID string
for it.NotDone() {
// possible members are users, groups or service principals
// we try to 'cast' each result as the corresponding type and diff
// if we found the object we're looking for
user, _ := it.Value().AsUser()
user, _ := v.AsUser()
if user != nil {
memberObjectID = *user.ObjectID
ids = append(ids, *user.ObjectID)
}

group, _ := it.Value().AsADGroup()
group, _ := v.AsADGroup()
if group != nil {
memberObjectID = *group.ObjectID
ids = append(ids, *group.ObjectID)
}

servicePrincipal, _ := it.Value().AsServicePrincipal()
servicePrincipal, _ := v.AsServicePrincipal()
if servicePrincipal != nil {
memberObjectID = *servicePrincipal.ObjectID
ids = append(ids, *servicePrincipal.ObjectID)
}

existingMembers = append(existingMembers, memberObjectID)
if err := it.NextWithContext(ctx); err != nil {
return nil, fmt.Errorf("Error during pagination of group members from Azure AD Group with ID %q: %+v", groupId, err)
if err := objects.NextWithContext(ctx); err != nil {
return nil, fmt.Errorf("Error during pagination of directory objects: %+v", err)
}
}

return ids, nil
}

func GroupAllMembers(client graphrbac.GroupsClient, ctx context.Context, groupId string) ([]string, error) {
members, err := client.GetGroupMembersComplete(ctx, groupId)

if err != nil {
return nil, fmt.Errorf("Error listing existing group members from Azure AD Group with ID %q: %+v", groupId, err)
}

existingMembers, err := DirectoryObjectListToIDs(members, ctx)
if err != nil {
return nil, fmt.Errorf("Error getting objects IDs of group members for Azure AD Group with ID %q: %+v", groupId, err)
}

log.Printf("[DEBUG] %d members in Azure AD group with ID: %q", len(existingMembers), groupId)

return existingMembers, nil
Expand Down Expand Up @@ -105,3 +140,46 @@ func GroupAddMembers(client graphrbac.GroupsClient, ctx context.Context, groupId

return nil
}

func GroupAllOwners(client graphrbac.GroupsClient, ctx context.Context, groupId string) ([]string, error) {
owners, err := client.ListOwnersComplete(ctx, groupId)

if err != nil {
return nil, fmt.Errorf("Error listing existing group owners from Azure AD Group with ID %q: %+v", groupId, err)
}

existingMembers, err := DirectoryObjectListToIDs(owners, ctx)
if err != nil {
return nil, fmt.Errorf("Error getting objects IDs of group owners for Azure AD Group with ID %q: %+v", groupId, err)
}

log.Printf("[DEBUG] %d members in Azure AD group with ID: %q", len(existingMembers), groupId)
return existingMembers, nil
}

func GroupAddOwner(client graphrbac.GroupsClient, ctx context.Context, groupId string, owner string) error {
ownerGraphURL := fmt.Sprintf("https://graph.windows.net/%s/directoryObjects/%s", client.TenantID, owner)

properties := graphrbac.AddOwnerParameters{
URL: &ownerGraphURL,
}

log.Printf("[DEBUG] Adding owner with id %q to Azure AD group with id %q", owner, groupId)
if _, err := client.AddOwner(ctx, groupId, properties); err != nil {
return fmt.Errorf("Error adding group owner %q to Azure AD Group with ID %q: %+v", owner, groupId, err)
}

return nil
}

func GroupAddOwners(client graphrbac.GroupsClient, ctx context.Context, groupId string, owner []string) error {
for _, ownerUuid := range owner {
err := GroupAddOwner(client, ctx, groupId, ownerUuid)

if err != nil {
return fmt.Errorf("Error while adding owners to Azure AD Group with ID %q: %+v", groupId, err)
}
}

return nil
}
58 changes: 58 additions & 0 deletions azuread/helpers/graph/object_resource.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package graph

import (
"fmt"
"strings"

"github.com/hashicorp/go-uuid"
)

type ObjectSubResourceId struct {
objectId string
subId string
Type string
}

func (id ObjectSubResourceId) String() string {
return id.objectId + "/" + id.Type + "/" + id.subId
}

func ParseObjectSubResourceId(idString, expectedType string) (ObjectSubResourceId, error) {
parts := strings.Split(idString, "/")
if len(parts) != 3 {
return ObjectSubResourceId{}, fmt.Errorf("Object Resource ID should be in the format {objectId}/{keyId} - but got %q", idString)
}

id := ObjectSubResourceId{
objectId: parts[0],
Type: parts[1],
subId: parts[2],
}

if _, err := uuid.ParseUUID(id.objectId); err != nil {
return ObjectSubResourceId{}, fmt.Errorf("Object ID isn't a valid UUID (%q): %+v", id.objectId, err)
}

if id.Type == "" {
return ObjectSubResourceId{}, fmt.Errorf("Type in {objectID}/{type}/{subID} should not blank")
}

if id.Type != expectedType {
return ObjectSubResourceId{}, fmt.Errorf("Type in {objectID}/{type}/{subID} was expected to be %s, got %s", expectedType, parts[2])
}

if _, err := uuid.ParseUUID(id.subId); err != nil {
return ObjectSubResourceId{}, fmt.Errorf("Object Sub Resource ID isn't a valid UUID (%q): %+v", id.subId, err)
}

return id, nil

}

func ObjectSubResourceIdFrom(objectId, typeId, subId string) ObjectSubResourceId {
return ObjectSubResourceId{
objectId: objectId,
Type: typeId,
subId: subId,
}
}
Loading