Skip to content

Commit

Permalink
Add new data source `google_cloud_identity_transitive_group_membershi…
Browse files Browse the repository at this point in the history
…ps` (#11337)
  • Loading branch information
SarahFrench authored Aug 8, 2024
1 parent d602092 commit c09412b
Show file tree
Hide file tree
Showing 6 changed files with 420 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ var handwrittenDatasources = map[string]*schema.Resource{
"google_cloud_asset_search_all_resources": cloudasset.DataSourceGoogleCloudAssetSearchAllResources(),
"google_cloud_identity_groups": cloudidentity.DataSourceGoogleCloudIdentityGroups(),
"google_cloud_identity_group_memberships": cloudidentity.DataSourceGoogleCloudIdentityGroupMemberships(),
"google_cloud_identity_group_transitive_memberships": cloudidentity.DataSourceGoogleCloudIdentityGroupTransitiveMemberships(),
"google_cloud_identity_group_lookup": cloudidentity.DataSourceGoogleCloudIdentityGroupLookup(),
"google_cloud_quotas_quota_info": cloudquotas.DataSourceGoogleCloudQuotasQuotaInfo(),
"google_cloud_quotas_quota_infos": cloudquotas.DataSourceGoogleCloudQuotasQuotaInfos(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
<% autogen_exception -%>
package cloudidentity

import (
"fmt"

"github.com/hashicorp/terraform-provider-google/google/tpgresource"
transport_tpg "github.com/hashicorp/terraform-provider-google/google/transport"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
<% unless version == 'ga' -%>
cloudidentity "google.golang.org/api/cloudidentity/v1beta1"
<% else -%>
"google.golang.org/api/cloudidentity/v1"
<% end -%>
)

func DataSourceGoogleCloudIdentityGroupTransitiveMemberships() *schema.Resource {

return &schema.Resource{
Read: dataSourceGoogleCloudIdentityGroupTransitiveMembershipsRead,

// We don't reuse schemas from google_cloud_identity_group_membership because data returned about
// transative memberships is structured differently, with information like expiry missing.
Schema: map[string]*schema.Schema{
"group": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
DiffSuppressFunc: tpgresource.CompareSelfLinkOrResourceName,
Description: `The name of the Group to get memberships from.`,
},
"memberships": {
Type: schema.TypeList,
Computed: true,
Description: `List of Cloud Identity group memberships.`,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"roles": {
Type: schema.TypeSet,
// Default schema.HashSchema is used.
Computed: true,
Description: `The membership role details`,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"role": {
Type: schema.TypeString,
Computed: true,
Description: `The name of the TransitiveMembershipRole. Possible values: ["OWNER", "MANAGER", "MEMBER"]`,
},
},
},
},
"preferred_member_key": {
Type: schema.TypeList,
Computed: true,
Description: `EntityKey of the member. Entity key has an id and a namespace. In case of discussion forums, the id will be an email address without a namespace.`,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"id": {
Type: schema.TypeString,
Computed: true,
Description: `The ID of the entity.

For Google-managed entities, the id must be the email address of an existing
group or user.

For external-identity-mapped entities, the id must be a string conforming
to the Identity Source's requirements.

Must be unique within a namespace.`,
},
"namespace": {
Type: schema.TypeString,
Computed: true,
Description: `The namespace in which the entity exists.

If not specified, the EntityKey represents a Google-managed entity
such as a Google user or a Google Group.

If specified, the EntityKey represents an external-identity-mapped group.
The namespace must correspond to an identity source created in Admin Console
and must be in the form of 'identitysources/{identity_source_id}'.`,
},
},
},
},
"member": {
Type: schema.TypeString,
Computed: true,
Description: `Resource name for this member.`,
},
"relation_type": {
Type: schema.TypeString,
Computed: true,
Description: `The relation between the group and the transitive member. The value can be DIRECT, INDIRECT, or DIRECT_AND_INDIRECT`,
},
},
},
},
},
}
}

