From 11073ef9672c9d3b248383e7b9f9708169636542 Mon Sep 17 00:00:00 2001
From: Alexander Holmquist <48073242+Yowgf@users.noreply.github.com>
Date: Sat, 6 Aug 2022 02:42:01 -0300
Subject: [PATCH] ENG-8938: Add data source to retrieve user groups (#264)
* (WIP) Add cyral_role data source
* (WIP) Add test and fix resource details
* Fix data source and improve descriptions
* Fix regexp name filter
* Add documentation example and finish up details
* Fix merge with main
---
cyral/data_source_cyral_role.go | 171 ++++++++++++++++++
cyral/data_source_cyral_role_test.go | 57 ++++++
cyral/provider.go | 1 +
docs/data-sources/role.md | 57 ++++++
.../data-sources/cyral_role/data-source.tf | 4 +
5 files changed, 290 insertions(+)
create mode 100644 cyral/data_source_cyral_role.go
create mode 100644 cyral/data_source_cyral_role_test.go
create mode 100644 docs/data-sources/role.md
create mode 100644 examples/data-sources/cyral_role/data-source.tf
diff --git a/cyral/data_source_cyral_role.go b/cyral/data_source_cyral_role.go
new file mode 100644
index 00000000..41872eba
--- /dev/null
+++ b/cyral/data_source_cyral_role.go
@@ -0,0 +1,171 @@
+package cyral
+
+import (
+ "fmt"
+ "net/http"
+ "regexp"
+
+ "github.com/google/uuid"
+ "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
+
+ "github.com/cyralinc/terraform-provider-cyral/client"
+)
+
+type GetUserGroupsResponse struct {
+ Groups []*UserGroup `json:"groups,omitempty"`
+}
+
+func (resp *GetUserGroupsResponse) WriteToSchema(d *schema.ResourceData) error {
+ nameFilter := d.Get("name").(string)
+ var nameFilterRegexp *regexp.Regexp
+ if nameFilter != "" {
+ var err error
+ if nameFilterRegexp, err = regexp.Compile(nameFilter); err != nil {
+ return fmt.Errorf("provided name filter is invalid "+
+ "regexp: %w", err)
+ }
+ }
+
+ roleList := []interface{}{}
+ for _, group := range resp.Groups {
+ if group == nil {
+ continue
+ }
+
+ if nameFilterRegexp != nil {
+ if !nameFilterRegexp.MatchString(group.Name) {
+ continue
+ }
+ }
+
+ argumentVals := map[string]interface{}{
+ "id": group.ID,
+ "name": group.Name,
+ "description": group.Description,
+ "roles": group.Roles,
+ "members": group.Members,
+ }
+ ssoGroups := []interface{}{}
+ for _, mapping := range group.Mappings {
+ if mapping == nil {
+ continue
+ }
+ ssoGroups = append(ssoGroups, map[string]interface{}{
+ "id": mapping.Id,
+ "group_name": mapping.GroupName,
+ "idp_id": mapping.IdentityProviderId,
+ "idp_name": mapping.IdentityProviderName,
+ })
+ }
+ argumentVals["sso_groups"] = ssoGroups
+ roleList = append(roleList, argumentVals)
+ }
+ if err := d.Set("role_list", roleList); err != nil {
+ return err
+ }
+ d.SetId(uuid.New().String())
+ return nil
+}
+
+type UserGroup struct {
+ ID string `json:"id,omitempty"`
+ Name string `json:"name,omitempty"`
+ Description string `json:"description,omitempty"`
+ Roles []string `json:"roles,omitempty"`
+ Members []string `json:"members"`
+ Mappings []*SSOGroup `json:"mappings"`
+}
+
+func dataSourceRoleReadConfig() ResourceOperationConfig {
+ return ResourceOperationConfig{
+ Name: "RoleDataSourceRead",
+ HttpMethod: http.MethodGet,
+ CreateURL: func(d *schema.ResourceData, c *client.Client) string {
+ return fmt.Sprintf("https://%s/v1/users/groups", c.ControlPlane)
+ },
+ NewResponseData: func(_ *schema.ResourceData) ResponseData { return &GetUserGroupsResponse{} },
+ }
+}
+
+func dataSourceRole() *schema.Resource {
+ return &schema.Resource{
+ Description: "Retrieve and filter [roles](https://cyral.com/docs/account-administration/acct-manage-cyral-roles/) that exist in the Cyral Control Plane.",
+ ReadContext: ReadResource(dataSourceRoleReadConfig()),
+ Schema: map[string]*schema.Schema{
+ "name": {
+ Description: "Filter the results by a regular expression (regex) that matches names of existing roles.",
+ Type: schema.TypeString,
+ Optional: true,
+ },
+ "role_list": {
+ Description: "List of existing roles satisfying given filter criteria.",
+ Computed: true,
+ Type: schema.TypeList,
+ Elem: &schema.Resource{
+ Schema: map[string]*schema.Schema{
+ "id": {
+ Description: "ID of the role in the Cyral environment.",
+ Type: schema.TypeString,
+ Computed: true,
+ },
+ "name": {
+ Description: "Role name.",
+ Type: schema.TypeString,
+ Computed: true,
+ },
+ "description": {
+ Description: "Role description.",
+ Type: schema.TypeString,
+ Computed: true,
+ },
+ "roles": {
+ Description: "IDs of the specific permission roles this role is allowed to assume (e.g. `View Datamaps`, `View Audit Logs`, etc).",
+ Type: schema.TypeList,
+ Computed: true,
+ Elem: &schema.Schema{
+ Type: schema.TypeString,
+ },
+ },
+ "members": {
+ Description: "IDs of the users that belong to this role.",
+ Type: schema.TypeList,
+ Computed: true,
+ Elem: &schema.Schema{
+ Type: schema.TypeString,
+ },
+ },
+ "sso_groups": {
+ Description: `SSO groups mapped to this role. An SSO group mapping means that this role was automatically granted to a user because there's a rule such as "If a user is an 'Engineer' (SSO group) in a specific Identity Provider, make them a 'Super Admin' (role) in Cyral".`,
+ Type: schema.TypeList,
+ Computed: true,
+ Elem: &schema.Resource{
+ Schema: map[string]*schema.Schema{
+ "id": {
+ Description: "The ID of the SSO group mapping.",
+ Type: schema.TypeString,
+ Computed: true,
+ },
+ "group_name": {
+ Description: "The name of a group configured in the identity provider, e.g. 'Engineer', 'Admin', 'Everyone', etc.",
+ Type: schema.TypeString,
+ Computed: true,
+ },
+ "idp_id": {
+ Description: "ID of the identity provider integration.",
+ Type: schema.TypeString,
+ Computed: true,
+ },
+ "idp_name": {
+ Description: "Display name of the identity provider integration.",
+ Type: schema.TypeString,
+ Computed: true,
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ }
+}
diff --git a/cyral/data_source_cyral_role_test.go b/cyral/data_source_cyral_role_test.go
new file mode 100644
index 00000000..ec54c2e3
--- /dev/null
+++ b/cyral/data_source_cyral_role_test.go
@@ -0,0 +1,57 @@
+package cyral
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
+)
+
+// TODO: currently, we just test that the configs are valid. We need to add ACC
+// tests for a full scenario, containing roles, IdPs and user groups (see
+// resources `cyral_role` and `cyral_role_sso_groups`. -aholmquist 2022-08-05
+/*
+func roleDataSourceTestUserGroupsAndRoleNames() ([]*UserGroup, []string) {
+ return []*UserGroup{
+ {
+ Name: "tf-provider-test-user-group-1",
+ Description: "description-1",
+ },
+ {
+ Name: "tf-provider-test-user-group-2",
+ Description: "description-2",
+ },
+ }, []string{
+ "tf-provider-test-role-1",
+ "tf-provider-test-role-2",
+ }
+}
+*/
+
+func TestAccRoleDataSource(t *testing.T) {
+ resource.Test(t, resource.TestCase{
+ ProviderFactories: providerFactories,
+ Steps: []resource.TestStep{
+ {
+ Config: roleDataSourceConfig(
+ "main_test",
+ "tf-provider-test-user-group-1",
+ []string{}),
+ },
+ {
+ Config: roleDataSourceConfig(
+ "main_test",
+ "tf-provider-test-user-group-2",
+ []string{}),
+ },
+ },
+ })
+}
+
+func roleDataSourceConfig(dsourceName, nameFilter string, dependsOn []string) string {
+ return fmt.Sprintf(`
+ data "cyral_role" "%s" {
+ name = "%s"
+ depends_on = [%s]
+ }`, dsourceName, nameFilter, formatAttributes(dependsOn))
+}
diff --git a/cyral/provider.go b/cyral/provider.go
index 23923210..937cbcb5 100644
--- a/cyral/provider.go
+++ b/cyral/provider.go
@@ -120,6 +120,7 @@ func Provider() *schema.Provider {
"cyral_datalabel": dataSourceDatalabel(),
"cyral_integration_idp": dataSourceIntegrationIdP(),
"cyral_repository": dataSourceRepository(),
+ "cyral_role": dataSourceRole(),
"cyral_saml_certificate": dataSourceSAMLCertificate(),
"cyral_saml_configuration": dataSourceSAMLConfiguration(),
"cyral_sidecar_bound_ports": dataSourceSidecarBoundPorts(),
diff --git a/docs/data-sources/role.md b/docs/data-sources/role.md
new file mode 100644
index 00000000..72ad2049
--- /dev/null
+++ b/docs/data-sources/role.md
@@ -0,0 +1,57 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "cyral_role Data Source - cyral"
+subcategory: ""
+description: |-
+ Retrieve and filter roles https://cyral.com/docs/account-administration/acct-manage-cyral-roles/ that exist in the Cyral Control Plane.
+---
+
+# cyral_role (Data Source)
+
+Retrieve and filter [roles](https://cyral.com/docs/account-administration/acct-manage-cyral-roles/) that exist in the Cyral Control Plane.
+
+## Example Usage
+
+```terraform
+data "cyral_role" "admin_roles" {
+ # Optional. Filter roles with name that matches regular expression.
+ name = "^.*Admin$"
+}
+```
+
+
+
+## Schema
+
+### Optional
+
+- `name` (String) Filter the results by a regular expression (regex) that matches names of existing roles.
+
+### Read-Only
+
+- `id` (String) The ID of this resource.
+- `role_list` (List of Object) List of existing roles satisfying given filter criteria. (see [below for nested schema](#nestedatt--role_list))
+
+
+
+### Nested Schema for `role_list`
+
+Read-Only:
+
+- `description` (String)
+- `id` (String)
+- `members` (List of String)
+- `name` (String)
+- `roles` (List of String)
+- `sso_groups` (List of Object) (see [below for nested schema](#nestedobjatt--role_list--sso_groups))
+
+
+
+### Nested Schema for `role_list.sso_groups`
+
+Read-Only:
+
+- `group_name` (String)
+- `id` (String)
+- `idp_id` (String)
+- `idp_name` (String)
diff --git a/examples/data-sources/cyral_role/data-source.tf b/examples/data-sources/cyral_role/data-source.tf
new file mode 100644
index 00000000..a5bcd0a0
--- /dev/null
+++ b/examples/data-sources/cyral_role/data-source.tf
@@ -0,0 +1,4 @@
+data "cyral_role" "admin_roles" {
+ # Optional. Filter roles with name that matches regular expression.
+ name = "^.*Admin$"
+}