diff --git a/internal/services/groups/client/client.go b/internal/services/groups/client/client.go index 6fa0ecca4e..16a0efc8b7 100644 --- a/internal/services/groups/client/client.go +++ b/internal/services/groups/client/client.go @@ -4,35 +4,83 @@ package client import ( + administrativeunitmemberBeta "github.com/hashicorp/go-azure-sdk/microsoft-graph/directory/beta/administrativeunitmember" + "github.com/hashicorp/go-azure-sdk/microsoft-graph/directoryobjects/stable/directoryobject" + groupBeta "github.com/hashicorp/go-azure-sdk/microsoft-graph/groups/beta/group" + memberBeta "github.com/hashicorp/go-azure-sdk/microsoft-graph/groups/beta/member" + memberofBeta "github.com/hashicorp/go-azure-sdk/microsoft-graph/groups/beta/memberof" + ownerBeta "github.com/hashicorp/go-azure-sdk/microsoft-graph/groups/beta/owner" + transitivememberBeta "github.com/hashicorp/go-azure-sdk/microsoft-graph/groups/beta/transitivemember" "github.com/hashicorp/terraform-provider-azuread/internal/common" - "github.com/manicminer/hamilton/msgraph" ) +// Note: Whilst it is technically possible that we could use both the Stable and Beta APIs for groups (retaining use of +// Beta APIs solely for those properties that require it), we are currently using the Beta APIs pretty much across the +// board owing to the complexity of the azuread_group resource, and known bugs when retrieving members with the Stable API. + type Client struct { - AdministrativeUnitsClient *msgraph.AdministrativeUnitsClient - DirectoryObjectsClient *msgraph.DirectoryObjectsClient - GroupsClient *msgraph.GroupsClient + AdministrativeUnitMemberClientBeta *administrativeunitmemberBeta.AdministrativeUnitMemberClient + DirectoryObjectClient *directoryobject.DirectoryObjectClient + GroupClientBeta *groupBeta.GroupClient + GroupMemberClientBeta *memberBeta.MemberClient + GroupMemberOfClientBeta *memberofBeta.MemberOfClient + GroupOwnerClientBeta *ownerBeta.OwnerClient + GroupTransitiveMemberClientBeta *transitivememberBeta.TransitiveMemberClient } -func NewClient(o *common.ClientOptions) *Client { - administrativeUnitsClient := msgraph.NewAdministrativeUnitsClient() - o.ConfigureClient(&administrativeUnitsClient.BaseClient) +func NewClient(o *common.ClientOptions) (*Client, error) { + administrativeUnitMemberClientBeta, err := administrativeunitmemberBeta.NewAdministrativeUnitMemberClientWithBaseURI(o.Environment.MicrosoftGraph) + if err != nil { + return nil, err + } + o.Configure(administrativeUnitMemberClientBeta.Client) + + directoryObjectClient, err := directoryobject.NewDirectoryObjectClientWithBaseURI(o.Environment.MicrosoftGraph) + if err != nil { + return nil, err + } + o.Configure(directoryObjectClient.Client) + + // resourceBehaviorOptions & resourceProvisioningOptions fields not supported in v1.0 API + groupClientBeta, err := groupBeta.NewGroupClientWithBaseURI(o.Environment.MicrosoftGraph) + if err != nil { + return nil, err + } + o.Configure(groupClientBeta.Client) - // SDK uses wrong endpoint for v1.0 API, see https://github.com/manicminer/hamilton/issues/222 - administrativeUnitsClient.BaseClient.ApiVersion = msgraph.VersionBeta + // Group members not returned in full when using v1.0 API, see https://github.com/hashicorp/terraform-provider-azuread/issues/1018 + memberClientBeta, err := memberBeta.NewMemberClientWithBaseURI(o.Environment.MicrosoftGraph) + if err != nil { + return nil, err + } + o.Configure(memberClientBeta.Client) - directoryObjectsClient := msgraph.NewDirectoryObjectsClient() - o.ConfigureClient(&directoryObjectsClient.BaseClient) + memberOfClientBeta, err := memberofBeta.NewMemberOfClientWithBaseURI(o.Environment.MicrosoftGraph) + if err != nil { + return nil, err + } + o.Configure(memberOfClientBeta.Client) - groupsClient := msgraph.NewGroupsClient() - o.ConfigureClient(&groupsClient.BaseClient) + ownerClientBeta, err := ownerBeta.NewOwnerClientWithBaseURI(o.Environment.MicrosoftGraph) + if err != nil { + return nil, err + } + o.Configure(ownerClientBeta.Client) // Group members not returned in full when using v1.0 API, see https://github.com/hashicorp/terraform-provider-azuread/issues/1018 - groupsClient.BaseClient.ApiVersion = msgraph.VersionBeta + transitiveMemberClientBeta, err := transitivememberBeta.NewTransitiveMemberClientWithBaseURI(o.Environment.MicrosoftGraph) + if err != nil { + return nil, err + } + o.Configure(transitiveMemberClientBeta.Client) return &Client{ - AdministrativeUnitsClient: administrativeUnitsClient, - DirectoryObjectsClient: directoryObjectsClient, - GroupsClient: groupsClient, - } + AdministrativeUnitMemberClientBeta: administrativeUnitMemberClientBeta, + DirectoryObjectClient: directoryObjectClient, + GroupClientBeta: groupClientBeta, + GroupMemberClientBeta: memberClientBeta, + GroupMemberOfClientBeta: memberOfClientBeta, + GroupOwnerClientBeta: ownerClientBeta, + GroupTransitiveMemberClientBeta: transitiveMemberClientBeta, + }, nil } diff --git a/internal/services/groups/constants.go b/internal/services/groups/constants.go new file mode 100644 index 0000000000..289d2816d6 --- /dev/null +++ b/internal/services/groups/constants.go @@ -0,0 +1,79 @@ +package groups + +const ( + groupResourceName = "azuread_group" + groupDuplicateValueError = "Request contains a property with duplicate values" +) + +const ( + GroupTypeDynamicMembership = "DynamicMembership" + GroupTypeUnified = "Unified" +) + +var possibleValuesForGroupType = []string{GroupTypeDynamicMembership, GroupTypeUnified} + +const ( + GroupResourceBehaviorOptionAllowOnlyMembersToPost = "AllowOnlyMembersToPost" + GroupResourceBehaviorOptionCalendarMemberReadOnly = "CalendarMemberReadOnly" + GroupResourceBehaviorOptionConnectorsDisabled = "ConnectorsDisabled" + GroupResourceBehaviorOptionHideGroupInOutlook = "HideGroupInOutlook" + GroupResourceBehaviorOptionSkipExchangeInstantOn = "SkipExchangeInstantOn" + GroupResourceBehaviorOptionSubscribeMembersToCalendarEventsDisabled = "SubscribeMembersToCalendarEventsDisabled" + GroupResourceBehaviorOptionSubscribeNewGroupMembers = "SubscribeNewGroupMembers" + GroupResourceBehaviorOptionWelcomeEmailDisabled = "WelcomeEmailDisabled" +) + +var possibleValuesForGroupResourceBehaviorOptions = []string{ + GroupResourceBehaviorOptionAllowOnlyMembersToPost, + GroupResourceBehaviorOptionCalendarMemberReadOnly, + GroupResourceBehaviorOptionConnectorsDisabled, + GroupResourceBehaviorOptionHideGroupInOutlook, + GroupResourceBehaviorOptionSkipExchangeInstantOn, + GroupResourceBehaviorOptionSubscribeMembersToCalendarEventsDisabled, + GroupResourceBehaviorOptionSubscribeNewGroupMembers, + GroupResourceBehaviorOptionWelcomeEmailDisabled, +} + +const ( + GroupResourceProvisioningOptionTeam = "Team" +) + +var possibleValuesForGroupResourceProvisioningOptions = []string{GroupResourceProvisioningOptionTeam} + +const ( + GroupThemeNone = "" + GroupThemeBlue = "Blue" + GroupThemeGreen = "Green" + GroupThemeOrange = "Orange" + GroupThemePink = "Pink" + GroupThemePurple = "Purple" + GroupThemeRed = "Red" + GroupThemeTeal = "Teal" +) + +var possibleValuesForGroupTheme = []string{ + GroupThemeNone, + GroupThemeBlue, + GroupThemeGreen, + GroupThemeOrange, + GroupThemePink, + GroupThemePurple, + GroupThemeRed, + GroupThemeTeal, +} + +const ( + GroupVisibilityHiddenMembership = "Hiddenmembership" + GroupVisibilityPrivate = "Private" + GroupVisibilityPublic = "Public" +) + +var possibleValuesForGroupVisibility = []string{GroupVisibilityHiddenMembership, GroupVisibilityPrivate, GroupVisibilityPublic} + +const ( + OnPremisesGroupTypeUniversalDistributionGroup = "UniversalDistributionGroup" + OnPremisesGroupTypeUniversalMailEnabledSecurityGroup = "UniversalMailEnabledSecurityGroup" + OnPremisesGroupTypeUniversalSecurityGroup = "UniversalSecurityGroup" +) + +var possibleValuesForOnPremisesGroupType = []string{OnPremisesGroupTypeUniversalDistributionGroup, OnPremisesGroupTypeUniversalMailEnabledSecurityGroup, OnPremisesGroupTypeUniversalSecurityGroup} diff --git a/internal/services/groups/group_data_source.go b/internal/services/groups/group_data_source.go index c36f6f7464..aa47ca6e4d 100644 --- a/internal/services/groups/group_data_source.go +++ b/internal/services/groups/group_data_source.go @@ -7,16 +7,20 @@ import ( "context" "errors" "fmt" - "net/http" + "slices" "time" "github.com/hashicorp/go-azure-helpers/lang/pointer" - "github.com/hashicorp/go-azure-sdk/sdk/odata" + "github.com/hashicorp/go-azure-helpers/lang/response" + "github.com/hashicorp/go-azure-sdk/microsoft-graph/common-types/beta" + groupBeta "github.com/hashicorp/go-azure-sdk/microsoft-graph/groups/beta/group" + memberBeta "github.com/hashicorp/go-azure-sdk/microsoft-graph/groups/beta/member" + ownerBeta "github.com/hashicorp/go-azure-sdk/microsoft-graph/groups/beta/owner" + transitivememberBeta "github.com/hashicorp/go-azure-sdk/microsoft-graph/groups/beta/transitivemember" "github.com/hashicorp/terraform-provider-azuread/internal/clients" - "github.com/hashicorp/terraform-provider-azuread/internal/tf" - "github.com/hashicorp/terraform-provider-azuread/internal/tf/pluginsdk" - "github.com/hashicorp/terraform-provider-azuread/internal/tf/validation" - "github.com/manicminer/hamilton/msgraph" + "github.com/hashicorp/terraform-provider-azuread/internal/helpers/tf" + "github.com/hashicorp/terraform-provider-azuread/internal/helpers/tf/pluginsdk" + "github.com/hashicorp/terraform-provider-azuread/internal/helpers/tf/validation" ) func groupDataSource() *pluginsdk.Resource { @@ -255,11 +259,12 @@ func groupDataSource() *pluginsdk.Resource { } func groupDataSourceRead(ctx context.Context, d *pluginsdk.ResourceData, meta interface{}) pluginsdk.Diagnostics { - client := meta.(*clients.Client).Groups.GroupsClient - client.BaseClient.DisableRetries = true - defer func() { client.BaseClient.DisableRetries = false }() + client := meta.(*clients.Client).Groups.GroupClientBeta + memberClient := meta.(*clients.Client).Groups.GroupMemberClientBeta + ownerClient := meta.(*clients.Client).Groups.GroupOwnerClientBeta + transitiveMemberClient := meta.(*clients.Client).Groups.GroupTransitiveMemberClientBeta - var group msgraph.Group + var foundGroup beta.Group var displayName string if v, ok := d.GetOk("display_name"); ok { @@ -288,9 +293,17 @@ func groupDataSourceRead(ctx context.Context, d *pluginsdk.ResourceData, meta in filter = fmt.Sprintf("%s and securityEnabled eq %t", filter, *securityEnabled) } - groups, _, err := client.List(ctx, odata.Query{Filter: filter}) + resp, err := client.ListGroups(ctx, groupBeta.ListGroupsOperationOptions{Filter: &filter}) if err != nil { - return tf.ErrorDiagPathF(err, "display_name", "No group found matching specified filter (%s)", filter) + if response.WasNotFound(resp.HttpResponse) { + return tf.ErrorDiagF(err, "No group found matching specified filter (%s)", filter) + } + return tf.ErrorDiagF(err, "Retrieving groups for filter (%s)", filter) + } + + groups := resp.Model + if groups == nil { + return tf.ErrorDiagF(errors.New("model was nil"), "Retrieving groups for filter (%s)", filter) } count := len(*groups) @@ -300,7 +313,8 @@ func groupDataSourceRead(ctx context.Context, d *pluginsdk.ResourceData, meta in return tf.ErrorDiagPathF(err, "display_name", "No group found matching specified filter (%s)", filter) } - group = (*groups)[0] + foundGroup = (*groups)[0] + } else if mailNickname != "" { filter := fmt.Sprintf("mailNickname eq '%s'", mailNickname) if mailEnabled != nil { @@ -310,9 +324,17 @@ func groupDataSourceRead(ctx context.Context, d *pluginsdk.ResourceData, meta in filter = fmt.Sprintf("%s and securityEnabled eq %t", filter, *securityEnabled) } - groups, _, err := client.List(ctx, odata.Query{Filter: filter}) + resp, err := client.ListGroups(ctx, groupBeta.ListGroupsOperationOptions{Filter: &filter}) if err != nil { - return tf.ErrorDiagPathF(err, "mail_nickname", "No group found matching specified filter (%s)", filter) + if response.WasNotFound(resp.HttpResponse) { + return tf.ErrorDiagF(err, "No group found matching specified filter (%s)", filter) + } + return tf.ErrorDiagF(err, "Retrieving groups for filter (%s)", filter) + } + + groups := resp.Model + if groups == nil { + return tf.ErrorDiagF(errors.New("model was nil"), "Retrieving groups for filter (%s)", filter) } count := len(*groups) @@ -322,105 +344,108 @@ func groupDataSourceRead(ctx context.Context, d *pluginsdk.ResourceData, meta in return tf.ErrorDiagPathF(err, "mail_nickname", "No group found matching specified filter (%s)", filter) } - group = (*groups)[0] + foundGroup = (*groups)[0] + } else if objectId, ok := d.Get("object_id").(string); ok && objectId != "" { - g, status, err := client.Get(ctx, objectId, odata.Query{}) + resp, err := client.GetGroup(ctx, beta.NewGroupID(objectId), groupBeta.DefaultGetGroupOperationOptions()) if err != nil { - if status == http.StatusNotFound { + if response.WasNotFound(resp.HttpResponse) { return tf.ErrorDiagPathF(nil, "object_id", "No group found with object ID: %q", objectId) } return tf.ErrorDiagF(err, "Retrieving group with object ID: %q", objectId) } - if g == nil { + + groupResult := resp.Model + if groupResult == nil { return tf.ErrorDiagPathF(nil, "object_id", "Group not found with object ID: %q", objectId) } - if mailEnabled != nil && (g.MailEnabled == nil || *g.MailEnabled != *mailEnabled) { + if mailEnabled != nil && groupResult.MailEnabled.GetOrZero() != *mailEnabled { var actual string - if g.MailEnabled == nil { + if groupResult.MailEnabled == nil { actual = "nil" } else { - actual = fmt.Sprintf("%t", *g.MailEnabled) + actual = fmt.Sprintf("%t", groupResult.MailEnabled.GetOrZero()) } return tf.ErrorDiagPathF(nil, "mail_enabled", "Group with object ID %q does not have the specified mail_enabled setting (expected: %t, actual: %s)", objectId, *mailEnabled, actual) } - if securityEnabled != nil && (g.SecurityEnabled == nil || *g.SecurityEnabled != *securityEnabled) { + if securityEnabled != nil && groupResult.SecurityEnabled.GetOrZero() != *securityEnabled { var actual string - if g.SecurityEnabled == nil { + if groupResult.SecurityEnabled == nil { actual = "nil" } else { - actual = fmt.Sprintf("%t", *g.SecurityEnabled) + actual = fmt.Sprintf("%t", groupResult.SecurityEnabled.GetOrZero()) } return tf.ErrorDiagPathF(nil, "security_enabled", "Group with object ID %q does not have the specified security_enabled setting (expected: %t, actual: %s)", objectId, *securityEnabled, actual) } - group = *g + foundGroup = *groupResult } - if group.ID() == nil { + if foundGroup.Id == nil { return tf.ErrorDiagF(errors.New("API returned group with nil object ID"), "Bad API Response") } - d.SetId(*group.ID()) - - tf.Set(d, "assignable_to_role", group.IsAssignableToRole) - tf.Set(d, "behaviors", tf.FlattenStringSlicePtr(group.ResourceBehaviorOptions)) - tf.Set(d, "description", group.Description) - tf.Set(d, "display_name", group.DisplayName) - tf.Set(d, "mail", group.Mail) - tf.Set(d, "mail_enabled", group.MailEnabled) - tf.Set(d, "mail_nickname", group.MailNickname) - tf.Set(d, "object_id", group.ID()) - tf.Set(d, "onpremises_domain_name", group.OnPremisesDomainName) - tf.Set(d, "onpremises_netbios_name", group.OnPremisesNetBiosName) - tf.Set(d, "onpremises_sam_account_name", group.OnPremisesSamAccountName) - tf.Set(d, "onpremises_security_identifier", group.OnPremisesSecurityIdentifier) - tf.Set(d, "onpremises_sync_enabled", group.OnPremisesSyncEnabled) - tf.Set(d, "preferred_language", group.PreferredLanguage) - tf.Set(d, "provisioning_options", tf.FlattenStringSlicePtr(group.ResourceProvisioningOptions)) - tf.Set(d, "proxy_addresses", tf.FlattenStringSlicePtr(group.ProxyAddresses)) - tf.Set(d, "security_enabled", group.SecurityEnabled) - tf.Set(d, "theme", group.Theme) - tf.Set(d, "types", group.GroupTypes) - tf.Set(d, "visibility", group.Visibility) + d.SetId(*foundGroup.Id) + + tf.Set(d, "assignable_to_role", foundGroup.IsAssignableToRole.GetOrZero()) + tf.Set(d, "behaviors", tf.FlattenStringSlicePtr(foundGroup.ResourceBehaviorOptions)) + tf.Set(d, "description", foundGroup.Description.GetOrZero()) + tf.Set(d, "display_name", foundGroup.DisplayName.GetOrZero()) + tf.Set(d, "mail", foundGroup.Mail.GetOrZero()) + tf.Set(d, "mail_enabled", foundGroup.MailEnabled.GetOrZero()) + tf.Set(d, "mail_nickname", foundGroup.MailNickname.GetOrZero()) + tf.Set(d, "object_id", foundGroup.Id) + tf.Set(d, "onpremises_domain_name", foundGroup.OnPremisesDomainName.GetOrZero()) + tf.Set(d, "onpremises_netbios_name", foundGroup.OnPremisesNetBiosName.GetOrZero()) + tf.Set(d, "onpremises_sam_account_name", foundGroup.OnPremisesSamAccountName.GetOrZero()) + tf.Set(d, "onpremises_security_identifier", foundGroup.OnPremisesSecurityIdentifier.GetOrZero()) + tf.Set(d, "onpremises_sync_enabled", foundGroup.OnPremisesSyncEnabled.GetOrZero()) + tf.Set(d, "preferred_language", foundGroup.PreferredLanguage.GetOrZero()) + tf.Set(d, "provisioning_options", tf.FlattenStringSlicePtr(foundGroup.ResourceProvisioningOptions)) + tf.Set(d, "proxy_addresses", tf.FlattenStringSlicePtr(foundGroup.ProxyAddresses)) + tf.Set(d, "security_enabled", foundGroup.SecurityEnabled.GetOrZero()) + tf.Set(d, "theme", foundGroup.Theme.GetOrZero()) + tf.Set(d, "types", pointer.From(foundGroup.GroupTypes)) + tf.Set(d, "visibility", foundGroup.Visibility.GetOrZero()) dynamicMembership := make([]interface{}, 0) - if group.MembershipRule != nil { + if foundGroup.MembershipRule != nil { enabled := true - if group.MembershipRuleProcessingState != nil && *group.MembershipRuleProcessingState == "Paused" { + if foundGroup.MembershipRuleProcessingState != nil && foundGroup.MembershipRuleProcessingState.GetOrZero() == "Paused" { enabled = false } dynamicMembership = append(dynamicMembership, map[string]interface{}{ "enabled": enabled, - "rule": group.MembershipRule, + "rule": foundGroup.MembershipRule, }) } tf.Set(d, "dynamic_membership", dynamicMembership) - if group.WritebackConfiguration != nil { - tf.Set(d, "writeback_enabled", group.WritebackConfiguration.IsEnabled) - tf.Set(d, "onpremises_group_type", group.WritebackConfiguration.OnPremisesGroupType) + if foundGroup.WritebackConfiguration != nil { + tf.Set(d, "writeback_enabled", foundGroup.WritebackConfiguration.IsEnabled) + tf.Set(d, "onpremises_group_type", foundGroup.WritebackConfiguration.OnPremisesGroupType) } var allowExternalSenders, autoSubscribeNewMembers, hideFromAddressLists, hideFromOutlookClients bool - if group.GroupTypes != nil && hasGroupType(*group.GroupTypes, msgraph.GroupTypeUnified) { - groupExtra, err := groupGetAdditional(ctx, client, d.Id()) + if foundGroup.GroupTypes != nil && slices.Contains(*foundGroup.GroupTypes, GroupTypeUnified) { + groupExtra, err := groupGetAdditional(ctx, client, beta.NewGroupID(d.Id())) if err != nil { return tf.ErrorDiagF(err, "Could not retrieve group with object ID %q", d.Id()) } if groupExtra != nil { if groupExtra.AllowExternalSenders != nil { - allowExternalSenders = *groupExtra.AllowExternalSenders + allowExternalSenders = groupExtra.AllowExternalSenders.GetOrZero() } if groupExtra.AutoSubscribeNewMembers != nil { - autoSubscribeNewMembers = *groupExtra.AutoSubscribeNewMembers + autoSubscribeNewMembers = groupExtra.AutoSubscribeNewMembers.GetOrZero() } if groupExtra.HideFromAddressLists != nil { - hideFromAddressLists = *groupExtra.HideFromAddressLists + hideFromAddressLists = groupExtra.HideFromAddressLists.GetOrZero() } if groupExtra.HideFromOutlookClients != nil { - hideFromOutlookClients = *groupExtra.HideFromOutlookClients + hideFromOutlookClients = groupExtra.HideFromOutlookClients.GetOrZero() } } } @@ -432,24 +457,41 @@ func groupDataSourceRead(ctx context.Context, d *pluginsdk.ResourceData, meta in includeTransitiveMembers := d.Get("include_transitive_members").(bool) var members *[]string - var err error if includeTransitiveMembers { - members, _, err = client.ListTransitiveMembers(ctx, d.Id()) + resp, err := transitiveMemberClient.ListTransitiveMembers(ctx, beta.NewGroupID(d.Id()), transitivememberBeta.DefaultListTransitiveMembersOperationOptions()) if err != nil { return tf.ErrorDiagF(err, "Could not retrieve transitive group members for group with object ID: %q", d.Id()) } + if resp.Model != nil { + transitiveMembers := make([]string, 0) + for _, object := range *resp.Model { + transitiveMembers = append(transitiveMembers, pointer.From(object.DirectoryObject().Id)) + } + members = &transitiveMembers + } } else { - members, _, err = client.ListMembers(ctx, d.Id()) + resp, err := memberClient.ListMembers(ctx, beta.NewGroupID(d.Id()), memberBeta.DefaultListMembersOperationOptions()) if err != nil { return tf.ErrorDiagF(err, "Could not retrieve group members for group with object ID: %q", d.Id()) } + if resp.Model != nil { + directMembers := make([]string, 0) + for _, object := range *resp.Model { + directMembers = append(directMembers, pointer.From(object.DirectoryObject().Id)) + } + members = &directMembers + } } tf.Set(d, "members", members) - owners, _, err := client.ListOwners(ctx, d.Id()) + resp, err := ownerClient.ListOwners(ctx, beta.NewGroupID(d.Id()), ownerBeta.DefaultListOwnersOperationOptions()) if err != nil { return tf.ErrorDiagF(err, "Could not retrieve group owners for group with object ID: %q", d.Id()) } + owners := make([]string, 0) + for _, object := range *resp.Model { + owners = append(owners, pointer.From(object.DirectoryObject().Id)) + } tf.Set(d, "owners", owners) return nil diff --git a/internal/services/groups/group_member_resource.go b/internal/services/groups/group_member_resource.go index d41d342195..d303008e47 100644 --- a/internal/services/groups/group_member_resource.go +++ b/internal/services/groups/group_member_resource.go @@ -5,22 +5,21 @@ package groups import ( "context" - "errors" - "fmt" "log" - "net/http" "strings" "time" "github.com/hashicorp/go-azure-helpers/lang/pointer" - "github.com/hashicorp/go-azure-sdk/sdk/odata" + "github.com/hashicorp/go-azure-helpers/lang/response" + "github.com/hashicorp/go-azure-sdk/microsoft-graph/common-types/beta" + groupBeta "github.com/hashicorp/go-azure-sdk/microsoft-graph/groups/beta/group" + memberBeta "github.com/hashicorp/go-azure-sdk/microsoft-graph/groups/beta/member" "github.com/hashicorp/terraform-provider-azuread/internal/clients" - "github.com/hashicorp/terraform-provider-azuread/internal/helpers" + "github.com/hashicorp/terraform-provider-azuread/internal/helpers/consistency" + "github.com/hashicorp/terraform-provider-azuread/internal/helpers/tf" + "github.com/hashicorp/terraform-provider-azuread/internal/helpers/tf/pluginsdk" + "github.com/hashicorp/terraform-provider-azuread/internal/helpers/tf/validation" "github.com/hashicorp/terraform-provider-azuread/internal/services/groups/parse" - "github.com/hashicorp/terraform-provider-azuread/internal/tf" - "github.com/hashicorp/terraform-provider-azuread/internal/tf/pluginsdk" - "github.com/hashicorp/terraform-provider-azuread/internal/tf/validation" - "github.com/manicminer/hamilton/msgraph" ) func groupMemberResource() *pluginsdk.Resource { @@ -61,125 +60,101 @@ func groupMemberResource() *pluginsdk.Resource { } func groupMemberResourceCreate(ctx context.Context, d *pluginsdk.ResourceData, meta interface{}) pluginsdk.Diagnostics { - client := meta.(*clients.Client).Groups.GroupsClient - directoryObjectsClient := meta.(*clients.Client).Groups.DirectoryObjectsClient - tenantId := meta.(*clients.Client).TenantID - groupId := d.Get("group_object_id").(string) - memberId := d.Get("member_object_id").(string) + client := meta.(*clients.Client).Groups.GroupClientBeta + memberClient := meta.(*clients.Client).Groups.GroupMemberClientBeta - id := parse.NewGroupMemberID(groupId, memberId) + id := beta.NewGroupIdMemberID(d.Get("group_object_id").(string), d.Get("member_object_id").(string)) + groupId := beta.NewGroupID(id.GroupId) + resourceId := parse.NewGroupMemberID(id.GroupId, id.DirectoryObjectId) tf.LockByName(groupResourceName, id.GroupId) defer tf.UnlockByName(groupResourceName, id.GroupId) - group, status, err := client.Get(ctx, groupId, odata.Query{}) - if err != nil { - if status == http.StatusNotFound { - return tf.ErrorDiagPathF(nil, "object_id", "Group with object ID %q was not found", groupId) + if resp, err := client.GetGroup(ctx, groupId, groupBeta.DefaultGetGroupOperationOptions()); err != nil { + if response.WasNotFound(resp.HttpResponse) { + return tf.ErrorDiagPathF(nil, "object_id", "%s was not found", groupId) } - return tf.ErrorDiagPathF(err, "object_id", "Retrieving group with object ID: %q", groupId) + return tf.ErrorDiagPathF(err, "object_id", "Retrieving %s", groupId) } - existingMembers, _, err := client.ListMembers(ctx, id.GroupId) + resp, err := memberClient.ListMembers(ctx, groupId, memberBeta.DefaultListMembersOperationOptions()) if err != nil { - return tf.ErrorDiagF(err, "Listing existing members for group with object ID: %q", id.GroupId) + return tf.ErrorDiagF(err, "Listing existing members for %s", groupId) } + + existingMembers := resp.Model if existingMembers != nil { for _, v := range *existingMembers { - if strings.EqualFold(v, memberId) { - return tf.ImportAsExistsDiag("azuread_group_member", id.String()) + if strings.EqualFold(pointer.From(v.DirectoryObject().Id), id.DirectoryObjectId) { + return tf.ImportAsExistsDiag("azuread_group_member", resourceId.String()) } } } - memberObject, _, err := directoryObjectsClient.Get(ctx, memberId, odata.Query{}) - if err != nil { - return tf.ErrorDiagF(err, "Could not retrieve principal object %q", memberId) - } - if memberObject == nil { - return tf.ErrorDiagF(errors.New("returned memberObject was nil"), "Could not retrieve member principal object %q", memberId) - } - memberObject.ODataId = (*odata.Id)(pointer.To(fmt.Sprintf("%s/v1.0/%s/directoryObjects/%s", - client.BaseClient.Endpoint, tenantId, memberId))) + memberId := beta.NewDirectoryObjectID(id.DirectoryObjectId) - group.Members = &msgraph.Members{*memberObject} + memberRef := beta.ReferenceCreate{ + ODataId: pointer.To(client.Client.BaseUri + memberId.ID()), + } - if _, err := client.AddMembers(ctx, group); err != nil { - return tf.ErrorDiagF(err, "Adding group member %q to group %q", memberId, groupId) + if _, err = memberClient.AddMemberRef(ctx, groupId, memberRef, memberBeta.DefaultAddMemberRefOperationOptions()); err != nil { + return tf.ErrorDiagF(err, "Adding %s", id) } - d.SetId(id.String()) + d.SetId(resourceId.String()) + return groupMemberResourceRead(ctx, d, meta) } func groupMemberResourceRead(ctx context.Context, d *pluginsdk.ResourceData, meta interface{}) pluginsdk.Diagnostics { - client := meta.(*clients.Client).Groups.GroupsClient + client := meta.(*clients.Client).Groups.GroupMemberClientBeta - id, err := parse.GroupMemberID(d.Id()) + resourceId, err := parse.GroupMemberID(d.Id()) if err != nil { return tf.ErrorDiagPathF(err, "id", "Parsing Group Member ID %q", d.Id()) } + id := beta.NewGroupIdMemberID(resourceId.GroupId, resourceId.MemberId) - members, status, err := client.ListMembers(ctx, id.GroupId) - if err != nil { - if status == http.StatusNotFound { - log.Printf("[DEBUG] Group with ID %q was not found - removing group member with ID %q from state", id.GroupId, d.Id()) - d.SetId("") - return nil - } - return tf.ErrorDiagF(err, "Retrieving members for group with object ID: %q", id.GroupId) - } - - var memberObjectId string - if members != nil { - for _, objectId := range *members { - if strings.EqualFold(objectId, id.MemberId) { - memberObjectId = objectId - break - } - } - } - - if memberObjectId == "" { - log.Printf("[DEBUG] Member with ID %q was not found in Group %q - removing from state", id.MemberId, id.GroupId) + if member, err := groupGetMember(ctx, client, id); err != nil { + return tf.ErrorDiagF(err, "Retrieving member %q for group with object ID: %q", id.DirectoryObjectId, id.GroupId) + } else if member == nil { + log.Printf("[DEBUG] %s - removing from state", id) d.SetId("") return nil } tf.Set(d, "group_object_id", id.GroupId) - tf.Set(d, "member_object_id", memberObjectId) + tf.Set(d, "member_object_id", id.DirectoryObjectId) return nil } func groupMemberResourceDelete(ctx context.Context, d *pluginsdk.ResourceData, meta interface{}) pluginsdk.Diagnostics { - client := meta.(*clients.Client).Groups.GroupsClient + client := meta.(*clients.Client).Groups.GroupMemberClientBeta - id, err := parse.GroupMemberID(d.Id()) + resourceId, err := parse.GroupMemberID(d.Id()) if err != nil { return tf.ErrorDiagPathF(err, "id", "Parsing Group Member ID %q", d.Id()) } + id := beta.NewGroupIdMemberID(resourceId.GroupId, resourceId.MemberId) tf.LockByName(groupResourceName, id.GroupId) defer tf.UnlockByName(groupResourceName, id.GroupId) - if _, err := client.RemoveMembers(ctx, id.GroupId, &[]string{id.MemberId}); err != nil { - return tf.ErrorDiagF(err, "Removing member %q from group with object ID: %q", id.MemberId, id.GroupId) + if _, err := client.RemoveMemberRef(ctx, id, memberBeta.DefaultRemoveMemberRefOperationOptions()); err != nil { + return tf.ErrorDiagF(err, "Removing %s", id) } // Wait for membership link to be deleted - if err := helpers.WaitForDeletion(ctx, func(ctx context.Context) (*bool, error) { - defer func() { client.BaseClient.DisableRetries = false }() - client.BaseClient.DisableRetries = true - if _, status, err := client.GetMember(ctx, id.GroupId, id.MemberId); err != nil { - if status == http.StatusNotFound { - return pointer.To(false), nil - } + if err := consistency.WaitForDeletion(ctx, func(ctx context.Context) (*bool, error) { + if member, err := groupGetMember(ctx, client, id); err != nil { return nil, err + } else if member == nil { + return pointer.To(false), nil } return pointer.To(true), nil }); err != nil { - return tf.ErrorDiagF(err, "Waiting for removal of member %q from group with object ID %q", id.MemberId, id.GroupId) + return tf.ErrorDiagF(err, "Waiting for removal of %s", id) } return nil diff --git a/internal/services/groups/group_member_resource_test.go b/internal/services/groups/group_member_resource_test.go index cda6849ce4..8aa78d4fd4 100644 --- a/internal/services/groups/group_member_resource_test.go +++ b/internal/services/groups/group_member_resource_test.go @@ -6,10 +6,12 @@ package groups_test import ( "context" "fmt" - "strings" "testing" "github.com/hashicorp/go-azure-helpers/lang/pointer" + "github.com/hashicorp/go-azure-helpers/lang/response" + "github.com/hashicorp/go-azure-sdk/microsoft-graph/common-types/beta" + memberBeta "github.com/hashicorp/go-azure-sdk/microsoft-graph/groups/beta/member" "github.com/hashicorp/terraform-plugin-testing/terraform" "github.com/hashicorp/terraform-provider-azuread/internal/acceptance" "github.com/hashicorp/terraform-provider-azuread/internal/acceptance/check" @@ -138,29 +140,33 @@ func TestAccGroupMember_requiresImport(t *testing.T) { } func (r GroupMemberResource) Exists(ctx context.Context, clients *clients.Client, state *terraform.InstanceState) (*bool, error) { - client := clients.Groups.GroupsClient - client.BaseClient.DisableRetries = true - defer func() { client.BaseClient.DisableRetries = false }() + client := clients.Groups.GroupMemberClientBeta id, err := parse.GroupMemberID(state.ID) if err != nil { return nil, fmt.Errorf("parsing Group Member ID: %v", err) } - members, _, err := client.ListMembers(ctx, id.GroupId) + options := memberBeta.ListMembersOperationOptions{ + Filter: pointer.To(fmt.Sprintf("id eq '%s'", id.MemberId)), + } + resp, err := client.ListMembers(ctx, beta.NewGroupID(id.GroupId), options) if err != nil { - return nil, fmt.Errorf("failed to retrieve Group members (groupId: %q): %+v", id.GroupId, err) + if response.WasNotFound(resp.HttpResponse) { + return pointer.To(false), nil + } + return nil, fmt.Errorf("failed to retrieve group member %q (group ID: %q): %+v", id.MemberId, id.GroupId, err) } - if members != nil { - for _, objectId := range *members { - if strings.EqualFold(objectId, id.MemberId) { + if resp.Model != nil { + for _, member := range *resp.Model { + if pointer.From(member.DirectoryObject().Id) == id.MemberId { return pointer.To(true), nil } } } - return nil, fmt.Errorf("Member %q was not found in Group %q", id.MemberId, id.GroupId) + return pointer.To(false), nil } func (GroupMemberResource) template(data acceptance.TestData) string { diff --git a/internal/services/groups/group_resource.go b/internal/services/groups/group_resource.go index 64fdf01de8..d891ab68d5 100644 --- a/internal/services/groups/group_resource.go +++ b/internal/services/groups/group_resource.go @@ -9,30 +9,36 @@ import ( "fmt" "log" "net/http" + "reflect" "regexp" + "slices" "strings" "time" "github.com/hashicorp/go-azure-helpers/lang/pointer" + "github.com/hashicorp/go-azure-helpers/lang/response" + "github.com/hashicorp/go-azure-sdk/microsoft-graph/common-types/beta" + "github.com/hashicorp/go-azure-sdk/microsoft-graph/common-types/stable" + administrativeunitmemberBeta "github.com/hashicorp/go-azure-sdk/microsoft-graph/directory/beta/administrativeunitmember" + "github.com/hashicorp/go-azure-sdk/microsoft-graph/directoryobjects/stable/directoryobject" + groupBeta "github.com/hashicorp/go-azure-sdk/microsoft-graph/groups/beta/group" + memberBeta "github.com/hashicorp/go-azure-sdk/microsoft-graph/groups/beta/member" + memberofBeta "github.com/hashicorp/go-azure-sdk/microsoft-graph/groups/beta/memberof" + ownerBeta "github.com/hashicorp/go-azure-sdk/microsoft-graph/groups/beta/owner" + "github.com/hashicorp/go-azure-sdk/sdk/nullable" "github.com/hashicorp/go-azure-sdk/sdk/odata" "github.com/hashicorp/go-uuid" "github.com/hashicorp/terraform-provider-azuread/internal/clients" - "github.com/hashicorp/terraform-provider-azuread/internal/helpers" - "github.com/hashicorp/terraform-provider-azuread/internal/tf" - "github.com/hashicorp/terraform-provider-azuread/internal/tf/pluginsdk" - "github.com/hashicorp/terraform-provider-azuread/internal/tf/validation" - "github.com/manicminer/hamilton/msgraph" -) - -const ( - groupResourceName = "azuread_group" - groupDuplicateValueError = "Request contains a property with duplicate values" + "github.com/hashicorp/terraform-provider-azuread/internal/helpers/consistency" + "github.com/hashicorp/terraform-provider-azuread/internal/helpers/tf" + "github.com/hashicorp/terraform-provider-azuread/internal/helpers/tf/pluginsdk" + "github.com/hashicorp/terraform-provider-azuread/internal/helpers/tf/validation" ) func groupResource() *pluginsdk.Resource { return &pluginsdk.Resource{ CreateContext: groupResourceCreate, - ReadContext: groupResourceRead, + ReadContext: groupResourceReadFunc(false), UpdateContext: groupResourceUpdate, DeleteContext: groupResourceDelete, @@ -54,10 +60,10 @@ func groupResource() *pluginsdk.Resource { Schema: map[string]*pluginsdk.Schema{ "display_name": { - Description: "The display name for the group", - Type: pluginsdk.TypeString, - Required: true, - ValidateDiagFunc: validation.ValidateDiag(validation.StringIsNotEmpty), + Description: "The display name for the group", + Type: pluginsdk.TypeString, + Required: true, + ValidateFunc: validation.StringIsNotEmpty, }, "administrative_unit_ids": { @@ -90,17 +96,8 @@ func groupResource() *pluginsdk.Resource { Optional: true, ForceNew: true, Elem: &pluginsdk.Schema{ - Type: pluginsdk.TypeString, - ValidateFunc: validation.StringInSlice([]string{ - msgraph.GroupResourceBehaviorOptionAllowOnlyMembersToPost, - msgraph.GroupResourceBehaviorOptionCalendarMemberReadOnly, - msgraph.GroupResourceBehaviorOptionConnectorsDisabled, - msgraph.GroupResourceBehaviorOptionHideGroupInOutlook, - msgraph.GroupResourceBehaviorOptionSkipExchangeInstantOn, - msgraph.GroupResourceBehaviorOptionSubscribeMembersToCalendarEventsDisabled, - msgraph.GroupResourceBehaviorOptionSubscribeNewGroupMembers, - msgraph.GroupResourceBehaviorOptionWelcomeEmailDisabled, - }, false), + Type: pluginsdk.TypeString, + ValidateFunc: validation.StringInSlice(possibleValuesForGroupResourceBehaviorOptions, false), }, }, @@ -124,10 +121,10 @@ func groupResource() *pluginsdk.Resource { }, "rule": { - Description: "Rule to determine members for a dynamic group. Required when `group_types` contains 'DynamicMembership'", - Type: pluginsdk.TypeString, - Required: true, - ValidateDiagFunc: validation.ValidateDiag(validation.StringLenBetween(0, 3072)), + Description: "Rule to determine members for a dynamic group. Required when `group_types` contains 'DynamicMembership'", + Type: pluginsdk.TypeString, + Required: true, + ValidateFunc: validation.StringLenBetween(0, 3072), }, }, }, @@ -178,21 +175,17 @@ func groupResource() *pluginsdk.Resource { ConflictsWith: []string{"dynamic_membership"}, Set: pluginsdk.HashString, Elem: &pluginsdk.Schema{ - Type: pluginsdk.TypeString, - ValidateDiagFunc: validation.ValidateDiag(validation.IsUUID), + Type: pluginsdk.TypeString, + ValidateFunc: validation.IsUUID, }, }, "onpremises_group_type": { - Description: "Indicates the target on-premise group type the group will be written back as", - Type: pluginsdk.TypeString, - Optional: true, - Computed: true, - ValidateFunc: validation.StringInSlice([]string{ - msgraph.UniversalDistributionGroup, - msgraph.UniversalSecurityGroup, - msgraph.UniversalMailEnabledSecurityGroup, - }, false), + Description: "Indicates the target on-premise group type the group will be written back as", + Type: pluginsdk.TypeString, + Optional: true, + Computed: true, + ValidateFunc: validation.StringInSlice(possibleValuesForOnPremisesGroupType, false), }, "owners": { @@ -204,8 +197,8 @@ func groupResource() *pluginsdk.Resource { MaxItems: 100, Set: pluginsdk.HashString, Elem: &pluginsdk.Schema{ - Type: pluginsdk.TypeString, - ValidateDiagFunc: validation.ValidateDiag(validation.IsUUID), + Type: pluginsdk.TypeString, + ValidateFunc: validation.IsUUID, }, }, @@ -222,10 +215,8 @@ func groupResource() *pluginsdk.Resource { Optional: true, ForceNew: true, Elem: &pluginsdk.Schema{ - Type: pluginsdk.TypeString, - ValidateFunc: validation.StringInSlice([]string{ - msgraph.GroupResourceProvisioningOptionTeam, - }, false), + Type: pluginsdk.TypeString, + ValidateFunc: validation.StringInSlice(possibleValuesForGroupResourceProvisioningOptions, false), }, }, @@ -237,19 +228,10 @@ func groupResource() *pluginsdk.Resource { }, "theme": { - Description: "The colour theme for a Microsoft 365 group", - Type: pluginsdk.TypeString, - Optional: true, - ValidateFunc: validation.StringInSlice([]string{ - string(msgraph.GroupThemeNone), - string(msgraph.GroupThemeBlue), - string(msgraph.GroupThemeGreen), - string(msgraph.GroupThemeOrange), - string(msgraph.GroupThemePink), - string(msgraph.GroupThemePurple), - string(msgraph.GroupThemeRed), - string(msgraph.GroupThemeTeal), - }, false), + Description: "The colour theme for a Microsoft 365 group", + Type: pluginsdk.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice(possibleValuesForGroupTheme, false), }, "types": { @@ -258,24 +240,17 @@ func groupResource() *pluginsdk.Resource { Optional: true, ForceNew: true, Elem: &pluginsdk.Schema{ - Type: pluginsdk.TypeString, - ValidateFunc: validation.StringInSlice([]string{ - "DynamicMembership", - msgraph.GroupTypeUnified, - }, false), + Type: pluginsdk.TypeString, + ValidateFunc: validation.StringInSlice(possibleValuesForGroupType, false), }, }, "visibility": { - Description: "Specifies the group join policy and group content visibility", - Type: pluginsdk.TypeString, - Optional: true, - Computed: true, - ValidateFunc: validation.StringInSlice([]string{ - msgraph.GroupVisibilityHiddenMembership, - msgraph.GroupVisibilityPrivate, - msgraph.GroupVisibilityPublic, - }, false), + Description: "Specifies the group join policy and group content visibility", + Type: pluginsdk.TypeString, + Optional: true, + Computed: true, + ValidateFunc: validation.StringInSlice(possibleValuesForGroupVisibility, false), }, "writeback_enabled": { @@ -346,7 +321,7 @@ func groupResource() *pluginsdk.Resource { } func groupResourceCustomizeDiff(ctx context.Context, diff *pluginsdk.ResourceDiff, meta interface{}) error { - client := meta.(*clients.Client).Groups.GroupsClient + client := meta.(*clients.Client).Groups.GroupClientBeta // Check for duplicate names oldDisplayName, newDisplayName := diff.GetChange("display_name") @@ -358,11 +333,11 @@ func groupResourceCustomizeDiff(ctx context.Context, diff *pluginsdk.ResourceDif } if result != nil && len(*result) > 0 { for _, existingGroup := range *result { - if existingGroup.ID() == nil { + if existingGroup.Id == nil { return fmt.Errorf("API error: group returned with nil object ID during duplicate name check") } - if diff.Id() == "" || diff.Id() == *existingGroup.ID() { - return tf.ImportAsDuplicateError("azuread_group", *existingGroup.ID(), newDisplayName.(string)) + if diff.Id() == "" || diff.Id() == *existingGroup.Id { + return tf.ImportAsDuplicateError("azuread_group", *existingGroup.Id, newDisplayName.(string)) } } } @@ -370,20 +345,20 @@ func groupResourceCustomizeDiff(ctx context.Context, diff *pluginsdk.ResourceDif mailEnabled := diff.Get("mail_enabled").(bool) securityEnabled := diff.Get("security_enabled").(bool) - groupTypes := make([]msgraph.GroupType, 0) + groupTypes := make([]string, 0) for _, v := range diff.Get("types").(*pluginsdk.Set).List() { groupTypes = append(groupTypes, v.(string)) } - if hasGroupType(groupTypes, msgraph.GroupTypeDynamicMembership) && diff.Get("dynamic_membership.#").(int) == 0 { - return fmt.Errorf("`dynamic_membership` must be specified when `types` contains %q", msgraph.GroupTypeDynamicMembership) + if slices.Contains(groupTypes, GroupTypeDynamicMembership) && diff.Get("dynamic_membership.#").(int) == 0 { + return fmt.Errorf("`dynamic_membership` must be specified when `types` contains %q", GroupTypeDynamicMembership) } - if mailEnabled && !hasGroupType(groupTypes, msgraph.GroupTypeUnified) { - return fmt.Errorf("`types` must contain %q for mail-enabled groups", msgraph.GroupTypeUnified) + if mailEnabled && !slices.Contains(groupTypes, GroupTypeUnified) { + return fmt.Errorf("`types` must contain %q for mail-enabled groups", GroupTypeUnified) } - if !mailEnabled && hasGroupType(groupTypes, msgraph.GroupTypeUnified) { + if !mailEnabled && slices.Contains(groupTypes, GroupTypeUnified) { return fmt.Errorf("`mail_enabled` must be true for unified groups") } @@ -397,7 +372,7 @@ func groupResourceCustomizeDiff(ctx context.Context, diff *pluginsdk.ResourceDif visibilityOld, visibilityNew := diff.GetChange("visibility") - if !hasGroupType(groupTypes, msgraph.GroupTypeUnified) { + if !slices.Contains(groupTypes, GroupTypeUnified) { if autoSubscribeNewMembers, ok := diff.GetOk("auto_subscribe_new_members"); ok && autoSubscribeNewMembers.(bool) { return fmt.Errorf("`auto_subscribe_new_members` is only supported for unified groups") } @@ -426,13 +401,13 @@ func groupResourceCustomizeDiff(ctx context.Context, diff *pluginsdk.ResourceDif return fmt.Errorf("`theme` is only supported for unified groups") } - if visibilityNew.(string) == msgraph.GroupVisibilityHiddenMembership { - return fmt.Errorf("`visibility` can only be %q for unified groups", msgraph.GroupVisibilityHiddenMembership) + if visibilityNew.(string) == GroupVisibilityHiddenMembership { + return fmt.Errorf("`visibility` can only be %q for unified groups", GroupVisibilityHiddenMembership) } } - if (visibilityOld.(string) == msgraph.GroupVisibilityPrivate || visibilityOld.(string) == msgraph.GroupVisibilityPublic) && - visibilityNew.(string) == msgraph.GroupVisibilityHiddenMembership { + if (visibilityOld.(string) == GroupVisibilityPrivate || visibilityOld.(string) == GroupVisibilityPublic) && + visibilityNew.(string) == GroupVisibilityHiddenMembership { diff.ForceNew("visibility") } @@ -440,11 +415,14 @@ func groupResourceCustomizeDiff(ctx context.Context, diff *pluginsdk.ResourceDif } func groupResourceCreate(ctx context.Context, d *pluginsdk.ResourceData, meta interface{}) pluginsdk.Diagnostics { - client := meta.(*clients.Client).Groups.GroupsClient - directoryObjectsClient := meta.(*clients.Client).Groups.DirectoryObjectsClient - administrativeUnitsClient := meta.(*clients.Client).Groups.AdministrativeUnitsClient + client := meta.(*clients.Client).Groups.GroupClientBeta + ownerClient := meta.(*clients.Client).Groups.GroupOwnerClientBeta + memberClient := meta.(*clients.Client).Groups.GroupMemberClientBeta + directoryObjectClient := meta.(*clients.Client).Groups.DirectoryObjectClient + administrativeUnitMemberClient := meta.(*clients.Client).Groups.AdministrativeUnitMemberClientBeta + callerId := meta.(*clients.Client).ObjectID - tenantId := meta.(*clients.Client).TenantID + callerODataId := fmt.Sprintf("%s%s", client.Client.BaseUri, beta.NewDirectoryObjectID(callerId).ID()) displayName := d.Get("display_name").(string) @@ -456,14 +434,14 @@ func groupResourceCreate(ctx context.Context, d *pluginsdk.ResourceData, meta in } if result != nil && len(*result) > 0 { existingGroup := (*result)[0] - if existingGroup.ID() == nil { + if existingGroup.Id == nil { return tf.ErrorDiagF(errors.New("API returned group with nil object ID during duplicate name check"), "Bad API response") } - return tf.ImportAsDuplicateDiag("azuread_group", *existingGroup.ID(), displayName) + return tf.ImportAsDuplicateDiag("azuread_group", *existingGroup.Id, displayName) } } - groupTypes := make([]msgraph.GroupType, 0) + groupTypes := make([]string, 0) for _, v := range d.Get("types").(*pluginsdk.Set).List() { groupTypes = append(groupTypes, v.(string)) } @@ -477,124 +455,110 @@ func groupResourceCreate(ctx context.Context, d *pluginsdk.ResourceData, meta in mailNickname = v.(string) } - behaviorOptions := make([]msgraph.GroupResourceBehaviorOption, 0) + behaviorOptions := make([]string, 0) for _, v := range d.Get("behaviors").(*pluginsdk.Set).List() { behaviorOptions = append(behaviorOptions, v.(string)) } - provisioningOptions := make([]msgraph.GroupResourceProvisioningOption, 0) + provisioningOptions := make([]string, 0) for _, v := range d.Get("provisioning_options").(*pluginsdk.Set).List() { provisioningOptions = append(provisioningOptions, v.(string)) } - var writebackConfiguration *msgraph.GroupWritebackConfiguration + var writebackConfiguration *beta.GroupWritebackConfiguration if v := d.Get("writeback_enabled").(bool); v { - writebackConfiguration = &msgraph.GroupWritebackConfiguration{ - IsEnabled: pointer.To(d.Get("writeback_enabled").(bool)), + writebackConfiguration = &beta.GroupWritebackConfiguration{ + IsEnabled: nullable.Value(d.Get("writeback_enabled").(bool)), + OmitDiscriminatedValue: true, } if onPremisesGroupType := d.Get("onpremises_group_type").(string); onPremisesGroupType != "" { - writebackConfiguration.OnPremisesGroupType = pointer.To(onPremisesGroupType) + writebackConfiguration.OnPremisesGroupType = nullable.Value(onPremisesGroupType) } } description := d.Get("description").(string) - odataType := odata.TypeGroup - properties := msgraph.Group{ - DirectoryObject: msgraph.DirectoryObject{ - ODataType: &odataType, - }, - Description: tf.NullableString(description), - DisplayName: pointer.To(displayName), + properties := beta.Group{ + Description: nullable.NoZero(description), + DisplayName: nullable.Value(displayName), GroupTypes: &groupTypes, - IsAssignableToRole: pointer.To(d.Get("assignable_to_role").(bool)), - MailEnabled: pointer.To(mailEnabled), - MailNickname: pointer.To(mailNickname), - MembershipRule: tf.NullableString(""), + IsAssignableToRole: nullable.Value(d.Get("assignable_to_role").(bool)), + MailEnabled: nullable.Value(mailEnabled), + MailNickname: nullable.Value(mailNickname), + MembershipRule: nullable.NoZero(""), ResourceBehaviorOptions: &behaviorOptions, ResourceProvisioningOptions: &provisioningOptions, - SecurityEnabled: pointer.To(securityEnabled), + SecurityEnabled: nullable.Value(securityEnabled), WritebackConfiguration: writebackConfiguration, } if v, ok := d.GetOk("dynamic_membership"); ok && len(v.([]interface{})) > 0 { if d.Get("dynamic_membership.0.enabled").(bool) { - properties.MembershipRuleProcessingState = pointer.To("On") + properties.MembershipRuleProcessingState = nullable.Value("On") } else { - properties.MembershipRuleProcessingState = pointer.To("Paused") + properties.MembershipRuleProcessingState = nullable.Value("Paused") } - properties.MembershipRule = tf.NullableString(d.Get("dynamic_membership.0.rule").(string)) + properties.MembershipRule = nullable.Value(d.Get("dynamic_membership.0.rule").(string)) } if theme := d.Get("theme").(string); theme != "" { - properties.Theme = tf.NullableString(theme) + properties.Theme = nullable.Value(theme) } if visibility := d.Get("visibility").(string); visibility != "" { - properties.Visibility = pointer.To(visibility) + properties.Visibility = nullable.Value(visibility) } // Sort the owners into two slices, the first containing up to 20 and the rest overflowing to the second slice - var ownersFirst20, ownersExtra msgraph.Owners - - // getOwnerObject retrieves and validates a DirectoryObject for a given object ID - getOwnerObject := func(ctx context.Context, id string) (*msgraph.DirectoryObject, error) { - ownerObject, _, err := directoryObjectsClient.Get(ctx, id, odata.Query{}) - if err != nil { - return nil, err - } - if ownerObject == nil { - return nil, errors.New("ownerObject was nil") - } - if ownerObject.ID() == nil { - return nil, errors.New("ownerObject ID was nil") - } - ownerObject.ODataId = (*odata.Id)(pointer.To(fmt.Sprintf("%s/v1.0/%s/directoryObjects/%s", - client.BaseClient.Endpoint, tenantId, id))) - - if ownerObject.ODataType == nil { - return nil, errors.New("ownerObject ODataType was nil") - } - return ownerObject, nil - } + var ownersFirst20 []string + var ownersExtra []beta.ReferenceCreate // Retrieve and set the initial owners, which can be up to 20 in total when creating the group. - // First look for the calling principal, then prefer users, followed by service principals, to try and avoid - // ownership-related API validation errors for Microsoft 365 groups. + // First look for the calling principal, then prefer users, followed by service principals, and lastly groups, + // to try and avoid ownership-related API validation errors for Microsoft 365 groups, which require that a User + // be an explicit owner for new groups. if v, ok := d.GetOk("owners"); ok { owners := v.(*pluginsdk.Set).List() ownerCount := 0 - // First look for the calling principal in the specified owners; it should always be included in the initial - // owners to avoid orphaning a group when the caller doesn't have the Groups.ReadWrite.All scope. + // First look for the calling principal in the specified owners; when specified it should always be included in + // the initial owners to avoid orphaning a group when the caller doesn't have the Groups.ReadWrite.All scope. for _, ownerId := range owners { - ownerObject, err := getOwnerObject(ctx, ownerId.(string)) - if err != nil { - return tf.ErrorDiagF(err, "Could not retrieve owner principal object %q", ownerId) - } - if strings.EqualFold(*ownerObject.ID(), callerId) { - if ownerCount < 20 { - ownersFirst20 = append(ownersFirst20, *ownerObject) - } else { - ownersExtra = append(ownersExtra, *ownerObject) - } + if strings.EqualFold(ownerId.(string), callerId) { + ownersFirst20 = append(ownersFirst20, callerODataId) ownerCount++ } } // Then look for users, and finally service principals - for _, t := range []odata.Type{odata.TypeUser, odata.TypeServicePrincipal} { - for _, ownerId := range owners { - ownerObject, err := getOwnerObject(ctx, ownerId.(string)) + for _, t := range []stable.DirectoryObject{stable.User{}, stable.ServicePrincipal{}, stable.Group{}} { + for _, ownerIdRaw := range owners { + ownerId := ownerIdRaw.(string) + + // We already added the caller above + if strings.EqualFold(ownerId, callerId) { + continue + } + + resp, err := directoryObjectClient.GetDirectoryObject(ctx, stable.NewDirectoryObjectID(ownerId), directoryobject.DefaultGetDirectoryObjectOperationOptions()) if err != nil { return tf.ErrorDiagF(err, "Could not retrieve owner principal object %q", ownerId) } - if *ownerObject.ODataType == t && !strings.EqualFold(*ownerObject.ID(), callerId) { + + ownerObject := resp.Model + if ownerObject == nil { + return tf.ErrorDiagF(errors.New("ownerObject model was nil"), "Could not retrieve owner principal object %q", ownerId) + } + + if reflect.TypeOf(ownerObject) == reflect.TypeOf(t) { if ownerCount < 20 { - ownersFirst20 = append(ownersFirst20, *ownerObject) + ownersFirst20 = append(ownersFirst20, fmt.Sprintf("%s%s", client.Client.BaseUri, beta.NewDirectoryObjectID(ownerId).ID())) } else { - ownersExtra = append(ownersExtra, *ownerObject) + ownerRef := beta.ReferenceCreate{ + ODataId: pointer.To(client.Client.BaseUri + beta.NewDirectoryObjectID(ownerId).ID()), + } + ownersExtra = append(ownersExtra, ownerRef) } ownerCount++ } @@ -605,75 +569,117 @@ func groupResourceCreate(ctx context.Context, d *pluginsdk.ResourceData, meta in if len(ownersFirst20) == 0 { // The calling principal is the default owner if no others are specified. This is the default API behaviour, so // we're being explicit about this in order to minimise confusion and avoid inconsistent API behaviours. - callerObject, err := getOwnerObject(ctx, callerId) - if err != nil { - return tf.ErrorDiagF(err, "Could not retrieve calling principal object %q", callerId) - } - ownersFirst20 = msgraph.Owners{*callerObject} + ownersFirst20 = []string{fmt.Sprintf("%s%s", client.Client.BaseUri, beta.NewDirectoryObjectID(callerId).ID())} } // Set the initial owners, which either be the calling principal, or up to 20 of the owners specified in configuration - properties.Owners = &ownersFirst20 + properties.Owners_ODataBind = &ownersFirst20 - var group *msgraph.Group - var status int - var err error + var groupObjectId string if v, ok := d.GetOk("administrative_unit_ids"); ok { administrativeUnitIds := tf.ExpandStringSlice(v.(*pluginsdk.Set).List()) - for i, administrativeUnitId := range administrativeUnitIds { + + for i, auId := range administrativeUnitIds { + administrativeUnitId := beta.NewDirectoryAdministrativeUnitID(auId) + // Create the group in the first administrative unit, as this requires fewer permissions than creating it at tenant level if i == 0 { - group, status, err = administrativeUnitsClient.CreateGroup(ctx, administrativeUnitId, &properties) + resp, err := administrativeUnitMemberClient.CreateAdministrativeUnitMember(ctx, administrativeUnitId, &properties, administrativeunitmemberBeta.DefaultCreateAdministrativeUnitMemberOperationOptions()) if err != nil { - if status == http.StatusBadRequest && regexp.MustCompile(groupDuplicateValueError).MatchString(err.Error()) { - // Retry the request, without the calling principal as owner - newOwners := make(msgraph.Owners, 0) - for _, o := range *properties.Owners { - if id := o.ID(); id != nil && *id != callerId { - newOwners = append(newOwners, o) + if response.WasBadRequest(resp.HttpResponse) && regexp.MustCompile(groupDuplicateValueError).MatchString(err.Error()) { + // Retry the group creation, without the calling principal as owner + ownersWithoutCallingPrincipal := make([]string, 0) + for _, o := range *properties.Owners_ODataBind { + if o != callerODataId { + ownersWithoutCallingPrincipal = append(ownersWithoutCallingPrincipal, o) } } - // No point in retrying if the caller wasn't specified - if len(newOwners) == len(*properties.Owners) { - log.Printf("[DEBUG] Not retrying group creation for %q within AU %q as owner was not specified", displayName, administrativeUnitId) - return tf.ErrorDiagF(err, "Creating group in administrative unit with ID %q, %q", administrativeUnitId, displayName) + // No point in retrying if the caller wasn't specified as an owner + if len(ownersWithoutCallingPrincipal) == len(*properties.Owners) { + log.Printf("[DEBUG] Not retrying group creation for %q within %s as owner was not specified", displayName, administrativeUnitId) + return tf.ErrorDiagF(err, "Creating group in %s", administrativeUnitId) } // If the API is refusing the calling principal as owner, it will typically automatically append the caller in the background, // and subsequent GETs for the group will include the calling principal as owner, as if it were specified when creating. - log.Printf("[DEBUG] Retrying group creation for %q within AU %q without calling principal as owner", displayName, administrativeUnitId) - properties.Owners = &newOwners - group, _, err = administrativeUnitsClient.CreateGroup(ctx, administrativeUnitId, &properties) + log.Printf("[DEBUG] Retrying group creation for %q within %s without calling principal as owner", displayName, administrativeUnitId) + if len(ownersWithoutCallingPrincipal) == 0 { + properties.Owners_ODataBind = nil + } else { + properties.Owners_ODataBind = &ownersWithoutCallingPrincipal + } + + resp, err = administrativeUnitMemberClient.CreateAdministrativeUnitMember(ctx, administrativeUnitId, &properties, administrativeunitmemberBeta.DefaultCreateAdministrativeUnitMemberOperationOptions()) if err != nil { - return tf.ErrorDiagF(err, "Creating group in administrative unit with ID %q, %q", administrativeUnitId, displayName) + return tf.ErrorDiagF(err, "Creating group in %s", administrativeUnitId) } + + if resp.Model == nil { + return tf.ErrorDiagF(errors.New("returned model was nil"), "Creating group in %s", administrativeUnitId) + } + + // Obtain the new group ID + newGroup, ok := resp.Model.(beta.Group) + if !ok { + return tf.ErrorDiagF(errors.New("returned model was not a group"), "Creating group in %s", administrativeUnitId) + } + groupObjectId = pointer.From(newGroup.Id) + } else { - return tf.ErrorDiagF(err, "Creating group in administrative unit with ID %q, %q", administrativeUnitId, displayName) + return tf.ErrorDiagF(err, "Creating group in %s", administrativeUnitId) } } + + if resp.Model == nil { + return tf.ErrorDiagF(errors.New("returned model was nil"), "Creating group in %s", administrativeUnitId) + } + + // Obtain the new group ID + newGroup, ok := resp.Model.(beta.Group) + if !ok { + return tf.ErrorDiagF(errors.New("returned model was not a group"), "Creating group in %s", administrativeUnitId) + } + groupObjectId = pointer.From(newGroup.Id) + } else { - err = addGroupToAdministrativeUnit(ctx, administrativeUnitsClient, tenantId, administrativeUnitId, group) - if err != nil { - return tf.ErrorDiagF(err, "Adding group %q to administrative unit with object ID: %q", *group.ID(), administrativeUnitId) + ref := beta.ReferenceCreate{ + ODataId: pointer.To(fmt.Sprintf("%s%s", client.Client.BaseUri, beta.NewDirectoryObjectID(groupObjectId).ID())), + } + if _, err := administrativeUnitMemberClient.AddAdministrativeUnitMemberRef(ctx, administrativeUnitId, ref, administrativeunitmemberBeta.DefaultAddAdministrativeUnitMemberRefOperationOptions()); err != nil { + return tf.ErrorDiagF(err, "Adding group %q to %s", groupObjectId, administrativeUnitId) } } } + } else { - group, status, err = client.Create(ctx, properties) + options := groupBeta.CreateGroupOperationOptions{ + RetryFunc: func(resp *http.Response, o *odata.OData) (bool, error) { + if response.WasNotFound(resp) { + return true, nil + } else if response.WasBadRequest(resp) && o != nil && o.Error != nil { + return o.Error.Match("One or more property values specified are invalid") || + o.Error.Match("does not exist or one of its queried reference-property objects are not present"), nil + } + return false, nil + }, + } + + // Create the group at the tenant level + resp, err := client.CreateGroup(ctx, properties, options) if err != nil { - if status == http.StatusBadRequest && regexp.MustCompile(groupDuplicateValueError).MatchString(err.Error()) { - // Retry the request, without the calling principal as owner - newOwners := make(msgraph.Owners, 0) - for _, o := range *properties.Owners { - if id := o.ID(); id != nil && *id != callerId { - newOwners = append(newOwners, o) + if response.WasBadRequest(resp.HttpResponse) && regexp.MustCompile(groupDuplicateValueError).MatchString(err.Error()) { + // Retry the group creation, without the calling principal as owner + ownersWithoutCallingPrincipal := make([]string, 0) + for _, o := range *properties.Owners_ODataBind { + if o != callerODataId { + ownersWithoutCallingPrincipal = append(ownersWithoutCallingPrincipal, o) } } - // No point in retrying if the caller wasn't specified - if len(newOwners) == len(*properties.Owners) { + // No point in retrying if the caller wasn't specified as an owner + if len(ownersWithoutCallingPrincipal) == len(*properties.Owners) { log.Printf("[DEBUG] Not retrying group creation for %q as owner was not specified", displayName) return tf.ErrorDiagF(err, "Creating group %q", displayName) } @@ -681,269 +687,251 @@ func groupResourceCreate(ctx context.Context, d *pluginsdk.ResourceData, meta in // If the API is refusing the calling principal as owner, it will typically automatically append the caller in the background, // and subsequent GETs for the group will include the calling principal as owner, as if it were specified when creating. log.Printf("[DEBUG] Retrying group creation for %q without calling principal as owner", displayName) - if len(newOwners) == 0 { - properties.Owners = nil + if len(ownersWithoutCallingPrincipal) == 0 { + properties.Owners_ODataBind = nil } else { - properties.Owners = &newOwners + properties.Owners_ODataBind = &ownersWithoutCallingPrincipal } - group, _, err = client.Create(ctx, properties) + resp, err := client.CreateGroup(ctx, properties, groupBeta.DefaultCreateGroupOperationOptions()) if err != nil { return tf.ErrorDiagF(err, "Creating group %q", displayName) } + + if resp.Model == nil { + return tf.ErrorDiagF(errors.New("returned model was nil"), "Creating group %q", displayName) + } + + groupObjectId = pointer.From(resp.Model.Id) } else { return tf.ErrorDiagF(err, "Creating group %q", displayName) } } + + if resp.Model == nil { + return tf.ErrorDiagF(errors.New("returned model was nil"), "Creating group %q", displayName) + } + + groupObjectId = pointer.From(resp.Model.Id) } - if group.ID() == nil { - return tf.ErrorDiagF(errors.New("API returned group with nil object ID"), "Bad API Response") + if groupObjectId == "" { + return tf.ErrorDiagF(errors.New("unable to obtain group object ID"), "Creating group %q", displayName) } - d.SetId(*group.ID()) + d.SetId(groupObjectId) + id := beta.NewGroupID(groupObjectId) // Attempt to patch the newly created group and set the display name, which will tell us whether it exists yet, then set it back to the desired value. // The SDK handles retries for us here in the event of 404, 429 or 5xx, then returns after giving up. - uuid, err := uuid.GenerateUUID() + uid, err := uuid.GenerateUUID() if err != nil { return tf.ErrorDiagF(err, "Failed to generate a UUID") } - tempDisplayName := fmt.Sprintf("TERRAFORM_UPDATE_%s", uuid) + tempDisplayName := fmt.Sprintf("TERRAFORM_UPDATE_%s", uid) for _, displayNameToSet := range []string{tempDisplayName, displayName} { - status, err := client.Update(ctx, msgraph.Group{ - DirectoryObject: msgraph.DirectoryObject{ - Id: group.ID(), + updateOptions := groupBeta.UpdateGroupOperationOptions{ + RetryFunc: func(resp *http.Response, o *odata.OData) (bool, error) { + return response.WasNotFound(resp), nil }, - DisplayName: pointer.To(displayNameToSet), - }) + } + resp, err := client.UpdateGroup(ctx, id, beta.Group{ + DisplayName: nullable.Value(displayNameToSet), + }, updateOptions) if err != nil { - if status == http.StatusNotFound { - return tf.ErrorDiagF(err, "Timed out whilst waiting for new group to be replicated in Azure AD") + if response.WasNotFound(resp.HttpResponse) { + return tf.ErrorDiagF(err, "Timed out whilst waiting for new %s to be replicated in Azure AD", id) } - return tf.ErrorDiagF(err, "Failed to patch group with object ID %q after creating", *group.ID()) + return tf.ErrorDiagF(err, "Failed to patch %s after creating", id) } } // Wait for DisplayName to be updated - if err := helpers.WaitForUpdate(ctx, func(ctx context.Context) (*bool, error) { - defer func() { client.BaseClient.DisableRetries = false }() - client.BaseClient.DisableRetries = true - group, status, err := client.Get(ctx, *group.ID(), odata.Query{}) + if err := consistency.WaitForUpdate(ctx, func(ctx context.Context) (*bool, error) { + resp, err := client.GetGroup(ctx, id, groupBeta.DefaultGetGroupOperationOptions()) if err != nil { - if status == http.StatusNotFound { + if response.WasNotFound(resp.HttpResponse) { return pointer.To(false), nil } return nil, err } - return pointer.To(group.DisplayName != nil && *group.DisplayName == displayName), nil + group := resp.Model + return pointer.To(group != nil && group.DisplayName.GetOrZero() == displayName), nil }); err != nil { - return tf.ErrorDiagF(err, "Waiting for update of `display_name` for group with object ID %q", *group.ID()) + return tf.ErrorDiagF(err, "Waiting for update of `display_name` for %s", id) } - if hasGroupType(groupTypes, msgraph.GroupTypeUnified) { + if slices.Contains(groupTypes, GroupTypeUnified) { // Newly created Unified groups now get a description added out-of-band, so we'll wait a couple of minutes to see if this appears and then clear it // See https://github.com/microsoftgraph/msgraph-metadata/issues/331 if description == "" { // Ignoring the error result here because the description might not be updated out of band, in which case we skip over this - if updated, _ := helpers.WaitForUpdateWithTimeout(ctx, 2*time.Minute, func(ctx context.Context) (*bool, error) { - defer func() { client.BaseClient.DisableRetries = false }() - client.BaseClient.DisableRetries = true - group, _, err := client.Get(ctx, *group.ID(), odata.Query{}) + if updated, _ := consistency.WaitForUpdateWithTimeout(ctx, 2*time.Minute, func(ctx context.Context) (*bool, error) { + resp, err := client.GetGroup(ctx, id, groupBeta.DefaultGetGroupOperationOptions()) if err != nil { return nil, err } - return pointer.To(group.Description != nil && *group.Description != ""), nil + group := resp.Model + return pointer.To(group != nil && !group.Description.IsNull() && group.Description.GetOrZero() != ""), nil }); updated { - status, err = client.Update(ctx, msgraph.Group{ - DirectoryObject: msgraph.DirectoryObject{ - Id: group.ID(), + updateOptions := groupBeta.UpdateGroupOperationOptions{ + RetryFunc: func(resp *http.Response, o *odata.OData) (bool, error) { + return response.WasNotFound(resp), nil }, - Description: tf.NullableString(""), - }) + } + resp, err := client.UpdateGroup(ctx, id, beta.Group{ + Description: nullable.NoZero(""), + }, updateOptions) if err != nil { - if status == http.StatusNotFound { - return tf.ErrorDiagF(err, "Timed out whilst waiting for new group to be replicated in Azure AD") + if response.WasNotFound(resp.HttpResponse) { + return tf.ErrorDiagF(err, "Timed out whilst waiting for new %s to be replicated in Azure AD", id) } - return tf.ErrorDiagF(err, "Failed to patch `description` for group with object ID %q after creating", *group.ID()) + return tf.ErrorDiagF(err, "Failed to patch `description` for %s after creating", id) } // Wait for Description to be removed - if err = helpers.WaitForUpdate(ctx, func(ctx context.Context) (*bool, error) { - defer func() { client.BaseClient.DisableRetries = false }() - client.BaseClient.DisableRetries = true - group, _, err = client.Get(ctx, *group.ID(), odata.Query{}) + if err = consistency.WaitForUpdate(ctx, func(ctx context.Context) (*bool, error) { + resp, err := client.GetGroup(ctx, id, groupBeta.DefaultGetGroupOperationOptions()) if err != nil { return nil, err } - return pointer.To(group.Description == nil || *group.Description == ""), nil + group := resp.Model + return pointer.To(group != nil && group.Description.IsNull()), nil }); err != nil { - return tf.ErrorDiagF(err, "Waiting to remove `description` for group with object ID %q", *group.ID()) + return tf.ErrorDiagF(err, "Waiting to remove `description` for %s", id) } } } - // The following unified group properties in this block only support delegated auth + // The following unified group properties in this block only support delegated authentication. // Application-authenticated requests will return a 4xx error, so we only // set these when explicitly configured, as they each default to false anyway // See https://docs.microsoft.com/en-us/graph/known-issues#groups // AllowExternalSenders can only be set in its own PATCH request; including other properties returns a 400 if allowExternalSenders, ok := d.GetOkExists("external_senders_allowed"); ok { //nolint:staticcheck - if _, err := client.Update(ctx, msgraph.Group{ - DirectoryObject: msgraph.DirectoryObject{ - Id: group.ID(), - }, - AllowExternalSenders: pointer.To(allowExternalSenders.(bool)), - }); err != nil { - return tf.ErrorDiagF(err, "Failed to set `external_senders_allowed` for group with object ID %q", *group.ID()) + if _, err = client.UpdateGroup(ctx, id, beta.Group{ + AllowExternalSenders: nullable.Value(allowExternalSenders.(bool)), + }, groupBeta.DefaultUpdateGroupOperationOptions()); err != nil { + return tf.CheckDelegatedAuthDiagF(err, "Failed to set `external_senders_allowed` for %s", id) } // Wait for AllowExternalSenders to be updated - if err := helpers.WaitForUpdate(ctx, func(ctx context.Context) (*bool, error) { - defer func() { client.BaseClient.DisableRetries = false }() - client.BaseClient.DisableRetries = true - groupExtra, err := groupGetAdditional(ctx, client, *group.ID()) + if err := consistency.WaitForUpdate(ctx, func(ctx context.Context) (*bool, error) { + groupExtra, err := groupGetAdditional(ctx, client, id) if err != nil { return nil, err } - return pointer.To(groupExtra != nil && groupExtra.AllowExternalSenders != nil && *groupExtra.AllowExternalSenders == allowExternalSenders), nil + return pointer.To(groupExtra != nil && groupExtra.AllowExternalSenders.GetOrZero() == allowExternalSenders), nil }); err != nil { - return tf.ErrorDiagF(err, "Waiting for update of `external_senders_allowed` for group with object ID %q", *group.ID()) + return tf.ErrorDiagF(err, "Waiting for update of `external_senders_allowed` for %s", id) } } // AutoSubscribeNewMembers can only be set in its own PATCH request; including other properties returns a 400 if autoSubscribeNewMembers, ok := d.GetOkExists("auto_subscribe_new_members"); ok { //nolint:staticcheck - if _, err := client.Update(ctx, msgraph.Group{ - DirectoryObject: msgraph.DirectoryObject{ - Id: group.ID(), - }, - AutoSubscribeNewMembers: pointer.To(autoSubscribeNewMembers.(bool)), - }); err != nil { - return tf.ErrorDiagF(err, "Failed to set `auto_subscribe_new_members` for group with object ID %q", *group.ID()) + if _, err = client.UpdateGroup(ctx, id, beta.Group{ + AutoSubscribeNewMembers: nullable.Value(autoSubscribeNewMembers.(bool)), + }, groupBeta.DefaultUpdateGroupOperationOptions()); err != nil { + return tf.CheckDelegatedAuthDiagF(err, "Failed to set `auto_subscribe_new_members` for %s", id) } // Wait for AutoSubscribeNewMembers to be updated - if err := helpers.WaitForUpdate(ctx, func(ctx context.Context) (*bool, error) { - defer func() { client.BaseClient.DisableRetries = false }() - client.BaseClient.DisableRetries = true - groupExtra, err := groupGetAdditional(ctx, client, *group.ID()) + if err = consistency.WaitForUpdate(ctx, func(ctx context.Context) (*bool, error) { + groupExtra, err := groupGetAdditional(ctx, client, id) if err != nil { return nil, err } - return pointer.To(groupExtra != nil && groupExtra.AutoSubscribeNewMembers != nil && *groupExtra.AutoSubscribeNewMembers == autoSubscribeNewMembers), nil + return pointer.To(groupExtra != nil && groupExtra.AutoSubscribeNewMembers.GetOrZero() == autoSubscribeNewMembers), nil }); err != nil { - return tf.ErrorDiagF(err, "Waiting for update of `auto_subscribe_new_members` for group with object ID %q", *group.ID()) + return tf.ErrorDiagF(err, "Waiting for update of `auto_subscribe_new_members` for %s", id) } } // HideFromAddressLists can only be set in its own PATCH request; including other properties returns a 400 if hideFromAddressList, ok := d.GetOkExists("hide_from_address_lists"); ok { //nolint:staticcheck - if _, err := client.Update(ctx, msgraph.Group{ - DirectoryObject: msgraph.DirectoryObject{ - Id: group.ID(), - }, - HideFromAddressLists: pointer.To(hideFromAddressList.(bool)), - }); err != nil { - return tf.ErrorDiagF(err, "Failed to set `hide_from_address_lists` for group with object ID %q", *group.ID()) + if _, err = client.UpdateGroup(ctx, id, beta.Group{ + HideFromAddressLists: nullable.Value(hideFromAddressList.(bool)), + }, groupBeta.DefaultUpdateGroupOperationOptions()); err != nil { + return tf.CheckDelegatedAuthDiagF(err, "Failed to set `hide_from_address_lists` for %s", id) } // Wait for HideFromAddressLists to be updated - if err := helpers.WaitForUpdate(ctx, func(ctx context.Context) (*bool, error) { - defer func() { client.BaseClient.DisableRetries = false }() - client.BaseClient.DisableRetries = true - groupExtra, err := groupGetAdditional(ctx, client, *group.ID()) + if err = consistency.WaitForUpdate(ctx, func(ctx context.Context) (*bool, error) { + groupExtra, err := groupGetAdditional(ctx, client, id) if err != nil { return nil, err } - return pointer.To(groupExtra != nil && groupExtra.HideFromAddressLists != nil && *groupExtra.HideFromAddressLists == hideFromAddressList), nil + return pointer.To(groupExtra != nil && groupExtra.HideFromAddressLists.GetOrZero() == hideFromAddressList), nil }); err != nil { - return tf.ErrorDiagF(err, "Waiting for update of `hide_from_address_lists` for group with object ID %q", *group.ID()) + return tf.ErrorDiagF(err, "Waiting for update of `hide_from_address_lists` for %s", id) } } // HideFromOutlookClients can only be set in its own PATCH request; including other properties returns a 400 if hideFromOutlookClients, ok := d.GetOkExists("hide_from_outlook_clients"); ok { //nolint:staticcheck - if _, err := client.Update(ctx, msgraph.Group{ - DirectoryObject: msgraph.DirectoryObject{ - Id: group.ID(), - }, - HideFromOutlookClients: pointer.To(hideFromOutlookClients.(bool)), - }); err != nil { - return tf.ErrorDiagF(err, "Failed to set `hide_from_outlook_clients` for group with object ID %q", *group.ID()) + if _, err = client.UpdateGroup(ctx, id, beta.Group{ + HideFromOutlookClients: nullable.Value(hideFromOutlookClients.(bool)), + }, groupBeta.DefaultUpdateGroupOperationOptions()); err != nil { + return tf.CheckDelegatedAuthDiagF(err, "Failed to set `hide_from_outlook_clients` for %s", id) } // Wait for HideFromOutlookClients to be updated - if err := helpers.WaitForUpdate(ctx, func(ctx context.Context) (*bool, error) { - defer func() { client.BaseClient.DisableRetries = false }() - client.BaseClient.DisableRetries = true - groupExtra, err := groupGetAdditional(ctx, client, *group.ID()) + if err = consistency.WaitForUpdate(ctx, func(ctx context.Context) (*bool, error) { + groupExtra, err := groupGetAdditional(ctx, client, id) if err != nil { return nil, err } - return pointer.To(groupExtra != nil && groupExtra.HideFromOutlookClients != nil && *groupExtra.HideFromOutlookClients == hideFromOutlookClients), nil + return pointer.To(groupExtra != nil && groupExtra.HideFromOutlookClients.GetOrZero() == hideFromOutlookClients), nil }); err != nil { - return tf.ErrorDiagF(err, "Waiting for update of `hide_from_outlook_clients` for group with object ID %q", *group.ID()) + return tf.ErrorDiagF(err, "Waiting for update of `hide_from_outlook_clients` for %s", id) } } } // Add any remaining owners after the group is created - if len(ownersExtra) > 0 { - group.Owners = &ownersExtra - if _, err := client.AddOwners(ctx, group); err != nil { - return tf.ErrorDiagF(err, "Could not add owners to group with object ID: %q", d.Id()) + for _, o := range ownersExtra { + if _, err = ownerClient.AddOwnerRef(ctx, id, o, ownerBeta.DefaultAddOwnerRefOperationOptions()); err != nil { + return tf.ErrorDiagF(err, "Could not add owners to %s", id) } } // Add members after the group is created - members := make(msgraph.Members, 0) if v, ok := d.GetOk("members"); ok { for _, memberId := range v.(*pluginsdk.Set).List() { - memberObject, _, err := directoryObjectsClient.Get(ctx, memberId.(string), odata.Query{}) - if err != nil { - return tf.ErrorDiagF(err, "Could not retrieve member principal object %q", memberId) + ref := beta.ReferenceCreate{ + ODataId: pointer.To(client.Client.BaseUri + beta.NewDirectoryObjectID(memberId.(string)).ID()), } - if memberObject == nil { - return tf.ErrorDiagF(errors.New("memberObject was nil"), "Could not retrieve member principal object %q", memberId) + if _, err = memberClient.AddMemberRef(ctx, id, ref, memberBeta.DefaultAddMemberRefOperationOptions()); err != nil { + return tf.ErrorDiagF(err, "Could not add members to group with object ID: %q", d.Id()) } - memberObject.ODataId = (*odata.Id)(pointer.To(fmt.Sprintf("%s/v1.0/%s/directoryObjects/%s", - client.BaseClient.Endpoint, tenantId, memberId))) - - members = append(members, *memberObject) - } - } - if len(members) > 0 { - group.Members = &members - if _, err := client.AddMembers(ctx, group); err != nil { - return tf.ErrorDiagF(err, "Could not add members to group with object ID: %q", d.Id()) } } - // We have observed that when creating a group with an administrative_unit_id and querying the group with the /groups endpoint and specifying $select=allowExternalSenders,autoSubscribeNewMembers,hideFromAddressLists,hideFromOutlookClients, it returns a 404 for ~11 minutes. + enableRetries := false if _, ok := d.GetOk("administrative_unit_ids"); ok { - meta.(*clients.Client).Groups.GroupsClient.BaseClient.DisableRetries = false - meta.(*clients.Client).Groups.GroupsClient.BaseClient.RetryableClient.RetryWaitMax = 1 * time.Minute - meta.(*clients.Client).Groups.GroupsClient.BaseClient.RetryableClient.RetryWaitMin = 10 * time.Second - meta.(*clients.Client).Groups.GroupsClient.BaseClient.RetryableClient.RetryMax = 15 + // It has been observed that when creating a group within an administrative unit and querying the group with the `/groups` endpoint whilst + // specifying `$select=allowExternalSenders,autoSubscribeNewMembers,hideFromAddressLists,hideFromOutlookClients, a 404 is returned for ~11 minutes. + enableRetries = true } - return groupResourceRead(ctx, d, meta) + return groupResourceReadFunc(enableRetries)(ctx, d, meta) } func groupResourceUpdate(ctx context.Context, d *pluginsdk.ResourceData, meta interface{}) pluginsdk.Diagnostics { - client := meta.(*clients.Client).Groups.GroupsClient - directoryObjectsClient := meta.(*clients.Client).Groups.DirectoryObjectsClient - administrativeUnitClient := meta.(*clients.Client).Groups.AdministrativeUnitsClient + client := meta.(*clients.Client).Groups.GroupClientBeta + ownerClient := meta.(*clients.Client).Groups.GroupOwnerClientBeta + memberClient := meta.(*clients.Client).Groups.GroupMemberClientBeta + memberOfClient := meta.(*clients.Client).Groups.GroupMemberOfClientBeta + administrativeUnitMemberClient := meta.(*clients.Client).Groups.AdministrativeUnitMemberClientBeta + callerId := meta.(*clients.Client).ObjectID - tenantId := meta.(*clients.Client).TenantID - groupId := d.Id() + id := beta.NewGroupID(d.Id()) displayName := d.Get("display_name").(string) - tf.LockByName(groupResourceName, groupId) - defer tf.UnlockByName(groupResourceName, groupId) + tf.LockByName(groupResourceName, id.GroupId) + defer tf.UnlockByName(groupResourceName, id.GroupId) // Perform this check at apply time to catch any duplicate names created during the same apply if d.Get("prevent_duplicate_names").(bool) { @@ -953,220 +941,192 @@ func groupResourceUpdate(ctx context.Context, d *pluginsdk.ResourceData, meta in } if result != nil && len(*result) > 0 { for _, existingGroup := range *result { - if existingGroup.ID() == nil { + if existingGroup.Id == nil { return tf.ErrorDiagF(errors.New("API returned group with nil object ID during duplicate name check"), "Bad API response") } - if *existingGroup.ID() != groupId { - return tf.ImportAsDuplicateDiag("azuread_group", *existingGroup.ID(), displayName) + if *existingGroup.Id != id.GroupId { + return tf.ImportAsDuplicateDiag("azuread_group", *existingGroup.Id, displayName) } } } } - group := msgraph.Group{ - DirectoryObject: msgraph.DirectoryObject{ - Id: pointer.To(groupId), - }, - Description: tf.NullableString(d.Get("description").(string)), - DisplayName: pointer.To(displayName), - MailEnabled: pointer.To(d.Get("mail_enabled").(bool)), - MembershipRule: tf.NullableString(""), - SecurityEnabled: pointer.To(d.Get("security_enabled").(bool)), + group := beta.Group{ + Description: nullable.NoZero(d.Get("description").(string)), + DisplayName: nullable.Value(displayName), + MailEnabled: nullable.Value(d.Get("mail_enabled").(bool)), + MembershipRule: nullable.NoZero(""), + SecurityEnabled: nullable.Value(d.Get("security_enabled").(bool)), } if d.HasChange("writeback_enabled") || d.HasChange("onpremises_group_type") { - group.WritebackConfiguration = &msgraph.GroupWritebackConfiguration{ - IsEnabled: pointer.To(d.Get("writeback_enabled").(bool)), + group.WritebackConfiguration = &beta.GroupWritebackConfiguration{ + IsEnabled: nullable.Value(d.Get("writeback_enabled").(bool)), + OmitDiscriminatedValue: true, } if onPremisesGroupType := d.Get("onpremises_group_type").(string); onPremisesGroupType != "" { - group.WritebackConfiguration.OnPremisesGroupType = pointer.To(onPremisesGroupType) + group.WritebackConfiguration.OnPremisesGroupType = nullable.Value(onPremisesGroupType) } } if v, ok := d.GetOk("dynamic_membership"); ok && len(v.([]interface{})) > 0 { if d.Get("dynamic_membership.0.enabled").(bool) { - group.MembershipRuleProcessingState = pointer.To("On") + group.MembershipRuleProcessingState = nullable.Value("On") } else { - group.MembershipRuleProcessingState = pointer.To("Paused") + group.MembershipRuleProcessingState = nullable.Value("Paused") } - group.MembershipRule = tf.NullableString(d.Get("dynamic_membership.0.rule").(string)) + group.MembershipRule = nullable.Value(d.Get("dynamic_membership.0.rule").(string)) } if theme := d.Get("theme").(string); theme != "" { - group.Theme = tf.NullableString(theme) + group.Theme = nullable.Value(theme) } if d.HasChange("visibility") { - group.Visibility = pointer.To(d.Get("visibility").(string)) + group.Visibility = nullable.Value(d.Get("visibility").(string)) } - if _, err := client.Update(ctx, group); err != nil { - return tf.ErrorDiagF(err, "Updating group with ID: %q", d.Id()) + if _, err := client.UpdateGroup(ctx, id, group, groupBeta.DefaultUpdateGroupOperationOptions()); err != nil { + return tf.ErrorDiagF(err, "Updating %s", id) } - groupTypes := make([]msgraph.GroupType, 0) + groupTypes := make([]string, 0) for _, v := range d.Get("types").(*pluginsdk.Set).List() { groupTypes = append(groupTypes, v.(string)) } // The following properties can only be set or unset for Unified groups, other group types will return a 4xx error. - if hasGroupType(groupTypes, msgraph.GroupTypeUnified) { + if slices.Contains(groupTypes, GroupTypeUnified) { // The unified group properties in this block only support delegated auth - // Application-authenticated requests will return a 4xx error, so we only - // set these when explicitly configured, and when the value differs. + // Application-authenticated requests will return a 403 or 404 error, so we + // only set these when explicitly configured, and when the value differs. // See https://docs.microsoft.com/en-us/graph/known-issues#groups - extra, err := groupGetAdditional(ctx, client, *group.ID()) + extra, err := groupGetAdditional(ctx, client, id) if err != nil { - return tf.ErrorDiagF(err, "Retrieving extra fields for group with ID: %q", *group.ID()) + return tf.ErrorDiagF(err, "Retrieving extra fields for %s", id) } // AllowExternalSenders can only be set in its own PATCH request; including other properties returns a 400 - if v, ok := d.GetOkExists("external_senders_allowed"); ok && (extra == nil || extra.AllowExternalSenders == nil || *extra.AllowExternalSenders != v.(bool)) { //nolint:staticcheck - if _, err := client.Update(ctx, msgraph.Group{ - DirectoryObject: msgraph.DirectoryObject{ - Id: group.ID(), - }, - AllowExternalSenders: pointer.To(v.(bool)), - }); err != nil { - return tf.ErrorDiagF(err, "Failed to set `external_senders_allowed` for group with object ID %q", *group.ID()) + if v, ok := d.GetOkExists("external_senders_allowed"); ok && (extra == nil || extra.AllowExternalSenders.GetOrZero() != v.(bool)) { //nolint:staticcheck + if _, err = client.UpdateGroup(ctx, id, beta.Group{ + AllowExternalSenders: nullable.Value(v.(bool)), + }, groupBeta.DefaultUpdateGroupOperationOptions()); err != nil { + return tf.CheckDelegatedAuthDiagF(err, "Failed to set `external_senders_allowed` for %s", id) } // Wait for AllowExternalSenders to be updated - if err := helpers.WaitForUpdate(ctx, func(ctx context.Context) (*bool, error) { - defer func() { client.BaseClient.DisableRetries = false }() - client.BaseClient.DisableRetries = true - groupExtra, err := groupGetAdditional(ctx, client, *group.ID()) + if err = consistency.WaitForUpdate(ctx, func(ctx context.Context) (*bool, error) { + groupExtra, err := groupGetAdditional(ctx, client, id) if err != nil { return nil, err } - return pointer.To(groupExtra != nil && groupExtra.AllowExternalSenders != nil && *groupExtra.AllowExternalSenders == v.(bool)), nil + return pointer.To(groupExtra != nil && groupExtra.AllowExternalSenders.GetOrZero() == v.(bool)), nil }); err != nil { - return tf.ErrorDiagF(err, "Waiting for update of `external_senders_allowed` for group with object ID %q", *group.ID()) + return tf.ErrorDiagF(err, "Waiting for update of `external_senders_allowed` for %s", id) } } // AutoSubscribeNewMembers can only be set in its own PATCH request; including other properties returns a 400 - if v, ok := d.GetOkExists("auto_subscribe_new_members"); ok && (extra == nil || extra.AutoSubscribeNewMembers == nil || *extra.AutoSubscribeNewMembers != v.(bool)) { //nolint:staticcheck - if _, err := client.Update(ctx, msgraph.Group{ - DirectoryObject: msgraph.DirectoryObject{ - Id: group.ID(), - }, - AutoSubscribeNewMembers: pointer.To(v.(bool)), - }); err != nil { - return tf.ErrorDiagF(err, "Failed to set `auto_subscribe_new_members` for group with object ID %q", *group.ID()) + if v, ok := d.GetOkExists("auto_subscribe_new_members"); ok && (extra == nil || extra.AutoSubscribeNewMembers.GetOrZero() != v.(bool)) { //nolint:staticcheck + if _, err = client.UpdateGroup(ctx, id, beta.Group{ + AutoSubscribeNewMembers: nullable.Value(v.(bool)), + }, groupBeta.DefaultUpdateGroupOperationOptions()); err != nil { + return tf.CheckDelegatedAuthDiagF(err, "Failed to set `auto_subscribe_new_members` for %s", id) } // Wait for AutoSubscribeNewMembers to be updated - if err := helpers.WaitForUpdate(ctx, func(ctx context.Context) (*bool, error) { - defer func() { client.BaseClient.DisableRetries = false }() - client.BaseClient.DisableRetries = true - groupExtra, err := groupGetAdditional(ctx, client, *group.ID()) + if err = consistency.WaitForUpdate(ctx, func(ctx context.Context) (*bool, error) { + groupExtra, err := groupGetAdditional(ctx, client, id) if err != nil { return nil, err } - return pointer.To(groupExtra != nil && groupExtra.AutoSubscribeNewMembers != nil && *groupExtra.AutoSubscribeNewMembers == v.(bool)), nil + return pointer.To(groupExtra != nil && groupExtra.AutoSubscribeNewMembers.GetOrZero() == v.(bool)), nil }); err != nil { - return tf.ErrorDiagF(err, "Waiting for update of `auto_subscribe_new_members` for group with object ID %q", *group.ID()) + return tf.ErrorDiagF(err, "Waiting for update of `auto_subscribe_new_members` for %s", id) } } // HideFromAddressLists can only be set in its own PATCH request; including other properties returns a 400 - if v, ok := d.GetOkExists("hide_from_address_lists"); ok && (extra == nil || extra.HideFromAddressLists == nil || *extra.HideFromAddressLists != v.(bool)) { //nolint:staticcheck - if _, err := client.Update(ctx, msgraph.Group{ - DirectoryObject: msgraph.DirectoryObject{ - Id: group.ID(), - }, - HideFromAddressLists: pointer.To(v.(bool)), - }); err != nil { - return tf.ErrorDiagF(err, "Failed to set `hide_from_address_lists` for group with object ID %q", *group.ID()) + if v, ok := d.GetOkExists("hide_from_address_lists"); ok && (extra == nil || extra.HideFromAddressLists.GetOrZero() != v.(bool)) { //nolint:staticcheck + if _, err = client.UpdateGroup(ctx, id, beta.Group{ + HideFromAddressLists: nullable.Value(v.(bool)), + }, groupBeta.DefaultUpdateGroupOperationOptions()); err != nil { + return tf.CheckDelegatedAuthDiagF(err, "Failed to set `hide_from_address_lists` for %s", id) } // Wait for HideFromAddressLists to be updated - if err := helpers.WaitForUpdate(ctx, func(ctx context.Context) (*bool, error) { - defer func() { client.BaseClient.DisableRetries = false }() - client.BaseClient.DisableRetries = true - groupExtra, err := groupGetAdditional(ctx, client, *group.ID()) + if err = consistency.WaitForUpdate(ctx, func(ctx context.Context) (*bool, error) { + groupExtra, err := groupGetAdditional(ctx, client, id) if err != nil { return nil, err } - return pointer.To(groupExtra != nil && groupExtra.HideFromAddressLists != nil && *groupExtra.HideFromAddressLists == v.(bool)), nil + return pointer.To(groupExtra != nil && groupExtra.HideFromAddressLists.GetOrZero() == v.(bool)), nil }); err != nil { - return tf.ErrorDiagF(err, "Waiting for update of `hide_from_address_lists` for group with object ID %q", *group.ID()) + return tf.ErrorDiagF(err, "Waiting for update of `hide_from_address_lists` for %s", id) } } // HideFromOutlookClients can only be set in its own PATCH request; including other properties returns a 400 - if v, ok := d.GetOkExists("hide_from_outlook_clients"); ok && (extra == nil || extra.HideFromOutlookClients == nil || *extra.HideFromOutlookClients != v.(bool)) { //nolint:staticcheck - if _, err := client.Update(ctx, msgraph.Group{ - DirectoryObject: msgraph.DirectoryObject{ - Id: group.ID(), - }, - HideFromOutlookClients: pointer.To(v.(bool)), - }); err != nil { - return tf.ErrorDiagF(err, "Failed to set `hide_from_outlook_clients` for group with object ID %q", *group.ID()) + if v, ok := d.GetOkExists("hide_from_outlook_clients"); ok && (extra == nil || extra.HideFromOutlookClients.GetOrZero() != v.(bool)) { //nolint:staticcheck + if _, err = client.UpdateGroup(ctx, id, beta.Group{ + HideFromOutlookClients: nullable.Value(v.(bool)), + }, groupBeta.DefaultUpdateGroupOperationOptions()); err != nil { + return tf.CheckDelegatedAuthDiagF(err, "Failed to set `hide_from_outlook_clients` for %s", id) } // Wait for HideFromOutlookClients to be updated - if err := helpers.WaitForUpdate(ctx, func(ctx context.Context) (*bool, error) { - defer func() { client.BaseClient.DisableRetries = false }() - client.BaseClient.DisableRetries = true - groupExtra, err := groupGetAdditional(ctx, client, *group.ID()) + if err = consistency.WaitForUpdate(ctx, func(ctx context.Context) (*bool, error) { + groupExtra, err := groupGetAdditional(ctx, client, id) if err != nil { return nil, err } - return pointer.To(groupExtra != nil && groupExtra.HideFromOutlookClients != nil && *groupExtra.HideFromOutlookClients == v.(bool)), nil + return pointer.To(groupExtra != nil && groupExtra.HideFromOutlookClients.GetOrZero() == v.(bool)), nil }); err != nil { - return tf.ErrorDiagF(err, "Waiting for update of `hide_from_outlook_clients` for group with object ID %q", *group.ID()) + return tf.ErrorDiagF(err, "Waiting for update of `hide_from_outlook_clients` for %s", id) } } } if d.HasChange("members") { - members, _, err := client.ListMembers(ctx, *group.ID()) + resp, err := memberClient.ListMembers(ctx, id, memberBeta.DefaultListMembersOperationOptions()) if err != nil { - return tf.ErrorDiagF(err, "Could not retrieve members for group with object ID: %q", d.Id()) + return tf.ErrorDiagF(err, "Could not retrieve members for %s", id) + } + + existingMembers := make([]string, 0) + for resp.Model != nil { + for _, m := range *resp.Model { + existingMembers = append(existingMembers, pointer.From(m.DirectoryObject().Id)) + } } - existingMembers := *members desiredMembers := *tf.ExpandStringSlicePtr(d.Get("members").(*pluginsdk.Set).List()) membersForRemoval := tf.Difference(existingMembers, desiredMembers) membersToAdd := tf.Difference(desiredMembers, existingMembers) - if len(membersForRemoval) > 0 { - if _, err = client.RemoveMembers(ctx, d.Id(), &membersForRemoval); err != nil { - return tf.ErrorDiagF(err, "Could not remove members from group with object ID: %q", d.Id()) + for _, v := range membersForRemoval { + memberId := beta.NewGroupIdMemberID(id.GroupId, v) + if _, err = memberClient.RemoveMemberRef(ctx, memberId, memberBeta.DefaultRemoveMemberRefOperationOptions()); err != nil { + return tf.ErrorDiagF(err, "removing %s", memberId) } } - if len(membersToAdd) > 0 { - newMembers := make(msgraph.Members, 0) - for _, memberId := range membersToAdd { - memberObject, _, err := directoryObjectsClient.Get(ctx, memberId, odata.Query{}) - if err != nil { - return tf.ErrorDiagF(err, "Could not retrieve principal object %q", memberId) - } - if memberObject == nil { - return tf.ErrorDiagF(errors.New("returned memberObject was nil"), "Could not retrieve member principal object %q", memberId) - } - memberObject.ODataId = (*odata.Id)(pointer.To(fmt.Sprintf("%s/v1.0/%s/directoryObjects/%s", - client.BaseClient.Endpoint, tenantId, memberId))) - - newMembers = append(newMembers, *memberObject) + for _, v := range membersToAdd { + ref := beta.ReferenceCreate{ + ODataId: pointer.To(client.Client.BaseUri + beta.NewDirectoryObjectID(v).ID()), } - - group.Members = &newMembers - if _, err := client.AddMembers(ctx, &group); err != nil { - return tf.ErrorDiagF(err, "Could not add members to group with object ID: %q", d.Id()) + if _, err = memberClient.AddMemberRef(ctx, id, ref, memberBeta.DefaultAddMemberRefOperationOptions()); err != nil { + return tf.ErrorDiagF(err, "removing %s", beta.NewGroupIdMemberID(id.GroupId, v)) } } } if v, ok := d.GetOk("owners"); ok && d.HasChange("owners") { - owners, _, err := client.ListOwners(ctx, *group.ID()) + resp, err := ownerClient.ListOwners(ctx, id, ownerBeta.DefaultListOwnersOperationOptions()) if err != nil { - return tf.ErrorDiagF(err, "Could not retrieve owners for group with object ID: %q", d.Id()) + return tf.ErrorDiagF(err, "Could not retrieve members for %s", id) } // If all owners are removed, restore the calling principal as the sole owner, in order to meet API @@ -1177,48 +1137,49 @@ func groupResourceUpdate(ctx context.Context, d *pluginsdk.ResourceData, meta in desiredOwners = []string{callerId} } - existingOwners := *owners + existingOwners := make([]string, 0) + for resp.Model != nil { + for _, o := range *resp.Model { + existingOwners = append(existingOwners, pointer.From(o.DirectoryObject().Id)) + } + } + ownersForRemoval := tf.Difference(existingOwners, desiredOwners) ownersToAdd := tf.Difference(desiredOwners, existingOwners) - if len(ownersToAdd) > 0 { - newOwners := make(msgraph.Owners, 0) - for _, ownerId := range ownersToAdd { - ownerObject, _, err := directoryObjectsClient.Get(ctx, ownerId, odata.Query{}) - if err != nil { - return tf.ErrorDiagF(err, "Could not retrieve owner principal object %q", ownerId) - } - if ownerObject == nil { - return tf.ErrorDiagF(errors.New("returned ownerObject was nil"), "Could not retrieve owner principal object %q", ownerId) - } - ownerObject.ODataId = (*odata.Id)(pointer.To(fmt.Sprintf("%s/v1.0/%s/directoryObjects/%s", - client.BaseClient.Endpoint, tenantId, ownerId))) - - newOwners = append(newOwners, *ownerObject) + // Add new owners first to avoid leaving the group without any owners + for _, v := range ownersToAdd { + ref := beta.ReferenceCreate{ + ODataId: pointer.To(client.Client.BaseUri + beta.NewDirectoryObjectID(v).ID()), } - - group.Owners = &newOwners - if _, err := client.AddOwners(ctx, &group); err != nil { - return tf.ErrorDiagF(err, "Could not add owners to group with object ID: %q", d.Id()) + if _, err = ownerClient.AddOwnerRef(ctx, id, ref, ownerBeta.DefaultAddOwnerRefOperationOptions()); err != nil { + return tf.ErrorDiagF(err, "removing %s", beta.NewGroupIdOwnerID(id.GroupId, v)) } } - if len(ownersForRemoval) > 0 { - if _, err = client.RemoveOwners(ctx, d.Id(), &ownersForRemoval); err != nil { - return tf.ErrorDiagF(err, "Could not remove owners from group with object ID: %q", d.Id()) + for _, v := range ownersForRemoval { + ownerId := beta.NewGroupIdOwnerID(id.GroupId, v) + if _, err = ownerClient.RemoveOwnerRef(ctx, ownerId, ownerBeta.DefaultRemoveOwnerRefOperationOptions()); err != nil { + return tf.ErrorDiagF(err, "removing %s", ownerId) } } } if v := d.Get("administrative_unit_ids"); d.HasChange("administrative_unit_ids") { - administrativeUnits, _, err := client.ListAdministrativeUnitMemberships(ctx, *group.ID()) + resp, err := memberOfClient.ListMemberOfs(ctx, id, memberofBeta.DefaultListMemberOfsOperationOptions()) if err != nil { - return tf.ErrorDiagPathF(err, "administrative_units", "Could not retrieve administrative units for group with object ID %q", d.Id()) + return tf.ErrorDiagPathF(err, "administrative_units", "retrieving administrative unit memberships for %s", id) + } + + if resp.Model == nil { + return tf.ErrorDiagPathF(errors.New("model was nil"), "administrative_units", "retrieving administrative unit memberships for %s", id) } var existingAdministrativeUnits []string - for _, administrativeUnit := range *administrativeUnits { - existingAdministrativeUnits = append(existingAdministrativeUnits, *administrativeUnit.ID) + for _, obj := range *resp.Model { + if _, ok := obj.(beta.AdministrativeUnit); ok { + existingAdministrativeUnits = append(existingAdministrativeUnits, *obj.DirectoryObject().Id) + } } desiredAdministrativeUnits := tf.ExpandStringSlice(v.(*pluginsdk.Set).List()) @@ -1226,184 +1187,200 @@ func groupResourceUpdate(ctx context.Context, d *pluginsdk.ResourceData, meta in administrativeUnitsToJoin := tf.Difference(desiredAdministrativeUnits, existingAdministrativeUnits) if len(administrativeUnitsToJoin) > 0 { - for _, newAdministrativeUnitId := range administrativeUnitsToJoin { - err := addGroupToAdministrativeUnit(ctx, administrativeUnitClient, tenantId, newAdministrativeUnitId, &group) - if err != nil { - return tf.ErrorDiagF(err, "Could not add group %q to administrative unit with object ID: %q", *group.ID(), newAdministrativeUnitId) + for _, v := range administrativeUnitsToJoin { + newAdministrativeUnitId := beta.NewDirectoryAdministrativeUnitID(v) + ref := beta.ReferenceCreate{ + ODataId: pointer.To(fmt.Sprintf("%s%s", client.Client.BaseUri, beta.NewDirectoryObjectID(id.GroupId).ID())), + } + if _, err = administrativeUnitMemberClient.AddAdministrativeUnitMemberRef(ctx, newAdministrativeUnitId, ref, administrativeunitmemberBeta.DefaultAddAdministrativeUnitMemberRefOperationOptions()); err != nil { + return tf.ErrorDiagF(err, "Could not add %s as member of %s", id, newAdministrativeUnitId) } } } if len(administrativeUnitsToLeave) > 0 { - for _, oldAdministrativeUnitId := range administrativeUnitsToLeave { - memberIds := []string{d.Id()} - if _, err := administrativeUnitClient.RemoveMembers(ctx, oldAdministrativeUnitId, &memberIds); err != nil { - return tf.ErrorDiagF(err, "Could not remove group from administrative unit with object ID: %q", oldAdministrativeUnitId) + for _, v := range administrativeUnitsToLeave { + memberId := beta.NewDirectoryAdministrativeUnitIdMemberID(v, id.GroupId) + if _, err = administrativeUnitMemberClient.RemoveAdministrativeUnitMemberRef(ctx, memberId, administrativeunitmemberBeta.DefaultRemoveAdministrativeUnitMemberRefOperationOptions()); err != nil { + return tf.ErrorDiagF(err, "Could not remove %s", memberId) } } } } - return groupResourceRead(ctx, d, meta) + return groupResourceReadFunc(false)(ctx, d, meta) } -func groupResourceRead(ctx context.Context, d *pluginsdk.ResourceData, meta interface{}) pluginsdk.Diagnostics { - client := meta.(*clients.Client).Groups.GroupsClient +// groupResourceReadFunc returns a ReadContextFunc with optional retries. This is necessary when creating new groups +// within administrative units, since some GET requests return a 404 for up to ~11 minutes after the group is created, +// even if that group has been updated several times. +func groupResourceReadFunc(enableRetries bool) pluginsdk.ReadContextFunc { + return func(ctx context.Context, d *pluginsdk.ResourceData, meta interface{}) pluginsdk.Diagnostics { + client := meta.(*clients.Client).Groups.GroupClientBeta + ownerClient := meta.(*clients.Client).Groups.GroupOwnerClientBeta + memberClient := meta.(*clients.Client).Groups.GroupMemberClientBeta + memberOfClient := meta.(*clients.Client).Groups.GroupMemberOfClientBeta + + id := beta.NewGroupID(d.Id()) + + options := groupBeta.DefaultGetGroupOperationOptions() + if enableRetries { + // Keep retrying on 404 for up to 12 minutes to defeat extended replication delays + startTimeForRetries := time.Now() + options.RetryFunc = func(resp *http.Response, o *odata.OData) (bool, error) { + if response.WasNotFound(resp) && time.Now().Sub(startTimeForRetries).Minutes() < 12 { + return true, nil + } + return false, nil + } + } - group, status, err := client.Get(ctx, d.Id(), odata.Query{}) - if err != nil { - if status == http.StatusNotFound { - log.Printf("[DEBUG] Group with ID %q was not found - removing from state", d.Id()) - d.SetId("") - return nil + resp, err := client.GetGroup(ctx, id, options) + if err != nil { + if response.WasNotFound(resp.HttpResponse) { + log.Printf("[DEBUG] %s was not found - removing from state", id) + d.SetId("") + return nil + } + return tf.ErrorDiagF(err, "Retrieving %s", id) } - return tf.ErrorDiagF(err, "Retrieving group with object ID: %q", d.Id()) - } - tf.Set(d, "assignable_to_role", group.IsAssignableToRole) - tf.Set(d, "behaviors", tf.FlattenStringSlicePtr(group.ResourceBehaviorOptions)) - tf.Set(d, "description", group.Description) - tf.Set(d, "display_name", group.DisplayName) - tf.Set(d, "mail_enabled", group.MailEnabled) - tf.Set(d, "mail", group.Mail) - tf.Set(d, "mail_nickname", group.MailNickname) - tf.Set(d, "object_id", group.ID()) - tf.Set(d, "onpremises_domain_name", group.OnPremisesDomainName) - tf.Set(d, "onpremises_netbios_name", group.OnPremisesNetBiosName) - tf.Set(d, "onpremises_sam_account_name", group.OnPremisesSamAccountName) - tf.Set(d, "onpremises_security_identifier", group.OnPremisesSecurityIdentifier) - tf.Set(d, "onpremises_sync_enabled", group.OnPremisesSyncEnabled) - tf.Set(d, "preferred_language", group.PreferredLanguage) - tf.Set(d, "provisioning_options", tf.FlattenStringSlicePtr(group.ResourceProvisioningOptions)) - tf.Set(d, "proxy_addresses", tf.FlattenStringSlicePtr(group.ProxyAddresses)) - tf.Set(d, "security_enabled", group.SecurityEnabled) - tf.Set(d, "theme", group.Theme) - tf.Set(d, "types", group.GroupTypes) - tf.Set(d, "visibility", group.Visibility) - - dynamicMembership := make([]interface{}, 0) - if group.MembershipRule != nil { - enabled := true - if group.MembershipRuleProcessingState != nil && *group.MembershipRuleProcessingState == "Paused" { - enabled = false - } - dynamicMembership = append(dynamicMembership, map[string]interface{}{ - "enabled": enabled, - "rule": group.MembershipRule, - }) - } - tf.Set(d, "dynamic_membership", dynamicMembership) + group := resp.Model + if group == nil { + return tf.ErrorDiagF(errors.New("model was nil"), "Retrieving %s", id) + } - if group.WritebackConfiguration != nil { - tf.Set(d, "writeback_enabled", group.WritebackConfiguration.IsEnabled) - tf.Set(d, "onpremises_group_type", group.WritebackConfiguration.OnPremisesGroupType) - } + tf.Set(d, "assignable_to_role", group.IsAssignableToRole.GetOrZero()) + tf.Set(d, "behaviors", tf.FlattenStringSlicePtr(group.ResourceBehaviorOptions)) + tf.Set(d, "description", group.Description.GetOrZero()) + tf.Set(d, "display_name", group.DisplayName.GetOrZero()) + tf.Set(d, "mail_enabled", group.MailEnabled.GetOrZero()) + tf.Set(d, "mail", group.Mail.GetOrZero()) + tf.Set(d, "mail_nickname", group.MailNickname.GetOrZero()) + tf.Set(d, "object_id", pointer.From(group.Id)) + tf.Set(d, "onpremises_domain_name", group.OnPremisesDomainName.GetOrZero()) + tf.Set(d, "onpremises_netbios_name", group.OnPremisesNetBiosName.GetOrZero()) + tf.Set(d, "onpremises_sam_account_name", group.OnPremisesSamAccountName.GetOrZero()) + tf.Set(d, "onpremises_security_identifier", group.OnPremisesSecurityIdentifier.GetOrZero()) + tf.Set(d, "onpremises_sync_enabled", group.OnPremisesSyncEnabled.GetOrZero()) + tf.Set(d, "preferred_language", group.PreferredLanguage.GetOrZero()) + tf.Set(d, "provisioning_options", tf.FlattenStringSlicePtr(group.ResourceProvisioningOptions)) + tf.Set(d, "proxy_addresses", tf.FlattenStringSlicePtr(group.ProxyAddresses)) + tf.Set(d, "security_enabled", group.SecurityEnabled.GetOrZero()) + tf.Set(d, "theme", group.Theme.GetOrZero()) + tf.Set(d, "types", tf.FlattenStringSlicePtr(group.GroupTypes)) + tf.Set(d, "visibility", group.Visibility.GetOrZero()) + + dynamicMembership := make([]interface{}, 0) + if !group.MembershipRule.IsNull() { + enabled := true + if group.MembershipRuleProcessingState.GetOrZero() == "Paused" { + enabled = false + } + dynamicMembership = append(dynamicMembership, map[string]interface{}{ + "enabled": enabled, + "rule": group.MembershipRule.GetOrZero(), + }) + } + tf.Set(d, "dynamic_membership", dynamicMembership) - var allowExternalSenders, autoSubscribeNewMembers, hideFromAddressLists, hideFromOutlookClients bool - if group.GroupTypes != nil && hasGroupType(*group.GroupTypes, msgraph.GroupTypeUnified) { - groupExtra, err := groupGetAdditional(ctx, client, d.Id()) - if err != nil { - return tf.ErrorDiagF(err, "Could not retrieve group with object UID %q", d.Id()) + if group.WritebackConfiguration != nil { + tf.Set(d, "writeback_enabled", group.WritebackConfiguration.IsEnabled.GetOrZero()) + tf.Set(d, "onpremises_group_type", group.WritebackConfiguration.OnPremisesGroupType.GetOrZero()) } - if groupExtra != nil { - if groupExtra.AllowExternalSenders != nil { - allowExternalSenders = *groupExtra.AllowExternalSenders + var allowExternalSenders, autoSubscribeNewMembers, hideFromAddressLists, hideFromOutlookClients bool + if group.GroupTypes != nil && slices.Contains(*group.GroupTypes, GroupTypeUnified) { + // Retrieve these properties in a separate request to sidestep API bugs + groupExtra, err := groupGetAdditional(ctx, client, id) + if err != nil { + return tf.ErrorDiagF(err, "Could not retrieve group with object UID %q", d.Id()) } - if groupExtra.AutoSubscribeNewMembers != nil { - autoSubscribeNewMembers = *groupExtra.AutoSubscribeNewMembers + + if groupExtra != nil { + allowExternalSenders = groupExtra.AllowExternalSenders.GetOrZero() + autoSubscribeNewMembers = groupExtra.AutoSubscribeNewMembers.GetOrZero() + hideFromAddressLists = groupExtra.HideFromAddressLists.GetOrZero() + hideFromOutlookClients = groupExtra.HideFromOutlookClients.GetOrZero() } - if groupExtra.HideFromAddressLists != nil { - hideFromAddressLists = *groupExtra.HideFromAddressLists + } + + tf.Set(d, "auto_subscribe_new_members", autoSubscribeNewMembers) + tf.Set(d, "external_senders_allowed", allowExternalSenders) + tf.Set(d, "hide_from_address_lists", hideFromAddressLists) + tf.Set(d, "hide_from_outlook_clients", hideFromOutlookClients) + + owners := make([]string, 0) + if resp, err := ownerClient.ListOwners(ctx, id, ownerBeta.DefaultListOwnersOperationOptions()); err != nil { + return tf.ErrorDiagPathF(err, "owners", "Could not retrieve owners for %s", id) + } else if resp.Model != nil { + for _, o := range *resp.Model { + owners = append(owners, pointer.From(o.DirectoryObject().Id)) } - if groupExtra.HideFromOutlookClients != nil { - hideFromOutlookClients = *groupExtra.HideFromOutlookClients + } + tf.Set(d, "owners", owners) + + members := make([]string, 0) + if resp, err := memberClient.ListMembers(ctx, id, memberBeta.DefaultListMembersOperationOptions()); err != nil { + return tf.ErrorDiagPathF(err, "members", "Could not retrieve members for %s", id) + } else if resp.Model != nil { + for _, o := range *resp.Model { + members = append(members, pointer.From(o.DirectoryObject().Id)) } } - } - - tf.Set(d, "auto_subscribe_new_members", autoSubscribeNewMembers) - tf.Set(d, "external_senders_allowed", allowExternalSenders) - tf.Set(d, "hide_from_address_lists", hideFromAddressLists) - tf.Set(d, "hide_from_outlook_clients", hideFromOutlookClients) - - owners, _, err := client.ListOwners(ctx, *group.ID()) - if err != nil { - return tf.ErrorDiagPathF(err, "owners", "Could not retrieve owners for group with object ID %q", d.Id()) - } - tf.Set(d, "owners", owners) - - members, _, err := client.ListMembers(ctx, *group.ID()) - if err != nil { - return tf.ErrorDiagPathF(err, "owners", "Could not retrieve members for group with object ID %q", d.Id()) - } - tf.Set(d, "members", members) - - administrativeUnits, _, err := client.ListAdministrativeUnitMemberships(ctx, *group.ID()) - if err != nil { - return tf.ErrorDiagPathF(err, "administrative_units", "Could not retrieve administrative units for group with object ID %q", d.Id()) - } - - auIdMembers := make([]string, 0) - for _, administrativeUnit := range *administrativeUnits { - auIdMembers = append(auIdMembers, *administrativeUnit.ID) - } + tf.Set(d, "members", members) + + administrativeUnitIds := make([]string, 0) + if resp, err := memberOfClient.ListMemberOfs(ctx, id, memberofBeta.DefaultListMemberOfsOperationOptions()); err != nil { + return tf.ErrorDiagPathF(err, "members", "Could not retrieve members for %s", id) + } else if resp.Model != nil { + for _, obj := range *resp.Model { + if _, ok := obj.(beta.AdministrativeUnit); ok { + administrativeUnitIds = append(administrativeUnitIds, *obj.DirectoryObject().Id) + } + } + } + tf.Set(d, "administrative_unit_ids", administrativeUnitIds) - if len(auIdMembers) > 0 { - tf.Set(d, "administrative_unit_ids", &auIdMembers) - } else { - tf.Set(d, "administrative_unit_ids", nil) - } + preventDuplicates := false + if v := d.Get("prevent_duplicate_names").(bool); v { + preventDuplicates = v + } + tf.Set(d, "prevent_duplicate_names", preventDuplicates) - preventDuplicates := false - if v := d.Get("prevent_duplicate_names").(bool); v { - preventDuplicates = v + return nil } - tf.Set(d, "prevent_duplicate_names", preventDuplicates) - - return nil } func groupResourceDelete(ctx context.Context, d *pluginsdk.ResourceData, meta interface{}) pluginsdk.Diagnostics { - client := meta.(*clients.Client).Groups.GroupsClient - groupId := d.Id() + client := meta.(*clients.Client).Groups.GroupClientBeta + id := beta.NewGroupID(d.Id()) - _, status, err := client.Get(ctx, groupId, odata.Query{}) + // Get the group before attempting deletion + resp, err := client.GetGroup(ctx, id, groupBeta.DefaultGetGroupOperationOptions()) if err != nil { - if status == http.StatusNotFound { - return tf.ErrorDiagPathF(fmt.Errorf("Group was not found"), "id", "Retrieving group with object ID %q", groupId) + if response.WasNotFound(resp.HttpResponse) { + return tf.ErrorDiagPathF(errors.New("group was not found"), "id", "Retrieving %s", id) } - return tf.ErrorDiagPathF(err, "id", "Retrieving group with object ID: %q", groupId) + return tf.ErrorDiagPathF(err, "id", "Retrieving %s", id) } - if _, err := client.Delete(ctx, groupId); err != nil { - return tf.ErrorDiagF(err, "Deleting group with object ID: %q", groupId) + if _, err = client.DeleteGroup(ctx, id, groupBeta.DefaultDeleteGroupOperationOptions()); err != nil { + return tf.ErrorDiagF(err, "Deleting %s", id) } // Wait for group object to be deleted - if err := helpers.WaitForDeletion(ctx, func(ctx context.Context) (*bool, error) { - defer func() { client.BaseClient.DisableRetries = false }() - client.BaseClient.DisableRetries = true - if _, status, err := client.Get(ctx, groupId, odata.Query{}); err != nil { - if status == http.StatusNotFound { + if err := consistency.WaitForDeletion(ctx, func(ctx context.Context) (*bool, error) { + if resp, err := client.GetGroup(ctx, id, groupBeta.DefaultGetGroupOperationOptions()); err != nil { + if response.WasNotFound(resp.HttpResponse) { return pointer.To(false), nil } return nil, err } return pointer.To(true), nil }); err != nil { - return tf.ErrorDiagF(err, "Waiting for deletion of group with object ID %q", groupId) + return tf.ErrorDiagF(err, "Waiting for deletion of %s", id) } return nil } - -func addGroupToAdministrativeUnit(ctx context.Context, auClient *msgraph.AdministrativeUnitsClient, tenantId, administrativeUnitId string, group *msgraph.Group) error { - members := msgraph.Members{ - group.DirectoryObject, - } - members[0].ODataId = (*odata.Id)(pointer.To(fmt.Sprintf("%s/v1.0/%s/directoryObjects/%s", - auClient.BaseClient.Endpoint, tenantId, *group.DirectoryObject.ID()))) - _, err := auClient.AddMembers(ctx, administrativeUnitId, &members) - return err -} diff --git a/internal/services/groups/group_resource_test.go b/internal/services/groups/group_resource_test.go index fb51147605..325e59f79b 100644 --- a/internal/services/groups/group_resource_test.go +++ b/internal/services/groups/group_resource_test.go @@ -6,11 +6,12 @@ package groups_test import ( "context" "fmt" - "net/http" "testing" "github.com/hashicorp/go-azure-helpers/lang/pointer" - "github.com/hashicorp/go-azure-sdk/sdk/odata" + "github.com/hashicorp/go-azure-helpers/lang/response" + "github.com/hashicorp/go-azure-sdk/microsoft-graph/common-types/beta" + groupBeta "github.com/hashicorp/go-azure-sdk/microsoft-graph/groups/beta/group" "github.com/hashicorp/terraform-plugin-testing/terraform" "github.com/hashicorp/terraform-provider-azuread/internal/acceptance" "github.com/hashicorp/terraform-provider-azuread/internal/acceptance/check" @@ -51,13 +52,13 @@ func TestAccGroup_basicUnified(t *testing.T) { }) } -func TestAccGroup_complete(t *testing.T) { +func TestAccGroup_completeUnified(t *testing.T) { data := acceptance.BuildTestData(t, "azuread_group", "test") r := GroupResource{} data.ResourceTest(t, r, []acceptance.TestStep{ { - Config: r.complete(data), + Config: r.completeUnified(data), Check: acceptance.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), ), @@ -66,13 +67,13 @@ func TestAccGroup_complete(t *testing.T) { }) } -func TestAccGroup_update(t *testing.T) { +func TestAccGroup_updateUnified(t *testing.T) { data := acceptance.BuildTestData(t, "azuread_group", "test") r := GroupResource{} data.ResourceTest(t, r, []acceptance.TestStep{ { - Config: r.basic(data), + Config: r.basicUnified(data), Check: acceptance.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), ), @@ -86,7 +87,7 @@ func TestAccGroup_update(t *testing.T) { }, data.ImportStep(), { - Config: r.complete(data), + Config: r.completeUnified(data), Check: acceptance.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), ), @@ -553,18 +554,17 @@ func TestAccGroup_writebackUnified(t *testing.T) { } func (r GroupResource) Exists(ctx context.Context, clients *clients.Client, state *terraform.InstanceState) (*bool, error) { - client := clients.Groups.GroupsClient - client.BaseClient.DisableRetries = true - defer func() { client.BaseClient.DisableRetries = false }() + client := clients.Groups.GroupClientBeta + id := beta.NewGroupID(state.ID) - group, status, err := client.Get(ctx, state.ID, odata.Query{}) + resp, err := client.GetGroup(ctx, id, groupBeta.DefaultGetGroupOperationOptions()) if err != nil { - if status == http.StatusNotFound { - return nil, fmt.Errorf("Group with object ID %q does not exist", state.ID) + if response.WasNotFound(resp.HttpResponse) { + return pointer.To(false), nil } - return nil, fmt.Errorf("failed to retrieve Group with object ID %q: %+v", state.ID, err) + return nil, fmt.Errorf("failed to retrieve %s: %v", id, err) } - return pointer.To(group.ID() != nil && *group.ID() == state.ID), nil + return pointer.To(true), nil } func (GroupResource) templateDiverseDirectoryObjects(data acceptance.TestData) string { @@ -659,8 +659,9 @@ resource "azuread_group" "test" { func (r GroupResource) unifiedAsUser(data acceptance.TestData) string { return fmt.Sprintf(` provider "azuread" { - client_id = "" - use_cli = true + client_id = "" + client_id_file_path = "" + use_cli = true } %[1]s @@ -670,8 +671,9 @@ provider "azuread" { func (GroupResource) unifiedWithExtraSettings(data acceptance.TestData) string { return fmt.Sprintf(` provider "azuread" { - client_id = "" - use_cli = true + client_id = "" + client_id_file_path = "" + use_cli = true } resource "azuread_group" "test" { @@ -708,7 +710,7 @@ resource "azuread_group" "test" { `, data.RandomInteger, onPremisesGroupType) } -func (GroupResource) complete(data acceptance.TestData) string { +func (GroupResource) completeUnified(data acceptance.TestData) string { return fmt.Sprintf(` data "azuread_domains" "test" { only_initial = true diff --git a/internal/services/groups/groups.go b/internal/services/groups/groups.go index 403f70a4f0..b4d4da5beb 100644 --- a/internal/services/groups/groups.go +++ b/internal/services/groups/groups.go @@ -7,10 +7,12 @@ import ( "context" "fmt" "math/rand" - "net/http" - "github.com/hashicorp/go-azure-sdk/sdk/odata" - "github.com/manicminer/hamilton/msgraph" + "github.com/hashicorp/go-azure-helpers/lang/pointer" + "github.com/hashicorp/go-azure-helpers/lang/response" + "github.com/hashicorp/go-azure-sdk/microsoft-graph/common-types/beta" + groupBeta "github.com/hashicorp/go-azure-sdk/microsoft-graph/groups/beta/group" + memberBeta "github.com/hashicorp/go-azure-sdk/microsoft-graph/groups/beta/member" ) func groupDefaultMailNickname() string { @@ -23,19 +25,20 @@ func groupDefaultMailNickname() string { return resultString[:8] + "-" + resultString[8:] } -func groupFindByName(ctx context.Context, client *msgraph.GroupsClient, displayName string) (*[]msgraph.Group, error) { - query := odata.Query{ - Filter: fmt.Sprintf("displayName eq '%s'", displayName), +func groupFindByName(ctx context.Context, client *groupBeta.GroupClient, displayName string) (*[]beta.Group, error) { + options := groupBeta.ListGroupsOperationOptions{ + Filter: pointer.To(fmt.Sprintf("displayName eq '%s'", displayName)), } - groups, _, err := client.List(ctx, query) + + resp, err := client.ListGroups(ctx, options) if err != nil { - return nil, fmt.Errorf("unable to list Groups with filter %q: %+v", query.Filter, err) + return nil, fmt.Errorf("unable to list Groups with filter %q: %v", *options.Filter, err) } - result := make([]msgraph.Group, 0) - if groups != nil { - for _, group := range *groups { - if group.DisplayName != nil && *group.DisplayName == displayName { + result := make([]beta.Group, 0) + if resp.Model != nil { + for _, group := range *resp.Model { + if group.DisplayName != nil && group.DisplayName.GetOrZero() == displayName { result = append(result, group) } } @@ -44,26 +47,52 @@ func groupFindByName(ctx context.Context, client *msgraph.GroupsClient, displayN return &result, nil } -func groupGetAdditional(ctx context.Context, client *msgraph.GroupsClient, id string) (*msgraph.Group, error) { - query := odata.Query{Select: []string{"allowExternalSenders", "autoSubscribeNewMembers", "hideFromAddressLists", "hideFromOutlookClients"}} - groupExtra, status, err := client.Get(ctx, id, query) +func groupGetAdditional(ctx context.Context, client *groupBeta.GroupClient, id beta.GroupId) (*beta.Group, error) { + options := groupBeta.GetGroupOperationOptions{ + Select: &[]string{ + "allowExternalSenders", + "autoSubscribeNewMembers", + "hideFromAddressLists", + "hideFromOutlookClients", + }, + } + + resp, err := client.GetGroup(ctx, id, options) if err != nil { - if status == http.StatusNotFound { + if response.WasNotFound(resp.HttpResponse) { // API returns 404 when these M365-only fields are requested for a group in a non-M365 tenant, so we // don't raise an error in this case and proceed as if they are not set. // See https://github.com/microsoftgraph/msgraph-metadata/issues/333 return nil, nil } + return nil, fmt.Errorf("retrieving additional fields: %+v", err) } - return groupExtra, nil + + return resp.Model, nil } -func hasGroupType(groupTypes []msgraph.GroupType, value msgraph.GroupType) bool { - for _, v := range groupTypes { - if value == v { - return true +func groupGetMember(ctx context.Context, client *memberBeta.MemberClient, id beta.GroupIdMemberId) (*beta.DirectoryObject, error) { + options := memberBeta.ListMembersOperationOptions{ + Filter: pointer.To(fmt.Sprintf("id eq '%s'", id.DirectoryObjectId)), + } + + resp, err := client.ListMembers(ctx, beta.NewGroupID(id.GroupId), options) + if err != nil { + if response.WasNotFound(resp.HttpResponse) { + return nil, nil + } else { + return nil, err } } - return false + + if resp.Model != nil { + for _, member := range *resp.Model { + if member.DirectoryObject().Id != nil && *member.DirectoryObject().Id == id.DirectoryObjectId { + return &member, nil + } + } + } + + return nil, nil } diff --git a/internal/services/groups/groups_data_source.go b/internal/services/groups/groups_data_source.go index c04762b3db..679b545bc2 100644 --- a/internal/services/groups/groups_data_source.go +++ b/internal/services/groups/groups_data_source.go @@ -9,16 +9,18 @@ import ( "encoding/base64" "errors" "fmt" - "net/http" + "github.com/hashicorp/go-azure-sdk/sdk/odata" "strings" "time" - "github.com/hashicorp/go-azure-sdk/sdk/odata" + "github.com/hashicorp/go-azure-helpers/lang/pointer" + "github.com/hashicorp/go-azure-helpers/lang/response" + "github.com/hashicorp/go-azure-sdk/microsoft-graph/common-types/beta" + groupBeta "github.com/hashicorp/go-azure-sdk/microsoft-graph/groups/beta/group" "github.com/hashicorp/terraform-provider-azuread/internal/clients" - "github.com/hashicorp/terraform-provider-azuread/internal/tf" - "github.com/hashicorp/terraform-provider-azuread/internal/tf/pluginsdk" - "github.com/hashicorp/terraform-provider-azuread/internal/tf/validation" - "github.com/manicminer/hamilton/msgraph" + "github.com/hashicorp/terraform-provider-azuread/internal/helpers/tf" + "github.com/hashicorp/terraform-provider-azuread/internal/helpers/tf/pluginsdk" + "github.com/hashicorp/terraform-provider-azuread/internal/helpers/tf/validation" ) func groupsDataSource() *pluginsdk.Resource { @@ -37,8 +39,8 @@ func groupsDataSource() *pluginsdk.Resource { Computed: true, ExactlyOneOf: []string{"display_names", "display_name_prefix", "object_ids", "return_all"}, Elem: &pluginsdk.Schema{ - Type: pluginsdk.TypeString, - ValidateDiagFunc: validation.ValidateDiag(validation.IsUUID), + Type: pluginsdk.TypeString, + ValidateFunc: validation.IsUUID, }, }, @@ -49,18 +51,18 @@ func groupsDataSource() *pluginsdk.Resource { Computed: true, ExactlyOneOf: []string{"display_names", "display_name_prefix", "object_ids", "return_all"}, Elem: &pluginsdk.Schema{ - Type: pluginsdk.TypeString, - ValidateDiagFunc: validation.ValidateDiag(validation.StringIsNotEmpty), + Type: pluginsdk.TypeString, + ValidateFunc: validation.StringIsNotEmpty, }, }, "display_name_prefix": { - Description: "Common display name prefix of the groups", - Type: pluginsdk.TypeString, - Optional: true, - Computed: true, - ExactlyOneOf: []string{"display_names", "display_name_prefix", "object_ids", "return_all"}, - ValidateDiagFunc: validation.ValidateDiag(validation.StringIsNotEmpty), + Description: "Common display name prefix of the groups", + Type: pluginsdk.TypeString, + Optional: true, + Computed: true, + ExactlyOneOf: []string{"display_names", "display_name_prefix", "object_ids", "return_all"}, + ValidateFunc: validation.StringIsNotEmpty, }, "ignore_missing": { @@ -99,11 +101,9 @@ func groupsDataSource() *pluginsdk.Resource { } func groupsDataSourceRead(ctx context.Context, d *pluginsdk.ResourceData, meta interface{}) pluginsdk.Diagnostics { - client := meta.(*clients.Client).Groups.GroupsClient - client.BaseClient.DisableRetries = true - defer func() { client.BaseClient.DisableRetries = false }() + client := meta.(*clients.Client).Groups.GroupClientBeta - var groups []msgraph.Group + var groups []beta.Group var expectedCount int var ignoreMissing = d.Get("ignore_missing").(bool) var returnAll = d.Get("return_all").(bool) @@ -124,46 +124,53 @@ func groupsDataSourceRead(ctx context.Context, d *pluginsdk.ResourceData, meta i } if returnAll { - result, _, err := client.List(ctx, odata.Query{Filter: strings.Join(filter, " and ")}) + options := groupBeta.ListGroupsOperationOptions{ + Filter: pointer.To(strings.Join(filter, " and ")), + } + resp, err := client.ListGroups(ctx, options) if err != nil { return tf.ErrorDiagF(err, "Could not retrieve groups") } - if result == nil { - return tf.ErrorDiagF(errors.New("API returned nil result"), "Bad API Response") + if resp.Model == nil { + return tf.ErrorDiagF(errors.New("model was nil"), "Bad API Response") } - if len(*result) == 0 { + if len(*resp.Model) == 0 { return tf.ErrorDiagPathF(err, "return_all", "No groups found") } - groups = append(groups, *result...) + groups = append(groups, *resp.Model...) } else if displayNamePrefix != "" { - query := odata.Query{Filter: fmt.Sprintf("startsWith(displayName, '%s')", displayNamePrefix)} - result, _, err := client.List(ctx, query) + options := groupBeta.ListGroupsOperationOptions{ + Filter: pointer.To(strings.Join(append(filter, fmt.Sprintf("startsWith(displayName, '%s')", odata.EscapeSingleQuote(displayNamePrefix))), " and ")), + } + resp, err := client.ListGroups(ctx, options) if err != nil { return tf.ErrorDiagPathF(err, "display_name_prefix", "No groups found with display name prefix: %q", displayNamePrefix) } - if result == nil { + if resp.Model == nil { return tf.ErrorDiagF(errors.New("API returned nil result"), "Bad API response") } - if len(*result) == 0 { + if len(*resp.Model) == 0 { return tf.ErrorDiagPathF(err, "display_name_prefix", "No groups found with display name prefix: %q", displayNamePrefix) } - groups = append(groups, *result...) + groups = append(groups, *resp.Model...) } else if len(displayNames) > 0 { expectedCount = len(displayNames) for _, v := range displayNames { displayName := v.(string) - query := odata.Query{Filter: strings.Join(append(filter, fmt.Sprintf("displayName eq '%s'", displayName)), " and ")} - result, _, err := client.List(ctx, query) + options := groupBeta.ListGroupsOperationOptions{ + Filter: pointer.To(strings.Join(append(filter, fmt.Sprintf("displayName eq '%s'", odata.EscapeSingleQuote(displayName))), " and ")), + } + resp, err := client.ListGroups(ctx, options) if err != nil { return tf.ErrorDiagPathF(err, "display_names", "No group found with display name: %q", displayName) } - if result == nil { - return tf.ErrorDiagF(errors.New("API returned nil result"), "Bad API response") + if resp.Model == nil { + return tf.ErrorDiagF(errors.New("model was nil"), "Bad API response") } - count := len(*result) + count := len(*resp.Model) if count > 1 { return tf.ErrorDiagPathF(err, "display_names", "More than one group found with display name: %q", displayName) } else if count == 0 { @@ -173,46 +180,46 @@ func groupsDataSourceRead(ctx context.Context, d *pluginsdk.ResourceData, meta i return tf.ErrorDiagPathF(err, "display_names", "No group found with display name: %q", displayName) } - groups = append(groups, (*result)[0]) + groups = append(groups, (*resp.Model)[0]) } } else if objectIds, ok := d.Get("object_ids").([]interface{}); ok && len(objectIds) > 0 { expectedCount = len(objectIds) for _, v := range objectIds { - objectId := v.(string) - group, status, err := client.Get(ctx, objectId, odata.Query{}) + id := beta.NewGroupID(v.(string)) + resp, err := client.GetGroup(ctx, id, groupBeta.DefaultGetGroupOperationOptions()) if err != nil { - if status == http.StatusNotFound { + if response.WasNotFound(resp.HttpResponse) { if ignoreMissing { continue } - return tf.ErrorDiagPathF(err, "object_id", "No group found with object ID: %q", objectId) + return tf.ErrorDiagPathF(err, "object_id", "No group found with object ID: %q", id.GroupId) } - return tf.ErrorDiagPathF(err, "object_id", "Retrieving group with object ID: %q", objectId) + return tf.ErrorDiagPathF(err, "object_id", "Retrieving group with object ID: %q", id.GroupId) } - if group == nil { - return tf.ErrorDiagF(errors.New("API returned nil group"), "Bad API response") + if resp.Model == nil { + return tf.ErrorDiagF(errors.New("model was nil"), "Bad API response") } - groups = append(groups, *group) + groups = append(groups, *resp.Model) } } if !returnAll && !ignoreMissing && displayNamePrefix == "" && len(groups) != expectedCount { - return tf.ErrorDiagF(fmt.Errorf("Expected: %d, Actual: %d", expectedCount, len(groups)), "Unexpected number of groups returned") + return tf.ErrorDiagF(fmt.Errorf("expected: %d, actual: %d", expectedCount, len(groups)), "Unexpected number of groups returned") } newDisplayNames := make([]string, 0) newObjectIds := make([]string, 0) for _, group := range groups { - if group.ID() == nil { - return tf.ErrorDiagF(errors.New("API returned group with nil object ID"), "Bad API response") + if group.Id == nil { + return tf.ErrorDiagF(errors.New("group ID was nil"), "Bad API response") } - if group.DisplayName == nil { - return tf.ErrorDiagF(errors.New("API returned group with nil displayName"), "Bad API response") + if group.DisplayName.IsNull() { + return tf.ErrorDiagF(errors.New("displayName was nil"), "Bad API response") } - newObjectIds = append(newObjectIds, *group.ID()) - newDisplayNames = append(newDisplayNames, *group.DisplayName) + newObjectIds = append(newObjectIds, *group.Id) + newDisplayNames = append(newDisplayNames, group.DisplayName.GetOrZero()) } h := sha1.New() diff --git a/internal/services/groups/groups_data_source_test.go b/internal/services/groups/groups_data_source_test.go index ac5fa533f9..b9b72027c9 100644 --- a/internal/services/groups/groups_data_source_test.go +++ b/internal/services/groups/groups_data_source_test.go @@ -8,8 +8,10 @@ import ( "fmt" "regexp" "testing" + "time" - "github.com/hashicorp/go-azure-sdk/sdk/odata" + "github.com/hashicorp/go-azure-sdk/microsoft-graph/common-types/beta" + groupBeta "github.com/hashicorp/go-azure-sdk/microsoft-graph/groups/beta/group" "github.com/hashicorp/terraform-provider-azuread/internal/acceptance" "github.com/hashicorp/terraform-provider-azuread/internal/acceptance/check" "github.com/hashicorp/terraform-provider-azuread/internal/clients" @@ -22,6 +24,9 @@ func TestAccGroupsDataSource_byDisplayNames(t *testing.T) { r := GroupsDataSource{} data.DataSourceTest(t, []acceptance.TestStep{ + { + Config: r.template(data), + }, { Config: r.byDisplayNames(data), Check: acceptance.ComposeAggregateTestCheckFunc( @@ -37,6 +42,9 @@ func TestAccGroupsDataSource_byDisplayNamesIgnoreMissing(t *testing.T) { r := GroupsDataSource{} data.DataSourceTest(t, []acceptance.TestStep{ + { + Config: r.template(data), + }, { Config: r.byDisplayNamesIgnoreMissing(data), Check: acceptance.ComposeAggregateTestCheckFunc( @@ -53,6 +61,9 @@ func TestAccGroupsDataSource_byDisplayNamePrefix(t *testing.T) { moreThanZero := regexp.MustCompile("^[1-9][0-9]*$") data.DataSourceTest(t, []acceptance.TestStep{ + { + Config: r.template(data), + }, { Config: r.byDisplayNamePrefix(data), Check: acceptance.ComposeAggregateTestCheckFunc( @@ -68,6 +79,9 @@ func TestAccGroupsDataSource_byObjectIds(t *testing.T) { r := GroupsDataSource{} data.DataSourceTest(t, []acceptance.TestStep{ + { + Config: r.template(data), + }, { Config: r.byObjectIds(data), Check: acceptance.ComposeAggregateTestCheckFunc( @@ -94,10 +108,14 @@ func TestAccGroupsDataSource_noNames(t *testing.T) { func TestAccGroupsDataSource_returnAll(t *testing.T) { data := acceptance.BuildTestData(t, "data.azuread_groups", "test") + r := GroupsDataSource{} data.DataSourceTest(t, []acceptance.TestStep{ { - Config: GroupsDataSource{}.returnAll(), + Config: r.template(data), + }, + { + Config: r.returnAll(), Check: acceptance.ComposeAggregateTestCheckFunc( check.That(data.ResourceName).Key("display_names.#").Exists(), check.That(data.ResourceName).Key("object_ids.#").Exists(), @@ -108,10 +126,14 @@ func TestAccGroupsDataSource_returnAll(t *testing.T) { func TestAccGroupsDataSource_returnAllMailEnabled(t *testing.T) { data := acceptance.BuildTestData(t, "data.azuread_groups", "test") + r := GroupsDataSource{} data.DataSourceTest(t, []acceptance.TestStep{ { - Config: GroupsDataSource{}.returnAllMailEnabled(data), + Config: r.template(data), + }, + { + Config: r.returnAllMailEnabled(data), Check: acceptance.ComposeAggregateTestCheckFunc( check.That(data.ResourceName).Key("display_names.#").Exists(), check.That(data.ResourceName).Key("object_ids.#").Exists(), @@ -123,10 +145,14 @@ func TestAccGroupsDataSource_returnAllMailEnabled(t *testing.T) { func TestAccGroupsDataSource_returnAllSecurityEnabled(t *testing.T) { data := acceptance.BuildTestData(t, "data.azuread_groups", "test") + r := GroupsDataSource{} data.DataSourceTest(t, []acceptance.TestStep{ { - Config: GroupsDataSource{}.returnAllSecurityEnabled(data), + Config: r.template(data), + }, + { + Config: r.returnAllSecurityEnabled(data), Check: acceptance.ComposeAggregateTestCheckFunc( check.That(data.ResourceName).Key("display_names.#").Exists(), check.That(data.ResourceName).Key("object_ids.#").Exists(), @@ -138,10 +164,14 @@ func TestAccGroupsDataSource_returnAllSecurityEnabled(t *testing.T) { func TestAccGroupsDataSource_returnAllMailNotSecurityEnabled(t *testing.T) { data := acceptance.BuildTestData(t, "data.azuread_groups", "test") + r := GroupsDataSource{} data.DataSourceTest(t, []acceptance.TestStep{ { - Config: GroupsDataSource{}.returnAllMailNotSecurityEnabled(data), + Config: r.template(data), + }, + { + Config: r.returnAllMailNotSecurityEnabled(data), Check: acceptance.ComposeAggregateTestCheckFunc( check.That(data.ResourceName).Key("display_names.#").Exists(), check.That(data.ResourceName).Key("object_ids.#").Exists(), @@ -153,10 +183,14 @@ func TestAccGroupsDataSource_returnAllMailNotSecurityEnabled(t *testing.T) { func TestAccGroupsDataSource_returnAllSecurityNotMailEnabled(t *testing.T) { data := acceptance.BuildTestData(t, "data.azuread_groups", "test") + r := GroupsDataSource{} data.DataSourceTest(t, []acceptance.TestStep{ { - Config: GroupsDataSource{}.returnAllSecurityNotMailEnabled(data), + Config: r.template(data), + }, + { + Config: r.returnAllSecurityNotMailEnabled(data), Check: acceptance.ComposeAggregateTestCheckFunc( check.That(data.ResourceName).Key("display_names.#").Exists(), check.That(data.ResourceName).Key("object_ids.#").Exists(), @@ -184,34 +218,36 @@ func testCheckHasOnlySecurityEnabledGroupsNotMailEnabledGroups() check.KeyValida func testCheckGroupsDataSource(hasMailGroupsOnly, hasSecurityGroupsOnly, hasNoMailGroups, hasNoSecurityGroups bool) check.KeyValidationFunc { return func(ctx context.Context, clients *clients.Client, values []interface{}) error { - client := clients.Groups.GroupsClient + ctx, _ = context.WithTimeout(ctx, 5*time.Minute) + client := clients.Groups.GroupClientBeta for _, v := range values { - oid := v.(string) - group, _, err := client.Get(ctx, oid, odata.Query{}) + id := beta.NewGroupID(v.(string)) + resp, err := client.GetGroup(ctx, id, groupBeta.DefaultGetGroupOperationOptions()) if err != nil { - return fmt.Errorf("retrieving group with object ID %q: %+oid", oid, err) + return fmt.Errorf("retrieving %s: %v", id, err) } + group := resp.Model if group == nil { - return fmt.Errorf("retrieving group with object ID %q: group was nil", oid) + return fmt.Errorf("retrieving %s: group was nil", id) } - if group.ID() == nil { - return fmt.Errorf("retrieving group with object ID %q: ID was nil", oid) + if group.Id == nil { + return fmt.Errorf("retrieving %s: ID was nil", id) } if group.DisplayName == nil { - return fmt.Errorf("retrieving group with object ID %q: DisplayName was nil", oid) + return fmt.Errorf("retrieving %s: DisplayName was nil", id) } - if hasMailGroupsOnly && group.MailEnabled != nil && !*group.MailEnabled { - return fmt.Errorf("expected only mail-enabled groups, encountered group %q (object ID: %q) which is not mail-enabled", *group.DisplayName, *group.ID()) + if hasMailGroupsOnly && !group.MailEnabled.GetOrZero() { + return fmt.Errorf("expected only mail-enabled groups, encountered %s which is not mail-enabled", id) } - if hasSecurityGroupsOnly && group.SecurityEnabled != nil && !*group.SecurityEnabled { - return fmt.Errorf("expected only security-enabled groups, encountered group %q (object ID: %q) which is not security-enabled", *group.DisplayName, *group.ID()) + if hasSecurityGroupsOnly && !group.SecurityEnabled.GetOrZero() { + return fmt.Errorf("expected only security-enabled groups, encountered %s which is not security-enabled", id) } - if hasNoMailGroups && group.MailEnabled != nil && *group.MailEnabled { - return fmt.Errorf("expected no mail-enabled groups, encountered group %q (object ID: %q) which is mail-enabled", *group.DisplayName, *group.ID()) + if hasNoMailGroups && group.MailEnabled.GetOrZero() { + return fmt.Errorf("expected no mail-enabled groups, encountered %s which is mail-enabled", id) } - if hasNoSecurityGroups && group.SecurityEnabled != nil && *group.SecurityEnabled { - return fmt.Errorf("expected no security-enabled groups, encountered group %q (object ID: %q) which is security-enabled", *group.DisplayName, *group.ID()) + if hasNoSecurityGroups && group.SecurityEnabled.GetOrZero() { + return fmt.Errorf("expected no security-enabled groups, encountered %s which is security-enabled", id) } } diff --git a/internal/services/groups/registration.go b/internal/services/groups/registration.go index 76e9ba770d..9a1dfe8323 100644 --- a/internal/services/groups/registration.go +++ b/internal/services/groups/registration.go @@ -3,7 +3,7 @@ package groups -import "github.com/hashicorp/terraform-provider-azuread/internal/tf/pluginsdk" +import "github.com/hashicorp/terraform-provider-azuread/internal/helpers/tf/pluginsdk" type Registration struct{}