func dataSourceGoogleCloudIdentityGroupTransitiveMembershipsRead(d *schema.ResourceData, meta interface{}) error {
config := meta.(*transport_tpg.Config)
userAgent, err := tpgresource.GenerateUserAgentString(d, config.UserAgent)
if err != nil {
return err
}

result := []map[string]interface{}{}
membershipsCall := config.NewCloudIdentityClient(userAgent).Groups.Memberships.SearchTransitiveMemberships(d.Get("group").(string))
if config.UserProjectOverride {
billingProject := ""
// err may be nil - project isn't required for this resource
if project, err := tpgresource.GetProject(d, config); err == nil {
billingProject = project
}

// err == nil indicates that the billing_project value was found
if bp, err := tpgresource.GetBillingProject(d, config); err == nil {
billingProject = bp
}

if billingProject != "" {
membershipsCall.Header().Set("X-Goog-User-Project", billingProject)
}
}

err = membershipsCall.Pages(config.Context, func(resp *cloudidentity.SearchTransitiveMembershipsResponse) error {
for _, member := range resp.Memberships {
result = append(result, map[string]interface{}{
"member": member.Member,
"relation_type": member.RelationType,
"roles": flattenCloudIdentityGroupTransitiveMembershipsRoles(member.Roles),
"preferred_member_key": flattenCloudIdentityGroupsEntityKeyList(member.PreferredMemberKey),
})
}

return nil
})
if err != nil {
return transport_tpg.HandleDataSourceNotFoundError(err, d, fmt.Sprintf("CloudIdentityGroupMemberships %q", d.Id()), "")
}

if err := d.Set("memberships", result); err != nil {
return fmt.Errorf("Error setting memberships: %s", err)
}

group := d.Get("group")
d.SetId(fmt.Sprintf("%s/transitiveMemberships", group.(string))) // groups/{group_id}/transitiveMemberships
return nil
}

func flattenCloudIdentityGroupTransitiveMembershipsRoles(roles []*cloudidentity.TransitiveMembershipRole) []interface{} {
transformed := []interface{}{}

for _, role := range roles {
transformed = append(transformed, map[string]interface{}{
"role": role.Role,
})
}
return transformed
}

// flattenCloudIdentityGroupsEntityKeyList is a version of flattenCloudIdentityGroupsEntityKey that
// can accept a list of EntityKeys
func flattenCloudIdentityGroupsEntityKeyList(entityKeys []*cloudidentity.EntityKey) []interface{} {
transformed := []interface{}{}

for _, key := range entityKeys {
transformed = append(transformed, map[string]interface{}{
"id": key.Id,
"namespace": key.Namespace,
})
}

return transformed
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
package cloudidentity_test

import (
"fmt"
"strconv"
"testing"

"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/hashicorp/terraform-plugin-testing/terraform"
"github.com/hashicorp/terraform-provider-google/google/acctest"
"github.com/hashicorp/terraform-provider-google/google/envvar"
)

func testAccDataSourceCloudIdentityGroupTransitiveMemberships_basicTest(t *testing.T) {

randString := acctest.RandString(t, 10)
context := map[string]interface{}{
"org_domain": envvar.GetTestOrgDomainFromEnv(t),
"cust_id": envvar.GetTestCustIdFromEnv(t),
"identity_user": envvar.GetTestIdentityUserFromEnv(t),
"random_suffix": randString,
"group_b_id": fmt.Sprintf("tf-test-group-b-%s@%s", randString, envvar.GetTestOrgDomainFromEnv(t)),
}

memberId := acctest.Nprintf("%{identity_user}@%{org_domain}", context)
groupBId := context["group_b_id"].(string)

acctest.VcrTest(t, resource.TestCase{
PreCheck: func() { acctest.AccTestPreCheck(t) },
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t),
ExternalProviders: map[string]resource.ExternalProvider{
"time": {},
},
Steps: []resource.TestStep{
{
Config: testAccCloudIdentityGroupTransitiveMembershipConfig(context),
Check: resource.ComposeTestCheckFunc(
// Finds two members of Group A (1 direct, 1 indirect)
resource.TestCheckResourceAttr("data.google_cloud_identity_group_transitive_memberships.members",
"memberships.#", "2"),
// Group B is a member of Group A; DIRECT membership to A
checkGroupTransitiveMembershipRelationship("data.google_cloud_identity_group_transitive_memberships.members", groupBId, "DIRECT"),
// User is a member of Group B; INDIRECT membership to A
checkGroupTransitiveMembershipRelationship("data.google_cloud_identity_group_transitive_memberships.members", memberId, "INDIRECT"),
),
},
},
})
}

