diff --git a/azuread/helpers/graph/group.go b/azuread/helpers/graph/group.go new file mode 100644 index 0000000000..68308e4159 --- /dev/null +++ b/azuread/helpers/graph/group.go @@ -0,0 +1,76 @@ +package graph + +import ( + "context" + "fmt" + "log" + + "github.com/Azure/azure-sdk-for-go/services/graphrbac/1.6/graphrbac" +) + +func GroupAllMembers(client graphrbac.GroupsClient, ctx context.Context, groupId string) ([]string, error) { + it, 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 := 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() + if user != nil { + memberObjectID = *user.ObjectID + } + + group, _ := it.Value().AsADGroup() + if group != nil { + memberObjectID = *group.ObjectID + } + + servicePrincipal, _ := it.Value().AsServicePrincipal() + if servicePrincipal != nil { + memberObjectID = *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) + } + } + + log.Printf("[DEBUG] %d members in Azure AD group with ID: %q", len(existingMembers), groupId) + + return existingMembers, nil +} + +func GroupAddMember(client graphrbac.GroupsClient, ctx context.Context, groupId string, member string) error { + memberGraphURL := fmt.Sprintf("https://graph.windows.net/%s/directoryObjects/%s", client.TenantID, member) + + properties := graphrbac.GroupAddMemberParameters{ + URL: &memberGraphURL, + } + + log.Printf("[DEBUG] Adding member with id %q to Azure AD group with id %q", member, groupId) + if _, err := client.AddMember(ctx, groupId, properties); err != nil { + return fmt.Errorf("Error adding group member %q to Azure AD Group with ID %q: %+v", member, groupId, err) + } + + return nil +} + +func GroupAddMembers(client graphrbac.GroupsClient, ctx context.Context, groupId string, members []string) error { + for _, memberUuid := range members { + err := GroupAddMember(client, ctx, groupId, memberUuid) + + if err != nil { + return fmt.Errorf("Error while adding members to Azure AD Group with ID %q: %+v", groupId, err) + } + } + + return nil +} diff --git a/azuread/helpers/slices/slices.go b/azuread/helpers/slices/slices.go new file mode 100644 index 0000000000..f0b7ca972c --- /dev/null +++ b/azuread/helpers/slices/slices.go @@ -0,0 +1,16 @@ +package slices + +// difference returns the elements in `a` that aren't in `b`. +func Difference(a, b []string) []string { + mb := make(map[string]struct{}, len(b)) + for _, x := range b { + mb[x] = struct{}{} + } + var diff []string + for _, x := range a { + if _, found := mb[x]; !found { + diff = append(diff, x) + } + } + return diff +} diff --git a/azuread/provider.go b/azuread/provider.go index dc5c2488f5..0998c752f8 100644 --- a/azuread/provider.go +++ b/azuread/provider.go @@ -86,6 +86,7 @@ func Provider() terraform.ResourceProvider { "azuread_application": resourceApplication(), "azuread_application_password": resourceApplicationPassword(), "azuread_group": resourceGroup(), + "azuread_group_member": resourceGroupMember(), "azuread_service_principal": resourceServicePrincipal(), "azuread_service_principal_password": resourceServicePrincipalPassword(), "azuread_user": resourceUser(), diff --git a/azuread/resource_group.go b/azuread/resource_group.go index 82c686d3fb..980d355dd1 100644 --- a/azuread/resource_group.go +++ b/azuread/resource_group.go @@ -4,6 +4,10 @@ import ( "fmt" "log" + "github.com/terraform-providers/terraform-provider-azuread/azuread/helpers/slices" + "github.com/terraform-providers/terraform-provider-azuread/azuread/helpers/tf" + "github.com/terraform-providers/terraform-provider-azuread/azuread/helpers/validate" + "github.com/Azure/azure-sdk-for-go/services/graphrbac/1.6/graphrbac" "github.com/google/uuid" "github.com/hashicorp/terraform/helper/schema" @@ -18,6 +22,7 @@ func resourceGroup() *schema.Resource { return &schema.Resource{ Create: resourceGroupCreate, Read: resourceGroupRead, + Update: resourceGroupUpdate, Delete: resourceGroupDelete, Importer: &schema.ResourceImporter{ @@ -36,6 +41,18 @@ func resourceGroup() *schema.Resource { Type: schema.TypeString, Computed: true, }, + + "members": { + Type: schema.TypeSet, + Optional: true, + Computed: true, + Set: schema.HashString, + ForceNew: false, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validate.UUID, + }, + }, }, } } @@ -62,6 +79,15 @@ func resourceGroupCreate(d *schema.ResourceData, meta interface{}) error { } d.SetId(*group.ObjectID) + // Add members if specified + if v, ok := d.GetOk("members"); ok { + members := tf.ExpandStringSlicePtr(v.(*schema.Set).List()) + + if err := graph.GroupAddMembers(client, ctx, *group.ObjectID, *members); err != nil { + return err + } + } + _, err = graph.WaitForReplication(func() (interface{}, error) { return client.Get(ctx, *group.ObjectID) }) @@ -89,9 +115,48 @@ func resourceGroupRead(d *schema.ResourceData, meta interface{}) error { d.Set("name", resp.DisplayName) d.Set("object_id", resp.ObjectID) + + members, err := graph.GroupAllMembers(client, ctx, d.Id()) + if err != nil { + return err + } + + d.Set("members", members) + return nil } +func resourceGroupUpdate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*ArmClient).groupsClient + ctx := meta.(*ArmClient).StopContext + + if v, ok := d.GetOkExists("members"); ok && d.HasChange("members") { + existingMembers, err := graph.GroupAllMembers(client, ctx, d.Id()) + if err != nil { + return err + } + + desiredMembers := *tf.ExpandStringSlicePtr(v.(*schema.Set).List()) + membersForRemoval := slices.Difference(existingMembers, desiredMembers) + membersToAdd := slices.Difference(desiredMembers, existingMembers) + + for _, existingMember := range membersForRemoval { + log.Printf("[DEBUG] Removing member with id %q from Azure AD group with id %q", existingMember, d.Id()) + if resp, err := client.RemoveMember(ctx, d.Id(), existingMember); err != nil { + if !ar.ResponseWasNotFound(resp) { + return fmt.Errorf("Error Deleting group member %q from Azure AD Group with ID %q: %+v", existingMember, d.Id(), err) + } + } + } + + if err := graph.GroupAddMembers(client, ctx, d.Id(), membersToAdd); err != nil { + return err + } + } + + return resourceGroupRead(d, meta) +} + func resourceGroupDelete(d *schema.ResourceData, meta interface{}) error { client := meta.(*ArmClient).groupsClient ctx := meta.(*ArmClient).StopContext diff --git a/azuread/resource_group_member.go b/azuread/resource_group_member.go new file mode 100644 index 0000000000..0afc87a2c9 --- /dev/null +++ b/azuread/resource_group_member.go @@ -0,0 +1,113 @@ +package azuread + +import ( + "fmt" + "strings" + + "github.com/terraform-providers/terraform-provider-azuread/azuread/helpers/graph" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/terraform-providers/terraform-provider-azuread/azuread/helpers/ar" + "github.com/terraform-providers/terraform-provider-azuread/azuread/helpers/validate" +) + +func resourceGroupMember() *schema.Resource { + return &schema.Resource{ + Create: resourceGroupMemberCreate, + Read: resourceGroupMemberRead, + Delete: resourceGroupMemberDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "group_object_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validate.UUID, + }, + "member_object_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validate.UUID, + }, + }, + } +} + +func resourceGroupMemberCreate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*ArmClient).groupsClient + ctx := meta.(*ArmClient).StopContext + + groupID := d.Get("group_object_id").(string) + memberID := d.Get("member_object_id").(string) + + if err := graph.GroupAddMember(client, ctx, groupID, memberID); err != nil { + return err + } + + id := fmt.Sprintf("%s/member/%s", groupID, memberID) + d.SetId(id) + + return resourceGroupMemberRead(d, meta) +} + +func resourceGroupMemberRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*ArmClient).groupsClient + ctx := meta.(*ArmClient).StopContext + + id := strings.Split(d.Id(), "/member/") + if len(id) != 2 { + return fmt.Errorf("ID should be in the format {groupObjectId}/member/{memberObjectId} - but got %q", d.Id()) + } + + groupID := id[0] + memberID := id[1] + + members, err := graph.GroupAllMembers(client, ctx, groupID) + if err != nil { + return fmt.Errorf("Error retrieving Azure AD Group members (groupObjectId: %q): %+v", groupID, err) + } + + var memberObjectID string + + for _, objectID := range members { + if objectID == memberID { + memberObjectID = objectID + } + } + + if memberObjectID == "" { + d.SetId("") + return fmt.Errorf("Azure AD Group Member not found - groupObjectId:%q / memberObjectId:%q", groupID, memberID) + } + + d.Set("group_object_id", groupID) + d.Set("member_object_id", memberObjectID) + + return nil +} + +func resourceGroupMemberDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*ArmClient).groupsClient + ctx := meta.(*ArmClient).StopContext + + id := strings.Split(d.Id(), "/member/") + if len(id) != 2 { + return fmt.Errorf("ID should be in the format {groupObjectId}/member/{memberObjectId} - but got %q", d.Id()) + } + + groupID := id[0] + memberID := id[1] + + resp, err := client.RemoveMember(ctx, groupID, memberID) + if err != nil { + if !ar.ResponseWasNotFound(resp) { + return fmt.Errorf("Error removing Member (memberObjectId: %q) from Azure AD Group (groupObjectId: %q): %+v", memberID, groupID, err) + } + } + + return nil +} diff --git a/azuread/resource_group_member_test.go b/azuread/resource_group_member_test.go new file mode 100644 index 0000000000..8b8ac21fa6 --- /dev/null +++ b/azuread/resource_group_member_test.go @@ -0,0 +1,224 @@ +package azuread + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/terraform-providers/terraform-provider-azuread/azuread/helpers/ar" +) + +func TestAccAzureADGroupMember_User(t *testing.T) { + resourceName := "azuread_group_member.test" + id := acctest.RandStringFromCharSet(7, acctest.CharSetAlphaNum) + password := id + "p@$$wR2" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureADGroupMemberDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureADGroupMember_User(id, password), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet(resourceName, "group_object_id"), + resource.TestCheckResourceAttrSet(resourceName, "member_object_id"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAzureADGroupMember_Group(t *testing.T) { + resourceName := "azuread_group_member.test" + id := acctest.RandStringFromCharSet(7, acctest.CharSetAlphaNum) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureADGroupMemberDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureADGroupMember_Group(id), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet(resourceName, "group_object_id"), + resource.TestCheckResourceAttrSet(resourceName, "member_object_id"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAzureADGroupMember_ServicePrincipal(t *testing.T) { + resourceName := "azuread_group_member.test" + id := acctest.RandStringFromCharSet(7, acctest.CharSetAlphaNum) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureADGroupMemberDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureADGroupMember_ServicePrincipal(id), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet(resourceName, "group_object_id"), + resource.TestCheckResourceAttrSet(resourceName, "member_object_id"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testCheckAzureADGroupMemberDestroy(s *terraform.State) error { + for _, rs := range s.RootModule().Resources { + if rs.Type != "azuread_group_member" { + continue + } + + client := testAccProvider.Meta().(*ArmClient).groupsClient + ctx := testAccProvider.Meta().(*ArmClient).StopContext + + groupID := rs.Primary.Attributes["group_object_id"] + memberID := rs.Primary.Attributes["member_object_id"] + + members, err := client.GetGroupMembersComplete(ctx, groupID) + if err != nil { + if ar.ResponseWasNotFound(members.Response().Response) { + return nil + } + + return err + } + + var memberObjectID string + for members.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, _ := members.Value().AsUser() + if user != nil { + if *user.ObjectID == memberID { + memberObjectID = *user.ObjectID + // we successfully found the directory object we're looking for, we can stop looping + // through the results + break + } + } + + group, _ := members.Value().AsADGroup() + if group != nil { + if *group.ObjectID == memberID { + memberObjectID = *group.ObjectID + // we successfully found the directory object we're looking for, we can stop looping + // through the results + break + } + } + + servicePrincipal, _ := members.Value().AsServicePrincipal() + if servicePrincipal != nil { + if *servicePrincipal.ObjectID == memberID { + memberObjectID = *servicePrincipal.ObjectID + // we successfully found the directory object we're looking for, we can stop looping + // through the results + break + } + } + + err = members.NextWithContext(ctx) + if err != nil { + return fmt.Errorf("Error listing Azure AD Group Members: %s", err) + } + } + + if memberObjectID != "" { + return fmt.Errorf("Azure AD group member still exists:\n%#v", memberObjectID) + } + } + + return nil +} + +func testAccAzureADGroupMember_User(id string, password string) string { + return fmt.Sprintf(` + +data "azuread_domains" "tenant_domain" { + only_initial = true +} + +resource "azuread_user" "test" { + user_principal_name = "acctestA%[1]s@${data.azuread_domains.tenant_domain.domains.0.domain_name}" + display_name = "acctestA%[1]s" + password = "%[2]s" +} + +resource "azuread_group" "test" { + name = "acctest%[1]s" +} + +resource "azuread_group_member" "test" { + group_object_id = "${azuread_group.test.object_id}" + member_object_id = "${azuread_user.test.object_id}" +} + +`, id, password) +} + +func testAccAzureADGroupMember_Group(id string) string { + return fmt.Sprintf(` + +resource "azuread_group" "testA" { + name = "acctestA%[1]s" +} + +resource "azuread_group" "testB" { + name = "acctestB%[1]s" +} + +resource "azuread_group_member" "test" { + group_object_id = "${azuread_group.testA.object_id}" + member_object_id = "${azuread_group.testB.object_id}" +} + +`, id) +} + +func testAccAzureADGroupMember_ServicePrincipal(id string) string { + return fmt.Sprintf(` + +resource "azuread_application" "test" { + name = "acctest%[1]s" +} + +resource "azuread_service_principal" "test" { + application_id = "${azuread_application.test.application_id}" +} + +resource "azuread_group" "test" { + name = "acctestA%[1]s" +} + +resource "azuread_group_member" "test" { + group_object_id = "${azuread_group.test.object_id}" + member_object_id = "${azuread_service_principal.test.object_id}" +} + +`, id) +} diff --git a/azuread/resource_group_test.go b/azuread/resource_group_test.go index 75e923e514..b46b20e73e 100644 --- a/azuread/resource_group_test.go +++ b/azuread/resource_group_test.go @@ -26,11 +26,35 @@ func TestAccAzureADGroup_basic(t *testing.T) { Steps: []resource.TestStep{ { Config: config, - Check: resource.ComposeTestCheckFunc( - testCheckAzureADGroupExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "name", fmt.Sprintf("acctest%s", id)), - resource.TestCheckResourceAttrSet(resourceName, "object_id"), - ), + Check: assertResourceWithMemberCount(id, "0"), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAzureADGroup_members(t *testing.T) { + resourceName := "azuread_group.test" + + id, err := uuid.GenerateUUID() + if err != nil { + t.Fatal(err) + } + + config := testAccAzureADGroupWithThreeMembers(id) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureADGroupDestroy, + Steps: []resource.TestStep{ + { + Config: config, + Check: assertResourceWithMemberCount(id, "3"), }, { ResourceName: resourceName, @@ -56,17 +80,100 @@ func TestAccAzureADGroup_complete(t *testing.T) { Steps: []resource.TestStep{ { Config: config, - Check: resource.ComposeTestCheckFunc( - testCheckAzureADGroupExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "name", fmt.Sprintf("acctest%s", id)), - resource.TestCheckResourceAttrSet(resourceName, "object_id"), - ), + Check: assertResourceWithMemberCount(id, "0"), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAzureADGroup_diverse(t *testing.T) { + resourceName := "azuread_group.test" + id, err := uuid.GenerateUUID() + if err != nil { + t.Fatal(err) + } + config := testAccAzureADGroupWithDiverseMembers(id) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureADGroupDestroy, + Steps: []resource.TestStep{ + { + Config: config, + Check: assertResourceWithMemberCount(id, "3"), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAzureADGroup_progression(t *testing.T) { + resourceName := "azuread_group.test" + id, err := uuid.GenerateUUID() + if err != nil { + t.Fatal(err) + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureADGroupDestroy, + Steps: []resource.TestStep{ + // Empty group with 0 members + { + Config: testAccAzureADGroup(id), + Check: assertResourceWithMemberCount(id, "0"), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + // Group with 1 member + { + Config: testAccAzureADGroupWithOneMember(id), + Check: assertResourceWithMemberCount(id, "1"), }, { ResourceName: resourceName, ImportState: true, ImportStateVerify: true, }, + // Group with multiple members + { + Config: testAccAzureADGroupWithThreeMembers(id), + Check: assertResourceWithMemberCount(id, "3"), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + // Group with a different member + { + Config: testAccAzureADGroupWithServicePrincipal(id), + Check: assertResourceWithMemberCount(id, "1"), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + // Empty group with 0 members + { + Config: testAccAzureADGroup(id), + Check: assertResourceWithMemberCount(id, "0"), + }, }, }) } @@ -117,10 +224,120 @@ func testCheckAzureADGroupDestroy(s *terraform.State) error { return nil } +func assertResourceWithMemberCount(id string, memberCount string) resource.TestCheckFunc { + resourceName := "azuread_group.test" + + return resource.ComposeTestCheckFunc( + testCheckAzureADGroupExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "name", fmt.Sprintf("acctest%s", id)), + resource.TestCheckResourceAttrSet(resourceName, "object_id"), + resource.TestCheckResourceAttr(resourceName, "members.#", memberCount), + ) +} + func testAccAzureADGroup(id string) string { return fmt.Sprintf(` resource "azuread_group" "test" { name = "acctest%s" + members = [] +} +`, id) +} + +func testAccAzureADGroupWithDiverseMembers(id string) string { + return fmt.Sprintf(` +data "azuread_domains" "tenant_domain" { + only_initial = true +} + +resource "azuread_application" "test_app_%[1]s" { + name = "app%[1]s" +} + +resource "azuread_service_principal" "test_sp_%[1]s" { + application_id = azuread_application.test_app_%[1]s.application_id +} + +resource "azuread_group" "test_g_%[1]s" { + name = "acctest%[1]s" +} + +resource "azuread_user" "acctest_user_%[1]s" { + user_principal_name = "acctest.%[1]s@${data.azuread_domains.tenant_domain.domains.0.domain_name}" + display_name = "acctest-%[1]s" + password = "%[1]s" +} + +resource "azuread_group" "test" { + name = "acctest%[1]s" + members = [ azuread_user.acctest_user_%[1]s.object_id, azuread_group.test_g_%[1]s.object_id, azuread_service_principal.test_sp_%[1]s.object_id ] +} +`, id) +} + +func testAccAzureADGroupWithOneMember(id string) string { + return fmt.Sprintf(` +data "azuread_domains" "tenant_domain" { + only_initial = true +} + +resource "azuread_user" "acctest_user_%[1]s" { + user_principal_name = "acctest.%[1]s@${data.azuread_domains.tenant_domain.domains.0.domain_name}" + display_name = "acctest-%[1]s" + password = "%[1]s" +} + +resource "azuread_group" "test" { + name = "acctest%[1]s" + members = [ azuread_user.acctest_user_%[1]s.object_id ] +} +`, id) +} + +func testAccAzureADGroupWithThreeMembers(id string) string { + return fmt.Sprintf(` +data "azuread_domains" "tenant_domain" { + only_initial = true +} + +resource "azuread_user" "acctest_user_%[2]s" { + user_principal_name = "acctest.%[2]s@${data.azuread_domains.tenant_domain.domains.0.domain_name}" + display_name = "acctest-%[2]s" + password = "%[2]s" +} + +resource "azuread_user" "acctest_user_%[3]s" { + user_principal_name = "acctest.%[3]s@${data.azuread_domains.tenant_domain.domains.0.domain_name}" + display_name = "acctest-%[3]s" + password = "%[3]s" +} + +resource "azuread_user" "acctest_user_%[4]s" { + user_principal_name = "acctest.%[4]s@${data.azuread_domains.tenant_domain.domains.0.domain_name}" + display_name = "acctest-%[4]s" + password = "%[4]s" +} + +resource "azuread_group" "test" { + name = "acctest%[1]s" + members = [ azuread_user.acctest_user_%[2]s.object_id, azuread_user.acctest_user_%[3]s.object_id, azuread_user.acctest_user_%[4]s.object_id ] +} +`, id, id+"a", id+"b", id+"c") +} + +func testAccAzureADGroupWithServicePrincipal(id string) string { + return fmt.Sprintf(` +resource "azuread_application" "test_app_%[1]s" { + name = "app%[1]s" +} + +resource "azuread_service_principal" "test_sp_%[1]s" { + application_id = azuread_application.test_app_%[1]s.application_id +} + +resource "azuread_group" "test" { + name = "acctest%[1]s" + members = [ azuread_service_principal.test_sp_%[1]s.object_id ] } `, id) } diff --git a/website/azuread.erb b/website/azuread.erb index 5357e7d384..6fdfbd9f22 100644 --- a/website/azuread.erb +++ b/website/azuread.erb @@ -92,6 +92,10 @@ azuread_group + > + azuread_group_member + + > azuread_service_principal diff --git a/website/docs/r/group.markdown b/website/docs/r/group.markdown index 6a55abaa87..f178629c93 100644 --- a/website/docs/r/group.markdown +++ b/website/docs/r/group.markdown @@ -15,9 +15,26 @@ Manages a Group within Azure Active Directory. ## Example Usage +*Basic example* + +```hcl +resource "azuread_group" "my_group" { + name = "MyGroup" +} +``` + +*A group with members* + ```hcl +resource "azuread_user" "my_user" { + display_name = "John Doe" + password = "notSecure123" + user_principal_name = "john.doe@terraform.onmicrosoft.com" +} + resource "azuread_group" "my_group" { name = "MyGroup" + members = [ azuread_user.my_user.object_id /*, more users */ ] } ``` @@ -25,10 +42,13 @@ resource "azuread_group" "my_group" { The following arguments are supported: -* `name` - (Required) The display name for the Group. +* `name` - (Required) The display name for the Group. Changing this forces a new resource to be created. +* `members` (Optional) A set of members who should be present in this Group. Supported Object types are Users, Groups or Service Principals. -> **NOTE:** Group names are not unique within Azure Active Directory. +-> **NOTE:** Do not use `azuread_group_member` at the same time as the `members` argument. + ## Attributes Reference The following attributes are exported: @@ -37,6 +57,8 @@ The following attributes are exported: * `name` - The Display Name of the Group. +* `members` - The Members of the Group. + ## Import Azure Active Directory Groups can be imported using the `object id`, e.g. diff --git a/website/docs/r/group_member.markdown b/website/docs/r/group_member.markdown new file mode 100644 index 0000000000..48ddd11f98 --- /dev/null +++ b/website/docs/r/group_member.markdown @@ -0,0 +1,57 @@ +--- +layout: "azuread" +page_title: "Azure Active Directory: azuread_group_member" +sidebar_current: "docs-azuread-resource-azuread-group-member" +description: |- + Manages a single Group Membership within Azure Active Directory. + +--- + +# azuread_group_member + +Manages a single Group Membership within Azure Active Directory. + +-> **NOTE:** Do not use this resource at the same time as `azuread_group.members`. + +## Example Usage + +```hcl + +data "azuread_user" "my_user" { + user_principal_name = "johndoe@hashicorp.com" +} + +resource "azuread_group" "my_group" { + name = "my_group" +} + +resource "azuread_group_member" "test" { + group_object_id = "${azuread_group.my_group.id}" + member_object_id = "${data.azuread_user.my_user.id}" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `group_object_id` - (Required) The Object ID of the Azure AD Group 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 Azure AD Object you want to add as a Member to the Group. Supported Object types are Users, Groups or Service Principals. Changing this forces a new resource to be created. + +-> **NOTE:** The Member object has to be present in your Azure Active Directory, either as a Member or a Guest. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The ID of the Azure AD Group Member. + +## Import + +Azure Active Directory Group Members can be imported using the `object id`, e.g. + +```shell +terraform import azuread_group_member.test 00000000-0000-0000-0000-000000000000/11111111-1111-1111-1111-111111111111 +``` + +-> **NOTE:** This ID format is unique to Terraform and is composed of the Azure AD Group Object ID and the target Member Object ID in the format `{GroupObjectID}/{MemberObjectID}`.