-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add new data source `google_cloud_identity_transitive_group_membershi…
…ps` (#11337)
- Loading branch information
1 parent
d602092
commit c09412b
Showing
6 changed files
with
420 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
180 changes: 180 additions & 0 deletions
180
...orm/services/cloudidentity/data_source_cloud_identity_group_transitive_memberships.go.erb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
166 changes: 166 additions & 0 deletions
166
...rm/services/cloudidentity/data_source_cloud_identity_group_transitive_memberships_test.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.