Skip to content

Commit

Permalink
azuread_group - support for the owners property (#62)
Browse files Browse the repository at this point in the history
closes #36
  • Loading branch information
tiwood authored and katbyte committed Jul 24, 2019
1 parent 1441fd8 commit 6980676
Show file tree
Hide file tree
Showing 20 changed files with 693 additions and 203 deletions.
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

0 comments on commit 6980676

Please sign in to comment.