Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

azuread_group: support for dynamic_memberships #695

Merged
merged 3 commits into from
Dec 15, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion docs/data-sources/group.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ The following attributes are exported:
* `behaviors` - A list of behaviors for a Microsoft 365 group, such as `AllowOnlyMembersToPost`, `HideGroupInOutlook`, `SubscribeNewGroupMembers` and `WelcomeEmailDisabled`. See [official documentation](https://docs.microsoft.com/en-us/graph/group-set-options) for more details.
* `description` - The optional description of the group.
* `display_name` - The display name for the group.
* `dynamic_membership` - A `dynamic_membership` block as documented below.
* `object_id` - The object ID of the group.
* `mail` - The SMTP address for the group.
* `mail_enabled` - Whether the group is mail-enabled.
Expand All @@ -58,5 +59,12 @@ The following attributes are exported:
* `proxy_addresses` - List of email addresses for the group that direct to the same group mailbox.
* `security_enabled` - Whether the group is a security group.
* `theme` - The colour theme for a Microsoft 365 group. Possible values are `Blue`, `Green`, `Orange`, `Pink`, `Purple`, `Red` or `Teal`. When no theme is set, the value is `null`.
* `types` - A list of group types configured for the group. The only supported type is `Unified`, which specifies a Microsoft 365 group.
* `types` - A list of group types configured for the group. Supported values are `DynamicMembership`, which denotes a group with dynamic membership, and `Unified`, which specifies a Microsoft 365 group.
* `visibility` - The group join policy and group content visibility. Possible values are `Private`, `Public`, or `Hiddenmembership`. Only Microsoft 365 groups can have `Hiddenmembership` visibility.

---

`dynamic_membership` block exports the following:

* `enabled` - Whether rule processing is "On" (true) or "Paused" (false).
* `rule` - The rule that determines membership of this group.
32 changes: 30 additions & 2 deletions docs/resources/group.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,24 @@ resource "azuread_group" "example" {
}
```

*Group with dynamic membership*

```terraform
data "azuread_client_config" "current" {}

resource "azuread_group" "example" {
display_name = "MyGroup"
owners = [data.azuread_client_config.current.object_id]
security_enabled = true
types = ["DynamicMembership"]

dynamic_membership {
enabled = true
rule = "user.department -eq \"Sales\""
}
}
```

## Argument Reference

The following arguments are supported:
Expand All @@ -88,9 +106,10 @@ The following arguments are supported:
* `behaviors` - (Optional) A set of behaviors for a Microsoft 365 group. Possible values are `AllowOnlyMembersToPost`, `HideGroupInOutlook`, `SubscribeNewGroupMembers` and `WelcomeEmailDisabled`. See [official documentation](https://docs.microsoft.com/en-us/graph/group-set-options) for more details. Changing this forces a new resource to be created.
* `description` - (Optional) The description for the group.
* `display_name` - (Required) The display name for the group.
* `dynamic_membership` - (Optional) A `dynamic_membership` block as documented below. Required when `types` contains `DynamicMembership`. Cannot be used with the `members` property.
* `mail_enabled` - (Optional) Whether the group is a mail enabled, with a shared group mailbox. At least one of `mail_enabled` or `security_enabled` must be specified. Only Microsoft 365 groups can be mail enabled (see the `types` property).
* `mail_nickname` - (Optional) The mail alias for the group, unique in the organisation. Required for mail-enabled groups. Changing this forces a new resource to be created.
* `members` - (Optional) A set of members who should be present in this group. Supported object types are Users, Groups or Service Principals.
* `members` - (Optional) A set of members who should be present in this group. Supported object types are Users, Groups or Service Principals. Cannot be used with the `dynamic_membership` block.

!> **Warning** Do not use the `members` property at the same time as the [azuread_group_member](https://registry.terraform.io/providers/hashicorp/azuread/latest/docs/resources/group_member) resource for the same group. Doing so will cause a conflict and group members will be removed.

Expand All @@ -102,14 +121,23 @@ The following arguments are supported:
* `provisioning_options` - (Optional) A set of provisioning options for a Microsoft 365 group. The only supported value is `Team`. See [official documentation](https://docs.microsoft.com/en-us/graph/group-set-options) for details. Changing this forces a new resource to be created.
* `security_enabled` - (Optional) Whether the group is a security group for controlling access to in-app resources. At least one of `security_enabled` or `mail_enabled` must be specified. A Microsoft 365 group can be security enabled _and_ mail enabled (see the `types` property).
* `theme` - (Optional) The colour theme for a Microsoft 365 group. Possible values are `Blue`, `Green`, `Orange`, `Pink`, `Purple`, `Red` or `Teal`. By default, no theme is set.
* `types` - (Optional) A set of group types to configure for the group. The only supported type is `Unified`, which specifies a Microsoft 365 group. Required when `mail_enabled` is true. Changing this forces a new resource to be created.
* `types` - (Optional) A set of group types to configure for the group. Supported values are `DynamicMembership`, which denotes a group with dynamic membership, and `Unified`, which specifies a Microsoft 365 group. Required when `mail_enabled` is true. Changing this forces a new resource to be created.

-> **Supported Group Types** At present, only security groups and Microsoft 365 groups can be created or managed with this resource. Distribution groups and mail-enabled security groups are not supported. Microsoft 365 groups can be security-enabled.

* `visibility` - (Optional) The group join policy and group content visibility. Possible values are `Private`, `Public`, or `Hiddenmembership`. Only Microsoft 365 groups can have `Hiddenmembership` visibility and this value must be set when the group is created. By default, security groups will receive `Private` visibility and Microsoft 365 groups will receive `Public` visibility.

-> **Group Name Uniqueness** Group names are not unique within Azure Active Directory. Use the `prevent_duplicate_names` argument to check for existing groups if you want to avoid name collisions.

---

`dynamic_membership` block supports the following:

* `enabled` - (Required) Whether rule processing is "On" (true) or "Paused" (false).
* `rule` - (Optional) The rule that determines membership of this group. For more information, see official documentation on [memmbership rules syntax](https://docs.microsoft.com/en-gb/azure/active-directory/enterprise-users/groups-dynamic-membership).

~> **Dynamic Group Memberships** Remember to include `DynamicMembership` in the set of `types` for the group when configuring a dynamic membership rule. Dynamic membership is a premium feature which requires an Azure Active Directory P1 or P2 license.

## Attributes Reference

In addition to all arguments above, the following attributes are exported:
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ require (
github.com/hashicorp/terraform-plugin-sdk/v2 v2.8.0
github.com/hashicorp/yamux v0.0.0-20210316155119-a95892c5f864 // indirect
github.com/klauspost/compress v1.12.2 // indirect
github.com/manicminer/hamilton v0.36.1
github.com/manicminer/hamilton v0.38.0
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
github.com/mitchellh/mapstructure v1.4.1 // indirect
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -299,8 +299,8 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/manicminer/hamilton v0.36.1 h1:rIHUAYP54u70yGcl1HZjo3/DXx7B6npzVFnDSVduttQ=
github.com/manicminer/hamilton v0.36.1/go.mod h1:IOYn2Dc9SUiZ7Ryw6c8Ay795vPPMnrCZe3MktS447dc=
github.com/manicminer/hamilton v0.38.0 h1:8MWpSyfgGrf7vmjTWU3/OQk1+RXYixf2in6pPrTkIRc=
github.com/manicminer/hamilton v0.38.0/go.mod h1:IOYn2Dc9SUiZ7Ryw6c8Ay795vPPMnrCZe3MktS447dc=
github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
Expand Down
33 changes: 33 additions & 0 deletions internal/services/groups/group_data_source.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,26 @@ func groupDataSource() *schema.Resource {
Computed: true,
},

"dynamic_membership": {
Description: "An optional block to configure dynamic membership for the group. Cannot be used with `members`",
Type: schema.TypeList,
Computed: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"enabled": {
Type: schema.TypeBool,
Computed: true,
},

"rule": {
Description: "Rule to determine members for a dynamic group. Required when `group_types` contains 'DynamicMembership'",
Type: schema.TypeString,
Computed: true,
},
},
},
},

"mail": {
Description: "The SMTP address for the group",
Type: schema.TypeString,
Expand Down Expand Up @@ -291,6 +311,19 @@ func groupDataSourceRead(ctx context.Context, d *schema.ResourceData, meta inter
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)

members, _, err := client.ListMembers(ctx, d.Id())
if err != nil {
return tf.ErrorDiagF(err, "Could not retrieve group members for group with object ID: %q", d.Id())
Expand Down
26 changes: 26 additions & 0 deletions internal/services/groups/group_data_source_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,22 @@ func TestAccGroupDataSource_byObjectIdWithSecurity(t *testing.T) {
})
}

func TestAccGroupDataSource_dynamicMembership(t *testing.T) {
data := acceptance.BuildTestData(t, "data.azuread_group", "test")

data.DataSourceTest(t, []resource.TestStep{
{
Config: GroupDataSource{}.dynamicMembership(data),
Check: resource.ComposeTestCheckFunc(
check.That(data.ResourceName).Key("display_name").HasValue(fmt.Sprintf("acctestGroup-%d", data.RandomInteger)),
check.That(data.ResourceName).Key("dynamic_membership.#").HasValue("1"),
check.That(data.ResourceName).Key("dynamic_membership.0.enabled").HasValue("true"),
check.That(data.ResourceName).Key("dynamic_membership.0.rule").HasValue("user.department -eq \"Sales\""),
),
},
})
}

func TestAccGroupDataSource_members(t *testing.T) {
data := acceptance.BuildTestData(t, "data.azuread_group", "test")

Expand Down Expand Up @@ -167,6 +183,16 @@ data "azuread_group" "test" {
`, GroupResource{}.withThreeMembers(data))
}

