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

Add support for fetching organization membership by username #660

Merged
merged 4 commits into from Oct 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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