From 94818049027f175dffbd6bb96d29a51311687a41 Mon Sep 17 00:00:00 2001 From: Hamid Reza Shakibi Date: Tue, 24 Oct 2023 07:15:51 +0330 Subject: [PATCH] [Feature] Add Kibana security roles (#435) * Add kibana role management api * Add kibana role data source * change validation method of kibana security role - Change kibana security role validation to terraform builtin - Make kibana role ValidateFunc function simpler by using built-in method * fix spelling mistakes * remove elasticsearch dependency from kibana security role * revert wrong validation methods * move kibana security role parts to expand functions * update document templates * add changelog entry * update kibana security role based on new functions * fix namings and changelog order --- CHANGELOG.md | 1 + docs/data-sources/kibana_security_role.md | 88 +++ docs/resources/kibana_security_role.md | 145 +++++ .../data-source.tf | 9 + .../import.sh | 1 + .../resource.tf | 52 ++ internal/kibana/role.go | 501 ++++++++++++++++++ internal/kibana/role_data_source.go | 168 ++++++ internal/kibana/role_data_source_test.go | 57 ++ internal/kibana/role_test.go | 162 ++++++ provider/provider.go | 3 + .../data-sources/kibana_security_role.md.tmpl | 17 + .../resources/kibana_security_role.md.tmpl | 23 + 13 files changed, 1227 insertions(+) create mode 100644 docs/data-sources/kibana_security_role.md create mode 100644 docs/resources/kibana_security_role.md create mode 100644 examples/data-sources/elasticstack_kibana_security_role/data-source.tf create mode 100644 examples/resources/elasticstack_kibana_security_role/import.sh create mode 100644 examples/resources/elasticstack_kibana_security_role/resource.tf create mode 100644 internal/kibana/role.go create mode 100644 internal/kibana/role_data_source.go create mode 100644 internal/kibana/role_data_source_test.go create mode 100644 internal/kibana/role_test.go create mode 100644 templates/data-sources/kibana_security_role.md.tmpl create mode 100644 templates/resources/kibana_security_role.md.tmpl diff --git a/CHANGELOG.md b/CHANGELOG.md index a8f4f2f4d..3e71cb0cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## [Unreleased] ### Added +- Add support for Kibana security role ([#435](https://github.com/elastic/terraform-provider-elasticstack/pull/435)) - Introduce `elasticstack_kibana_import_saved_objects` resource as an additive only way to manage Kibana saved objects ([#343](https://github.com/elastic/terraform-provider-elasticstack/pull/343)). - Add support for Terraform Plugin Framework ([#343](https://github.com/elastic/terraform-provider-elasticstack/pull/343)). - Fix fleet resources not having ID set on import ([#447](https://github.com/elastic/terraform-provider-elasticstack/pull/447)) diff --git a/docs/data-sources/kibana_security_role.md b/docs/data-sources/kibana_security_role.md new file mode 100644 index 000000000..78c1a1ed0 --- /dev/null +++ b/docs/data-sources/kibana_security_role.md @@ -0,0 +1,88 @@ +--- +subcategory: "Kibana" +layout: "" +page_title: "Elasticstack: elasticstack_kibana_security_role Data Source" +description: |- + Retrieve a specific Kibana role. See https://www.elastic.co/guide/en/kibana/master/role-management-specific-api-get.html +--- + +# Data Source: elasticstack_kibana_security_role + +Use this data source to get information about an existing Kibana role. + +## Example Usage + +```terraform +provider "elasticstack" { + elasticsearch {} + kibana {} +} + +data "elasticstack_kibana_security_role" "example" { + name = "sample_role" +} +``` + + +## Schema + +### Required + +- `name` (String) The name for the role. + +### Optional + +- `metadata` (String) Optional meta-data. + +### Read-Only + +- `elasticsearch` (Set of Object) Elasticsearch cluster and index privileges. (see [below for nested schema](#nestedatt--elasticsearch)) +- `id` (String) The ID of this resource. +- `kibana` (Set of Object) The list of objects that specify the Kibana privileges for the role. (see [below for nested schema](#nestedatt--kibana)) + + +### Nested Schema for `elasticsearch` + +Read-Only: + +- `cluster` (Set of String) +- `indices` (Set of Object) (see [below for nested schema](#nestedobjatt--elasticsearch--indices)) +- `run_as` (Set of String) + + +### Nested Schema for `elasticsearch.indices` + +Read-Only: + +- `field_security` (List of Object) (see [below for nested schema](#nestedobjatt--elasticsearch--indices--field_security)) +- `names` (Set of String) +- `privileges` (Set of String) +- `query` (String) + + +### Nested Schema for `elasticsearch.indices.field_security` + +Read-Only: + +- `except` (Set of String) +- `grant` (Set of String) + + + + + +### Nested Schema for `kibana` + +Read-Only: + +- `base` (Set of String) +- `feature` (Set of Object) (see [below for nested schema](#nestedobjatt--kibana--feature)) +- `spaces` (Set of String) + + +### Nested Schema for `kibana.feature` + +Read-Only: + +- `name` (String) +- `privileges` (Set of String) diff --git a/docs/resources/kibana_security_role.md b/docs/resources/kibana_security_role.md new file mode 100644 index 000000000..9fef6eaca --- /dev/null +++ b/docs/resources/kibana_security_role.md @@ -0,0 +1,145 @@ +--- +subcategory: "Kibana" +layout: "" +page_title: "Elasticstack: elasticstack_kibana_security_role Resource" +description: |- + Creates or updates a Kibana role. +--- + +# Resource: elasticstack_kibana_security_role + +Creates or updates a Kibana role. See https://www.elastic.co/guide/en/kibana/master/role-management-api-put.html + +## Example Usage + +```terraform +provider "elasticstack" { + elasticsearch {} + kibana {} +} + +resource "elasticstack_kibana_role" "example" { + name = "sample_role" + elasticsearch { + cluster = ["create_snapshot"] + indices { + field_security { + grant = ["test"] + except = [] + } + names = ["test"] + privileges = ["create", "read", "write"] + } + } + kibana { + base = ["all"] + spaces = ["default"] + } + kibana { + feature { + name = "actions" + privileges = ["read"] + } + feature { + name = "discover" + privileges = ["minimal_read", "url_create", "store_search_session"] + } + feature { + name = "observabilityCases" + privileges = ["minimal_read", "cases_delete"] + } + feature { + name = "osquery" + privileges = ["minimal_read", "live_queries_all", "run_saved_queries", "saved_queries_read", "packs_all"] + } + feature { + name = "rulesSettings" + privileges = ["minimal_read", "readFlappingSettings"] + } + feature { + name = "securitySolutionCases" + privileges = ["minimal_read", "cases_delete"] + } + + spaces = ["Default"] + } +} +``` + + +## Schema + +### Required + +- `elasticsearch` (Block Set, Min: 1, Max: 1) Elasticsearch cluster and index privileges. (see [below for nested schema](#nestedblock--elasticsearch)) +- `name` (String) The name for the role. + +### Optional + +- `kibana` (Block Set) The list of objects that specify the Kibana privileges for the role. (see [below for nested schema](#nestedblock--kibana)) +- `metadata` (String) Optional meta-data. + +### Read-Only + +- `id` (String) The ID of this resource. + + +### Nested Schema for `elasticsearch` + +Optional: + +- `cluster` (Set of String) List of the cluster privileges. +- `indices` (Block Set) A list of indices permissions entries. (see [below for nested schema](#nestedblock--elasticsearch--indices)) +- `run_as` (Set of String) A list of usernames the owners of this role can impersonate. + + +### Nested Schema for `elasticsearch.indices` + +Required: + +- `names` (Set of String) A list of indices (or index name patterns) to which the permissions in this entry apply. +- `privileges` (Set of String) The index level privileges that the owners of the role have on the specified indices. + +Optional: + +- `field_security` (Block List, Max: 1) The document fields that the owners of the role have read access to. (see [below for nested schema](#nestedblock--elasticsearch--indices--field_security)) +- `query` (String) A search query that defines the documents the owners of the role have read access to. + + +### Nested Schema for `elasticsearch.indices.field_security` + +Optional: + +- `except` (Set of String) List of the fields to which the grants will not be applied. +- `grant` (Set of String) List of the fields to grant the access to. + + + + + +### Nested Schema for `kibana` + +Required: + +- `spaces` (Set of String) The spaces to apply the privileges to. To grant access to all spaces, set to ["*"], or omit the value. + +Optional: + +- `base` (Set of String) A base privilege. When specified, the base must be ["all"] or ["read"]. +- `feature` (Block Set) List of privileges for specific features. When the feature privileges are specified, you are unable to use the "base" section. (see [below for nested schema](#nestedblock--kibana--feature)) + + +### Nested Schema for `kibana.feature` + +Required: + +- `name` (String) Feature name. +- `privileges` (Set of String) Feature privileges. + +## Import + +Import is supported using the following syntax: + +```shell +terraform import elasticstack_kibana_security_role.example_role +``` diff --git a/examples/data-sources/elasticstack_kibana_security_role/data-source.tf b/examples/data-sources/elasticstack_kibana_security_role/data-source.tf new file mode 100644 index 000000000..9eee8b9fb --- /dev/null +++ b/examples/data-sources/elasticstack_kibana_security_role/data-source.tf @@ -0,0 +1,9 @@ + +provider "elasticstack" { + elasticsearch {} + kibana {} +} + +data "elasticstack_kibana_security_role" "example" { + name = "sample_role" +} diff --git a/examples/resources/elasticstack_kibana_security_role/import.sh b/examples/resources/elasticstack_kibana_security_role/import.sh new file mode 100644 index 000000000..9564e1387 --- /dev/null +++ b/examples/resources/elasticstack_kibana_security_role/import.sh @@ -0,0 +1 @@ +terraform import elasticstack_kibana_security_role.example_role diff --git a/examples/resources/elasticstack_kibana_security_role/resource.tf b/examples/resources/elasticstack_kibana_security_role/resource.tf new file mode 100644 index 000000000..4c62c6514 --- /dev/null +++ b/examples/resources/elasticstack_kibana_security_role/resource.tf @@ -0,0 +1,52 @@ + +provider "elasticstack" { + elasticsearch {} + kibana {} +} + +resource "elasticstack_kibana_role" "example" { + name = "sample_role" + elasticsearch { + cluster = ["create_snapshot"] + indices { + field_security { + grant = ["test"] + except = [] + } + names = ["test"] + privileges = ["create", "read", "write"] + } + } + kibana { + base = ["all"] + spaces = ["default"] + } + kibana { + feature { + name = "actions" + privileges = ["read"] + } + feature { + name = "discover" + privileges = ["minimal_read", "url_create", "store_search_session"] + } + feature { + name = "observabilityCases" + privileges = ["minimal_read", "cases_delete"] + } + feature { + name = "osquery" + privileges = ["minimal_read", "live_queries_all", "run_saved_queries", "saved_queries_read", "packs_all"] + } + feature { + name = "rulesSettings" + privileges = ["minimal_read", "readFlappingSettings"] + } + feature { + name = "securitySolutionCases" + privileges = ["minimal_read", "cases_delete"] + } + + spaces = ["Default"] + } +} diff --git a/internal/kibana/role.go b/internal/kibana/role.go new file mode 100644 index 000000000..03c58eab2 --- /dev/null +++ b/internal/kibana/role.go @@ -0,0 +1,501 @@ +package kibana + +import ( + "context" + "encoding/json" + "fmt" + "strings" + + "github.com/disaster37/go-kibana-rest/v8/kbapi" + "github.com/elastic/terraform-provider-elasticstack/internal/clients" + "github.com/elastic/terraform-provider-elasticstack/internal/utils" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +func ResourceRole() *schema.Resource { + roleSchema := map[string]*schema.Schema{ + "name": { + Description: "The name for the role.", + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "elasticsearch": { + Description: "Elasticsearch cluster and index privileges.", + Type: schema.TypeSet, + Required: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "cluster": { + Description: "List of the cluster privileges.", + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "indices": { + Description: "A list of indices permissions entries.", + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "field_security": { + Description: "The document fields that the owners of the role have read access to.", + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "grant": { + Description: "List of the fields to grant the access to.", + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "except": { + Description: "List of the fields to which the grants will not be applied.", + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }, + }, + }, + "query": { + Description: "A search query that defines the documents the owners of the role have read access to.", + Type: schema.TypeString, + ValidateFunc: validation.StringIsJSON, + DiffSuppressFunc: utils.DiffJsonSuppress, + Optional: true, + }, + "names": { + Description: "A list of indices (or index name patterns) to which the permissions in this entry apply.", + Type: schema.TypeSet, + Required: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "privileges": { + Description: "The index level privileges that the owners of the role have on the specified indices.", + Type: schema.TypeSet, + Required: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }, + }, + }, + "run_as": { + Description: "A list of usernames the owners of this role can impersonate.", + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }, + }, + }, + "kibana": { + Description: "The list of objects that specify the Kibana privileges for the role.", + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "base": { + Description: "A base privilege. When specified, the base must be [\"all\"] or [\"read\"].", + Type: schema.TypeSet, + Optional: true, + MaxItems: 1, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.StringInSlice([]string{"all", "read"}, true), + }, + }, + "feature": { + Description: "List of privileges for specific features. When the feature privileges are specified, you are unable to use the \"base\" section.", + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Description: "Feature name.", + Type: schema.TypeString, + Required: true, + }, + "privileges": { + Description: "Feature privileges.", + Type: schema.TypeSet, + Required: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }, + }, + }, + "spaces": { + Description: "The spaces to apply the privileges to. To grant access to all spaces, set to [\"*\"], or omit the value.", + Type: schema.TypeSet, + Required: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }, + }, + }, + "metadata": { + Description: "Optional meta-data.", + Type: schema.TypeString, + Optional: true, + Computed: true, + ValidateFunc: validation.StringIsJSON, + DiffSuppressFunc: utils.DiffJsonSuppress, + }, + } + + return &schema.Resource{ + Description: "Creates a Kibana role. See, https://www.elastic.co/guide/en/kibana/master/role-management-api-put.html", + + CreateContext: resourceRoleUpsert, + UpdateContext: resourceRoleUpsert, + ReadContext: resourceRoleRead, + DeleteContext: resourceRoleDelete, + + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: roleSchema, + } +} + +func resourceRoleUpsert(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client, diags := clients.NewApiClientFromSDKResource(d, meta) + if diags.HasError() { + return diags + } + + kibana, err := client.GetKibanaClient() + if err != nil { + return diag.FromErr(err) + } + queryParams := "" + if d.IsNewResource() { + queryParams = "?createOnly=true" + } + kibanaRole := kbapi.KibanaRole{ + Name: fmt.Sprintf("%s%s", d.Get("name").(string), queryParams), + Kibana: []kbapi.KibanaRoleKibana{}, + Elasticsearch: &kbapi.KibanaRoleElasticsearch{}, + } + + if v, ok := d.GetOk("kibana"); ok { + kibanaRole.Kibana, diags = expandKibanaRoleKibana(v) + if diags != nil { + return diags + } + } + + if v, ok := d.GetOk("elasticsearch"); ok { + kibanaRole.Elasticsearch = expandKibanaRoleElasticsearch(v) + } + + if v, ok := d.GetOk("metadata"); ok { + kibanaRole.Metadata, diags = expandKibanaRoleMetadata(v) + if diags != nil { + return diags + } + } + + roleManageResponse, err := kibana.KibanaRoleManagement.CreateOrUpdate(&kibanaRole) + if err != nil { + return diag.FromErr(err) + } + + d.SetId(roleManageResponse.Name) + return resourceRoleRead(ctx, d, meta) +} + +func resourceRoleRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client, diags := clients.NewApiClientFromSDKResource(d, meta) + if diags.HasError() { + return diags + } + + name := d.Id() + + kibana, err := client.GetKibanaClient() + if err != nil { + return diag.FromErr(err) + } + + role, err := kibana.KibanaRoleManagement.Get(name) + if role == nil && err == nil { + d.SetId("") + return diags + } + if err != nil { + return diag.FromErr(err) + } + + // set the fields + if err := d.Set("name", role.Name); err != nil { + return diag.FromErr(err) + } + if err := d.Set("elasticsearch", flattenKibanaRoleElasticsearchData(role.Elasticsearch)); err != nil { + return diag.FromErr(err) + } + if err := d.Set("kibana", flattenKibanaRoleKibanaData(&role.Kibana)); err != nil { + return diag.FromErr(err) + } + if role.Metadata != nil { + metadata, err := json.Marshal(role.Metadata) + if err != nil { + return diag.FromErr(err) + } + if err := d.Set("metadata", string(metadata)); err != nil { + return diag.FromErr(err) + } + } + return diags +} + +func resourceRoleDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client, diags := clients.NewApiClientFromSDKResource(d, meta) + if diags.HasError() { + return diags + } + resourceId := d.Id() + + kibana, err := client.GetKibanaClient() + if err != nil { + return diag.FromErr(err) + } + + err = kibana.KibanaRoleManagement.Delete(resourceId) + if err != nil { + return diag.FromErr(err) + } + + d.SetId("") + return diags +} + +// Helper functions + +func expandKibanaRoleMetadata(v interface{}) (map[string]interface{}, diag.Diagnostics) { + metadata := make(map[string]interface{}) + if err := json.NewDecoder(strings.NewReader(v.(string))).Decode(&metadata); err != nil { + return nil, diag.FromErr(err) + } + return metadata, nil +} + +func expandKibanaRoleElasticsearch(v interface{}) *kbapi.KibanaRoleElasticsearch { + elasticConfig := &kbapi.KibanaRoleElasticsearch{} + + if definedElasticConfigs := v.(*schema.Set); definedElasticConfigs.Len() > 0 { + userElasticConfig := definedElasticConfigs.List()[0].(map[string]interface{}) + if v, ok := userElasticConfig["cluster"]; ok { + definedCluster := v.(*schema.Set) + cls := make([]string, definedCluster.Len()) + for i, cl := range definedCluster.List() { + cls[i] = cl.(string) + } + elasticConfig.Cluster = cls + + if v, ok := userElasticConfig["indices"]; ok { + definedIndices := v.(*schema.Set) + indices := make([]kbapi.KibanaRoleElasticsearchIndice, definedIndices.Len()) + for i, idx := range definedIndices.List() { + index := idx.(map[string]interface{}) + + definedNames := index["names"].(*schema.Set) + names := make([]string, definedNames.Len()) + for i, name := range definedNames.List() { + names[i] = name.(string) + } + definedPrivileges := index["privileges"].(*schema.Set) + privileges := make([]string, definedPrivileges.Len()) + for i, pr := range definedPrivileges.List() { + privileges[i] = pr.(string) + } + + newIndex := kbapi.KibanaRoleElasticsearchIndice{ + Names: names, + Privileges: privileges, + } + + if query := index["query"].(string); query != "" { + newIndex.Query = &query + } + if fieldSec := index["field_security"].([]interface{}); len(fieldSec) > 0 { + fieldSecurity := map[string]interface{}{} + // there must be only 1 entry + definedFieldSec := fieldSec[0].(map[string]interface{}) + + // grants + if gr := definedFieldSec["grant"].(*schema.Set); gr != nil { + grants := make([]string, gr.Len()) + for i, grant := range gr.List() { + grants[i] = grant.(string) + } + fieldSecurity["grant"] = grants + } + // except + if exp := definedFieldSec["except"].(*schema.Set); exp != nil { + excepts := make([]string, exp.Len()) + for i, except := range exp.List() { + excepts[i] = except.(string) + } + fieldSecurity["except"] = excepts + } + newIndex.FieldSecurity = fieldSecurity + } + + indices[i] = newIndex + } + elasticConfig.Indices = indices + } + + if v, ok := userElasticConfig["run_as"]; ok { + definedRuns := v.(*schema.Set) + runs := make([]string, definedRuns.Len()) + for i, run := range definedRuns.List() { + runs[i] = run.(string) + } + elasticConfig.RunAs = runs + } + } + } + return elasticConfig +} + +func expandKibanaRoleKibana(v interface{}) ([]kbapi.KibanaRoleKibana, diag.Diagnostics) { + kibanaConfigs := []kbapi.KibanaRoleKibana{} + definedKibanaConfigs := v.(*schema.Set) + + for _, item := range definedKibanaConfigs.List() { + each := item.(map[string]interface{}) + config := kbapi.KibanaRoleKibana{ + Base: []string{}, + Feature: map[string][]string{}, + } + + if basePrivileges, ok := each["base"].(*schema.Set); ok && basePrivileges.Len() > 0 { + if _features, ok := each["feature"].(*schema.Set); ok && _features.Len() > 0 { + return nil, diag.Errorf("Only one of the `feature` or `base` privileges allowed!") + } + config.Base = make([]string, basePrivileges.Len()) + for i, name := range basePrivileges.List() { + config.Base[i] = name.(string) + } + } else if kibanaFeatures, ok := each["feature"].(*schema.Set); ok && kibanaFeatures.Len() > 0 { + for _, item := range kibanaFeatures.List() { + featureData := item.(map[string]interface{}) + featurePrivileges := featureData["privileges"].(*schema.Set) + _features := make([]string, featurePrivileges.Len()) + for i, f := range featurePrivileges.List() { + _features[i] = f.(string) + } + config.Feature[featureData["name"].(string)] = _features + } + } else { + return nil, diag.Errorf("Either on of the `feature` or `base` privileges must be set for kibana role!") + } + + if roleSpaces, ok := each["spaces"].(*schema.Set); ok && roleSpaces.Len() > 0 { + config.Spaces = make([]string, roleSpaces.Len()) + for i, name := range roleSpaces.List() { + config.Spaces[i] = name.(string) + } + } + kibanaConfigs = append(kibanaConfigs, config) + } + return kibanaConfigs, nil +} + +func flattenKibanaRoleIndicesData(indices *[]kbapi.KibanaRoleElasticsearchIndice) []interface{} { + if indices != nil { + oindx := make([]interface{}, len(*indices)) + + for i, index := range *indices { + oi := make(map[string]interface{}) + oi["names"] = index.Names + oi["privileges"] = index.Privileges + oi["query"] = index.Query + + if index.FieldSecurity != nil { + fsec := make(map[string]interface{}) + if grant_v, ok := index.FieldSecurity["grant"]; ok { + fsec["grant"] = grant_v + } + if except_v, ok := index.FieldSecurity["except"]; ok { + fsec["except"] = except_v + } + oi["field_security"] = []interface{}{fsec} + } + oindx[i] = oi + } + return oindx + } + return make([]interface{}, 0) +} + +func flattenKibanaRoleElasticsearchData(elastic *kbapi.KibanaRoleElasticsearch) []interface{} { + if elastic != nil { + result := make(map[string]interface{}) + if len(elastic.Cluster) > 0 { + result["cluster"] = elastic.Cluster + } + result["indices"] = flattenKibanaRoleIndicesData(&elastic.Indices) + if len(elastic.RunAs) > 0 { + result["run_as"] = elastic.RunAs + } + return []interface{}{result} + } + return make([]interface{}, 0) +} + +func flattenKibanaRoleKibanaFeatureData(features map[string][]string) []interface{} { + if features != nil { + result := make([]interface{}, len(features)) + i := 0 + for k, v := range features { + m := make(map[string]interface{}) + m["name"] = k + m["privileges"] = v + result[i] = m + i += 1 + } + return result + } + return make([]interface{}, 0) +} + +func flattenKibanaRoleKibanaData(kibana_configs *[]kbapi.KibanaRoleKibana) []interface{} { + if kibana_configs != nil { + result := make([]interface{}, len(*kibana_configs)) + for i, index := range *kibana_configs { + nk := make(map[string]interface{}) + nk["base"] = index.Base + nk["feature"] = flattenKibanaRoleKibanaFeatureData(index.Feature) + nk["spaces"] = index.Spaces + result[i] = nk + } + return result + } + return make([]interface{}, 0) +} diff --git a/internal/kibana/role_data_source.go b/internal/kibana/role_data_source.go new file mode 100644 index 000000000..aebd96a52 --- /dev/null +++ b/internal/kibana/role_data_source.go @@ -0,0 +1,168 @@ +package kibana + +import ( + "context" + + "github.com/elastic/terraform-provider-elasticstack/internal/utils" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +func DataSourceRole() *schema.Resource { + roleSchema := map[string]*schema.Schema{ + "name": { + Description: "The name for the role.", + Type: schema.TypeString, + Required: true, + }, + "elasticsearch": { + Description: "Elasticsearch cluster and index privileges.", + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "cluster": { + Description: "List of the cluster privileges.", + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "indices": { + Description: "A list of indices permissions entries.", + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "field_security": { + Description: "The document fields that the owners of the role have read access to.", + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "grant": { + Description: "List of the fields to grant the access to.", + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "except": { + Description: "List of the fields to which the grants will not be applied.", + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }, + }, + }, + "query": { + Description: "A search query that defines the documents the owners of the role have read access to.", + Type: schema.TypeString, + Computed: true, + }, + "names": { + Description: "A list of indices (or index name patterns) to which the permissions in this entry apply.", + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "privileges": { + Description: "The index level privileges that the owners of the role have on the specified indices.", + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }, + }, + }, + "run_as": { + Description: "A list of usernames the owners of this role can impersonate.", + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }, + }, + }, + "kibana": { + Description: "The list of objects that specify the Kibana privileges for the role.", + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "base": { + Description: "A base privilege. When specified, the base must be [\"all\"] or [\"read\"].", + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "feature": { + Description: "List of privileges for specific features. When the feature privileges are specified, you are unable to use the \"base\" section.", + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Description: "Feature name.", + Type: schema.TypeString, + Computed: true, + }, + "privileges": { + Description: "Feature privileges.", + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }, + }, + }, + "spaces": { + Description: "The spaces to apply the privileges to. To grant access to all spaces, set to [\"*\"], or omit the value.", + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }, + }, + }, + "metadata": { + Description: "Optional meta-data.", + Type: schema.TypeString, + Optional: true, + Computed: true, + ValidateFunc: validation.StringIsJSON, + DiffSuppressFunc: utils.DiffJsonSuppress, + }, + } + + return &schema.Resource{ + Description: "Retrieve a specific role. See, https://www.elastic.co/guide/en/kibana/current/role-management-specific-api-get.html", + ReadContext: dataSourceSecurityRoleRead, + Schema: roleSchema, + } +} + +func dataSourceSecurityRoleRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + roleId := d.Get("name").(string) + d.SetId(roleId) + + return resourceRoleRead(ctx, d, meta) +} diff --git a/internal/kibana/role_data_source_test.go b/internal/kibana/role_data_source_test.go new file mode 100644 index 000000000..3afaeeb5a --- /dev/null +++ b/internal/kibana/role_data_source_test.go @@ -0,0 +1,57 @@ +package kibana_test + +import ( + "testing" + + "github.com/elastic/terraform-provider-elasticstack/internal/acctest" + "github.com/elastic/terraform-provider-elasticstack/internal/utils" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccDataSourceKibanaSecurityRole(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ProtoV5ProviderFactories: acctest.Providers, + Steps: []resource.TestStep{ + { + Config: testAccDataSourceSecurityRole, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("data.elasticstack_kibana_security_role.test", "name", "data_source_test"), + resource.TestCheckNoResourceAttr("data.elasticstack_kibana_security_role.test", "kibana.0.feature.#"), + resource.TestCheckNoResourceAttr("data.elasticstack_kibana_security_role.test", "elasticsearch.0.indices.0.field_security.#"), + utils.TestCheckResourceListAttr("data.elasticstack_kibana_security_role.test", "elasticsearch.0.run_as", []string{"elastic", "kibana"}), + utils.TestCheckResourceListAttr("data.elasticstack_kibana_security_role.test", "kibana.0.base", []string{"all"}), + utils.TestCheckResourceListAttr("data.elasticstack_kibana_security_role.test", "kibana.0.spaces", []string{"default"}), + ), + }, + }, + }) +} + +const testAccDataSourceSecurityRole = ` +provider "elasticstack" { + elasticsearch {} + kibana {} +} + + +resource "elasticstack_kibana_security_role" "test" { + name = "data_source_test" + elasticsearch { + cluster = [ "create_snapshot" ] + indices { + names = ["sample"] + privileges = ["create", "read", "write"] + } + run_as = ["kibana", "elastic"] + } + kibana { + base = [ "all" ] + spaces = ["default"] + } +} + +data "elasticstack_kibana_security_role" "test" { + name = elasticstack_kibana_security_role.test.name +} +` diff --git a/internal/kibana/role_test.go b/internal/kibana/role_test.go new file mode 100644 index 000000000..7e8d3178f --- /dev/null +++ b/internal/kibana/role_test.go @@ -0,0 +1,162 @@ +package kibana_test + +import ( + "fmt" + "testing" + + "github.com/elastic/terraform-provider-elasticstack/internal/acctest" + "github.com/elastic/terraform-provider-elasticstack/internal/clients" + "github.com/elastic/terraform-provider-elasticstack/internal/utils" + sdkacctest "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestAccResourceKibanaSecurityRole(t *testing.T) { + // generate a random role name + roleName := sdkacctest.RandStringFromCharSet(10, sdkacctest.CharSetAlphaNum) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + CheckDestroy: checkResourceSecurityRoleDestroy, + ProtoV5ProviderFactories: acctest.Providers, + Steps: []resource.TestStep{ + { + Config: testAccResourceSecurityRoleCreate(roleName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("elasticstack_kibana_security_role.test", "name", roleName), + resource.TestCheckNoResourceAttr("elasticstack_kibana_security_role.test", "kibana.0.base.#"), + resource.TestCheckNoResourceAttr("elasticstack_kibana_security_role.test", "elasticsearch.0.run_as.#"), + utils.TestCheckResourceListAttr("elasticstack_kibana_security_role.test", "elasticsearch.0.indices.0.names", []string{"sample"}), + utils.TestCheckResourceListAttr("elasticstack_kibana_security_role.test", "elasticsearch.0.indices.0.field_security.0.grant", []string{"sample"}), + utils.TestCheckResourceListAttr("elasticstack_kibana_security_role.test", "kibana.0.feature.2.privileges", []string{"minimal_read", "store_search_session", "url_create"}), + utils.TestCheckResourceListAttr("elasticstack_kibana_security_role.test", "kibana.0.spaces", []string{"default"}), + ), + }, + { + Config: testAccResourceSecurityRoleUpdate(roleName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("elasticstack_kibana_security_role.test", "name", roleName), + resource.TestCheckNoResourceAttr("elasticstack_kibana_security_role.test", "kibana.0.feature.#"), + resource.TestCheckNoResourceAttr("elasticstack_kibana_security_role.test", "elasticsearch.0.indices.0.field_security.#"), + utils.TestCheckResourceListAttr("elasticstack_kibana_security_role.test", "elasticsearch.0.run_as", []string{"elastic", "kibana"}), + utils.TestCheckResourceListAttr("elasticstack_kibana_security_role.test", "kibana.0.base", []string{"all"}), + utils.TestCheckResourceListAttr("elasticstack_kibana_security_role.test", "kibana.0.spaces", []string{"default"}), + ), + }, + }, + }) +} + +func testAccResourceSecurityRoleCreate(roleName string) string { + return fmt.Sprintf(` +provider "elasticstack" { + elasticsearch {} + kibana {} +} + +resource "elasticstack_kibana_security_role" "test" { + name = "%s" + elasticsearch { + cluster = [ "create_snapshot" ] + indices { + field_security { + grant = ["sample"] + except = [] + } + names = ["sample"] + privileges = ["create", "read", "write"] + } + } + kibana { + feature { + name = "actions" + privileges = ["read"] + } + feature { + name = "advancedSettings" + privileges = ["read"] + } + feature { + name = "discover" + privileges = ["minimal_read", "url_create", "store_search_session"] + } + feature { + name = "generalCases" + privileges = ["minimal_read", "cases_delete"] + } + feature { + name = "observabilityCases" + privileges = ["minimal_read", "cases_delete"] + } + feature { + name = "osquery" + privileges = ["minimal_read", "live_queries_all", "run_saved_queries", "saved_queries_read", "packs_all"] + } + feature { + name = "rulesSettings" + privileges = ["minimal_read", "readFlappingSettings"] + } + feature { + name = "securitySolutionCases" + privileges = ["minimal_read", "cases_delete"] + } + feature { + name = "visualize" + privileges = ["minimal_read", "url_create"] + } + + spaces = ["default"] + } +} + `, roleName) +} + +func testAccResourceSecurityRoleUpdate(roleName string) string { + return fmt.Sprintf(` +provider "elasticstack" { + elasticsearch {} + kibana {} +} + +resource "elasticstack_kibana_security_role" "test" { + name = "%s" + elasticsearch { + cluster = [ "create_snapshot" ] + indices { + names = ["sample"] + privileges = ["create", "read", "write"] + } + run_as = ["kibana", "elastic"] + } + kibana { + base = [ "all" ] + spaces = ["default"] + } +} + `, roleName) +} + +func checkResourceSecurityRoleDestroy(s *terraform.State) error { + client, err := clients.NewAcceptanceTestingClient() + if err != nil { + return err + } + + for _, rs := range s.RootModule().Resources { + if rs.Type != "elasticstack_kibana_security_role" { + continue + } + compId := rs.Primary.ID + + kibanaClient, err := client.GetKibanaClient() + if err != nil { + return err + } + res, err := kibanaClient.KibanaRoleManagement.Get(compId) + if err != nil || res != nil { + return fmt.Errorf("Role (%s) still exists", compId) + } + } + return nil +} diff --git a/provider/provider.go b/provider/provider.go index beb174451..f62476e06 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -78,6 +78,8 @@ func New(version string) *schema.Provider { "elasticstack_elasticsearch_snapshot_repository": cluster.DataSourceSnapshotRespository(), "elasticstack_elasticsearch_enrich_policy": enrich.DataSourceEnrichPolicy(), + "elasticstack_kibana_security_role": kibana.DataSourceRole(), + "elasticstack_fleet_enrollment_tokens": fleet.DataSourceEnrollmentTokens(), }, ResourcesMap: map[string]*schema.Resource{ @@ -104,6 +106,7 @@ func New(version string) *schema.Provider { "elasticstack_kibana_alerting_rule": kibana.ResourceAlertingRule(), "elasticstack_kibana_space": kibana.ResourceSpace(), "elasticstack_kibana_action_connector": kibana.ResourceActionConnector(), + "elasticstack_kibana_security_role": kibana.ResourceRole(), "elasticstack_kibana_slo": kibana.ResourceSlo(), "elasticstack_fleet_agent_policy": fleet.ResourceAgentPolicy(), diff --git a/templates/data-sources/kibana_security_role.md.tmpl b/templates/data-sources/kibana_security_role.md.tmpl new file mode 100644 index 000000000..176c583af --- /dev/null +++ b/templates/data-sources/kibana_security_role.md.tmpl @@ -0,0 +1,17 @@ +--- +subcategory: "Kibana" +layout: "" +page_title: "Elasticstack: elasticstack_kibana_security_role Data Source" +description: |- + Retrieve a specific Kibana role. See https://www.elastic.co/guide/en/kibana/master/role-management-specific-api-get.html +--- + +# Data Source: elasticstack_kibana_security_role + +Use this data source to get information about an existing Kibana role. + +## Example Usage + +{{ tffile "examples/data-sources/elasticstack_kibana_security_role/data-source.tf" }} + +{{ .SchemaMarkdown | trimspace }} diff --git a/templates/resources/kibana_security_role.md.tmpl b/templates/resources/kibana_security_role.md.tmpl new file mode 100644 index 000000000..661d0bcb9 --- /dev/null +++ b/templates/resources/kibana_security_role.md.tmpl @@ -0,0 +1,23 @@ +--- +subcategory: "Kibana" +layout: "" +page_title: "Elasticstack: elasticstack_kibana_security_role Resource" +description: |- + Creates or updates a Kibana role. +--- + +# Resource: elasticstack_kibana_security_role + +Creates or updates a Kibana role. See https://www.elastic.co/guide/en/kibana/master/role-management-api-put.html + +## Example Usage + +{{ tffile "examples/resources/elasticstack_kibana_security_role/resource.tf" }} + +{{ .SchemaMarkdown | trimspace }} + +## Import + +Import is supported using the following syntax: + +{{ codefile "shell" "examples/resources/elasticstack_kibana_security_role/import.sh" }}