From c09412b4825168c00bcf4e0fe81f97c0ad03c5ef Mon Sep 17 00:00:00 2001
From: Sarah French <15078782+SarahFrench@users.noreply.github.com>
Date: Thu, 8 Aug 2024 12:01:38 +0100
Subject: [PATCH] Add new data source
`google_cloud_identity_transitive_group_memberships` (#11337)
---
.../provider/provider_mmv1_resources.go.erb | 1 +
...entity_group_transitive_memberships.go.erb | 180 ++++++++++++++++++
...ntity_group_transitive_memberships_test.go | 166 ++++++++++++++++
.../resource_cloud_identity_group_test.go.erb | 7 +-
...ud_identity_group_membership.html.markdown | 11 +-
..._transitive_group_membership.html.markdown | 60 ++++++
6 files changed, 420 insertions(+), 5 deletions(-)
create mode 100644 mmv1/third_party/terraform/services/cloudidentity/data_source_cloud_identity_group_transitive_memberships.go.erb
create mode 100644 mmv1/third_party/terraform/services/cloudidentity/data_source_cloud_identity_group_transitive_memberships_test.go
create mode 100644 mmv1/third_party/terraform/website/docs/d/cloud_identity_transitive_group_membership.html.markdown
diff --git a/mmv1/third_party/terraform/provider/provider_mmv1_resources.go.erb b/mmv1/third_party/terraform/provider/provider_mmv1_resources.go.erb
index 4941b2ea2104..976ddd8ac421 100644
--- a/mmv1/third_party/terraform/provider/provider_mmv1_resources.go.erb
+++ b/mmv1/third_party/terraform/provider/provider_mmv1_resources.go.erb
@@ -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(),
diff --git a/mmv1/third_party/terraform/services/cloudidentity/data_source_cloud_identity_group_transitive_memberships.go.erb b/mmv1/third_party/terraform/services/cloudidentity/data_source_cloud_identity_group_transitive_memberships.go.erb
new file mode 100644
index 000000000000..80c883febbbb
--- /dev/null
+++ b/mmv1/third_party/terraform/services/cloudidentity/data_source_cloud_identity_group_transitive_memberships.go.erb
@@ -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
+}
diff --git a/mmv1/third_party/terraform/services/cloudidentity/data_source_cloud_identity_group_transitive_memberships_test.go b/mmv1/third_party/terraform/services/cloudidentity/data_source_cloud_identity_group_transitive_memberships_test.go
new file mode 100644
index 000000000000..31f3896a74f4
--- /dev/null
+++ b/mmv1/third_party/terraform/services/cloudidentity/data_source_cloud_identity_group_transitive_memberships_test.go
@@ -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
+ }
+}
diff --git a/mmv1/third_party/terraform/services/cloudidentity/resource_cloud_identity_group_test.go.erb b/mmv1/third_party/terraform/services/cloudidentity/resource_cloud_identity_group_test.go.erb
index a4ab8746b348..66f865338bdd 100644
--- a/mmv1/third_party/terraform/services/cloudidentity/resource_cloud_identity_group_test.go.erb
+++ b/mmv1/third_party/terraform/services/cloudidentity/resource_cloud_identity_group_test.go.erb
@@ -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 {
diff --git a/mmv1/third_party/terraform/website/docs/d/cloud_identity_group_membership.html.markdown b/mmv1/third_party/terraform/website/docs/d/cloud_identity_group_membership.html.markdown
index 0141ad037489..897bbef9af4d 100644
--- a/mmv1/third_party/terraform/website/docs/d/cloud_identity_group_membership.html.markdown
+++ b/mmv1/third_party/terraform/website/docs/d/cloud_identity_group_membership.html.markdown
@@ -1,7 +1,7 @@
---
subcategory: "Cloud Identity"
description: |-
- Get list of the Cloud Identity Group Memberships within a Group.
+ Get a list of the Cloud Identity Group Memberships within a Group.
---
# google_cloud_identity_group_memberships
@@ -10,6 +10,13 @@ Use this data source to get list of the Cloud Identity Group Memberships within
https://cloud.google.com/identity/docs/concepts/overview#memberships
+To get more information about GroupMembership, see:
+
+* [API documentation](https://cloud.google.com/identity/docs/reference/rest/v1/groups.memberships)
+* How-to Guides
+ * [Official Documentation](https://cloud.google.com/identity/docs/how-to/memberships-google-groups)
+
+
## Example Usage
```tf
@@ -20,7 +27,7 @@ data "google_cloud_identity_group_memberships" "members" {
## Argument Reference
-* `group` - The parent Group resource under which to lookup the Membership names. Must be of the form groups/{group_id}.
+* `group` - (Required) The parent Group resource under which to lookup the Membership names. Must be of the form groups/{group_id}.
## Attributes Reference
diff --git a/mmv1/third_party/terraform/website/docs/d/cloud_identity_transitive_group_membership.html.markdown b/mmv1/third_party/terraform/website/docs/d/cloud_identity_transitive_group_membership.html.markdown
new file mode 100644
index 000000000000..8600462de624
--- /dev/null
+++ b/mmv1/third_party/terraform/website/docs/d/cloud_identity_transitive_group_membership.html.markdown
@@ -0,0 +1,60 @@
+---
+subcategory: "Cloud Identity"
+description: |-
+ Get a list of direct and indirect Cloud Identity Group Memberships within a Group.
+---
+
+# google_cloud_identity_group_transitive_memberships
+
+Use this data source to get list of the Cloud Identity Group Memberships within a given Group. Whereas `google_cloud_identity_group_memberships` returns details of only direct members of the group, `google_cloud_identity_group_transitive_memberships` will return details about both direct and indirect members. For example, a user is an indirect member of Group A if the user is a direct member of Group B and Group B is a direct member of Group A.
+
+To get more information about TransitiveGroupMembership, see:
+
+* [API documentation](https://cloud.google.com/identity/docs/reference/rest/v1/groups.memberships/searchTransitiveMemberships)
+* How-to Guides
+ * [Official Documentation](https://cloud.google.com/identity/docs/how-to/memberships-google-groups)
+
+## Example Usage
+
+```tf
+data "google_cloud_identity_group_transitive_memberships" "members" {
+ group = "groups/123eab45c6defghi"
+}
+```
+
+## Argument Reference
+
+* `group` - (Required) The parent Group resource to search transitive memberships in. Must be of the form groups/{group_id}.
+
+## Attributes Reference
+
+In addition to the arguments listed above, the following attributes are exported:
+
+* `memberships` - The list of memberships under the given group. Structure is [documented below](#nested_memberships).
+
+The `memberships` block contains:
+
+* `roles` - The TransitiveMembershipRoles that apply to the Membership. Structure is [documented below](#nested_roles).
+
+* `member` - EntityKey of the member. This value will be either a userKey in the format `users/000000000000000000000` with a numerical id or a groupKey in the format `groups/000ab0000ab0000` with a hexadecimal id.
+
+* `relation_type` - The relation between the group and the transitive member. The value can be DIRECT, INDIRECT, or DIRECT_AND_INDIRECT.
+
+* `preferred_member_key` -
+ (Optional)
+ EntityKey of the member. Structure is [documented below](#nested_preferred_member_key).
+
+The `roles` block supports:
+
+* `role` - The name of the TransitiveMembershipRole. One of OWNER, MANAGER, MEMBER.
+
+The `preferred_member_key` block supports:
+
+* `id` - The ID of the entity. For Google-managed entities, the id is the email address of an existing
+ group or user. For external-identity-mapped entities, the id is a string conforming
+ to the Identity Source's requirements.
+
+* `namespace` - The namespace in which the entity exists.
+ If not populated, the EntityKey represents a Google-managed entity
+ such as a Google user or a Google Group.
+ If populated, the EntityKey represents an external-identity-mapped group.