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.