From c846075f54cab0713c50db287a46f754a57c2fa5 Mon Sep 17 00:00:00 2001 From: pchanvallon Date: Thu, 2 Mar 2023 17:17:30 +0100 Subject: [PATCH] Find user and users by using employee ID --- docs/data-sources/user.md | 3 +- docs/data-sources/users.md | 5 +- internal/services/users/user_data_source.go | 43 +++++++++--- .../services/users/user_data_source_test.go | 41 +++++++++++ internal/services/users/user_resource_test.go | 4 +- internal/services/users/users_data_source.go | 57 ++++++++++++++-- .../services/users/users_data_source_test.go | 68 +++++++++++++++++++ 7 files changed, 203 insertions(+), 18 deletions(-) diff --git a/docs/data-sources/user.md b/docs/data-sources/user.md index a3eddf0a6b..1e8ba4e633 100644 --- a/docs/data-sources/user.md +++ b/docs/data-sources/user.md @@ -26,12 +26,13 @@ data "azuread_user" "example" { The following arguments are supported: +* `employee_id` - (Optional) The employee identifier assigned to the user by the organisation. * `mail` - (Optional) The SMTP address for the user. * `mail_nickname` - (Optional) The email alias of the user. * `object_id` - (Optional) The object ID of the user. * `user_principal_name` - (Optional) The user principal name (UPN) of the user. -~> One of `user_principal_name`, `object_id`, `mail` or `mail_nickname` must be specified. +~> One of `user_principal_name`, `object_id`, `mail`, `mail_nickname` or `employee_id` must be specified. ## Attributes Reference diff --git a/docs/data-sources/users.md b/docs/data-sources/users.md index 265dac233a..e607520ae6 100644 --- a/docs/data-sources/users.md +++ b/docs/data-sources/users.md @@ -26,18 +26,20 @@ data "azuread_users" "users" { The following arguments are supported: +* `employee_ids` - (Optional) The employee identifiers assigned to the users by the organisation. * `ignore_missing` - (Optional) Ignore missing users and return users that were found. The data source will still fail if no users are found. Cannot be specified with `return_all`. Defaults to `false`. * `mail_nicknames` - (Optional) The email aliases of the users. * `object_ids` - (Optional) The object IDs of the users. * `return_all` - (Optional) When `true`, the data source will return all users. Cannot be used with `ignore_missing`. Defaults to `false`. * `user_principal_names` - (Optional) The user principal names (UPNs) of the users. -~> Either `return_all`, or one of `user_principal_names`, `object_ids` or `mail_nicknames` must be specified. These _may_ be specified as an empty list, in which case no results will be returned. +~> Either `return_all`, or one of `user_principal_names`, `object_ids`, `mail_nicknames` or `employee_id` must be specified. These _may_ be specified as an empty list, in which case no results will be returned. ## Attributes Reference The following attributes are exported: +* `employee_ids` - The employee identifier assigned to the users by the organisation. * `mail_nicknames` - The email aliases of the users. * `object_ids` - The object IDs of the users. * `user_principal_names` - The user principal names (UPNs) of the users. @@ -49,6 +51,7 @@ The following attributes are exported: * `account_enabled` - Whether or not the account is enabled. * `display_name` - The display name of the user. +* `employee_id` - The employee identifier assigned to the user by the organisation. * `mail_nickname` - The email alias of the user. * `mail` - The primary email address of the user. * `object_id` - The object ID of the user. diff --git a/internal/services/users/user_data_source.go b/internal/services/users/user_data_source.go index e9f93906f2..02cab087a0 100644 --- a/internal/services/users/user_data_source.go +++ b/internal/services/users/user_data_source.go @@ -26,11 +26,20 @@ func userDataSource() *schema.Resource { }, Schema: map[string]*schema.Schema{ + "employee_id": { + Description: "The employee identifier assigned to the user by the organisation", + Type: schema.TypeList, + Optional: true, + ExactlyOneOf: []string{"employee_id", "mail", "mail_nickname", "object_id", "user_principal_name"}, + Computed: true, + ValidateDiagFunc: validate.NoEmptyStrings, + }, + "mail": { Description: "The SMTP address for the user", Type: schema.TypeString, Optional: true, - ExactlyOneOf: []string{"mail", "mail_nickname", "object_id", "user_principal_name"}, + ExactlyOneOf: []string{"employee_id", "mail", "mail_nickname", "object_id", "user_principal_name"}, Computed: true, ValidateDiagFunc: validate.NoEmptyStrings, }, @@ -39,7 +48,7 @@ func userDataSource() *schema.Resource { Description: "The email alias of the user", Type: schema.TypeString, Optional: true, - ExactlyOneOf: []string{"mail", "mail_nickname", "object_id", "user_principal_name"}, + ExactlyOneOf: []string{"employee_id", "mail", "mail_nickname", "object_id", "user_principal_name"}, Computed: true, ValidateDiagFunc: validate.NoEmptyStrings, }, @@ -49,7 +58,7 @@ func userDataSource() *schema.Resource { Type: schema.TypeString, Optional: true, Computed: true, - ExactlyOneOf: []string{"mail", "mail_nickname", "object_id", "user_principal_name"}, + ExactlyOneOf: []string{"employee_id", "mail", "mail_nickname", "object_id", "user_principal_name"}, ValidateDiagFunc: validate.UUID, }, @@ -58,7 +67,7 @@ func userDataSource() *schema.Resource { Type: schema.TypeString, Optional: true, Computed: true, - ExactlyOneOf: []string{"mail", "mail_nickname", "object_id", "user_principal_name"}, + ExactlyOneOf: []string{"employee_id", "mail", "mail_nickname", "object_id", "user_principal_name"}, ValidateDiagFunc: validate.NoEmptyStrings, }, @@ -137,12 +146,6 @@ func userDataSource() *schema.Resource { Computed: true, }, - "employee_id": { - Description: "The employee identifier assigned to the user by the organisation", - Type: schema.TypeString, - Computed: true, - }, - "employee_type": { Description: "Captures enterprise worker type. For example, Employee, Contractor, Consultant, or Vendor.", Type: schema.TypeString, @@ -384,8 +387,26 @@ func userDataSourceRead(ctx context.Context, d *schema.ResourceData, meta interf return tf.ErrorDiagPathF(err, "mail_nickname", "User not found with email alias: %q", mailNickname) } user = (*users)[0] + } else if employeeId, ok := d.Get("employee_id").(string); ok && employeeId != "" { + query := odata.Query{ + Filter: fmt.Sprintf("employeeId eq '%s'", utils.EscapeSingleQuote(employeeId)), + } + users, _, err := client.List(ctx, query) + if err != nil { + return tf.ErrorDiagF(err, "Finding user with employee ID: %q", employeeId) + } + if users == nil { + return tf.ErrorDiagF(errors.New("API returned nil result"), "Bad API Response") + } + count := len(*users) + if count > 1 { + return tf.ErrorDiagPathF(nil, "employee_id", "More than one user found with employee ID: %q", employeeId) + } else if count == 0 { + return tf.ErrorDiagPathF(err, "employee_id", "User not found with employee ID: %q", employeeId) + } + user = (*users)[0] } else { - return tf.ErrorDiagF(nil, "One of `object_id`, `user_principal_name` or `mail_nickname` must be supplied") + return tf.ErrorDiagF(nil, "One of `object_id`, `user_principal_name`, `mail_nickname` or `employee_id` must be supplied") } if user.ID() == nil { diff --git a/internal/services/users/user_data_source_test.go b/internal/services/users/user_data_source_test.go index 7b0cadb74b..509d088dbc 100644 --- a/internal/services/users/user_data_source_test.go +++ b/internal/services/users/user_data_source_test.go @@ -88,6 +88,25 @@ func TestAccUserDataSource_byMailNonexistent(t *testing.T) { }}) } +func TestAccUserDataSource_byEmployeeId(t *testing.T) { + data := acceptance.BuildTestData(t, "data.azuread_user", "test") + r := UserDataSource{} + + data.DataSourceTest(t, []resource.TestStep{{ + Config: r.byEmployeeId(data), + Check: r.testCheckFunc(data), + }}) +} + +func TestAccUserDataSource_byEmployeeIdNonexistent(t *testing.T) { + data := acceptance.BuildTestData(t, "data.azuread_user", "test") + + data.DataSourceTest(t, []resource.TestStep{{ + Config: UserDataSource{}.byEmployeeIdNonexistent(data), + ExpectError: regexp.MustCompile("User not found with employee ID:"), + }}) +} + func (UserDataSource) testCheckFunc(data acceptance.TestData) resource.TestCheckFunc { return resource.ComposeTestCheckFunc( check.That(data.ResourceName).Key("account_enabled").Exists(), @@ -198,3 +217,25 @@ data "azuread_user" "test" { } `, data.RandomInteger) } + +func (UserDataSource) byEmployeeId(data acceptance.TestData) string { + return fmt.Sprintf(` +%[1]s + +data "azuread_user" "test" { + employee_id = azuread_user.test.employee_id +} +`, UserResource{}.complete(data)) +} + +func (UserDataSource) byEmployeeIdNonexistent(data acceptance.TestData) string { + return ` +data "azuread_domains" "test" { + only_initial = true +} + +data "azuread_user" "test" { + employee_id = "not-a-real-employeeid" +} +` +} diff --git a/internal/services/users/user_resource_test.go b/internal/services/users/user_resource_test.go index 4721d6e276..e6e5dd4411 100644 --- a/internal/services/users/user_resource_test.go +++ b/internal/services/users/user_resource_test.go @@ -225,6 +225,7 @@ data "azuread_domains" "test" { resource "azuread_user" "testA" { user_principal_name = "acctestUser'%[1]d.A@${data.azuread_domains.test.domains.0.domain_name}" display_name = "acctestUser-%[1]d-A" + employee_id = "A%[3]s%[3]s" password = "%[2]s" } @@ -232,6 +233,7 @@ resource "azuread_user" "testB" { user_principal_name = "acctestUser.%[1]d.B@${data.azuread_domains.test.domains.0.domain_name}" display_name = "acctestUser-%[1]d-B" mail_nickname = "acctestUser-%[1]d-B" + employee_id = "B%[3]s%[3]s" password = "%[2]s" } @@ -240,7 +242,7 @@ resource "azuread_user" "testC" { display_name = "acctestUser-%[1]d-C" password = "%[2]s" } -`, data.RandomInteger, data.RandomPassword) +`, data.RandomInteger, data.RandomPassword, data.RandomString) } func (UserResource) withRandomProvider(data acceptance.TestData) string { diff --git a/internal/services/users/users_data_source.go b/internal/services/users/users_data_source.go index 6b887a6b57..28099f0134 100644 --- a/internal/services/users/users_data_source.go +++ b/internal/services/users/users_data_source.go @@ -29,12 +29,24 @@ func usersData() *schema.Resource { }, Schema: map[string]*schema.Schema{ + "employee_ids": { + Description: "The employee identifier assigned to the user by the organisation", + Type: schema.TypeList, + Optional: true, + Computed: true, + ExactlyOneOf: []string{"object_ids", "user_principal_names", "mail_nicknames", "employee_ids", "return_all"}, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateDiagFunc: validate.NoEmptyStrings, + }, + }, + "mail_nicknames": { Description: "The email aliases of the users", Type: schema.TypeList, Optional: true, Computed: true, - ExactlyOneOf: []string{"object_ids", "user_principal_names", "mail_nicknames", "return_all"}, + ExactlyOneOf: []string{"object_ids", "user_principal_names", "mail_nicknames", "employee_ids", "return_all"}, Elem: &schema.Schema{ Type: schema.TypeString, ValidateDiagFunc: validate.NoEmptyStrings, @@ -46,7 +58,7 @@ func usersData() *schema.Resource { Type: schema.TypeList, Optional: true, Computed: true, - ExactlyOneOf: []string{"object_ids", "user_principal_names", "mail_nicknames", "return_all"}, + ExactlyOneOf: []string{"object_ids", "user_principal_names", "mail_nicknames", "employee_ids", "return_all"}, Elem: &schema.Schema{ Type: schema.TypeString, ValidateDiagFunc: validate.UUID, @@ -58,7 +70,7 @@ func usersData() *schema.Resource { Type: schema.TypeList, Optional: true, Computed: true, - ExactlyOneOf: []string{"object_ids", "user_principal_names", "mail_nicknames", "return_all"}, + ExactlyOneOf: []string{"object_ids", "user_principal_names", "mail_nicknames", "employee_ids", "return_all"}, Elem: &schema.Schema{ Type: schema.TypeString, ValidateDiagFunc: validate.NoEmptyStrings, @@ -79,7 +91,7 @@ func usersData() *schema.Resource { Optional: true, Default: false, ConflictsWith: []string{"ignore_missing"}, - ExactlyOneOf: []string{"object_ids", "user_principal_names", "mail_nicknames", "return_all"}, + ExactlyOneOf: []string{"object_ids", "user_principal_names", "mail_nicknames", "employee_ids", "return_all"}, }, "users": { @@ -100,6 +112,12 @@ func usersData() *schema.Resource { Computed: true, }, + "employee_id": { + Description: "The employee identifier assigned to the user by the organisation", + Type: schema.TypeString, + Computed: true, + }, + "mail": { Description: "The primary email address of the user", Type: schema.TypeString, @@ -244,6 +262,31 @@ func usersDataSourceRead(ctx context.Context, d *schema.ResourceData, meta inter } users = append(users, (*result)[0]) } + } else if employeeIds, ok := d.Get("employee_ids").([]interface{}); ok && len(employeeIds) > 0 { + expectedCount = len(employeeIds) + for _, v := range employeeIds { + query := odata.Query{ + Filter: fmt.Sprintf("employeeId eq '%s'", utils.EscapeSingleQuote(v.(string))), + } + result, _, err := client.List(ctx, query) + if err != nil { + return tf.ErrorDiagF(err, "Finding user with employee ID: %q", v) + } + if result == nil { + return tf.ErrorDiagF(errors.New("API returned nil result"), "Bad API Response") + } + + count := len(*result) + if count > 1 { + return tf.ErrorDiagPathF(nil, "employee_ids", "More than one user found with employee ID: %q", v) + } else if count == 0 { + if ignoreMissing { + continue + } + return tf.ErrorDiagPathF(err, "employee_ids", "User not found with employee ID: %q", v) + } + users = append(users, (*result)[0]) + } } } @@ -255,6 +298,7 @@ func usersDataSourceRead(ctx context.Context, d *schema.ResourceData, meta inter upns := make([]string, 0) objectIds := make([]string, 0) mailNicknames := make([]string, 0) + employeeIds := make([]msgraph.StringNullWhenEmpty, 0) userList := make([]map[string]interface{}, 0) for _, u := range users { if u.ID() == nil || u.UserPrincipalName == nil { @@ -266,10 +310,14 @@ func usersDataSourceRead(ctx context.Context, d *schema.ResourceData, meta inter if u.MailNickname != nil { mailNicknames = append(mailNicknames, *u.MailNickname) } + if u.EmployeeId != nil { + employeeIds = append(employeeIds, *u.EmployeeId) + } user := make(map[string]interface{}) user["account_enabled"] = u.AccountEnabled user["display_name"] = u.DisplayName + user["employee_id"] = u.EmployeeId user["mail"] = u.Mail user["mail_nickname"] = u.MailNickname user["object_id"] = u.ID() @@ -288,6 +336,7 @@ func usersDataSourceRead(ctx context.Context, d *schema.ResourceData, meta inter } d.SetId("users#" + base64.URLEncoding.EncodeToString(h.Sum(nil))) + tf.Set(d, "employee_ids", employeeIds) tf.Set(d, "mail_nicknames", mailNicknames) tf.Set(d, "object_ids", objectIds) tf.Set(d, "user_principal_names", upns) diff --git a/internal/services/users/users_data_source_test.go b/internal/services/users/users_data_source_test.go index d39f37ab50..74e875499d 100644 --- a/internal/services/users/users_data_source_test.go +++ b/internal/services/users/users_data_source_test.go @@ -19,6 +19,8 @@ func TestAccUsersDataSource_byUserPrincipalNames(t *testing.T) { Check: resource.ComposeTestCheckFunc( check.That(data.ResourceName).Key("user_principal_names.#").HasValue("2"), check.That(data.ResourceName).Key("object_ids.#").HasValue("2"), + check.That(data.ResourceName).Key("mail_nicknames.#").HasValue("2"), + check.That(data.ResourceName).Key("employee_ids.#").HasValue("2"), check.That(data.ResourceName).Key("users.#").HasValue("2"), ), }}) @@ -32,6 +34,8 @@ func TestAccUsersDataSource_byUserPrincipalNamesIgnoreMissing(t *testing.T) { Check: resource.ComposeTestCheckFunc( check.That(data.ResourceName).Key("user_principal_names.#").HasValue("3"), check.That(data.ResourceName).Key("object_ids.#").HasValue("3"), + check.That(data.ResourceName).Key("mail_nicknames.#").HasValue("3"), + check.That(data.ResourceName).Key("employee_ids.#").HasValue("2"), check.That(data.ResourceName).Key("users.#").HasValue("3"), ), }}) @@ -45,6 +49,8 @@ func TestAccUsersDataSource_byObjectIds(t *testing.T) { Check: resource.ComposeTestCheckFunc( check.That(data.ResourceName).Key("user_principal_names.#").HasValue("2"), check.That(data.ResourceName).Key("object_ids.#").HasValue("2"), + check.That(data.ResourceName).Key("mail_nicknames.#").HasValue("2"), + check.That(data.ResourceName).Key("employee_ids.#").HasValue("2"), check.That(data.ResourceName).Key("users.#").HasValue("2"), ), }}) @@ -58,6 +64,8 @@ func TestAccUsersDataSource_byObjectIdsIgnoreMissing(t *testing.T) { Check: resource.ComposeTestCheckFunc( check.That(data.ResourceName).Key("user_principal_names.#").HasValue("2"), check.That(data.ResourceName).Key("object_ids.#").HasValue("2"), + check.That(data.ResourceName).Key("mail_nicknames.#").HasValue("2"), + check.That(data.ResourceName).Key("employee_ids.#").HasValue("2"), check.That(data.ResourceName).Key("users.#").HasValue("2"), ), }}) @@ -72,6 +80,7 @@ func TestAccUsersDataSource_byMailNicknames(t *testing.T) { check.That(data.ResourceName).Key("user_principal_names.#").HasValue("2"), check.That(data.ResourceName).Key("object_ids.#").HasValue("2"), check.That(data.ResourceName).Key("mail_nicknames.#").HasValue("2"), + check.That(data.ResourceName).Key("employee_ids.#").HasValue("2"), check.That(data.ResourceName).Key("users.#").HasValue("2"), ), }}) @@ -86,6 +95,37 @@ func TestAccUsersDataSource_byMailNicknamesIgnoreMissing(t *testing.T) { check.That(data.ResourceName).Key("user_principal_names.#").HasValue("2"), check.That(data.ResourceName).Key("object_ids.#").HasValue("2"), check.That(data.ResourceName).Key("mail_nicknames.#").HasValue("2"), + check.That(data.ResourceName).Key("employee_ids.#").HasValue("2"), + check.That(data.ResourceName).Key("users.#").HasValue("2"), + ), + }}) +} + +func TestAccUsersDataSource_byEmployeeIds(t *testing.T) { + data := acceptance.BuildTestData(t, "data.azuread_users", "test") + + data.DataSourceTest(t, []resource.TestStep{{ + Config: UsersDataSource{}.byEmployeeIds(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).Key("user_principal_names.#").HasValue("2"), + check.That(data.ResourceName).Key("object_ids.#").HasValue("2"), + check.That(data.ResourceName).Key("mail_nicknames.#").HasValue("2"), + check.That(data.ResourceName).Key("employee_ids.#").HasValue("2"), + check.That(data.ResourceName).Key("users.#").HasValue("2"), + ), + }}) +} + +func TestAccUsersDataSource_byEmployeeIdsIgnoreMissing(t *testing.T) { + data := acceptance.BuildTestData(t, "data.azuread_users", "test") + + data.DataSourceTest(t, []resource.TestStep{{ + Config: UsersDataSource{}.byEmployeeIdsIgnoreMissing(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).Key("user_principal_names.#").HasValue("2"), + check.That(data.ResourceName).Key("object_ids.#").HasValue("2"), + check.That(data.ResourceName).Key("mail_nicknames.#").HasValue("2"), + check.That(data.ResourceName).Key("employee_ids.#").HasValue("2"), check.That(data.ResourceName).Key("users.#").HasValue("2"), ), }}) @@ -100,6 +140,7 @@ func TestAccUsersDataSource_noNames(t *testing.T) { check.That(data.ResourceName).Key("user_principal_names.#").HasValue("0"), check.That(data.ResourceName).Key("object_ids.#").HasValue("0"), check.That(data.ResourceName).Key("mail_nicknames.#").HasValue("0"), + check.That(data.ResourceName).Key("employee_ids.#").HasValue("0"), check.That(data.ResourceName).Key("users.#").HasValue("0"), ), }}) @@ -114,6 +155,7 @@ func TestAccUsersDataSource_returnAll(t *testing.T) { check.That(data.ResourceName).Key("user_principal_names.#").Exists(), check.That(data.ResourceName).Key("object_ids.#").Exists(), check.That(data.ResourceName).Key("mail_nicknames.#").Exists(), + check.That(data.ResourceName).Key("employee_ids.#").Exists(), check.That(data.ResourceName).Key("users.#").Exists(), ), }}) @@ -198,6 +240,32 @@ data "azuread_users" "test" { `, UserResource{}.threeUsersABC(data), data.RandomInteger) } +func (UsersDataSource) byEmployeeIds(data acceptance.TestData) string { + return fmt.Sprintf(` +%[1]s + +data "azuread_users" "test" { + employee_ids = [azuread_user.testA.employee_id, azuread_user.testB.employee_id] +} +`, UserResource{}.threeUsersABC(data)) +} + +func (UsersDataSource) byEmployeeIdsIgnoreMissing(data acceptance.TestData) string { + return fmt.Sprintf(` +%[1]s + +data "azuread_users" "test" { + ignore_missing = true + + employee_ids = [ + azuread_user.testA.employee_id, + "not-a-real-employee-id", + azuread_user.testB.employee_id, + ] +} +`, UserResource{}.threeUsersABC(data)) +} + func (UsersDataSource) noNames() string { return ` data "azuread_users" "test" {