Skip to content

Commit

Permalink
azuread_group: support for dynamic memberships
Browse files Browse the repository at this point in the history
  • Loading branch information
manicminer committed Dec 7, 2021
1 parent 19c6e28 commit 9848ed1
Show file tree
Hide file tree
Showing 6 changed files with 212 additions and 9 deletions.
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:

* `processing_enabled` - Whether rule processing is "On" (true) or "Paused" (false).
* `rule` - The rule that determines membership of this group.
30 changes: 29 additions & 1 deletion 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 {
processing_enabled = true
rule = "user.department -eq \"Sales\""
}
}
```

## Argument Reference

The following arguments are supported:
Expand All @@ -88,6 +106,7 @@ 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`.
* `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.
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:

* `processing_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
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{
"processing_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{}{
"processing_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.processing_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
72 changes: 66 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{
"processing_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 @@ -398,11 +423,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.processing_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 +633,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.processing_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 +791,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{}{
"processing_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
50 changes: 49 additions & 1 deletion internal/services/groups/group_resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,35 @@ func TestAccGroup_behaviors(t *testing.T) {
})
}

func TestAccGroup_dynamicMembership(t *testing.T) {
data := acceptance.BuildTestData(t, "azuread_group", "test")
r := GroupResource{}

data.ResourceTest(t, r, []resource.TestStep{
{
Config: r.dynamicMembership(data),
Check: resource.ComposeTestCheckFunc(
check.That(data.ResourceName).ExistsInAzure(r),
),
},
data.ImportStep(),
{
Config: r.unified(data),
Check: resource.ComposeTestCheckFunc(
check.That(data.ResourceName).ExistsInAzure(r),
),
},
data.ImportStep(),
{
Config: r.dynamicMembership(data),
Check: resource.ComposeTestCheckFunc(
check.That(data.ResourceName).ExistsInAzure(r),
),
},
data.ImportStep(),
})
}

func TestAccGroup_owners(t *testing.T) {
data := acceptance.BuildTestData(t, "azuread_group", "test")
r := GroupResource{}
Expand Down Expand Up @@ -413,6 +442,7 @@ func (GroupResource) unified(data acceptance.TestData) string {
return fmt.Sprintf(`
resource "azuread_group" "test" {
display_name = "acctestGroup-%[1]d"
description = "Please delete me as this is a.test.AD group!"
types = ["Unified"]
mail_enabled = true
mail_nickname = "acctestGroup-%[1]d"
Expand All @@ -437,7 +467,7 @@ resource "azuread_user" "test" {
resource "azuread_group" "test" {
description = "Please delete me as this is a.test.AD group!"
display_name = "acctestGroup-complete-%[1]d"
types = ["Unified"]
types = ["DynamicMembership", "Unified"]
mail_enabled = true
mail_nickname = "acctestGroup-%[1]d"
security_enabled = true
Expand Down Expand Up @@ -476,6 +506,24 @@ resource "azuread_group" "test" {
`, data.RandomInteger)
}

func (GroupResource) dynamicMembership(data acceptance.TestData) string {
return fmt.Sprintf(`
resource "azuread_group" "test" {
display_name = "acctestGroup-%[1]d"
description = "Please delete me as this is a.test.AD group!"
types = ["DynamicMembership", "Unified"]
mail_enabled = true
mail_nickname = "acctestGroup-%[1]d"
security_enabled = true
dynamic_membership {
rule = "user.department -eq \"Sales\""
processing_enabled = true
}
}
`, data.RandomInteger)
}

func (GroupResource) provisioning(data acceptance.TestData) string {
return fmt.Sprintf(`
resource "azuread_group" "test" {
Expand Down

0 comments on commit 9848ed1

Please sign in to comment.