Skip to content

Commit

Permalink
Merge pull request #660 from hashicorp/user-email-search-orgmemberships
Browse files Browse the repository at this point in the history
Add support for fetching organization membership by username
  • Loading branch information
sebasslash committed Oct 21, 2022
2 parents 6de8f74 + e984771 commit 1f8a1e5
Show file tree
Hide file tree
Showing 10 changed files with 182 additions and 58 deletions.
7 changes: 4 additions & 3 deletions CHANGELOG.md
Expand Up @@ -2,14 +2,15 @@

FEATURES:
* d/tfe_organization_members: Add datasource for organization_members that returns a list of active members and members with pending invite in an organization. ([#635](https://github.com/hashicorp/terraform-provider-tfe/pull/635))
* d/tfe_organization_membership: Add new argument `username` to enable fetching an organization membership by username. ([#660](https://github.com/hashicorp/terraform-provider-tfe/pull/660))
* r/tfe_organization_membership: Add new computed attribute `username`. ([#660](https://github.com/hashicorp/terraform-provider-tfe/pull/660))
* r/tfe_team_organization_members: Add resource for managing team members via organization membership IDs ([#617](https://github.com/hashicorp/terraform-provider-tfe/pull/617))
* d/tfe_oauth_client: Adds `name`, `service_provider`, `service_provider_display_name`, `organization`, `callback_url`, and `created_at` fields, and enables searching for an OAuth client with `organization`, `name`, and `service_provider`. ([#599](https://github.com/hashicorp/terraform-provider-tfe/pull/599))

BUG FIXES:
* r/tfe_workspace: When assessments_enabled was the only change in to the resource the workspace was not being updated ([#641](https://github.com/hashicorp/terraform-provider-tfe/pull/641))

FEATURES:

* r/tfe_team_organization_members: Add resource for managing team members via organization membership IDs ([#617](https://github.com/hashicorp/terraform-provider-tfe/pull/617))
* d/tfe_oauth_client: Adds `name`, `service_provider`, `service_provider_display_name`, `organization`, `callback_url`, and `created_at` fields, and enables searching for an OAuth client with `organization`, `name`, and `service_provider`. ([#599](https://github.com/hashicorp/terraform-provider-tfe/pull/599))

## v0.37.0 (September 28, 2022)

Expand Down
4 changes: 2 additions & 2 deletions go.mod
Expand Up @@ -12,7 +12,7 @@ require (
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-retryablehttp v0.7.1 // indirect
github.com/hashicorp/go-slug v0.10.0
github.com/hashicorp/go-tfe v1.10.0
github.com/hashicorp/go-tfe v1.11.0
github.com/hashicorp/go-version v1.6.0
github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce
github.com/hashicorp/hcl/v2 v2.14.0 // indirect
Expand All @@ -28,7 +28,7 @@ require (
golang.org/x/oauth2 v0.0.0-20210622215436-a8dc77f794b6 // indirect
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6 // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 // indirect
golang.org/x/time v0.1.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect
)

Expand Down
8 changes: 4 additions & 4 deletions go.sum
Expand Up @@ -186,8 +186,8 @@ github.com/hashicorp/go-retryablehttp v0.7.1 h1:sUiuQAnLlbvmExtFQs72iFW/HXeUn8Z1
github.com/hashicorp/go-retryablehttp v0.7.1/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
github.com/hashicorp/go-slug v0.10.0 h1:mh4DDkBJTh9BuEjY/cv8PTo7k9OjT4PcW8PgZnJ4jTY=
github.com/hashicorp/go-slug v0.10.0/go.mod h1:Ib+IWBYfEfJGI1ZyXMGNbu2BU+aa3Dzu41RKLH301v4=
github.com/hashicorp/go-tfe v1.10.0 h1:mkEge/DSca8VQeBSAQbjEy8fWFHbrJA76M7dny5XlYc=
github.com/hashicorp/go-tfe v1.10.0/go.mod h1:uSWi2sPw7tLrqNIiASid9j3SprbbkPSJ/2s3X0mMemg=
github.com/hashicorp/go-tfe v1.11.0 h1:rubJ4Jfqg8luMc8znl4kzZHYcr9sIMZ4SI/m6uXuhZw=
github.com/hashicorp/go-tfe v1.11.0/go.mod h1:W9x3IWVZD3RWJ0EhsaZZfYBzJrWwdwjJn0HfeDvh6yU=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
Expand Down Expand Up @@ -484,8 +484,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 h1:ftMN5LMiBFjbzleLqtoBZk7KdJwhuybIU+FckUHgoyQ=
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.1.0 h1:xYY+Bajn2a7VBmTM5GikTmnK8ZuX8YgnQCqZpbBNtmA=
golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
Expand Down
61 changes: 13 additions & 48 deletions tfe/data_source_organization_membership.go
@@ -1,6 +1,7 @@
package tfe

import (
"context"
"fmt"

"github.com/hashicorp/go-tfe"
Expand All @@ -14,7 +15,13 @@ func dataSourceTFEOrganizationMembership() *schema.Resource {
Schema: map[string]*schema.Schema{
"email": {
Type: schema.TypeString,
Required: true,
Optional: true,
},

"username": {
Type: schema.TypeString,
Optional: true,
Computed: true,
},

"organization": {
Expand All @@ -35,56 +42,14 @@ func dataSourceTFEOrganizationMembershipRead(d *schema.ResourceData, meta interf

// Get the user email and organization.
email := d.Get("email").(string)
username := d.Get("username").(string)
organization := d.Get("organization").(string)

// Create an options struct.
options := &tfe.OrganizationMembershipListOptions{
Include: []tfe.OrgMembershipIncludeOpt{tfe.OrgMembershipUser},
Emails: []string{email},
}

oml, err := tfeClient.OrganizationMemberships.List(ctx, organization, options)
orgMember, err := fetchOrganizationMemberByNameOrEmail(context.Background(), tfeClient, organization, username, email)
if err != nil {
return fmt.Errorf("Error retrieving organization memberships: %w", err)
}

switch len(oml.Items) {
case 0:
return fmt.Errorf("Could not find organization membership for organization %s and email %s", organization, email)
case 1:
// We check this just in case a user's TFE instance only has one organization member
// and doesn't support the filter query param
if oml.Items[0].User.Email != email {
return fmt.Errorf("Could not find organization membership for organization %s and email %s", organization, email)
}

d.SetId(oml.Items[0].ID)
return resourceTFEOrganizationMembershipRead(d, meta)
default:
options = &tfe.OrganizationMembershipListOptions{
Include: []tfe.OrgMembershipIncludeOpt{tfe.OrgMembershipUser},
}

for {
for _, member := range oml.Items {
if member.User.Email == email {
d.SetId(member.ID)
return resourceTFEOrganizationMembershipRead(d, meta)
}
}

if oml.CurrentPage >= oml.TotalPages {
break
}

options.PageNumber = oml.NextPage

oml, err = tfeClient.OrganizationMemberships.List(ctx, organization, options)
if err != nil {
return fmt.Errorf("Error retrieving organization memberships: %w", err)
}
}
return fmt.Errorf("Could not find organization membership for organization %s: %w", organization, err)
}

return fmt.Errorf("Could not find organization membership for organization %s and email %s", organization, email)
d.SetId(orgMember.ID)
return resourceTFEOrganizationMembershipRead(d, meta)
}
74 changes: 74 additions & 0 deletions tfe/data_source_organization_membership_test.go
Expand Up @@ -3,6 +3,7 @@ package tfe
import (
"fmt"
"math/rand"
"regexp"
"testing"
"time"

Expand All @@ -22,6 +23,8 @@ func TestAccTFEOrganizationMembershipDataSource_basic(t *testing.T) {
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr(
"data.tfe_organization_membership.foobar", "email", "example@hashicorp.com"),
resource.TestCheckResourceAttr(
"data.tfe_organization_membership.foobar", "username", ""),
resource.TestCheckResourceAttr(
"data.tfe_organization_membership.foobar", "organization", orgName),
resource.TestCheckResourceAttrSet("data.tfe_organization_membership.foobar", "user_id"),
Expand All @@ -31,6 +34,52 @@ func TestAccTFEOrganizationMembershipDataSource_basic(t *testing.T) {
})
}

func TestAccTFEOrganizationMembershipDataSource_findByName(t *testing.T) {
// This test requires a user that exists in a TFC organization called "hashicorp".
// Our CI instance has a default organization "hashicorp" and prepopulates it
// with users (i.e TFE_USER1, etc) since we are unable to create users via the API.
// In order to run this against your own organization, simply modify the organization
// attribute in the test step config and set TFE_USER1 to the desired user you want to fetch.
resource.Test(t, resource.TestCase{
PreCheck: func() {
testAccPreCheck(t)
if TFE_USER1 == "" {
t.Skip("Please set TFE_USER1 to run this test")
}
},
Providers: testAccProviders,
Steps: []resource.TestStep{
{
Config: testAccTFEOrganizationMembershipDataSourceSearchUsername(TFE_USER1),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttrSet(
"data.tfe_organization_membership.foobar", "email"),
resource.TestCheckResourceAttr(
"data.tfe_organization_membership.foobar", "username", TFE_USER1),
resource.TestCheckResourceAttr(
"data.tfe_organization_membership.foobar", "organization", "hashicorp"),
resource.TestCheckResourceAttrSet("data.tfe_organization_membership.foobar", "user_id"),
),
},
},
})
}

func TestAccTFEOrganizationMembershipDataSource_missingParams(t *testing.T) {
rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int()

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
Steps: []resource.TestStep{
{
Config: testAccTFEOrganizationMembershipDataSourceMissingParams(rInt),
ExpectError: regexp.MustCompile("you must specify a username or email"),
},
},
})
}

func testAccTFEOrganizationMembershipDataSourceConfig(rInt int) string {
return fmt.Sprintf(`
resource "tfe_organization" "foobar" {
Expand All @@ -48,3 +97,28 @@ data "tfe_organization_membership" "foobar" {
organization = tfe_organization.foobar.name
}`, rInt)
}

func testAccTFEOrganizationMembershipDataSourceSearchUsername(username string) string {
return fmt.Sprintf(`
data "tfe_organization_membership" "foobar" {
username = "%s"
organization = "hashicorp"
}`, username)
}

func testAccTFEOrganizationMembershipDataSourceMissingParams(rInt int) string {
return fmt.Sprintf(`
resource "tfe_organization" "foobar" {
name = "tst-terraform-%d"
email = "admin@company.com"
}
resource "tfe_organization_membership" "foobar" {
email = "example@hashicorp.com"
organization = tfe_organization.foobar.id
}
data "tfe_organization_membership" "foobar" {
organization = tfe_organization.foobar.name
}`, rInt)
}
60 changes: 60 additions & 0 deletions tfe/organization_members_helpers.go
@@ -1,6 +1,7 @@
package tfe

import (
"context"
"fmt"
"log"

Expand Down Expand Up @@ -40,3 +41,62 @@ func fetchOrganizationMembers(client *tfe.Client, orgName string) ([]map[string]

return members, membersWaiting, nil
}

func fetchOrganizationMemberByNameOrEmail(ctx context.Context, client *tfe.Client, organization, username, email string) (*tfe.OrganizationMembership, error) {
if email == "" && username == "" {
return nil, fmt.Errorf("you must specify a username or email.")
}

options := &tfe.OrganizationMembershipListOptions{
Include: []tfe.OrgMembershipIncludeOpt{tfe.OrgMembershipUser},
}

if email != "" {
options.Emails = []string{email}
}

if username != "" {
options.Query = username
}

oml, err := client.OrganizationMemberships.List(ctx, organization, options)
if err != nil {
return nil, fmt.Errorf("failed to list organization memberships: %w", err)
}

switch len(oml.Items) {
case 0:
return nil, tfe.ErrResourceNotFound
case 1:
user := oml.Items[0].User

// We check this just in case a user's TFE instance only has one organization member
if user.Email != email && user.Username != username {
return nil, tfe.ErrResourceNotFound
}

return oml.Items[0], nil
default:
for {
for _, member := range oml.Items {
if (len(email) > 0 && member.User.Email == email) ||
(len(username) > 0 && member.User.Username == username) {
return member, nil
}
}

if oml.CurrentPage >= oml.TotalPages {
break
}

options.PageNumber = oml.NextPage

oml, err = client.OrganizationMemberships.List(ctx, organization, options)
if err != nil {
return nil, fmt.Errorf("failed to list organization memberships: %w", err)
}
}
}

return nil, tfe.ErrResourceNotFound
}
6 changes: 6 additions & 0 deletions tfe/resource_tfe_organization_membership.go
Expand Up @@ -34,6 +34,11 @@ func resourceTFEOrganizationMembership() *schema.Resource {
Type: schema.TypeString,
Computed: true,
},

"username": {
Type: schema.TypeString,
Computed: true,
},
},
}
}
Expand Down Expand Up @@ -84,6 +89,7 @@ func resourceTFEOrganizationMembershipRead(d *schema.ResourceData, meta interfac
d.Set("email", membership.Email)
d.Set("organization", membership.Organization.Name)
d.Set("user_id", membership.User.ID)
d.Set("username", membership.User.Username)

return nil
}
Expand Down
2 changes: 2 additions & 0 deletions tfe/resource_tfe_organization_membership_test.go
Expand Up @@ -32,6 +32,8 @@ func TestAccTFEOrganizationMembership_basic(t *testing.T) {
resource.TestCheckResourceAttr(
"tfe_organization_membership.foobar", "organization", orgName),
resource.TestCheckResourceAttrSet("tfe_organization_membership.foobar", "user_id"),
resource.TestCheckResourceAttr(
"tfe_organization_membership.foobar", "username", ""),
),
},
},
Expand Down
17 changes: 16 additions & 1 deletion website/docs/d/organization_membership.html.markdown
Expand Up @@ -18,23 +18,38 @@ be updated manually.

## Example Usage

### Fetch by email

```hcl
data "tfe_organization_membership" "test" {
organization = "my-org-name"
email = "user@company.com"
}
```

### Fetch by username

```
data "tfe_organization_membership" "test" {
organization = "my-org-name"
username = "my-username"
}
```

## Argument Reference

The following arguments are supported:

* `organization` - (Required) Name of the organization.
* `email` - (Required) Email of the user.
* `email` - (Optional) Email of the user.
* `username` - (Optional) The username of the user.

~> **NOTE:** While `email` and `username` are optional arguments, one or the other is required.

## Attributes Reference

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

* `id` - The organization membership ID.
* `user_id` - The ID of the user associated with the organization membership.
* `username` - The username of the user associated with the organization membership.
1 change: 1 addition & 0 deletions website/docs/r/organization_membership.html.markdown
Expand Up @@ -42,6 +42,7 @@ In addition to all arguments above, the following attributes are exported:

* `id` - The organization membership ID.
* `user_id` - The ID of the user associated with the organization membership.
* `username` - The username of the user associated with the organization membership.

Organization memberships can be imported; use `<ORGANIZATION MEMBERSHIP ID>` as the import ID. For
example:
Expand Down

0 comments on commit 1f8a1e5

Please sign in to comment.