Skip to content

Commit

Permalink
Merge pull request #1040 from pchanvallon/feat/find-user-by-employeeid
Browse files Browse the repository at this point in the history
Find user and users by using employee ID
  • Loading branch information
manicminer authored Jul 13, 2023
2 parents e2abc8c + 287b30e commit 89af90f
Show file tree
Hide file tree
Showing 7 changed files with 203 additions and 18 deletions.
3 changes: 2 additions & 1 deletion docs/data-sources/user.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
5 changes: 4 additions & 1 deletion docs/data-sources/users.md
Original file line number Diff line number Diff line change
Expand Up @@ -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_ids` 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 identifiers 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.
Expand All @@ -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.
Expand Down
43 changes: 32 additions & 11 deletions internal/services/users/user_data_source.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,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.TypeString,
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,
},
Expand All @@ -42,7 +51,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,
},
Expand All @@ -52,7 +61,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,
},

Expand All @@ -61,7 +70,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,
},

Expand Down Expand Up @@ -140,12 +149,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,
Expand Down Expand Up @@ -387,8 +390,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 {
Expand Down
41 changes: 41 additions & 0 deletions internal/services/users/user_data_source_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,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(),
Expand Down Expand Up @@ -201,3 +220,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"
}
`
}
4 changes: 3 additions & 1 deletion internal/services/users/user_resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -228,13 +228,15 @@ 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"
}
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"
}
Expand All @@ -243,7 +245,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 {
Expand Down
57 changes: 53 additions & 4 deletions internal/services/users/users_data_source.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,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,
Expand All @@ -49,7 +61,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,
Expand All @@ -61,7 +73,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,
Expand All @@ -82,7 +94,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": {
Expand All @@ -103,6 +115,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,
Expand Down Expand Up @@ -247,6 +265,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])
}
}
}

Expand All @@ -258,6 +301,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 {
Expand All @@ -269,10 +313,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()
Expand All @@ -291,6 +339,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)
Expand Down
Loading

0 comments on commit 89af90f

Please sign in to comment.