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

User & Group improvements #476

Merged
merged 15 commits into from
Jul 21, 2021
Merged
Changes from 1 commit
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
Prev Previous commit
Next Next commit
Groups: support the mail_nickname property and mail attribute
manicminer committed Jul 21, 2021

Verified

This commit was signed with the committer’s verified signature.
manicminer Tom Bamford
commit e1b6db91c28f659af62665d9ca0c92b672a720eb
2 changes: 2 additions & 0 deletions docs/data-sources/group.md
Original file line number Diff line number Diff line change
@@ -34,7 +34,9 @@ The following attributes are exported:
* `description` - The optional description of the group.
* `display_name` - The display name for the group.
* `object_id` - The object ID of the group.
* `mail` - The SMTP address for the group.
* `mail_enabled` - Whether the group is mail-enabled.
* `mail_nickname` - The mail alias for the group, unique in the organisation.
* `members` - The object IDs of the group members.
* `owners` - The object IDs of the group owners.
* `security_enabled` - Whether the group is a security group.
3 changes: 3 additions & 0 deletions docs/resources/group.md
Original file line number Diff line number Diff line change
@@ -23,6 +23,7 @@ resource "azuread_group" "example" {
resource "azuread_group" "example" {
display_name = "example"
mail_enabled = true
mail_nickname = "ExampleGroup"
security_enabled = true
types = ["Unified"]
}
@@ -56,6 +57,7 @@ The following arguments are supported:
* `description` - (Optional) The description for the group.
* `display_name` - (Required) The display name for the group.
* `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. A group can be mail enabled _and_ security enabled.
* `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.
* `owners` - (Optional) A set of owners who own this group. Supported object types are Users or Service Principals.
* `prevent_duplicate_names` - (Optional) If `true`, will return an error if an existing group is found with the same name. Defaults to `false`.
@@ -70,6 +72,7 @@ The following arguments are supported:

In addition to all arguments above, the following attributes are exported:

* `mail` - The SMTP address for the group.
* `object_id` - The object ID of the group.

## Import
14 changes: 14 additions & 0 deletions internal/services/groups/group_data_source.go
Original file line number Diff line number Diff line change
@@ -75,6 +75,18 @@ func groupDataSource() *schema.Resource {
Computed: true,
},

"mail": {
Description: "The SMTP address for the group",
Type: schema.TypeString,
Computed: true,
},

"mail_nickname": {
Description: "The mail alias for the group, unique in the organisation",
Type: schema.TypeString,
Computed: true,
},

"members": {
Description: "The object IDs of the group members",
Type: schema.TypeList,
@@ -189,7 +201,9 @@ func groupDataSourceRead(ctx context.Context, d *schema.ResourceData, meta inter
tf.Set(d, "assignable_to_role", group.IsAssignableToRole)
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, "security_enabled", group.SecurityEnabled)
tf.Set(d, "types", group.GroupTypes)
39 changes: 32 additions & 7 deletions internal/services/groups/group_resource.go
Original file line number Diff line number Diff line change
@@ -67,13 +67,28 @@ func groupResource() *schema.Resource {
Optional: true,
},

"mail": {
Description: "The SMTP address for the group",
Type: schema.TypeString,
Computed: true,
},

"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,
Optional: true,
AtLeastOneOf: []string{"mail_enabled", "security_enabled"},
},

"mail_nickname": {
Description: "The mail alias for the group, unique in the organisation",
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
ValidateDiagFunc: validate.MailNickname,
},

"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,
@@ -179,6 +194,10 @@ func groupResourceCustomizeDiff(ctx context.Context, diff *schema.ResourceDiff,
return fmt.Errorf("`mail_enabled` must be true for unified groups")
}

if mailNickname := diff.Get("mail_nickname").(string); mailEnabled && mailNickname == "" {
return fmt.Errorf("`mail_nickname` is required for mail-enabled groups")
}

if diff.Get("assignable_to_role").(bool) && !securityEnabled {
return fmt.Errorf("`assignable_to_role` can only be `true` for security-enabled groups")
}
@@ -206,24 +225,28 @@ func groupResourceCreate(ctx context.Context, d *schema.ResourceData, meta inter
}
}

