diff --git a/cyral/data_source_cyral_permission.go b/cyral/data_source_cyral_permission.go new file mode 100644 index 00000000..6e093c3b --- /dev/null +++ b/cyral/data_source_cyral_permission.go @@ -0,0 +1,77 @@ +package cyral + +import ( + "fmt" + "net/http" + + "github.com/google/uuid" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/cyralinc/terraform-provider-cyral/client" +) + +const ( + // Schema keys + PermissionDataSourcePermissionListKey = "permission_list" +) + +type PermissionDataSourceResponse struct { + // Permissions correspond to Roles in API. + Permissions []Permission `json:"roles"` +} + +func (response *PermissionDataSourceResponse) WriteToSchema(d *schema.ResourceData) error { + d.SetId(uuid.New().String()) + d.Set(PermissionDataSourcePermissionListKey, permissionsToInterfaceList(response.Permissions)) + return nil +} + +func dataSourcePermission() *schema.Resource { + return &schema.Resource{ + Description: "Retrieve all Cyral permissions. See also resource " + + "[`cyral_service_account`](../resources/service_account.md).", + ReadContext: ReadResource( + ResourceOperationConfig{ + Name: "PermissionDataSourceRead", + HttpMethod: http.MethodGet, + CreateURL: func(d *schema.ResourceData, c *client.Client) string { + return fmt.Sprintf("https://%s/v1/users/roles", c.ControlPlane) + }, + NewResponseData: func(d *schema.ResourceData) ResponseData { + return &PermissionDataSourceResponse{} + }, + }, + ), + Schema: map[string]*schema.Schema{ + IDKey: { + Description: "The data source identifier.", + Type: schema.TypeString, + Computed: true, + }, + PermissionDataSourcePermissionListKey: { + Description: "List of all existing Cyral permissions.", + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + IDKey: { + Description: "Permission identifier.", + Type: schema.TypeString, + Computed: true, + }, + NameKey: { + Description: "Permission name.", + Type: schema.TypeString, + Computed: true, + }, + DescriptionKey: { + Description: "Permission description.", + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + }, + } +} diff --git a/cyral/data_source_cyral_permission_test.go b/cyral/data_source_cyral_permission_test.go new file mode 100644 index 00000000..844ee130 --- /dev/null +++ b/cyral/data_source_cyral_permission_test.go @@ -0,0 +1,62 @@ +package cyral + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccPermissionDataSource(t *testing.T) { + testSteps := []resource.TestStep{} + dataSourceName1 := "permissions_1" + testSteps = append( + testSteps, + []resource.TestStep{ + accTestStepPermissionDataSource_RetrieveAllPermissions(dataSourceName1), + }..., + ) + + resource.ParallelTest(t, resource.TestCase{ + ProviderFactories: providerFactories, + Steps: testSteps, + }) +} + +func accTestStepPermissionDataSource_RetrieveAllPermissions(dataSourceName string) resource.TestStep { + dataSourceFullName := fmt.Sprintf("data.cyral_permission.%s", dataSourceName) + config := fmt.Sprintf(` + data "cyral_permission" "%s" { + } + `, dataSourceName) + var checks []resource.TestCheckFunc + for index, expectedPermissionName := range allPermissionNames { + checks = append(checks, + []resource.TestCheckFunc{ + resource.TestCheckResourceAttrSet( + dataSourceFullName, + fmt.Sprintf( + "%s.%d.%s", + PermissionDataSourcePermissionListKey, + index, + IDKey, + ), + ), + resource.TestCheckTypeSetElemNestedAttrs( + dataSourceFullName, + fmt.Sprintf("%s.*", PermissionDataSourcePermissionListKey), + map[string]string{NameKey: expectedPermissionName}, + ), + resource.TestCheckTypeSetElemNestedAttrs( + dataSourceFullName, + fmt.Sprintf("%s.*", PermissionDataSourcePermissionListKey), + map[string]string{DescriptionKey: expectedPermissionName}, + ), + }..., + ) + } + return resource.TestStep{ + Config: config, + Check: resource.ComposeTestCheckFunc(checks...), + } +} diff --git a/cyral/model_permission.go b/cyral/model_permission.go new file mode 100644 index 00000000..274f665d --- /dev/null +++ b/cyral/model_permission.go @@ -0,0 +1,139 @@ +package cyral + +import ( + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +type Permission struct { + Id string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` +} + +func permissionsToInterfaceList(permissions []Permission) []any { + permissionsInterfaceList := make([]any, len(permissions)) + for index, permission := range permissions { + permissionsInterfaceList[index] = map[string]any{ + IDKey: permission.Id, + NameKey: permission.Name, + DescriptionKey: permission.Description, + } + } + return permissionsInterfaceList +} + +var allPermissionNames = []string{ + "Approval Management", + "Modify Policies", + "Modify Roles", + "Modify Sidecars and Repositories", + "Modify Users", + "Repo Crawler", + "View Audit Logs", + "View Datamaps", + "View Integrations", + "View Policies", + "View Roles", + "View Users", + "Modify Integrations", +} + +const ( + // Schema keys + approvalManagementPermissionKey = "approval_management" + modifyPoliciesPermissionKey = "modify_policies" + modifyRolesPermissionKey = "modify_roles" + modifySidecarAndRepositoriesPermissionKey = "modify_sidecars_and_repositories" + modifyUsersPermissionKey = "modify_users" + repoCrawlerPermissionKey = "repo_crawler" + viewAuditLogsPermissionKey = "view_audit_logs" + viewDatamapsPermissionKey = "view_datamaps" + viewIntegrationsPermissionKey = "view_integrations" + viewPoliciesPermissionKey = "view_policies" + viewRolesPermissionKey = "view_roles" + viewUsersPermissionKey = "view_users" + modifyIntegrationsPermissionKey = "modify_integrations" +) + +var permissionsSchema = map[string]*schema.Schema{ + approvalManagementPermissionKey: { + Description: "Allows approving or denying approval requests on Cyral Control Plane. " + + "Defaults to `false`.", + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + modifyPoliciesPermissionKey: { + Description: "Allows modifying policies on Cyral Control Plane. Defaults to `false`.", + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + modifyRolesPermissionKey: { + Description: "Allows modifying roles on Cyral Control Plane. Defaults to `false`.", + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + modifySidecarAndRepositoriesPermissionKey: { + Description: "Allows modifying sidecars and repositories on Cyral Control Plane. Defaults to `false`.", + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + modifyUsersPermissionKey: { + Description: "Allows modifying users on Cyral Control Plane. Defaults to `false`.", + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + repoCrawlerPermissionKey: { + Description: "Allows running the Cyral repo crawler data classifier and user discovery. " + + "Defaults to `false`.", + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + viewAuditLogsPermissionKey: { + Description: "Allows viewing audit logs on Cyral Control Plane. Defaults to `false`.", + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + viewDatamapsPermissionKey: { + Description: "Allows viewing datamaps on Cyral Control Plane. Defaults to `false`.", + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + viewIntegrationsPermissionKey: { + Description: "Allows viewing integrations on Cyral Control Plane. Defaults to `false`.", + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + viewPoliciesPermissionKey: { + Description: "Allows viewing policies on Cyral Control Plane. Defaults to `false`.", + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + viewRolesPermissionKey: { + Description: "Allows viewing roles on Cyral Control Plane. Defaults to `false`.", + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + viewUsersPermissionKey: { + Description: "Allows viewing users on Cyral Control Plane. Defaults to `false`.", + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + modifyIntegrationsPermissionKey: { + Description: "Allows modifying integrations on Cyral Control Plane. Defaults to `false`.", + Type: schema.TypeBool, + Optional: true, + Default: false, + }, +} diff --git a/cyral/model_service_account.go b/cyral/model_service_account.go new file mode 100644 index 00000000..93cdd05f --- /dev/null +++ b/cyral/model_service_account.go @@ -0,0 +1,39 @@ +package cyral + +import ( + "fmt" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +type ServiceAccount struct { + DisplayName string `json:"displayName"` + ClientID string `json:"clientId,omitempty"` + ClientSecret string `json:"clientSecret,omitempty"` + // Permissions correspond to Roles in Cyral APIs. + PermissionIDs []string `json:"roleIds"` +} + +func (serviceAccount *ServiceAccount) ReadFromSchema(d *schema.ResourceData) error { + serviceAccount.DisplayName = d.Get(serviceAccountResourceDisplayNameKey).(string) + permissionIDs := convertFromInterfaceList[string]( + d.Get(serviceAccountResourcePermissionIDsKey).(*schema.Set).List(), + ) + if len(permissionIDs) == 0 { + return fmt.Errorf("at least one permission must be specified for the service account") + } + serviceAccount.PermissionIDs = permissionIDs + return nil +} + +func (serviceAccount *ServiceAccount) WriteToSchema(d *schema.ResourceData) error { + d.SetId(serviceAccount.ClientID) + d.Set(serviceAccountResourceDisplayNameKey, serviceAccount.DisplayName) + d.Set(serviceAccountResourceClientIDKey, serviceAccount.ClientID) + isCreateResponse := serviceAccount.ClientSecret != "" + if isCreateResponse { + d.Set(serviceAccountResourceClientSecretKey, serviceAccount.ClientSecret) + } + d.Set(serviceAccountResourcePermissionIDsKey, convertToInterfaceList(serviceAccount.PermissionIDs)) + return nil +} diff --git a/cyral/provider.go b/cyral/provider.go index 94b3a144..07a34c56 100644 --- a/cyral/provider.go +++ b/cyral/provider.go @@ -75,6 +75,7 @@ func Provider() *schema.Provider { "cyral_integration_idp": dataSourceIntegrationIdP(), "cyral_integration_idp_saml": dataSourceIntegrationIdPSAML(), "cyral_integration_logging": dataSourceIntegrationLogging(), + "cyral_permission": dataSourcePermission(), "cyral_repository": dataSourceRepository(), "cyral_role": dataSourceRole(), "cyral_saml_certificate": dataSourceSAMLCertificate(), @@ -122,6 +123,7 @@ func Provider() *schema.Provider { "cyral_repository_access_gateway": resourceRepositoryAccessGateway(), "cyral_role": resourceRole(), "cyral_role_sso_groups": resourceRoleSSOGroups(), + "cyral_service_account": resourceServiceAccount(), "cyral_sidecar": resourceSidecar(), "cyral_sidecar_credentials": resourceSidecarCredentials(), "cyral_sidecar_listener": resourceSidecarListener(), @@ -130,7 +132,7 @@ func Provider() *schema.Provider { } } -func providerConfigure(ctx context.Context, d *schema.ResourceData) (interface{}, diag.Diagnostics) { +func providerConfigure(ctx context.Context, d *schema.ResourceData) (any, diag.Diagnostics) { log.Printf("[DEBUG] Init providerConfigure") clientID, clientSecret, diags := getCredentials(d) diff --git a/cyral/resource.go b/cyral/resource.go index 4ed14e36..827a8176 100644 --- a/cyral/resource.go +++ b/cyral/resource.go @@ -48,7 +48,7 @@ type ResourceOperationConfig struct { NewResponseData func(d *schema.ResourceData) ResponseData } -func CRUDResources(resourceOperations []ResourceOperation) func(context.Context, *schema.ResourceData, interface{}) diag.Diagnostics { +func CRUDResources(resourceOperations []ResourceOperation) func(context.Context, *schema.ResourceData, any) diag.Diagnostics { return HandleRequests(resourceOperations) } @@ -106,8 +106,8 @@ func DeleteResource(deleteConfig ResourceOperationConfig) schema.DeleteContextFu func HandleRequests( resourceOperations []ResourceOperation, -) func(context.Context, *schema.ResourceData, interface{}) diag.Diagnostics { - return func(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { +) func(context.Context, *schema.ResourceData, any) diag.Diagnostics { + return func(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { for _, operation := range resourceOperations { log.Printf("[DEBUG] Init %s", operation.Config.Name) c := m.(*client.Client) diff --git a/cyral/resource_cyral_role.go b/cyral/resource_cyral_role.go index e4250654..0671482f 100644 --- a/cyral/resource_cyral_role.go +++ b/cyral/resource_cyral_role.go @@ -17,7 +17,7 @@ import ( type RoleDataRequest struct { Name string `json:"name,omitempty"` // Permissions correspond to Roles in API. - Permissions []string `json:"roles,omitempty"` + PermissionIDs []string `json:"roles,omitempty"` } // Roles correspond to Groups in API. @@ -25,13 +25,7 @@ type RoleDataResponse struct { Id string `json:"id,omitempty"` Name string `json:"name,omitempty"` // Permissions correspond to Roles in API. - Permissions []*PermissionInfo `json:"roles,omitempty"` -} - -type PermissionInfo struct { - Id string `json:"id"` - Name string `json:"name"` - Description string `json:"description"` + Permissions []*Permission `json:"roles,omitempty"` } func resourceRole() *schema.Resource { @@ -59,62 +53,7 @@ func resourceRole() *schema.Resource { Optional: true, MaxItems: 1, Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "modify_sidecars_and_repositories": { - Description: "Allows modifying sidecars and repositories for this role. Defaults to `false`.", - Type: schema.TypeBool, - Optional: true, - Default: false, - }, - "modify_users": { - Description: "Allows modifying users for this role. Defaults to `false`.", - Type: schema.TypeBool, - Optional: true, - Default: false, - }, - "modify_policies": { - Description: "Allows modifying policies for this role. Defaults to `false`.", - Type: schema.TypeBool, - Optional: true, - Default: false, - }, - "view_audit_logs": { - Description: "Allows viewing audit logs for this role. Defaults to `false`.", - Type: schema.TypeBool, - Optional: true, - Default: false, - }, - "modify_integrations": { - Description: "Allows modifying integrations for this role. Defaults to `false`.", - Type: schema.TypeBool, - Optional: true, - Default: false, - }, - "modify_roles": { - Description: "Allows modifying roles for this role. Defaults to `false`.", - Type: schema.TypeBool, - Optional: true, - Default: false, - }, - "view_datamaps": { - Description: "Allows viewing datamaps for this role. Defaults to `false`.", - Type: schema.TypeBool, - Optional: true, - Default: false, - }, - "approval_management": { - Description: "Allows approving or denying approval requests. Defaults to `false`.", - Type: schema.TypeBool, - Optional: true, - Default: false, - }, - "repo_crawler": { - Description: "Allows reporting of cyral_repository_user_accounts. Defaults to `false`.", - Type: schema.TypeBool, - Optional: true, - Default: false, - }, - }, + Schema: permissionsSchema, }, }, }, @@ -247,12 +186,12 @@ func getRoleDataFromResource(c *client.Client, d *schema.ResourceData) (RoleData } return RoleDataRequest{ - Name: d.Get("name").(string), - Permissions: resourcePermissionsIds, + Name: d.Get("name").(string), + PermissionIDs: resourcePermissionsIds, }, nil } -func flattenPermissions(permissions []*PermissionInfo) []interface{} { +func flattenPermissions(permissions []*Permission) []interface{} { flatPermissions := make([]interface{}, 1) permissionsMap := make(map[string]interface{}) @@ -271,17 +210,17 @@ func formatPermissionName(permissionName string) string { return permissionName } -func getPermissionsFromAPI(c *client.Client) ([]*PermissionInfo, error) { +func getPermissionsFromAPI(c *client.Client) ([]*Permission, error) { url := fmt.Sprintf("https://%s/v1/users/roles", c.ControlPlane) body, err := c.DoRequest(url, http.MethodGet, nil) if err != nil { - return []*PermissionInfo{}, err + return []*Permission{}, err } response := RoleDataResponse{} if err := json.Unmarshal(body, &response); err != nil { - return []*PermissionInfo{}, err + return []*Permission{}, err } return response.Permissions, nil diff --git a/cyral/resource_cyral_service_account.go b/cyral/resource_cyral_service_account.go new file mode 100644 index 00000000..911362ef --- /dev/null +++ b/cyral/resource_cyral_service_account.go @@ -0,0 +1,134 @@ +package cyral + +import ( + "fmt" + "net/http" + + "github.com/cyralinc/terraform-provider-cyral/client" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +const ( + // Schema keys + serviceAccountResourceDisplayNameKey = "display_name" + serviceAccountResourcePermissionIDsKey = "permission_ids" + serviceAccountResourceClientIDKey = "client_id" + serviceAccountResourceClientSecretKey = "client_secret" +) + +var ( + ReadServiceAccountConfig = ResourceOperationConfig{ + Name: "ServiceAccountRead", + HttpMethod: http.MethodGet, + CreateURL: func(d *schema.ResourceData, c *client.Client) string { + return fmt.Sprintf( + "https://%s/v1/users/serviceAccounts/%s", + c.ControlPlane, + d.Id(), + ) + }, + NewResponseData: func(_ *schema.ResourceData) ResponseData { + return &ServiceAccount{} + }, + RequestErrorHandler: &ReadIgnoreHttpNotFound{resName: "Service account"}, + } +) + +func resourceServiceAccount() *schema.Resource { + return &schema.Resource{ + Description: "Manages a Cyral Service Account (A.k.a: " + + "[Cyral API Access Key](https://cyral.com/docs/api-ref/api-intro/#api-access-key)). See also " + + "data source [`cyral_permission`](../data-sources/permission.md)." + + "\n\n-> **Note** This resource does not support importing, since the client secret cannot " + + "be read after the resource creation.", + CreateContext: CreateResource( + ResourceOperationConfig{ + Name: "ServiceAccountCreate", + HttpMethod: http.MethodPost, + CreateURL: func(d *schema.ResourceData, c *client.Client) string { + return fmt.Sprintf( + "https://%s/v1/users/serviceAccounts", + c.ControlPlane, + ) + }, + NewResourceData: func() ResourceData { + return &ServiceAccount{} + }, + NewResponseData: func(_ *schema.ResourceData) ResponseData { + return &ServiceAccount{} + }, + }, + ReadServiceAccountConfig, + ), + ReadContext: ReadResource(ReadServiceAccountConfig), + UpdateContext: UpdateResource( + ResourceOperationConfig{ + Name: "ServiceAccountUpdate", + HttpMethod: http.MethodPatch, + CreateURL: func(d *schema.ResourceData, c *client.Client) string { + return fmt.Sprintf( + "https://%s/v1/users/serviceAccounts/%s", + c.ControlPlane, + d.Id(), + ) + }, + NewResourceData: func() ResourceData { + return &ServiceAccount{} + }, + }, + ReadServiceAccountConfig, + ), + DeleteContext: DeleteResource( + ResourceOperationConfig{ + Name: "ServiceAccountDelete", + HttpMethod: http.MethodDelete, + CreateURL: func(d *schema.ResourceData, c *client.Client) string { + return fmt.Sprintf( + "https://%s/v1/users/serviceAccounts/%s", + c.ControlPlane, + d.Id(), + ) + }, + }, + ), + + Schema: map[string]*schema.Schema{ + serviceAccountResourceDisplayNameKey: { + Description: "The service account display name.", + Type: schema.TypeString, + Required: true, + }, + serviceAccountResourcePermissionIDsKey: { + Description: "A list of permission IDs that will be assigned to this service account. See " + + "also data source [`cyral_permission`](../data-sources/permission.md).", + Type: schema.TypeSet, + Required: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + IDKey: { + Description: fmt.Sprintf( + "The resource identifier. It's equal to `%s`.", + serviceAccountResourceClientIDKey, + ), + Type: schema.TypeString, + Computed: true, + }, + serviceAccountResourceClientIDKey: { + Description: "The service account client ID.", + Type: schema.TypeString, + Computed: true, + }, + serviceAccountResourceClientSecretKey: { + Description: "The service account client secret. **Note**: This resource is not able to recognize " + + "changes to the client secret after its creation, so keep in mind that if the client secret is " + + "rotated, the value present in this attribute will be outdated. If you need to rotate the client " + + "secret it's recommended that you recreate this terraform resource.", + Type: schema.TypeString, + Computed: true, + Sensitive: true, + }, + }, + } +} diff --git a/cyral/resource_cyral_service_account_test.go b/cyral/resource_cyral_service_account_test.go new file mode 100644 index 00000000..3e2eec28 --- /dev/null +++ b/cyral/resource_cyral_service_account_test.go @@ -0,0 +1,222 @@ +package cyral + +import ( + "fmt" + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccServiceAccountResource(t *testing.T) { + testSteps := []resource.TestStep{} + resourceName1 := "service_account_1" + testSteps = append( + testSteps, + []resource.TestStep{ + accTestStepServiceAccountResource_RequiredArgumentDisplayName(resourceName1), + accTestStepServiceAccountResource_RequiredArgumentPermissions(resourceName1), + accTestStepServiceAccountResource_EmptyPermissions(resourceName1), + accTestStepServiceAccountResource_SinglePermission(resourceName1), + accTestStepServiceAccountResource_DuplicatedPermission(resourceName1), + accTestStepServiceAccountResource_AllPermissions(resourceName1), + accTestStepServiceAccountResource_UpdatedFields(resourceName1), + }..., + ) + + resource.ParallelTest(t, resource.TestCase{ + ProviderFactories: providerFactories, + Steps: testSteps, + }) +} + +func accTestStepServiceAccountResource_RequiredArgumentDisplayName(resourceName string) resource.TestStep { + config := fmt.Sprintf(` + resource "cyral_service_account" "%s" { + } + `, resourceName) + return resource.TestStep{ + Config: config, + ExpectError: regexp.MustCompile( + fmt.Sprintf( + `The argument "%s" is required, but no definition was found.`, + serviceAccountResourceDisplayNameKey, + ), + ), + } +} + +func accTestStepServiceAccountResource_RequiredArgumentPermissions(resourceName string) resource.TestStep { + config := fmt.Sprintf(` + resource "cyral_service_account" "%s" { + display_name = "service-account-test" + } + `, resourceName, + ) + return resource.TestStep{ + Config: config, + ExpectError: regexp.MustCompile( + fmt.Sprintf( + `The argument "%s" is required, but no definition was found.`, + serviceAccountResourcePermissionIDsKey, + ), + ), + } +} + +func accTestStepServiceAccountResource_EmptyPermissions(resourceName string) resource.TestStep { + config := fmt.Sprintf(` + resource "cyral_service_account" "%s" { + display_name = "service-account-test" + permission_ids = [] + } + `, + resourceName, + ) + return resource.TestStep{ + Config: config, + ExpectError: regexp.MustCompile("at least one permission must be specified for the service account"), + } +} + +func accTestStepServiceAccountResource_SinglePermission(resourceName string) resource.TestStep { + displayName := accTestName("service-account", "service-account-1") + permissionNames := []string{"Modify Policies"} + config, check := getAccTestStepForServiceAccountResourceFullConfig( + resourceName, + displayName, + permissionNames, + ) + return resource.TestStep{ + Config: config, + Check: check, + } +} + +func accTestStepServiceAccountResource_DuplicatedPermission(resourceName string) resource.TestStep { + displayName := accTestName("service-account", "service-account-1") + permissionNames := []string{"Modify Policies", "Modify Policies"} + config, _ := getAccTestStepForServiceAccountResourceFullConfig( + resourceName, + displayName, + permissionNames, + ) + resourceFullName := fmt.Sprintf("cyral_service_account.%s", resourceName) + check := resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + resourceFullName, + serviceAccountResourceDisplayNameKey, + displayName, + ), + resource.TestCheckResourceAttr( + resourceFullName, + fmt.Sprintf("%s.#", serviceAccountResourcePermissionIDsKey), + "1", + ), + resource.TestCheckResourceAttrPair( + resourceFullName, + IDKey, + resourceFullName, + serviceAccountResourceClientIDKey, + ), + resource.TestCheckResourceAttrSet( + resourceFullName, + serviceAccountResourceClientIDKey, + ), + resource.TestCheckResourceAttrSet( + resourceFullName, + serviceAccountResourceClientSecretKey, + ), + ) + return resource.TestStep{ + Config: config, + Check: check, + } +} + +func accTestStepServiceAccountResource_AllPermissions(resourceName string) resource.TestStep { + displayName := accTestName("service-account", "service-account-1") + config, check := getAccTestStepForServiceAccountResourceFullConfig( + resourceName, + displayName, + allPermissionNames, + ) + return resource.TestStep{ + Config: config, + Check: check, + } +} + +func accTestStepServiceAccountResource_UpdatedFields(resourceName string) resource.TestStep { + displayName := accTestName("service-account", "service-account-1-updated") + permissionNames := []string{ + "Approval Management", + "Modify Roles", + "Modify Users", + "View Audit Logs", + "View Integrations", + "View Roles", + "Modify Integrations", + } + config, check := getAccTestStepForServiceAccountResourceFullConfig( + resourceName, + displayName, + permissionNames, + ) + return resource.TestStep{ + Config: config, + Check: check, + } +} + +func getAccTestStepForServiceAccountResourceFullConfig( + resourceName string, + displayName string, + permissionNames []string, +) (string, resource.TestCheckFunc) { + config := formatBasicDataSourcePermissionIntoConfig("permissions") + config += fmt.Sprintf(` + locals { + serviceAccountPermissions = %s + } + `, listToStr(permissionNames), + ) + config += fmt.Sprintf(` + resource "cyral_service_account" "%s" { + display_name = %q + permission_ids = [ + for permission in data.cyral_permission.permissions.%s: permission.id + if contains(local.serviceAccountPermissions, permission.name) + ] + } + `, resourceName, displayName, PermissionDataSourcePermissionListKey, + ) + resourceFullName := fmt.Sprintf("cyral_service_account.%s", resourceName) + checks := []resource.TestCheckFunc{ + resource.TestCheckResourceAttr( + resourceFullName, + serviceAccountResourceDisplayNameKey, + displayName, + ), + resource.TestCheckResourceAttr( + resourceFullName, + fmt.Sprintf("%s.#", serviceAccountResourcePermissionIDsKey), + fmt.Sprintf("%d", len(permissionNames)), + ), + resource.TestCheckResourceAttrPair( + resourceFullName, + IDKey, + resourceFullName, + serviceAccountResourceClientIDKey, + ), + resource.TestCheckResourceAttrSet( + resourceFullName, + serviceAccountResourceClientIDKey, + ), + resource.TestCheckResourceAttrSet( + resourceFullName, + serviceAccountResourceClientSecretKey, + ), + } + return config, resource.ComposeTestCheckFunc(checks...) +} diff --git a/cyral/resource_cyral_sidecar_listener.go b/cyral/resource_cyral_sidecar_listener.go index 4e91275c..caa15e7a 100644 --- a/cyral/resource_cyral_sidecar_listener.go +++ b/cyral/resource_cyral_sidecar_listener.go @@ -76,9 +76,9 @@ type CreateListenerAPIResponse struct { ListenerId string `json:"listenerId"` } -func (c CreateListenerAPIResponse) WriteToSchema(d *schema.ResourceData) error { - d.SetId(marshalComposedID([]string{d.Get(SidecarIDKey).(string), c.ListenerId}, "/")) - return d.Set(ListenerIDKey, c.ListenerId) +func (response CreateListenerAPIResponse) WriteToSchema(d *schema.ResourceData) error { + d.SetId(marshalComposedID([]string{d.Get(SidecarIDKey).(string), response.ListenerId}, "/")) + return d.Set(ListenerIDKey, response.ListenerId) } func (data ReadSidecarListenerAPIResponse) WriteToSchema(d *schema.ResourceData) error { diff --git a/cyral/testutils.go b/cyral/testutils.go index 59418b45..cb6540d4 100644 --- a/cyral/testutils.go +++ b/cyral/testutils.go @@ -173,6 +173,14 @@ func formatBasicIntegrationIdPSAMLDraftIntoConfig(resName, displayName, idpType ) } +func formatBasicDataSourcePermissionIntoConfig(resourceName string) string { + return fmt.Sprintf( + ` + data "cyral_permission" "%s" {} + `, resourceName, + ) +} + func notZeroRegex() *regexp.Regexp { return regexp.MustCompile("[^0]|([0-9]{2,})") } diff --git a/cyral/utils.go b/cyral/utils.go index 1f531345..5789ec00 100644 --- a/cyral/utils.go +++ b/cyral/utils.go @@ -14,6 +14,8 @@ import ( // Common keys. const ( IDKey = "id" + NameKey = "name" + DescriptionKey = "description" HostKey = "host" PortKey = "port" TypeKey = "type" @@ -23,6 +25,28 @@ const ( ListenerIDKey = "listener_id" ) +func convertToInterfaceList[T any](list []T) []any { + if list == nil { + return nil + } + interfaceList := make([]any, len(list)) + for index, item := range list { + interfaceList[index] = item + } + return interfaceList +} + +func convertFromInterfaceList[T any](interfaceList []any) []T { + if interfaceList == nil { + return nil + } + list := make([]T, len(interfaceList)) + for index, item := range interfaceList { + list[index] = item.(T) + } + return list +} + func urlQuery(kv map[string]string) string { queryStr := "?" for k, v := range kv { diff --git a/docs/data-sources/permission.md b/docs/data-sources/permission.md new file mode 100644 index 00000000..df1b7b3e --- /dev/null +++ b/docs/data-sources/permission.md @@ -0,0 +1,30 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "cyral_permission Data Source - terraform-provider-cyral" +subcategory: "" +description: |- + Retrieve all Cyral permissions. See also resource cyral_service_account ../resources/service_account.md. +--- + +# cyral_permission (Data Source) + +Retrieve all Cyral permissions. See also resource [`cyral_service_account`](../resources/service_account.md). + + + +## Schema + +### Read-Only + +- `id` (String) The data source identifier. +- `permission_list` (List of Object) List of all existing Cyral permissions. (see [below for nested schema](#nestedatt--permission_list)) + + + +### Nested Schema for `permission_list` + +Read-Only: + +- `description` (String) +- `id` (String) +- `name` (String) diff --git a/docs/resources/integration_logging.md b/docs/resources/integration_logging.md index d1f820da..42630949 100644 --- a/docs/resources/integration_logging.md +++ b/docs/resources/integration_logging.md @@ -44,7 +44,7 @@ module "cyral_sidecar" { client_id = cyral_sidecar_credentials.creds.client_id client_secret = cyral_sidecar_credentials.creds.client_secret - ... + # ... } ``` diff --git a/docs/resources/role.md b/docs/resources/role.md index 31ab7f87..186ec9e9 100644 --- a/docs/resources/role.md +++ b/docs/resources/role.md @@ -57,12 +57,16 @@ resource "cyral_role" "some_resource_name" { Optional: -- `approval_management` (Boolean) Allows approving or denying approval requests. Defaults to `false`. -- `modify_integrations` (Boolean) Allows modifying integrations for this role. Defaults to `false`. -- `modify_policies` (Boolean) Allows modifying policies for this role. Defaults to `false`. -- `modify_roles` (Boolean) Allows modifying roles for this role. Defaults to `false`. -- `modify_sidecars_and_repositories` (Boolean) Allows modifying sidecars and repositories for this role. Defaults to `false`. -- `modify_users` (Boolean) Allows modifying users for this role. Defaults to `false`. -- `repo_crawler` (Boolean) Allows reporting of cyral_repository_user_accounts. Defaults to `false`. -- `view_audit_logs` (Boolean) Allows viewing audit logs for this role. Defaults to `false`. -- `view_datamaps` (Boolean) Allows viewing datamaps for this role. Defaults to `false`. +- `approval_management` (Boolean) Allows approving or denying approval requests on Cyral Control Plane. Defaults to `false`. +- `modify_integrations` (Boolean) Allows modifying integrations on Cyral Control Plane. Defaults to `false`. +- `modify_policies` (Boolean) Allows modifying policies on Cyral Control Plane. Defaults to `false`. +- `modify_roles` (Boolean) Allows modifying roles on Cyral Control Plane. Defaults to `false`. +- `modify_sidecars_and_repositories` (Boolean) Allows modifying sidecars and repositories on Cyral Control Plane. Defaults to `false`. +- `modify_users` (Boolean) Allows modifying users on Cyral Control Plane. Defaults to `false`. +- `repo_crawler` (Boolean) Allows running the Cyral repo crawler data classifier and user discovery. Defaults to `false`. +- `view_audit_logs` (Boolean) Allows viewing audit logs on Cyral Control Plane. Defaults to `false`. +- `view_datamaps` (Boolean) Allows viewing datamaps on Cyral Control Plane. Defaults to `false`. +- `view_integrations` (Boolean) Allows viewing integrations on Cyral Control Plane. Defaults to `false`. +- `view_policies` (Boolean) Allows viewing policies on Cyral Control Plane. Defaults to `false`. +- `view_roles` (Boolean) Allows viewing roles on Cyral Control Plane. Defaults to `false`. +- `view_users` (Boolean) Allows viewing users on Cyral Control Plane. Defaults to `false`. diff --git a/docs/resources/service_account.md b/docs/resources/service_account.md new file mode 100644 index 00000000..eb886a98 --- /dev/null +++ b/docs/resources/service_account.md @@ -0,0 +1,61 @@ +# cyral_service_account (Resource) + +Manages a Cyral Service Account (A.k.a: [Cyral API Access Key](https://cyral.com/docs/api-ref/api-intro/#api-access-key)). See also data source [`cyral_permission`](../data-sources/permission.md). + +-> **Note** This resource does not support importing, since the client secret cannot be read after the resource creation. + +## Example Usage + +```terraform +### Service account with all permissions +data "cyral_permission" "this" {} + +resource "cyral_service_account" "this" { + display_name = "cyral-service-account-1" + permission_ids = [ + for p in data.cyral_permission.this.permission_list: p.id + ] +} + +output "client_id" { + value = cyral_service_account.this.client_id +} + +output "client_secret" { + sensitive = true + value = cyral_service_account.this.client_secret +} + +### Service account with specific permissions +data "cyral_permission" "this" {} + +locals { + saPermissions = [ + "Modify Policies", + "Modify Integrations", + ] +} + +resource "cyral_service_account" "this" { + display_name = "cyral-service-account-1" + permission_ids = [ + for p in data.cyral_permission.this.permission_list: p.id + if contains(local.saPermissions, p.name) + ] +} +``` + + + +## Schema + +### Required + +- `display_name` (String) The service account display name. +- `permission_ids` (Set of String) A list of permission IDs that will be assigned to this service account. See also data source [`cyral_permission`](../data-sources/permission.md). + +### Read-Only + +- `client_id` (String) The service account client ID. +- `client_secret` (String, Sensitive) The service account client secret. **Note**: This resource is not able to recognize changes to the client secret after its creation, so keep in mind that if the client secret is rotated, the value present in this attribute will be outdated. If you need to rotate the client secret it's recommended that you recreate this terraform resource. +- `id` (String) The resource identifier. It's equal to `client_id`. diff --git a/examples/resources/cyral_integration_logging/cloudwatch.tf b/examples/resources/cyral_integration_logging/cloudwatch.tf index 9c87b6ad..127279ac 100644 --- a/examples/resources/cyral_integration_logging/cloudwatch.tf +++ b/examples/resources/cyral_integration_logging/cloudwatch.tf @@ -34,5 +34,5 @@ module "cyral_sidecar" { client_id = cyral_sidecar_credentials.creds.client_id client_secret = cyral_sidecar_credentials.creds.client_secret - ... + # ... } diff --git a/examples/resources/cyral_service_account/resource.tf b/examples/resources/cyral_service_account/resource.tf new file mode 100644 index 00000000..41401392 --- /dev/null +++ b/examples/resources/cyral_service_account/resource.tf @@ -0,0 +1,36 @@ +### Service account with all permissions +data "cyral_permission" "this" {} + +resource "cyral_service_account" "this" { + display_name = "cyral-service-account-1" + permission_ids = [ + for p in data.cyral_permission.this.permission_list: p.id + ] +} + +output "client_id" { + value = cyral_service_account.this.client_id +} + +output "client_secret" { + sensitive = true + value = cyral_service_account.this.client_secret +} + +### Service account with specific permissions +data "cyral_permission" "this" {} + +locals { + saPermissions = [ + "Modify Policies", + "Modify Integrations", + ] +} + +resource "cyral_service_account" "this" { + display_name = "cyral-service-account-1" + permission_ids = [ + for p in data.cyral_permission.this.permission_list: p.id + if contains(local.saPermissions, p.name) + ] +} diff --git a/templates/resources/service_account.md.tmpl b/templates/resources/service_account.md.tmpl new file mode 100644 index 00000000..d8d7928b --- /dev/null +++ b/templates/resources/service_account.md.tmpl @@ -0,0 +1,9 @@ +# {{ .Name | trimspace }} ({{ .Type | trimspace }}) + +{{ .Description | trimspace }} + +## Example Usage + +{{ tffile "examples/resources/cyral_service_account/resource.tf" }} + +{{ .SchemaMarkdown | trimspace }}