// Create Group A, Group B
// Make Group B a member of Group A
// Make identity user a member of Group B; is a transitive member of Group A
func testAccCloudIdentityGroupTransitiveMembershipConfig(context map[string]interface{}) string {
return acctest.Nprintf(`
resource "google_cloud_identity_group" "group_a" {
display_name = "tf-test-group-a-%{random_suffix}"
parent = "customers/%{cust_id}"
group_key {
id = "tf-test-group-a-%{random_suffix}@%{org_domain}"
}
labels = {
"cloudidentity.googleapis.com/groups.discussion_forum" = ""
}
}
resource "google_cloud_identity_group" "group_b" {
display_name = "tf-test-group-b-%{random_suffix}"
parent = "customers/%{cust_id}"
group_key {
id = "%{group_b_id}"
}
labels = {
"cloudidentity.googleapis.com/groups.discussion_forum" = ""
}
}
resource "google_cloud_identity_group_membership" "group_b_membership_in_group_a" {
group = google_cloud_identity_group.group_a.id
preferred_member_key {
id = "%{group_b_id}"
}
roles {
name = "MEMBER"
}
}
// By putting the user in group B, they are also a member of group A via B
resource "google_cloud_identity_group_membership" "user_in_group_b" {
group = google_cloud_identity_group.group_b.id
preferred_member_key {
id = "%{identity_user}@%{org_domain}"
}
roles {
name = "MEMBER"
}
roles {
name = "MANAGER"
}
}
# Wait after adding user to group B to handle eventual consistency errors.
resource "time_sleep" "wait_15_seconds" {
depends_on = [google_cloud_identity_group_membership.user_in_group_b]
create_duration = "15s"
}
// Look for all members of Group A. This should return Group B and the user.
data "google_cloud_identity_group_transitive_memberships" "members" {
group = google_cloud_identity_group.group_a.id
depends_on = [
google_cloud_identity_group_membership.user_in_group_b,
time_sleep.wait_15_seconds
]
}
`, context)
}

func checkGroupTransitiveMembershipRelationship(datasourceName, memberId, expectedRelationType string) resource.TestCheckFunc {
return func(s *terraform.State) error {
ds, ok := s.RootModule().Resources[datasourceName]
if !ok {
return fmt.Errorf("root module has no resource called %s", datasourceName)
}

if ds.Primary.Attributes["memberships.#"] == "0" {
return fmt.Errorf("no memberships found in %s", datasourceName)
}

membersCount, err := strconv.Atoi(ds.Primary.Attributes["memberships.#"])
if err != nil {
return fmt.Errorf("error getting number of members, %v", err)
}
found := false
for i := 0; i < membersCount; i++ {
id := ds.Primary.Attributes[fmt.Sprintf("memberships.%d.preferred_member_key.0.id", i)]
relType := ds.Primary.Attributes[fmt.Sprintf("memberships.%d.relation_type", i)]
found = (id == memberId) && (relType == expectedRelationType)

if found {
break
}
}

if !found {
return fmt.Errorf("did not find a user with id %s and relation type %s in the memberships list", memberId, expectedRelationType)
}

return nil
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,10 @@ func TestAccCloudIdentityGroup(t *testing.T) {
"membership_with_member_key": testAccCloudIdentityGroupMembership_cloudIdentityGroupMembershipWithMemberKeyTest,
"membership_user_with_member_key": testAccCloudIdentityGroupMembership_cloudIdentityGroupMembershipUserWithMemberKeyTest,
<% end -%>
"data_source_basic": testAccDataSourceCloudIdentityGroups_basicTest,
"data_source_membership_basic": testAccDataSourceCloudIdentityGroupMemberships_basicTest,
"data_source_group_lookup": testAccDataSourceCloudIdentityGroupLookup_basicTest,
"data_source_basic": testAccDataSourceCloudIdentityGroups_basicTest,
"data_source_membership_basic": testAccDataSourceCloudIdentityGroupMemberships_basicTest,
"data_source_transitive_membership_basic": testAccDataSourceCloudIdentityGroupTransitiveMemberships_basicTest,
"data_source_group_lookup": testAccDataSourceCloudIdentityGroupLookup_basicTest,
}

for name, tc := range testCases {
Expand Down
Loading

0 comments on commit c09412b

Please sign in to comment.