diff --git a/CHANGELOG.md b/CHANGELOG.md
index 99ba9ab4..cb93c4a2 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)).
diff --git a/docs/data-sources/kibana_security_role.md b/docs/data-sources/kibana_security_role.md
new file mode 100644
index 00000000..78c1a1ed
--- /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 00000000..9fef6eac
--- /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 00000000..9eee8b9f
--- /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 00000000..9564e138
--- /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 00000000..4c62c651
--- /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 00000000..03c58eab
--- /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 00000000..aebd96a5
--- /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 00000000..3afaeeb5
--- /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 00000000..7e8d3178
--- /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 beb17445..f62476e0 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 00000000..176c583a
--- /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 00000000..661d0bcb
--- /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" }}