mailNickname, err := uuid.GenerateUUID()
if err != nil {
return tf.ErrorDiagF(err, "Failed to generate mailNickname")
}

groupTypes := make([]msgraph.GroupType, 0)
for _, v := range d.Get("types").(*schema.Set).List() {
groupTypes = append(groupTypes, msgraph.GroupType(v.(string)))
}

mailEnabled := d.Get("mail_enabled").(bool)
securityEnabled := d.Get("security_enabled").(bool)

// Mimic the portal and generate a random mailNickname for security groups
mailNickname := groupDefaultMailNickname()
if mailEnabled {
mailNickname = d.Get("mail_nickname").(string)
}

properties := msgraph.Group{
Description: utils.NullableString(d.Get("description").(string)),
DisplayName: utils.String(displayName),
GroupTypes: groupTypes,
IsAssignableToRole: utils.Bool(d.Get("assignable_to_role").(bool)),
MailEnabled: utils.Bool(d.Get("mail_enabled").(bool)),
MailEnabled: utils.Bool(mailEnabled),
MailNickname: utils.String(mailNickname),
SecurityEnabled: utils.Bool(d.Get("security_enabled").(bool)),
SecurityEnabled: utils.Bool(securityEnabled),
}

// Add the caller as the group owner to prevent lock-out after creation
@@ -394,6 +417,8 @@ func groupResourceRead(ctx context.Context, d *schema.ResourceData, meta interfa
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, "security_enabled", group.SecurityEnabled)
tf.Set(d, "types", group.GroupTypes)
17 changes: 9 additions & 8 deletions internal/services/groups/group_resource_test.go
Original file line number Diff line number Diff line change
@@ -368,6 +368,7 @@ resource "azuread_group" "test" {
display_name = "acctestGroup-%[1]d"
types = ["Unified"]
mail_enabled = true
mail_nickname = "acctestGroup-%[1]d"
security_enabled = true
}
`, data.RandomInteger)
@@ -386,14 +387,14 @@ resource "azuread_user" "test" {
}

resource "azuread_group" "test" {
assignable_to_role = true
description = "Please delete me as this is a.test.AD group!"
display_name = "acctestGroup-complete-%[1]d"
types = ["Unified"]
mail_enabled = true
security_enabled = true
members = [azuread_user.test.object_id]
owners = [azuread_user.test.object_id]
description = "Please delete me as this is a.test.AD group!"
display_name = "acctestGroup-complete-%[1]d"
types = ["Unified"]
mail_enabled = true
mail_nickname = "acctestGroup-%[1]d"
security_enabled = true
members = [azuread_user.test.object_id]
owners = [azuread_user.test.object_id]
}
`, data.RandomInteger, data.RandomPassword)
}
13 changes: 13 additions & 0 deletions internal/services/groups/groups.go
Original file line number Diff line number Diff line change
@@ -3,11 +3,24 @@ package groups
import (
"context"
"fmt"
"math/rand"
"time"

"github.com/manicminer/hamilton/msgraph"
"github.com/manicminer/hamilton/odata"
)

func groupDefaultMailNickname() string {
charSet := "0123456789abcdef"
result := make([]byte, 9)
rand.Seed(time.Now().UTC().UnixNano())
for i := 0; i < 9; i++ {
result[i] = charSet[rand.Intn(len(charSet))]
}
resultString := string(result)
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),
30 changes: 30 additions & 0 deletions internal/validate/diag.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package validate

import (
"fmt"

"github.com/hashicorp/go-cty/cty"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)

