internal/kibana/role.go (601 lines of code) (raw):

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/go-version" "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" ) var minSupportedRemoteIndicesVersion = version.Must(version.NewVersion("8.10.0")) 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, }, }, }, }, }, "remote_indices": { Description: "A list of remote indices permissions entries. Remote indices are effective for remote clusters configured with the API key based model. They have no effect for remote clusters configured with the certificate based model.", Type: schema.TypeSet, Optional: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "clusters": { Description: "A list of cluster aliases to which the permissions in this entry apply.", Type: schema.TypeSet, Required: true, Elem: &schema.Schema{ Type: schema.TypeString, }, }, "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\"]. When the base privileges are specified, you are unable to use the \"feature\" section.", 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 } serverVersion, diags := client.ServerVersion(ctx) if diags.HasError() { return diags } kibana, err := client.GetKibanaClient() if err != nil { return diag.FromErr(err) } kibanaRole := kbapi.KibanaRole{ Name: d.Get("name").(string), Kibana: []kbapi.KibanaRoleKibana{}, Elasticsearch: &kbapi.KibanaRoleElasticsearch{}, CreateOnly: d.IsNewResource(), } 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, diags = expandKibanaRoleElasticsearch(v, serverVersion) if diags != nil { return diags } } 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{}, serverVersion *version.Version) (*kbapi.KibanaRoleElasticsearch, diag.Diagnostics) { elasticConfig := &kbapi.KibanaRoleElasticsearch{} var diags diag.Diagnostics 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["remote_indices"]; ok { definedRemoteIndices := v.(*schema.Set) if definedRemoteIndices.Len() > 0 { if serverVersion.LessThan(minSupportedRemoteIndicesVersion) { return nil, diag.FromErr(fmt.Errorf("'remote_indices' is supported only for Kibana v%s and above", minSupportedRemoteIndicesVersion.String())) } } remote_indices := make([]kbapi.KibanaRoleElasticsearchRemoteIndice, definedRemoteIndices.Len()) for i, idx := range definedRemoteIndices.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) } definedClusters := index["clusters"].(*schema.Set) clusters := make([]string, definedClusters.Len()) for i, cluster := range definedClusters.List() { clusters[i] = cluster.(string) } definedPrivileges := index["privileges"].(*schema.Set) privileges := make([]string, definedPrivileges.Len()) for i, pr := range definedPrivileges.List() { privileges[i] = pr.(string) } newRemoteIndex := kbapi.KibanaRoleElasticsearchRemoteIndice{ Names: names, Clusters: clusters, Privileges: privileges, } if query := index["query"].(string); query != "" { newRemoteIndex.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 } newRemoteIndex.FieldSecurity = fieldSecurity } remote_indices[i] = newRemoteIndex } elasticConfig.RemoteIndices = remote_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, diags } 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{} { 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 } func flattenKibanaRoleRemoteIndicesData(indices []kbapi.KibanaRoleElasticsearchRemoteIndice) []interface{} { oindx := make([]interface{}, len(indices)) for i, index := range indices { oi := make(map[string]interface{}) oi["clusters"] = index.Clusters 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 } 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) result["remote_indices"] = flattenKibanaRoleRemoteIndicesData(elastic.RemoteIndices) 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) }