func (GroupDataSource) dynamicMembership(data acceptance.TestData) string {
return fmt.Sprintf(`
%[1]s

data "azuread_group" "test" {
object_id = azuread_group.test.object_id
}
`, GroupResource{}.dynamicMembership(data))
}

func (GroupDataSource) owners(data acceptance.TestData) string {
return fmt.Sprintf(`
%[1]s
Expand Down
76 changes: 70 additions & 6 deletions internal/services/groups/group_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,29 @@ func groupResource() *schema.Resource {
Optional: true,
},

"dynamic_membership": {
Description: "An optional block to configure dynamic membership for the group. Cannot be used with `members`",
Type: schema.TypeList,
Optional: true,
MaxItems: 1,
ConflictsWith: []string{"members"},
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"enabled": {
Type: schema.TypeBool,
Required: true,
},

"rule": {
Description: "Rule to determine members for a dynamic group. Required when `group_types` contains 'DynamicMembership'",
Type: schema.TypeString,
Required: true,
ValidateDiagFunc: validate.ValidateDiag(validation.StringLenBetween(0, 3072)),
},
},
},
},

"mail_enabled": {
Description: "Whether the group is a mail enabled, with a shared group mailbox. At least one of `mail_enabled` or `security_enabled` must be specified. A group can be mail enabled _and_ security enabled",
Type: schema.TypeBool,
Expand All @@ -102,11 +125,12 @@ func groupResource() *schema.Resource {
},

"members": {
Description: "A set of members who should be present in this group. Supported object types are Users, Groups or Service Principals",
Type: schema.TypeSet,
Optional: true,
Computed: true,
Set: schema.HashString,
Description: "A set of members who should be present in this group. Supported object types are Users, Groups or Service Principals",
Type: schema.TypeSet,
Optional: true,
Computed: true,
ConflictsWith: []string{"dynamic_membership"},
Set: schema.HashString,
Elem: &schema.Schema{
Type: schema.TypeString,
ValidateDiagFunc: validate.UUID,
Expand Down Expand Up @@ -171,13 +195,14 @@ func groupResource() *schema.Resource {
},

"types": {
Description: "A set of group types to configure for the group. The only supported type is `Unified`, which specifies a Microsoft 365 group. Required when `mail_enabled` is true",
Description: "A set of group types to configure for the group. `Unified` specifies a Microsoft 365 group. Required when `mail_enabled` is true",
Type: schema.TypeSet,
Optional: true,
ForceNew: true,
Elem: &schema.Schema{
Type: schema.TypeString,
ValidateFunc: validation.StringInSlice([]string{
"DynamicMembership",
msgraph.GroupTypeUnified,
}, false),
},
Expand Down Expand Up @@ -294,6 +319,10 @@ func groupResourceCustomizeDiff(ctx context.Context, diff *schema.ResourceDiff,
return false
}

if hasGroupType(msgraph.GroupTypeDynamicMembership) && diff.Get("dynamic_membership.#").(int) == 0 {
return fmt.Errorf("`dynamic_membership` must be specified when `types` contains %q", msgraph.GroupTypeDynamicMembership)
}

if mailEnabled && !hasGroupType(msgraph.GroupTypeUnified) {
return fmt.Errorf("`types` must contain %q for mail-enabled groups", msgraph.GroupTypeUnified)
}
Expand Down Expand Up @@ -398,11 +427,22 @@ func groupResourceCreate(ctx context.Context, d *schema.ResourceData, meta inter
IsAssignableToRole: utils.Bool(d.Get("assignable_to_role").(bool)),
MailEnabled: utils.Bool(mailEnabled),
MailNickname: utils.String(mailNickname),
MembershipRule: utils.NullableString(""),
ResourceBehaviorOptions: behaviorOptions,
ResourceProvisioningOptions: provisioningOptions,
SecurityEnabled: utils.Bool(securityEnabled),
}

if v, ok := d.GetOk("dynamic_membership"); ok && len(v.([]interface{})) > 0 {
if d.Get("dynamic_membership.0.enabled").(bool) {
properties.MembershipRuleProcessingState = utils.String("On")
} else {
properties.MembershipRuleProcessingState = utils.String("Paused")
}

properties.MembershipRule = utils.NullableString(d.Get("dynamic_membership.0.rule").(string))
}

if theme := d.Get("theme").(string); theme != "" {
properties.Theme = utils.NullableString(theme)
}
Expand Down Expand Up @@ -597,9 +637,20 @@ func groupResourceUpdate(ctx context.Context, d *schema.ResourceData, meta inter
Description: utils.NullableString(d.Get("description").(string)),
DisplayName: utils.String(displayName),
MailEnabled: utils.Bool(d.Get("mail_enabled").(bool)),
MembershipRule: utils.NullableString(""),
SecurityEnabled: utils.Bool(d.Get("security_enabled").(bool)),
}

if v, ok := d.GetOk("dynamic_membership"); ok && len(v.([]interface{})) > 0 {
if d.Get("dynamic_membership.0.enabled").(bool) {
group.MembershipRuleProcessingState = utils.String("On")
} else {
group.MembershipRuleProcessingState = utils.String("Paused")
}

group.MembershipRule = utils.NullableString(d.Get("dynamic_membership.0.rule").(string))
}

if theme := d.Get("theme").(string); theme != "" {
group.Theme = utils.NullableString(theme)
}
Expand Down Expand Up @@ -744,6 +795,19 @@ func groupResourceRead(ctx context.Context, d *schema.ResourceData, meta interfa
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)

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())
Expand Down
Loading