// ValidateDiag wraps a SchemaValidateFunc to build a Diagnostics from the warning and error slices
func ValidateDiag(validateFunc func(interface{}, string) ([]string, []error)) schema.SchemaValidateDiagFunc {
return func(i interface{}, path cty.Path) diag.Diagnostics {
warnings, errs := validateFunc(i, fmt.Sprintf("%+v", path))
var diags diag.Diagnostics
for _, warning := range warnings {
diags = append(diags, diag.Diagnostic{
Severity: diag.Warning,
Summary: warning,
})
}
for _, err := range errs {
diags = append(diags, diag.Diagnostic{
Severity: diag.Error,
Summary: err.Error(),
})
}
return diags
}
}
30 changes: 30 additions & 0 deletions internal/validate/mail.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package validate

import (
"regexp"

"github.com/hashicorp/go-cty/cty"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
)

func MailNickname(i interface{}, path cty.Path) (ret diag.Diagnostics) {
v, ok := i.(string)
if !ok {
ret = append(ret, diag.Diagnostic{
Severity: diag.Error,
Summary: "Expected a string value",
AttributePath: path,
})
return
}

if regexp.MustCompile(`[@()\\\[\]";:.<>, ]`).MatchString(v) {
ret = append(ret, diag.Diagnostic{
Severity: diag.Error,
Summary: "Value cannot contain these characters: @()\\[]\";:.<>,SPACE",
AttributePath: path,
})
}

return
}
81 changes: 81 additions & 0 deletions internal/validate/mail_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package validate

import (
"testing"

"github.com/hashicorp/go-cty/cty"
)

func TestMailNickname(t *testing.T) {
cases := []struct {
Value string
TestName string
ErrCount int
}{
{
Value: "initech",
TestName: "Alpha",
ErrCount: 0,
},
{
Value: "12345",
TestName: "Numeric",
ErrCount: 0,
},
{
Value: "floor3",
TestName: "Alphanumeric",
ErrCount: 0,
},
{
Value: "alias@",
TestName: "At-sign",
ErrCount: 1,
},
{
Value: "al\\ias",
TestName: "Backslash",
ErrCount: 1,
},
{
Value: "bob,bob",
TestName: "Comma",
ErrCount: 1,
},
{
Value: "group[1]",
TestName: "Brackets",
ErrCount: 1,
},
{
Value: "case(mondays)",
TestName: "Parentheses",
ErrCount: 1,
},
{
Value: "b0bby;.Tables\";",
TestName: "QuotesColons",
ErrCount: 1,
},
{
Value: "email me at this address",
TestName: "Spaces",
ErrCount: 1,
},
{
Value: "Bill<tps>",
TestName: "LtGt",
ErrCount: 1,
},
}

for _, tc := range cases {
t.Run(tc.TestName, func(t *testing.T) {
diags := MailNickname(tc.Value, cty.Path{})

if len(diags) != tc.ErrCount {
t.Fatalf("Expected MailNickname to have %d not %d errors for %q", tc.ErrCount, len(diags), tc.TestName)
}
})
}
}
24 changes: 0 additions & 24 deletions internal/validate/strings.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
package validate

import (
"fmt"
"regexp"
"strings"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"

"github.com/hashicorp/go-cty/cty"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
)
@@ -65,24 +62,3 @@ func StringIsEmailAddress(i interface{}, path cty.Path) (ret diag.Diagnostics) {

return
}

// ValidateDiag wraps a SchemaValidateFunc to build a Diagnostics from the warning and error slices
func ValidateDiag(validateFunc func(interface{}, string) ([]string, []error)) schema.SchemaValidateDiagFunc {
return func(i interface{}, path cty.Path) diag.Diagnostics {
warnings, errs := validateFunc(i, fmt.Sprintf("%+v", path))
var diags diag.Diagnostics
for _, warning := range warnings {
diags = append(diags, diag.Diagnostic{
Severity: diag.Warning,
Summary: warning,
})
}
for _, err := range errs {
diags = append(diags, diag.Diagnostic{
Severity: diag.Error,
Summary: err.Error(),
})
}
return diags
}
}