From a8d88a2315d220fc315022bb2e3dba17847e040c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Sep 2025 06:13:58 +0000 Subject: [PATCH 1/8] Initial plan From 0e9dc7a9e6f4bc4be8ccf92deb3bfd3ec4a91af2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Sep 2025 06:30:55 +0000 Subject: [PATCH 2/8] Implement Plugin Framework role resource structure and basic functionality Co-authored-by: tobio <444668+tobio@users.noreply.github.com> --- .../elasticsearch/security/role/acc_test.go | 323 ++++++++++++++ .../elasticsearch/security/role/create.go | 15 + .../elasticsearch/security/role/delete.go | 33 ++ .../elasticsearch/security/role/models.go | 46 ++ internal/elasticsearch/security/role/read.go | 406 ++++++++++++++++++ .../security/role/resource-description.md | 1 + .../elasticsearch/security/role/resource.go | 36 ++ .../elasticsearch/security/role/schema.go | 185 ++++++++ .../elasticsearch/security/role/update.go | 296 +++++++++++++ provider/plugin_framework.go | 2 + provider/provider.go | 1 - 11 files changed, 1343 insertions(+), 1 deletion(-) create mode 100644 internal/elasticsearch/security/role/acc_test.go create mode 100644 internal/elasticsearch/security/role/create.go create mode 100644 internal/elasticsearch/security/role/delete.go create mode 100644 internal/elasticsearch/security/role/models.go create mode 100644 internal/elasticsearch/security/role/read.go create mode 100644 internal/elasticsearch/security/role/resource-description.md create mode 100644 internal/elasticsearch/security/role/resource.go create mode 100644 internal/elasticsearch/security/role/schema.go create mode 100644 internal/elasticsearch/security/role/update.go diff --git a/internal/elasticsearch/security/role/acc_test.go b/internal/elasticsearch/security/role/acc_test.go new file mode 100644 index 000000000..a759f777a --- /dev/null +++ b/internal/elasticsearch/security/role/acc_test.go @@ -0,0 +1,323 @@ +package role_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/elasticsearch/security/role" + "github.com/elastic/terraform-provider-elasticstack/internal/versionutils" + sdkacctest "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" +) + +func TestAccResourceSecurityRole(t *testing.T) { + // generate a random username + roleName := sdkacctest.RandStringFromCharSet(10, sdkacctest.CharSetAlphaNum) + roleNameRemoteIndices := sdkacctest.RandStringFromCharSet(10, sdkacctest.CharSetAlphaNum) + roleNameDescription := sdkacctest.RandStringFromCharSet(10, sdkacctest.CharSetAlphaNum) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + CheckDestroy: checkResourceSecurityRoleDestroy, + ProtoV6ProviderFactories: acctest.Providers, + Steps: []resource.TestStep{ + { + Config: testAccResourceSecurityRoleCreate(roleName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("elasticstack_elasticsearch_security_role.test", "name", roleName), + resource.TestCheckResourceAttr("elasticstack_elasticsearch_security_role.test", "indices.0.allow_restricted_indices", "true"), + resource.TestCheckTypeSetElemAttr("elasticstack_elasticsearch_security_role.test", "indices.*.names.*", "index1"), + resource.TestCheckTypeSetElemAttr("elasticstack_elasticsearch_security_role.test", "indices.*.names.*", "index2"), + resource.TestCheckTypeSetElemAttr("elasticstack_elasticsearch_security_role.test", "cluster.*", "all"), + resource.TestCheckTypeSetElemAttr("elasticstack_elasticsearch_security_role.test", "run_as.*", "other_user"), + resource.TestCheckNoResourceAttr("elasticstack_elasticsearch_security_role.test", "global"), + ), + }, + { + Config: testAccResourceSecurityRoleUpdate(roleName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("elasticstack_elasticsearch_security_role.test", "name", roleName), + resource.TestCheckTypeSetElemAttr("elasticstack_elasticsearch_security_role.test", "indices.*.names.*", "index1"), + resource.TestCheckTypeSetElemAttr("elasticstack_elasticsearch_security_role.test", "indices.*.names.*", "index2"), + resource.TestCheckTypeSetElemAttr("elasticstack_elasticsearch_security_role.test", "cluster.*", "all"), + resource.TestCheckNoResourceAttr("elasticstack_elasticsearch_security_role.test", "run_as.#"), + resource.TestCheckNoResourceAttr("elasticstack_elasticsearch_security_role.test", "global"), + resource.TestCheckNoResourceAttr("elasticstack_elasticsearch_security_role.test", "applications.#"), + resource.TestCheckNoResourceAttr("elasticstack_elasticsearch_security_role.test", "indices.0.allow_restricted_indices"), + ), + }, + { + SkipFunc: versionutils.CheckIfVersionIsUnsupported(role.MinSupportedRemoteIndicesVersion), + Config: testAccResourceSecurityRoleRemoteIndicesCreate(roleNameRemoteIndices), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("elasticstack_elasticsearch_security_role.test", "name", roleNameRemoteIndices), + resource.TestCheckResourceAttr("elasticstack_elasticsearch_security_role.test", "indices.0.allow_restricted_indices", "true"), + resource.TestCheckTypeSetElemAttr("elasticstack_elasticsearch_security_role.test", "indices.*.names.*", "index1"), + resource.TestCheckTypeSetElemAttr("elasticstack_elasticsearch_security_role.test", "indices.*.names.*", "index2"), + resource.TestCheckTypeSetElemAttr("elasticstack_elasticsearch_security_role.test", "cluster.*", "all"), + resource.TestCheckTypeSetElemAttr("elasticstack_elasticsearch_security_role.test", "run_as.*", "other_user"), + resource.TestCheckNoResourceAttr("elasticstack_elasticsearch_security_role.test", "global"), + resource.TestCheckTypeSetElemAttr("elasticstack_elasticsearch_security_role.test", "remote_indices.*.clusters.*", "test-cluster"), + resource.TestCheckTypeSetElemAttr("elasticstack_elasticsearch_security_role.test", "remote_indices.*.names.*", "sample"), + ), + }, + { + SkipFunc: versionutils.CheckIfVersionIsUnsupported(role.MinSupportedRemoteIndicesVersion), + Config: testAccResourceSecurityRoleRemoteIndicesUpdate(roleNameRemoteIndices), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("elasticstack_elasticsearch_security_role.test", "name", roleNameRemoteIndices), + resource.TestCheckTypeSetElemAttr("elasticstack_elasticsearch_security_role.test", "indices.*.names.*", "index1"), + resource.TestCheckTypeSetElemAttr("elasticstack_elasticsearch_security_role.test", "indices.*.names.*", "index2"), + resource.TestCheckTypeSetElemAttr("elasticstack_elasticsearch_security_role.test", "cluster.*", "all"), + resource.TestCheckNoResourceAttr("elasticstack_elasticsearch_security_role.test", "run_as.#"), + resource.TestCheckNoResourceAttr("elasticstack_elasticsearch_security_role.test", "global"), + resource.TestCheckNoResourceAttr("elasticstack_elasticsearch_security_role.test", "applications.#"), + resource.TestCheckNoResourceAttr("elasticstack_elasticsearch_security_role.test", "indices.0.allow_restricted_indices"), + resource.TestCheckTypeSetElemAttr("elasticstack_elasticsearch_security_role.test", "remote_indices.*.clusters.*", "test-cluster2"), + resource.TestCheckTypeSetElemAttr("elasticstack_elasticsearch_security_role.test", "remote_indices.*.names.*", "sample2"), + ), + }, + { + SkipFunc: versionutils.CheckIfVersionIsUnsupported(role.MinSupportedDescriptionVersion), + Config: testAccResourceSecurityRoleDescriptionCreate(roleNameDescription), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("elasticstack_elasticsearch_security_role.test", "name", roleNameDescription), + resource.TestCheckResourceAttr("elasticstack_elasticsearch_security_role.test", "description", "test description"), + ), + }, + { + SkipFunc: versionutils.CheckIfVersionIsUnsupported(role.MinSupportedDescriptionVersion), + Config: testAccResourceSecurityRoleDescriptionUpdate(roleNameDescription), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("elasticstack_elasticsearch_security_role.test", "name", roleNameDescription), + resource.TestCheckResourceAttr("elasticstack_elasticsearch_security_role.test", "description", "updated test description"), + ), + }, + }, + }) +} + +func TestAccResourceSecurityRoleFromSDK(t *testing.T) { + roleName := sdkacctest.RandStringFromCharSet(10, sdkacctest.CharSetAlphaNum) + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + Steps: []resource.TestStep{ + { + // Create the role with the last provider version where the role resource was built on the SDK + ExternalProviders: map[string]resource.ExternalProvider{ + "elasticstack": { + Source: "elastic/elasticstack", + VersionConstraint: "0.11.17", + }, + }, + Config: testAccResourceSecurityRoleCreate(roleName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("elasticstack_elasticsearch_security_role.test", "name", roleName), + resource.TestCheckResourceAttr("elasticstack_elasticsearch_security_role.test", "indices.0.allow_restricted_indices", "true"), + resource.TestCheckTypeSetElemAttr("elasticstack_elasticsearch_security_role.test", "indices.*.names.*", "index1"), + resource.TestCheckTypeSetElemAttr("elasticstack_elasticsearch_security_role.test", "indices.*.names.*", "index2"), + resource.TestCheckTypeSetElemAttr("elasticstack_elasticsearch_security_role.test", "cluster.*", "all"), + resource.TestCheckTypeSetElemAttr("elasticstack_elasticsearch_security_role.test", "run_as.*", "other_user"), + ), + }, + { + ProtoV6ProviderFactories: acctest.Providers, + Config: testAccResourceSecurityRoleCreate(roleName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("elasticstack_elasticsearch_security_role.test", "name", roleName), + resource.TestCheckResourceAttr("elasticstack_elasticsearch_security_role.test", "indices.0.allow_restricted_indices", "true"), + resource.TestCheckTypeSetElemAttr("elasticstack_elasticsearch_security_role.test", "indices.*.names.*", "index1"), + resource.TestCheckTypeSetElemAttr("elasticstack_elasticsearch_security_role.test", "indices.*.names.*", "index2"), + resource.TestCheckTypeSetElemAttr("elasticstack_elasticsearch_security_role.test", "cluster.*", "all"), + resource.TestCheckTypeSetElemAttr("elasticstack_elasticsearch_security_role.test", "run_as.*", "other_user"), + ), + }, + }, + }) +} + +func testAccResourceSecurityRoleCreate(roleName string) string { + return fmt.Sprintf(` +provider "elasticstack" { + elasticsearch {} +} + +resource "elasticstack_elasticsearch_security_role" "test" { + name = "%s" + + cluster = ["all"] + + indices { + names = ["index1", "index2"] + privileges = ["all"] + allow_restricted_indices = true + } + + applications { + application = "myapp" + privileges = ["admin", "read"] + resources = ["*"] + } + + run_as = ["other_user"] + + metadata = jsonencode({ + version = 1 + }) +} + `, roleName) +} + +func testAccResourceSecurityRoleUpdate(roleName string) string { + return fmt.Sprintf(` +provider "elasticstack" { + elasticsearch {} +} + +resource "elasticstack_elasticsearch_security_role" "test" { + name = "%s" + + cluster = ["all"] + + indices { + names = ["index1", "index2"] + privileges = ["all"] + } + + metadata = jsonencode({ + version = 1 + }) +} + `, roleName) +} + +func testAccResourceSecurityRoleRemoteIndicesCreate(roleName string) string { + return fmt.Sprintf(` +provider "elasticstack" { + elasticsearch {} +} + +resource "elasticstack_elasticsearch_security_role" "test" { + name = "%s" + cluster = ["all"] + + indices { + names = ["index1", "index2"] + privileges = ["all"] + allow_restricted_indices = true + } + + remote_indices { + clusters = ["test-cluster"] + field_security { + grant = ["sample"] + except = [] + } + names = ["sample"] + privileges = ["create", "read", "write"] + } + + applications { + application = "myapp" + privileges = ["admin", "read"] + resources = ["*"] + } + + run_as = ["other_user"] + + metadata = jsonencode({ + version = 1 + }) +} + `, roleName) +} + +func testAccResourceSecurityRoleRemoteIndicesUpdate(roleName string) string { + return fmt.Sprintf(` +provider "elasticstack" { + elasticsearch {} +} + +resource "elasticstack_elasticsearch_security_role" "test" { + name = "%s" + cluster = ["all"] + + indices { + names = ["index1", "index2"] + privileges = ["all"] + } + + remote_indices { + clusters = ["test-cluster2"] + field_security { + grant = ["sample"] + except = [] + } + names = ["sample2"] + privileges = ["create", "read", "write"] + } + + metadata = jsonencode({ + version = 1 + }) +} + `, roleName) +} + +func testAccResourceSecurityRoleDescriptionCreate(roleName string) string { + return fmt.Sprintf(` +provider "elasticstack" { + elasticsearch {} +} + +resource "elasticstack_elasticsearch_security_role" "test" { + name = "%s" + description = "test description" +} + `, roleName) +} + +func testAccResourceSecurityRoleDescriptionUpdate(roleName string) string { + return fmt.Sprintf(` +provider "elasticstack" { + elasticsearch {} +} + +resource "elasticstack_elasticsearch_security_role" "test" { + name = "%s" + description = "updated test description" +} + `, 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_elasticsearch_security_role" { + continue + } + compId, _ := clients.CompositeIdFromStr(rs.Primary.ID) + + esClient, err := client.GetESClient() + if err != nil { + return err + } + req := esClient.Security.GetRole.WithName(compId.ResourceId) + res, err := esClient.Security.GetRole(req) + if err != nil { + return err + } + + if res.StatusCode != 404 { + return fmt.Errorf("role (%s) still exists", compId.ResourceId) + } + } + return nil +} \ No newline at end of file diff --git a/internal/elasticsearch/security/role/create.go b/internal/elasticsearch/security/role/create.go new file mode 100644 index 000000000..65c2b6d56 --- /dev/null +++ b/internal/elasticsearch/security/role/create.go @@ -0,0 +1,15 @@ +package role + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/resource" +) + +func (r *roleResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + diags := r.update(ctx, req.Plan, &resp.State) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} \ No newline at end of file diff --git a/internal/elasticsearch/security/role/delete.go b/internal/elasticsearch/security/role/delete.go new file mode 100644 index 000000000..820b06f32 --- /dev/null +++ b/internal/elasticsearch/security/role/delete.go @@ -0,0 +1,33 @@ +package role + +import ( + "context" + + "github.com/elastic/terraform-provider-elasticstack/internal/clients" + "github.com/elastic/terraform-provider-elasticstack/internal/clients/elasticsearch" + "github.com/elastic/terraform-provider-elasticstack/internal/diagutil" + "github.com/hashicorp/terraform-plugin-framework/resource" +) + +func (r *roleResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var data RoleData + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + compId, diags := clients.CompositeIdFromStrFw(data.Id.ValueString()) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + client, diags := clients.MaybeNewApiClientFromFrameworkResource(ctx, data.ElasticsearchConnection, r.client) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + sdkDiags := elasticsearch.DeleteRole(ctx, client, compId.ResourceId) + resp.Diagnostics.Append(diagutil.FrameworkDiagsFromSDK(sdkDiags)...) +} \ No newline at end of file diff --git a/internal/elasticsearch/security/role/models.go b/internal/elasticsearch/security/role/models.go new file mode 100644 index 000000000..4e5b8273d --- /dev/null +++ b/internal/elasticsearch/security/role/models.go @@ -0,0 +1,46 @@ +package role + +import ( + "github.com/hashicorp/terraform-plugin-framework/types" +) + +type RoleData struct { + Id types.String `tfsdk:"id"` + ElasticsearchConnection types.List `tfsdk:"elasticsearch_connection"` + Name types.String `tfsdk:"name"` + Description types.String `tfsdk:"description"` + Applications types.Set `tfsdk:"applications"` + Global types.String `tfsdk:"global"` + Cluster types.Set `tfsdk:"cluster"` + Indices types.Set `tfsdk:"indices"` + RemoteIndices types.Set `tfsdk:"remote_indices"` + Metadata types.String `tfsdk:"metadata"` + RunAs types.Set `tfsdk:"run_as"` +} + +type ApplicationData struct { + Application types.String `tfsdk:"application"` + Privileges types.Set `tfsdk:"privileges"` + Resources types.Set `tfsdk:"resources"` +} + +type IndexPermsData struct { + FieldSecurity types.List `tfsdk:"field_security"` + Names types.Set `tfsdk:"names"` + Privileges types.Set `tfsdk:"privileges"` + Query types.String `tfsdk:"query"` + AllowRestrictedIndices types.Bool `tfsdk:"allow_restricted_indices"` +} + +type RemoteIndexPermsData struct { + Clusters types.Set `tfsdk:"clusters"` + FieldSecurity types.List `tfsdk:"field_security"` + Query types.String `tfsdk:"query"` + Names types.Set `tfsdk:"names"` + Privileges types.Set `tfsdk:"privileges"` +} + +type FieldSecurityData struct { + Grant types.Set `tfsdk:"grant"` + Except types.Set `tfsdk:"except"` +} \ No newline at end of file diff --git a/internal/elasticsearch/security/role/read.go b/internal/elasticsearch/security/role/read.go new file mode 100644 index 000000000..cef288a31 --- /dev/null +++ b/internal/elasticsearch/security/role/read.go @@ -0,0 +1,406 @@ +package role + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/elastic/terraform-provider-elasticstack/internal/clients" + "github.com/elastic/terraform-provider-elasticstack/internal/clients/elasticsearch" + "github.com/elastic/terraform-provider-elasticstack/internal/diagutil" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" +) + +func (r *roleResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data RoleData + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + compId, diags := clients.CompositeIdFromStrFw(data.Id.ValueString()) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + roleId := compId.ResourceId + + client, diags := clients.MaybeNewApiClientFromFrameworkResource(ctx, data.ElasticsearchConnection, r.client) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + role, sdkDiags := elasticsearch.GetRole(ctx, client, roleId) + resp.Diagnostics.Append(diagutil.FrameworkDiagsFromSDK(sdkDiags)...) + if resp.Diagnostics.HasError() { + return + } + + if role == nil { + tflog.Warn(ctx, fmt.Sprintf(`Role "%s" not found, removing from state`, roleId)) + resp.State.RemoveResource(ctx) + return + } + + // Set the fields + data.Name = types.StringValue(roleId) + + // Set the description if it exists + if role.Description != nil { + data.Description = types.StringValue(*role.Description) + } else { + data.Description = types.StringNull() + } + + // Applications + if len(role.Applications) > 0 { + appElements := make([]attr.Value, len(role.Applications)) + for i, app := range role.Applications { + privSet, diags := types.SetValueFrom(ctx, types.StringType, app.Privileges) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resSet, diags := types.SetValueFrom(ctx, types.StringType, app.Resources) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + appObj, diags := types.ObjectValue(map[string]attr.Type{ + "application": types.StringType, + "privileges": types.SetType{ElemType: types.StringType}, + "resources": types.SetType{ElemType: types.StringType}, + }, map[string]attr.Value{ + "application": types.StringValue(app.Name), + "privileges": privSet, + "resources": resSet, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + appElements[i] = appObj + } + + appSet, diags := types.SetValue(types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "application": types.StringType, + "privileges": types.SetType{ElemType: types.StringType}, + "resources": types.SetType{ElemType: types.StringType}, + }, + }, appElements) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + data.Applications = appSet + } else { + data.Applications = types.SetNull(types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "application": types.StringType, + "privileges": types.SetType{ElemType: types.StringType}, + "resources": types.SetType{ElemType: types.StringType}, + }, + }) + } + + // Cluster + if len(role.Cluster) > 0 { + clusterSet, diags := types.SetValueFrom(ctx, types.StringType, role.Cluster) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + data.Cluster = clusterSet + } else { + data.Cluster = types.SetNull(types.StringType) + } + + // Global + if role.Global != nil { + global, err := json.Marshal(role.Global) + if err != nil { + resp.Diagnostics.AddError("JSON Marshal Error", fmt.Sprintf("Error marshaling global JSON: %s", err)) + return + } + data.Global = types.StringValue(string(global)) + } else { + data.Global = types.StringNull() + } + + // Indices + if len(role.Indices) > 0 { + indicesElements := make([]attr.Value, len(role.Indices)) + for i, index := range role.Indices { + namesSet, diags := types.SetValueFrom(ctx, types.StringType, index.Names) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + privSet, diags := types.SetValueFrom(ctx, types.StringType, index.Privileges) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + var queryVal types.String + if index.Query != nil { + queryVal = types.StringValue(*index.Query) + } else { + queryVal = types.StringNull() + } + + var allowRestrictedVal types.Bool + if index.AllowRestrictedIndices != nil { + allowRestrictedVal = types.BoolValue(*index.AllowRestrictedIndices) + } else { + allowRestrictedVal = types.BoolNull() + } + + var fieldSecList types.List + if index.FieldSecurity != nil { + grantSet, diags := types.SetValueFrom(ctx, types.StringType, index.FieldSecurity.Grant) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + exceptSet, diags := types.SetValueFrom(ctx, types.StringType, index.FieldSecurity.Except) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + fieldSecObj, diags := types.ObjectValue(map[string]attr.Type{ + "grant": types.SetType{ElemType: types.StringType}, + "except": types.SetType{ElemType: types.StringType}, + }, map[string]attr.Value{ + "grant": grantSet, + "except": exceptSet, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + fieldSecList, diags = types.ListValue(types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "grant": types.SetType{ElemType: types.StringType}, + "except": types.SetType{ElemType: types.StringType}, + }, + }, []attr.Value{fieldSecObj}) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + } else { + fieldSecList = types.ListNull(types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "grant": types.SetType{ElemType: types.StringType}, + "except": types.SetType{ElemType: types.StringType}, + }, + }) + } + + indexObj, diags := types.ObjectValue(map[string]attr.Type{ + "field_security": types.ListType{ElemType: types.ObjectType{AttrTypes: map[string]attr.Type{"grant": types.SetType{ElemType: types.StringType}, "except": types.SetType{ElemType: types.StringType}}}}, + "names": types.SetType{ElemType: types.StringType}, + "privileges": types.SetType{ElemType: types.StringType}, + "query": types.StringType, + "allow_restricted_indices": types.BoolType, + }, map[string]attr.Value{ + "field_security": fieldSecList, + "names": namesSet, + "privileges": privSet, + "query": queryVal, + "allow_restricted_indices": allowRestrictedVal, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + indicesElements[i] = indexObj + } + + indicesSet, diags := types.SetValue(types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "field_security": types.ListType{ElemType: types.ObjectType{AttrTypes: map[string]attr.Type{"grant": types.SetType{ElemType: types.StringType}, "except": types.SetType{ElemType: types.StringType}}}}, + "names": types.SetType{ElemType: types.StringType}, + "privileges": types.SetType{ElemType: types.StringType}, + "query": types.StringType, + "allow_restricted_indices": types.BoolType, + }, + }, indicesElements) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + data.Indices = indicesSet + } else { + data.Indices = types.SetNull(types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "field_security": types.ListType{ElemType: types.ObjectType{AttrTypes: map[string]attr.Type{"grant": types.SetType{ElemType: types.StringType}, "except": types.SetType{ElemType: types.StringType}}}}, + "names": types.SetType{ElemType: types.StringType}, + "privileges": types.SetType{ElemType: types.StringType}, + "query": types.StringType, + "allow_restricted_indices": types.BoolType, + }, + }) + } + + // Remote Indices + if len(role.RemoteIndices) > 0 { + remoteIndicesElements := make([]attr.Value, len(role.RemoteIndices)) + for i, remoteIndex := range role.RemoteIndices { + clustersSet, diags := types.SetValueFrom(ctx, types.StringType, remoteIndex.Clusters) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + namesSet, diags := types.SetValueFrom(ctx, types.StringType, remoteIndex.Names) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + privSet, diags := types.SetValueFrom(ctx, types.StringType, remoteIndex.Privileges) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + var queryVal types.String + if remoteIndex.Query != nil { + queryVal = types.StringValue(*remoteIndex.Query) + } else { + queryVal = types.StringNull() + } + + var fieldSecList types.List + if remoteIndex.FieldSecurity != nil { + grantSet, diags := types.SetValueFrom(ctx, types.StringType, remoteIndex.FieldSecurity.Grant) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + exceptSet, diags := types.SetValueFrom(ctx, types.StringType, remoteIndex.FieldSecurity.Except) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + fieldSecObj, diags := types.ObjectValue(map[string]attr.Type{ + "grant": types.SetType{ElemType: types.StringType}, + "except": types.SetType{ElemType: types.StringType}, + }, map[string]attr.Value{ + "grant": grantSet, + "except": exceptSet, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + fieldSecList, diags = types.ListValue(types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "grant": types.SetType{ElemType: types.StringType}, + "except": types.SetType{ElemType: types.StringType}, + }, + }, []attr.Value{fieldSecObj}) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + } else { + fieldSecList = types.ListNull(types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "grant": types.SetType{ElemType: types.StringType}, + "except": types.SetType{ElemType: types.StringType}, + }, + }) + } + + remoteIndexObj, diags := types.ObjectValue(map[string]attr.Type{ + "clusters": types.SetType{ElemType: types.StringType}, + "field_security": types.ListType{ElemType: types.ObjectType{AttrTypes: map[string]attr.Type{"grant": types.SetType{ElemType: types.StringType}, "except": types.SetType{ElemType: types.StringType}}}}, + "query": types.StringType, + "names": types.SetType{ElemType: types.StringType}, + "privileges": types.SetType{ElemType: types.StringType}, + }, map[string]attr.Value{ + "clusters": clustersSet, + "field_security": fieldSecList, + "query": queryVal, + "names": namesSet, + "privileges": privSet, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + remoteIndicesElements[i] = remoteIndexObj + } + + remoteIndicesSet, diags := types.SetValue(types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "clusters": types.SetType{ElemType: types.StringType}, + "field_security": types.ListType{ElemType: types.ObjectType{AttrTypes: map[string]attr.Type{"grant": types.SetType{ElemType: types.StringType}, "except": types.SetType{ElemType: types.StringType}}}}, + "query": types.StringType, + "names": types.SetType{ElemType: types.StringType}, + "privileges": types.SetType{ElemType: types.StringType}, + }, + }, remoteIndicesElements) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + data.RemoteIndices = remoteIndicesSet + } else { + data.RemoteIndices = types.SetNull(types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "clusters": types.SetType{ElemType: types.StringType}, + "field_security": types.ListType{ElemType: types.ObjectType{AttrTypes: map[string]attr.Type{"grant": types.SetType{ElemType: types.StringType}, "except": types.SetType{ElemType: types.StringType}}}}, + "query": types.StringType, + "names": types.SetType{ElemType: types.StringType}, + "privileges": types.SetType{ElemType: types.StringType}, + }, + }) + } + + // Metadata + if role.Metadata != nil { + metadata, err := json.Marshal(role.Metadata) + if err != nil { + resp.Diagnostics.AddError("JSON Marshal Error", fmt.Sprintf("Error marshaling metadata JSON: %s", err)) + return + } + data.Metadata = types.StringValue(string(metadata)) + } else { + data.Metadata = types.StringNull() + } + + // Run As + if len(role.RusAs) > 0 { + runAsSet, diags := types.SetValueFrom(ctx, types.StringType, role.RusAs) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + data.RunAs = runAsSet + } else { + data.RunAs = types.SetNull(types.StringType) + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} \ No newline at end of file diff --git a/internal/elasticsearch/security/role/resource-description.md b/internal/elasticsearch/security/role/resource-description.md new file mode 100644 index 000000000..de9f7bb00 --- /dev/null +++ b/internal/elasticsearch/security/role/resource-description.md @@ -0,0 +1 @@ +Adds and updates roles in the native realm. See the [security API put role documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-put-role.html) for more details. \ No newline at end of file diff --git a/internal/elasticsearch/security/role/resource.go b/internal/elasticsearch/security/role/resource.go new file mode 100644 index 000000000..ef0b4900d --- /dev/null +++ b/internal/elasticsearch/security/role/resource.go @@ -0,0 +1,36 @@ +package role + +import ( + "context" + + "github.com/elastic/terraform-provider-elasticstack/internal/clients" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" +) + +// Ensure provider defined types fully satisfy framework interfaces +var _ resource.Resource = &roleResource{} +var _ resource.ResourceWithConfigure = &roleResource{} +var _ resource.ResourceWithImportState = &roleResource{} + +func NewRoleResource() resource.Resource { + return &roleResource{} +} + +type roleResource struct { + client *clients.ApiClient +} + +func (r *roleResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_elasticsearch_security_role" +} + +func (r *roleResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + client, diags := clients.ConvertProviderData(req.ProviderData) + resp.Diagnostics.Append(diags...) + r.client = client +} + +func (r *roleResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} \ No newline at end of file diff --git a/internal/elasticsearch/security/role/schema.go b/internal/elasticsearch/security/role/schema.go new file mode 100644 index 000000000..f6eb8739e --- /dev/null +++ b/internal/elasticsearch/security/role/schema.go @@ -0,0 +1,185 @@ +package role + +import ( + "context" + _ "embed" + + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + + providerschema "github.com/elastic/terraform-provider-elasticstack/internal/schema" +) + +//go:embed resource-description.md +var roleResourceDescription string + +func (r *roleResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = GetSchema() +} + +func GetSchema() schema.Schema { + return schema.Schema{ + MarkdownDescription: roleResourceDescription, + Blocks: map[string]schema.Block{ + "elasticsearch_connection": providerschema.GetEsFWConnectionBlock("elasticsearch_connection", false), + }, + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + MarkdownDescription: "Internal identifier of the resource", + Computed: true, + }, + "name": schema.StringAttribute{ + MarkdownDescription: "The name of the role.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "description": schema.StringAttribute{ + MarkdownDescription: "The description of the role.", + Optional: true, + }, + "applications": schema.SetNestedAttribute{ + MarkdownDescription: "A list of application privilege entries.", + Optional: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "application": schema.StringAttribute{ + MarkdownDescription: "The name of the application to which this entry applies.", + Required: true, + }, + "privileges": schema.SetAttribute{ + MarkdownDescription: "A list of strings, where each element is the name of an application privilege or action.", + Required: true, + ElementType: types.StringType, + }, + "resources": schema.SetAttribute{ + MarkdownDescription: "A list resources to which the privileges are applied.", + Required: true, + ElementType: types.StringType, + }, + }, + }, + }, + "global": schema.StringAttribute{ + MarkdownDescription: "An object defining global privileges.", + Optional: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + "cluster": schema.SetAttribute{ + MarkdownDescription: "A list of cluster privileges. These privileges define the cluster level actions that users with this role are able to execute.", + Optional: true, + ElementType: types.StringType, + }, + "indices": schema.SetNestedAttribute{ + MarkdownDescription: "A list of indices permissions entries.", + Optional: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "field_security": schema.ListNestedAttribute{ + MarkdownDescription: "The document fields that the owners of the role have read access to.", + Optional: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "grant": schema.SetAttribute{ + MarkdownDescription: "List of the fields to grant the access to.", + Optional: true, + ElementType: types.StringType, + }, + "except": schema.SetAttribute{ + MarkdownDescription: "List of the fields to which the grants will not be applied.", + Optional: true, + ElementType: types.StringType, + }, + }, + }, + }, + "names": schema.SetAttribute{ + MarkdownDescription: "A list of indices (or index name patterns) to which the permissions in this entry apply.", + Required: true, + ElementType: types.StringType, + }, + "privileges": schema.SetAttribute{ + MarkdownDescription: "The index level privileges that the owners of the role have on the specified indices.", + Required: true, + ElementType: types.StringType, + }, + "query": schema.StringAttribute{ + MarkdownDescription: "A search query that defines the documents the owners of the role have read access to.", + Optional: true, + }, + "allow_restricted_indices": schema.BoolAttribute{ + MarkdownDescription: "Include matching restricted indices in names parameter. Usage is strongly discouraged as it can grant unrestricted operations on critical data, make the entire system unstable or leak sensitive information.", + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + }, + }, + }, + }, + "remote_indices": schema.SetNestedAttribute{ + MarkdownDescription: "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.", + Optional: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "clusters": schema.SetAttribute{ + MarkdownDescription: "A list of cluster aliases to which the permissions in this entry apply.", + Required: true, + ElementType: types.StringType, + }, + "field_security": schema.ListNestedAttribute{ + MarkdownDescription: "The document fields that the owners of the role have read access to.", + Optional: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "grant": schema.SetAttribute{ + MarkdownDescription: "List of the fields to grant the access to.", + Optional: true, + ElementType: types.StringType, + }, + "except": schema.SetAttribute{ + MarkdownDescription: "List of the fields to which the grants will not be applied.", + Optional: true, + ElementType: types.StringType, + }, + }, + }, + }, + "query": schema.StringAttribute{ + MarkdownDescription: "A search query that defines the documents the owners of the role have read access to.", + Optional: true, + }, + "names": schema.SetAttribute{ + MarkdownDescription: "A list of indices (or index name patterns) to which the permissions in this entry apply.", + Required: true, + ElementType: types.StringType, + }, + "privileges": schema.SetAttribute{ + MarkdownDescription: "The index level privileges that the owners of the role have on the specified indices.", + Required: true, + ElementType: types.StringType, + }, + }, + }, + }, + "metadata": schema.StringAttribute{ + MarkdownDescription: "Optional meta-data.", + Optional: true, + Computed: true, + }, + "run_as": schema.SetAttribute{ + MarkdownDescription: "A list of users that the owners of this role can impersonate.", + Optional: true, + ElementType: types.StringType, + }, + }, + } +} \ No newline at end of file diff --git a/internal/elasticsearch/security/role/update.go b/internal/elasticsearch/security/role/update.go new file mode 100644 index 000000000..166617577 --- /dev/null +++ b/internal/elasticsearch/security/role/update.go @@ -0,0 +1,296 @@ +package role + +import ( + "context" + "encoding/json" + "fmt" + "strings" + + "github.com/elastic/terraform-provider-elasticstack/internal/clients" + "github.com/elastic/terraform-provider-elasticstack/internal/clients/elasticsearch" + "github.com/elastic/terraform-provider-elasticstack/internal/diagutil" + "github.com/elastic/terraform-provider-elasticstack/internal/models" + "github.com/elastic/terraform-provider-elasticstack/internal/utils" + "github.com/hashicorp/go-version" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var ( + MinSupportedRemoteIndicesVersion = version.Must(version.NewVersion("8.10.0")) + MinSupportedDescriptionVersion = version.Must(version.NewVersion("8.15.0")) +) + +func (r *roleResource) update(ctx context.Context, plan tfsdk.Plan, state *tfsdk.State) diag.Diagnostics { + var data RoleData + var diags diag.Diagnostics + diags.Append(plan.Get(ctx, &data)...) + if diags.HasError() { + return diags + } + + roleId := data.Name.ValueString() + id, sdkDiags := r.client.ID(ctx, roleId) + diags.Append(diagutil.FrameworkDiagsFromSDK(sdkDiags)...) + if diags.HasError() { + return diags + } + + client, diags := clients.MaybeNewApiClientFromFrameworkResource(ctx, data.ElasticsearchConnection, r.client) + diags.Append(diags...) + if diags.HasError() { + return diags + } + + serverVersion, sdkDiags := client.ServerVersion(ctx) + diags.Append(diagutil.FrameworkDiagsFromSDK(sdkDiags)...) + if diags.HasError() { + return diags + } + + var role models.Role + role.Name = roleId + + // Add description to the role + if utils.IsKnown(data.Description) && !data.Description.IsNull() { + // Return an error if the server version is less than the minimum supported version + if serverVersion.LessThan(MinSupportedDescriptionVersion) { + diags.AddError("Unsupported Feature", fmt.Sprintf("'description' is supported only for Elasticsearch v%s and above", MinSupportedDescriptionVersion.String())) + return diags + } + + description := data.Description.ValueString() + role.Description = &description + } + + // Applications + if utils.IsKnown(data.Applications) && !data.Applications.IsNull() { + var applicationsList []ApplicationData + diags.Append(data.Applications.ElementsAs(ctx, &applicationsList, false)...) + if diags.HasError() { + return diags + } + + applications := make([]models.Application, len(applicationsList)) + for i, app := range applicationsList { + var privileges, resources []string + diags.Append(app.Privileges.ElementsAs(ctx, &privileges, false)...) + diags.Append(app.Resources.ElementsAs(ctx, &resources, false)...) + if diags.HasError() { + return diags + } + + applications[i] = models.Application{ + Name: app.Application.ValueString(), + Privileges: privileges, + Resources: resources, + } + } + role.Applications = applications + } + + // Global + if utils.IsKnown(data.Global) && !data.Global.IsNull() { + global := make(map[string]interface{}) + if err := json.NewDecoder(strings.NewReader(data.Global.ValueString())).Decode(&global); err != nil { + diags.AddError("Invalid JSON", fmt.Sprintf("Error parsing global JSON: %s", err)) + return diags + } + role.Global = global + } + + // Cluster + if utils.IsKnown(data.Cluster) && !data.Cluster.IsNull() { + var cluster []string + diags.Append(data.Cluster.ElementsAs(ctx, &cluster, false)...) + if diags.HasError() { + return diags + } + role.Cluster = cluster + } + + // Indices + if utils.IsKnown(data.Indices) && !data.Indices.IsNull() { + var indicesList []IndexPermsData + diags.Append(data.Indices.ElementsAs(ctx, &indicesList, false)...) + if diags.HasError() { + return diags + } + + indices := make([]models.IndexPerms, len(indicesList)) + for i, idx := range indicesList { + var names, privileges []string + diags.Append(idx.Names.ElementsAs(ctx, &names, false)...) + diags.Append(idx.Privileges.ElementsAs(ctx, &privileges, false)...) + if diags.HasError() { + return diags + } + + newIndex := models.IndexPerms{ + Names: names, + Privileges: privileges, + } + + if utils.IsKnown(idx.Query) && !idx.Query.IsNull() { + query := idx.Query.ValueString() + newIndex.Query = &query + } + + // Field Security + if utils.IsKnown(idx.FieldSecurity) && !idx.FieldSecurity.IsNull() { + var fieldSecList []FieldSecurityData + diags.Append(idx.FieldSecurity.ElementsAs(ctx, &fieldSecList, false)...) + if diags.HasError() { + return diags + } + + if len(fieldSecList) > 0 { + fieldSec := fieldSecList[0] + fieldSecurity := models.FieldSecurity{} + + if utils.IsKnown(fieldSec.Grant) && !fieldSec.Grant.IsNull() { + var grants []string + diags.Append(fieldSec.Grant.ElementsAs(ctx, &grants, false)...) + if diags.HasError() { + return diags + } + fieldSecurity.Grant = grants + } + + if utils.IsKnown(fieldSec.Except) && !fieldSec.Except.IsNull() { + var excepts []string + diags.Append(fieldSec.Except.ElementsAs(ctx, &excepts, false)...) + if diags.HasError() { + return diags + } + fieldSecurity.Except = excepts + } + + newIndex.FieldSecurity = &fieldSecurity + } + } + + if utils.IsKnown(idx.AllowRestrictedIndices) && !idx.AllowRestrictedIndices.IsNull() { + allowRestrictedIndices := idx.AllowRestrictedIndices.ValueBool() + newIndex.AllowRestrictedIndices = &allowRestrictedIndices + } + + indices[i] = newIndex + } + role.Indices = indices + } + + // Remote Indices + if utils.IsKnown(data.RemoteIndices) && !data.RemoteIndices.IsNull() { + var remoteIndicesList []RemoteIndexPermsData + diags.Append(data.RemoteIndices.ElementsAs(ctx, &remoteIndicesList, false)...) + if diags.HasError() { + return diags + } + + if len(remoteIndicesList) > 0 && serverVersion.LessThan(MinSupportedRemoteIndicesVersion) { + diags.AddError("Unsupported Feature", fmt.Sprintf("'remote_indices' is supported only for Elasticsearch v%s and above", MinSupportedRemoteIndicesVersion.String())) + return diags + } + + remoteIndices := make([]models.RemoteIndexPerms, len(remoteIndicesList)) + for i, remoteIdx := range remoteIndicesList { + var names, clusters, privileges []string + diags.Append(remoteIdx.Names.ElementsAs(ctx, &names, false)...) + diags.Append(remoteIdx.Clusters.ElementsAs(ctx, &clusters, false)...) + diags.Append(remoteIdx.Privileges.ElementsAs(ctx, &privileges, false)...) + if diags.HasError() { + return diags + } + + newRemoteIndex := models.RemoteIndexPerms{ + Names: names, + Clusters: clusters, + Privileges: privileges, + } + + if utils.IsKnown(remoteIdx.Query) && !remoteIdx.Query.IsNull() { + query := remoteIdx.Query.ValueString() + newRemoteIndex.Query = &query + } + + // Field Security + if utils.IsKnown(remoteIdx.FieldSecurity) && !remoteIdx.FieldSecurity.IsNull() { + var fieldSecList []FieldSecurityData + diags.Append(remoteIdx.FieldSecurity.ElementsAs(ctx, &fieldSecList, false)...) + if diags.HasError() { + return diags + } + + if len(fieldSecList) > 0 { + fieldSec := fieldSecList[0] + remoteFieldSecurity := models.FieldSecurity{} + + if utils.IsKnown(fieldSec.Grant) && !fieldSec.Grant.IsNull() { + var grants []string + diags.Append(fieldSec.Grant.ElementsAs(ctx, &grants, false)...) + if diags.HasError() { + return diags + } + remoteFieldSecurity.Grant = grants + } + + if utils.IsKnown(fieldSec.Except) && !fieldSec.Except.IsNull() { + var excepts []string + diags.Append(fieldSec.Except.ElementsAs(ctx, &excepts, false)...) + if diags.HasError() { + return diags + } + remoteFieldSecurity.Except = excepts + } + + newRemoteIndex.FieldSecurity = &remoteFieldSecurity + } + } + + remoteIndices[i] = newRemoteIndex + } + role.RemoteIndices = remoteIndices + } + + // Metadata + if utils.IsKnown(data.Metadata) && !data.Metadata.IsNull() { + metadata := make(map[string]interface{}) + if err := json.NewDecoder(strings.NewReader(data.Metadata.ValueString())).Decode(&metadata); err != nil { + diags.AddError("Invalid JSON", fmt.Sprintf("Error parsing metadata JSON: %s", err)) + return diags + } + role.Metadata = metadata + } + + // Run As + if utils.IsKnown(data.RunAs) && !data.RunAs.IsNull() { + var runAs []string + diags.Append(data.RunAs.ElementsAs(ctx, &runAs, false)...) + if diags.HasError() { + return diags + } + role.RusAs = runAs + } + + // Put the role + sdkDiags = elasticsearch.PutRole(ctx, client, &role) + diags.Append(diagutil.FrameworkDiagsFromSDK(sdkDiags)...) + if diags.HasError() { + return diags + } + + data.Id = types.StringValue(id.String()) + diags.Append(state.Set(ctx, &data)...) + return diags +} + +func (r *roleResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + diags := r.update(ctx, req.Plan, &resp.State) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} \ No newline at end of file diff --git a/provider/plugin_framework.go b/provider/plugin_framework.go index bba736241..fdbd25e3a 100644 --- a/provider/plugin_framework.go +++ b/provider/plugin_framework.go @@ -11,6 +11,7 @@ import ( "github.com/elastic/terraform-provider-elasticstack/internal/elasticsearch/index/index" "github.com/elastic/terraform-provider-elasticstack/internal/elasticsearch/index/indices" "github.com/elastic/terraform-provider-elasticstack/internal/elasticsearch/security/api_key" + "github.com/elastic/terraform-provider-elasticstack/internal/elasticsearch/security/role" "github.com/elastic/terraform-provider-elasticstack/internal/elasticsearch/security/role_mapping" "github.com/elastic/terraform-provider-elasticstack/internal/elasticsearch/security/system_user" "github.com/elastic/terraform-provider-elasticstack/internal/fleet/agent_policy" @@ -113,6 +114,7 @@ func (p *Provider) Resources(ctx context.Context) []func() resource.Resource { output.NewResource, server_host.NewResource, system_user.NewSystemUserResource, + role.NewRoleResource, maintenance_window.NewResource, enrich.NewEnrichPolicyResource, role_mapping.NewRoleMappingResource, diff --git a/provider/provider.go b/provider/provider.go index 3e78eee50..75de9cd30 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -88,7 +88,6 @@ func New(version string) *schema.Provider { "elasticstack_elasticsearch_index_template": index.ResourceTemplate(), "elasticstack_elasticsearch_ingest_pipeline": ingest.ResourceIngestPipeline(), "elasticstack_elasticsearch_logstash_pipeline": logstash.ResourceLogstashPipeline(), - "elasticstack_elasticsearch_security_role": security.ResourceRole(), "elasticstack_elasticsearch_security_user": security.ResourceUser(), "elasticstack_elasticsearch_snapshot_lifecycle": cluster.ResourceSlm(), "elasticstack_elasticsearch_snapshot_repository": cluster.ResourceSnapshotRepository(), From 59395dd4fae7f2eecf12d11b676e0cd7d14a34d4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Sep 2025 06:34:36 +0000 Subject: [PATCH 3/8] Complete Plugin Framework role resource migration with documentation Co-authored-by: tobio <444668+tobio@users.noreply.github.com> --- CHANGELOG.md | 1 + docs/resources/elasticsearch_security_role.md | 22 +++++----- .../elasticsearch/security/role/acc_test.go | 2 +- .../elasticsearch/security/role/create.go | 2 +- .../elasticsearch/security/role/delete.go | 2 +- .../elasticsearch/security/role/models.go | 12 +++--- internal/elasticsearch/security/role/read.go | 42 +++++++++---------- .../elasticsearch/security/role/resource.go | 2 +- .../elasticsearch/security/role/schema.go | 2 +- .../elasticsearch/security/role/update.go | 2 +- 10 files changed, 45 insertions(+), 44 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c78e4042..3031151bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ - Add support for `unenrollment_timeout` in `elasticstack_fleet_agent_policy` ([#1169](https://github.com/elastic/terraform-provider-elasticstack/issues/1169)) - Handle default value for `allow_restricted_indices` in `elasticstack_elasticsearch_security_api_key` ([#1315](https://github.com/elastic/terraform-provider-elasticstack/pull/1315)) - Fixed `nil` reference in kibana synthetics API client in case of response errors ([#1320](https://github.com/elastic/terraform-provider-elasticstack/pull/1320)) +- Migrate `elasticstack_elasticsearch_security_role` resource to Terraform Plugin Framework ([#1330](https://github.com/elastic/terraform-provider-elasticstack/pull/1330)) ## [0.11.17] - 2025-07-21 diff --git a/docs/resources/elasticsearch_security_role.md b/docs/resources/elasticsearch_security_role.md index 0e8a8cd89..cf2016ba7 100644 --- a/docs/resources/elasticsearch_security_role.md +++ b/docs/resources/elasticsearch_security_role.md @@ -55,21 +55,21 @@ output "role" { ### Optional -- `applications` (Block Set) A list of application privilege entries. (see [below for nested schema](#nestedblock--applications)) +- `applications` (Attributes Set) A list of application privilege entries. (see [below for nested schema](#nestedatt--applications)) - `cluster` (Set of String) A list of cluster privileges. These privileges define the cluster level actions that users with this role are able to execute. - `description` (String) The description of the role. -- `elasticsearch_connection` (Block List, Max: 1, Deprecated) Elasticsearch connection configuration block. This property will be removed in a future provider version. Configure the Elasticsearch connection via the provider configuration instead. (see [below for nested schema](#nestedblock--elasticsearch_connection)) +- `elasticsearch_connection` (Block List, Deprecated) Elasticsearch connection configuration block. (see [below for nested schema](#nestedblock--elasticsearch_connection)) - `global` (String) An object defining global privileges. -- `indices` (Block Set) A list of indices permissions entries. (see [below for nested schema](#nestedblock--indices)) +- `indices` (Attributes Set) A list of indices permissions entries. (see [below for nested schema](#nestedatt--indices)) - `metadata` (String) Optional meta-data. -- `remote_indices` (Block Set) 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. (see [below for nested schema](#nestedblock--remote_indices)) +- `remote_indices` (Attributes Set) 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. (see [below for nested schema](#nestedatt--remote_indices)) - `run_as` (Set of String) A list of users that the owners of this role can impersonate. ### Read-Only - `id` (String) Internal identifier of the resource - + ### Nested Schema for `applications` Required: @@ -100,7 +100,7 @@ Optional: - `username` (String) Username to use for API authentication to Elasticsearch. - + ### Nested Schema for `indices` Required: @@ -111,10 +111,10 @@ Required: Optional: - `allow_restricted_indices` (Boolean) Include matching restricted indices in names parameter. Usage is strongly discouraged as it can grant unrestricted operations on critical data, make the entire system unstable or leak sensitive information. -- `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--indices--field_security)) +- `field_security` (Attributes List) The document fields that the owners of the role have read access to. (see [below for nested schema](#nestedatt--indices--field_security)) - `query` (String) A search query that defines the documents the owners of the role have read access to. - + ### Nested Schema for `indices.field_security` Optional: @@ -124,7 +124,7 @@ Optional: - + ### Nested Schema for `remote_indices` Required: @@ -135,10 +135,10 @@ Required: 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--remote_indices--field_security)) +- `field_security` (Attributes List) The document fields that the owners of the role have read access to. (see [below for nested schema](#nestedatt--remote_indices--field_security)) - `query` (String) A search query that defines the documents the owners of the role have read access to. - + ### Nested Schema for `remote_indices.field_security` Optional: diff --git a/internal/elasticsearch/security/role/acc_test.go b/internal/elasticsearch/security/role/acc_test.go index a759f777a..6d47ca776 100644 --- a/internal/elasticsearch/security/role/acc_test.go +++ b/internal/elasticsearch/security/role/acc_test.go @@ -320,4 +320,4 @@ func checkResourceSecurityRoleDestroy(s *terraform.State) error { } } return nil -} \ No newline at end of file +} diff --git a/internal/elasticsearch/security/role/create.go b/internal/elasticsearch/security/role/create.go index 65c2b6d56..911e410ec 100644 --- a/internal/elasticsearch/security/role/create.go +++ b/internal/elasticsearch/security/role/create.go @@ -12,4 +12,4 @@ func (r *roleResource) Create(ctx context.Context, req resource.CreateRequest, r if resp.Diagnostics.HasError() { return } -} \ No newline at end of file +} diff --git a/internal/elasticsearch/security/role/delete.go b/internal/elasticsearch/security/role/delete.go index 820b06f32..36979a5c7 100644 --- a/internal/elasticsearch/security/role/delete.go +++ b/internal/elasticsearch/security/role/delete.go @@ -30,4 +30,4 @@ func (r *roleResource) Delete(ctx context.Context, req resource.DeleteRequest, r sdkDiags := elasticsearch.DeleteRole(ctx, client, compId.ResourceId) resp.Diagnostics.Append(diagutil.FrameworkDiagsFromSDK(sdkDiags)...) -} \ No newline at end of file +} diff --git a/internal/elasticsearch/security/role/models.go b/internal/elasticsearch/security/role/models.go index 4e5b8273d..f5d9068c5 100644 --- a/internal/elasticsearch/security/role/models.go +++ b/internal/elasticsearch/security/role/models.go @@ -25,11 +25,11 @@ type ApplicationData struct { } type IndexPermsData struct { - FieldSecurity types.List `tfsdk:"field_security"` - Names types.Set `tfsdk:"names"` - Privileges types.Set `tfsdk:"privileges"` - Query types.String `tfsdk:"query"` - AllowRestrictedIndices types.Bool `tfsdk:"allow_restricted_indices"` + FieldSecurity types.List `tfsdk:"field_security"` + Names types.Set `tfsdk:"names"` + Privileges types.Set `tfsdk:"privileges"` + Query types.String `tfsdk:"query"` + AllowRestrictedIndices types.Bool `tfsdk:"allow_restricted_indices"` } type RemoteIndexPermsData struct { @@ -43,4 +43,4 @@ type RemoteIndexPermsData struct { type FieldSecurityData struct { Grant types.Set `tfsdk:"grant"` Except types.Set `tfsdk:"except"` -} \ No newline at end of file +} diff --git a/internal/elasticsearch/security/role/read.go b/internal/elasticsearch/security/role/read.go index cef288a31..b7da99217 100644 --- a/internal/elasticsearch/security/role/read.go +++ b/internal/elasticsearch/security/role/read.go @@ -211,17 +211,17 @@ func (r *roleResource) Read(ctx context.Context, req resource.ReadRequest, resp } indexObj, diags := types.ObjectValue(map[string]attr.Type{ - "field_security": types.ListType{ElemType: types.ObjectType{AttrTypes: map[string]attr.Type{"grant": types.SetType{ElemType: types.StringType}, "except": types.SetType{ElemType: types.StringType}}}}, - "names": types.SetType{ElemType: types.StringType}, - "privileges": types.SetType{ElemType: types.StringType}, - "query": types.StringType, - "allow_restricted_indices": types.BoolType, + "field_security": types.ListType{ElemType: types.ObjectType{AttrTypes: map[string]attr.Type{"grant": types.SetType{ElemType: types.StringType}, "except": types.SetType{ElemType: types.StringType}}}}, + "names": types.SetType{ElemType: types.StringType}, + "privileges": types.SetType{ElemType: types.StringType}, + "query": types.StringType, + "allow_restricted_indices": types.BoolType, }, map[string]attr.Value{ - "field_security": fieldSecList, - "names": namesSet, - "privileges": privSet, - "query": queryVal, - "allow_restricted_indices": allowRestrictedVal, + "field_security": fieldSecList, + "names": namesSet, + "privileges": privSet, + "query": queryVal, + "allow_restricted_indices": allowRestrictedVal, }) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -233,11 +233,11 @@ func (r *roleResource) Read(ctx context.Context, req resource.ReadRequest, resp indicesSet, diags := types.SetValue(types.ObjectType{ AttrTypes: map[string]attr.Type{ - "field_security": types.ListType{ElemType: types.ObjectType{AttrTypes: map[string]attr.Type{"grant": types.SetType{ElemType: types.StringType}, "except": types.SetType{ElemType: types.StringType}}}}, - "names": types.SetType{ElemType: types.StringType}, - "privileges": types.SetType{ElemType: types.StringType}, - "query": types.StringType, - "allow_restricted_indices": types.BoolType, + "field_security": types.ListType{ElemType: types.ObjectType{AttrTypes: map[string]attr.Type{"grant": types.SetType{ElemType: types.StringType}, "except": types.SetType{ElemType: types.StringType}}}}, + "names": types.SetType{ElemType: types.StringType}, + "privileges": types.SetType{ElemType: types.StringType}, + "query": types.StringType, + "allow_restricted_indices": types.BoolType, }, }, indicesElements) resp.Diagnostics.Append(diags...) @@ -248,11 +248,11 @@ func (r *roleResource) Read(ctx context.Context, req resource.ReadRequest, resp } else { data.Indices = types.SetNull(types.ObjectType{ AttrTypes: map[string]attr.Type{ - "field_security": types.ListType{ElemType: types.ObjectType{AttrTypes: map[string]attr.Type{"grant": types.SetType{ElemType: types.StringType}, "except": types.SetType{ElemType: types.StringType}}}}, - "names": types.SetType{ElemType: types.StringType}, - "privileges": types.SetType{ElemType: types.StringType}, - "query": types.StringType, - "allow_restricted_indices": types.BoolType, + "field_security": types.ListType{ElemType: types.ObjectType{AttrTypes: map[string]attr.Type{"grant": types.SetType{ElemType: types.StringType}, "except": types.SetType{ElemType: types.StringType}}}}, + "names": types.SetType{ElemType: types.StringType}, + "privileges": types.SetType{ElemType: types.StringType}, + "query": types.StringType, + "allow_restricted_indices": types.BoolType, }, }) } @@ -403,4 +403,4 @@ func (r *roleResource) Read(ctx context.Context, req resource.ReadRequest, resp } resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) -} \ No newline at end of file +} diff --git a/internal/elasticsearch/security/role/resource.go b/internal/elasticsearch/security/role/resource.go index ef0b4900d..fc078b4ac 100644 --- a/internal/elasticsearch/security/role/resource.go +++ b/internal/elasticsearch/security/role/resource.go @@ -33,4 +33,4 @@ func (r *roleResource) Configure(_ context.Context, req resource.ConfigureReques func (r *roleResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) -} \ No newline at end of file +} diff --git a/internal/elasticsearch/security/role/schema.go b/internal/elasticsearch/security/role/schema.go index f6eb8739e..5d864129a 100644 --- a/internal/elasticsearch/security/role/schema.go +++ b/internal/elasticsearch/security/role/schema.go @@ -182,4 +182,4 @@ func GetSchema() schema.Schema { }, }, } -} \ No newline at end of file +} diff --git a/internal/elasticsearch/security/role/update.go b/internal/elasticsearch/security/role/update.go index 166617577..299973e69 100644 --- a/internal/elasticsearch/security/role/update.go +++ b/internal/elasticsearch/security/role/update.go @@ -293,4 +293,4 @@ func (r *roleResource) Update(ctx context.Context, req resource.UpdateRequest, r if resp.Diagnostics.HasError() { return } -} \ No newline at end of file +} From 73d4732c9ce9f01c0ef2de1b270d0ea6242e649e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Sep 2025 07:15:42 +0000 Subject: [PATCH 4/8] Address PR feedback: use jsontypes, remove redundant code, add helper functions Co-authored-by: tobio <444668+tobio@users.noreply.github.com> --- docs/resources/elasticsearch_security_role.md | 4 +- .../elasticsearch/security/role/create.go | 3 - .../elasticsearch/security/role/models.go | 538 +++++++++++++++++- internal/elasticsearch/security/role/read.go | 361 +----------- .../security/role/resource-description.md | 2 +- .../elasticsearch/security/role/schema.go | 28 +- .../elasticsearch/security/role/update.go | 224 +------- 7 files changed, 555 insertions(+), 605 deletions(-) diff --git a/docs/resources/elasticsearch_security_role.md b/docs/resources/elasticsearch_security_role.md index cf2016ba7..94f27e05d 100644 --- a/docs/resources/elasticsearch_security_role.md +++ b/docs/resources/elasticsearch_security_role.md @@ -4,12 +4,12 @@ page_title: "elasticstack_elasticsearch_security_role Resource - terraform-provider-elasticstack" subcategory: "Security" description: |- - Adds and updates roles in the native realm. See the security API put role documentation https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-put-role.html for more details. + Adds and updates roles in the native realm. See the role API documentation https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-put-role.html for more details. --- # elasticstack_elasticsearch_security_role (Resource) -Adds and updates roles in the native realm. See the [security API put role documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-put-role.html) for more details. +Adds and updates roles in the native realm. See the [role API documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-put-role.html) for more details. ## Example Usage diff --git a/internal/elasticsearch/security/role/create.go b/internal/elasticsearch/security/role/create.go index 911e410ec..64413577a 100644 --- a/internal/elasticsearch/security/role/create.go +++ b/internal/elasticsearch/security/role/create.go @@ -9,7 +9,4 @@ import ( func (r *roleResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { diags := r.update(ctx, req.Plan, &resp.State) resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } } diff --git a/internal/elasticsearch/security/role/models.go b/internal/elasticsearch/security/role/models.go index f5d9068c5..e6b8b1dd9 100644 --- a/internal/elasticsearch/security/role/models.go +++ b/internal/elasticsearch/security/role/models.go @@ -1,21 +1,29 @@ package role import ( + "context" + "encoding/json" + "fmt" + + "github.com/elastic/terraform-provider-elasticstack/internal/models" + "github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/types" ) type RoleData struct { - Id types.String `tfsdk:"id"` - ElasticsearchConnection types.List `tfsdk:"elasticsearch_connection"` - Name types.String `tfsdk:"name"` - Description types.String `tfsdk:"description"` - Applications types.Set `tfsdk:"applications"` - Global types.String `tfsdk:"global"` - Cluster types.Set `tfsdk:"cluster"` - Indices types.Set `tfsdk:"indices"` - RemoteIndices types.Set `tfsdk:"remote_indices"` - Metadata types.String `tfsdk:"metadata"` - RunAs types.Set `tfsdk:"run_as"` + Id types.String `tfsdk:"id"` + ElasticsearchConnection types.List `tfsdk:"elasticsearch_connection"` + Name types.String `tfsdk:"name"` + Description types.String `tfsdk:"description"` + Applications types.Set `tfsdk:"applications"` + Global jsontypes.Normalized `tfsdk:"global"` + Cluster types.Set `tfsdk:"cluster"` + Indices types.Set `tfsdk:"indices"` + RemoteIndices types.Set `tfsdk:"remote_indices"` + Metadata jsontypes.Normalized `tfsdk:"metadata"` + RunAs types.Set `tfsdk:"run_as"` } type ApplicationData struct { @@ -25,22 +33,510 @@ type ApplicationData struct { } type IndexPermsData struct { - FieldSecurity types.List `tfsdk:"field_security"` - Names types.Set `tfsdk:"names"` - Privileges types.Set `tfsdk:"privileges"` - Query types.String `tfsdk:"query"` - AllowRestrictedIndices types.Bool `tfsdk:"allow_restricted_indices"` + FieldSecurity types.List `tfsdk:"field_security"` + Names types.Set `tfsdk:"names"` + Privileges types.Set `tfsdk:"privileges"` + Query jsontypes.Normalized `tfsdk:"query"` + AllowRestrictedIndices types.Bool `tfsdk:"allow_restricted_indices"` } type RemoteIndexPermsData struct { - Clusters types.Set `tfsdk:"clusters"` - FieldSecurity types.List `tfsdk:"field_security"` - Query types.String `tfsdk:"query"` - Names types.Set `tfsdk:"names"` - Privileges types.Set `tfsdk:"privileges"` + Clusters types.Set `tfsdk:"clusters"` + FieldSecurity types.List `tfsdk:"field_security"` + Query jsontypes.Normalized `tfsdk:"query"` + Names types.Set `tfsdk:"names"` + Privileges types.Set `tfsdk:"privileges"` } type FieldSecurityData struct { Grant types.Set `tfsdk:"grant"` Except types.Set `tfsdk:"except"` } + +// toAPIModel converts the Terraform model to the API model +func (data *RoleData) toAPIModel(ctx context.Context) (*models.Role, diag.Diagnostics) { + var diags diag.Diagnostics + var role models.Role + + role.Name = data.Name.ValueString() + + // Description + if !data.Description.IsNull() { + description := data.Description.ValueString() + role.Description = &description + } + + // Applications + if !data.Applications.IsNull() { + var applicationsList []ApplicationData + diags.Append(data.Applications.ElementsAs(ctx, &applicationsList, false)...) + if diags.HasError() { + return nil, diags + } + + applications := make([]models.Application, len(applicationsList)) + for i, app := range applicationsList { + var privileges, resources []string + diags.Append(app.Privileges.ElementsAs(ctx, &privileges, false)...) + diags.Append(app.Resources.ElementsAs(ctx, &resources, false)...) + if diags.HasError() { + return nil, diags + } + + applications[i] = models.Application{ + Name: app.Application.ValueString(), + Privileges: privileges, + Resources: resources, + } + } + role.Applications = applications + } + + // Global + if !data.Global.IsNull() { + var global map[string]interface{} + if err := json.Unmarshal([]byte(data.Global.ValueString()), &global); err != nil { + diags.AddError("Invalid JSON", fmt.Sprintf("Error parsing global JSON: %s", err)) + return nil, diags + } + role.Global = global + } + + // Cluster + if !data.Cluster.IsNull() { + var cluster []string + diags.Append(data.Cluster.ElementsAs(ctx, &cluster, false)...) + if diags.HasError() { + return nil, diags + } + role.Cluster = cluster + } + + // Indices + if !data.Indices.IsNull() { + var indicesList []IndexPermsData + diags.Append(data.Indices.ElementsAs(ctx, &indicesList, false)...) + if diags.HasError() { + return nil, diags + } + + indices := make([]models.IndexPerms, len(indicesList)) + for i, idx := range indicesList { + var names, privileges []string + diags.Append(idx.Names.ElementsAs(ctx, &names, false)...) + diags.Append(idx.Privileges.ElementsAs(ctx, &privileges, false)...) + if diags.HasError() { + return nil, diags + } + + newIndex := models.IndexPerms{ + Names: names, + Privileges: privileges, + } + + if !idx.Query.IsNull() { + query := idx.Query.ValueString() + newIndex.Query = &query + } + + // Field Security + if !idx.FieldSecurity.IsNull() { + var fieldSecList []FieldSecurityData + diags.Append(idx.FieldSecurity.ElementsAs(ctx, &fieldSecList, false)...) + if diags.HasError() { + return nil, diags + } + + if len(fieldSecList) > 0 { + fieldSec := fieldSecList[0] + fieldSecurity := models.FieldSecurity{} + + if !fieldSec.Grant.IsNull() { + var grants []string + diags.Append(fieldSec.Grant.ElementsAs(ctx, &grants, false)...) + if diags.HasError() { + return nil, diags + } + fieldSecurity.Grant = grants + } + + if !fieldSec.Except.IsNull() { + var excepts []string + diags.Append(fieldSec.Except.ElementsAs(ctx, &excepts, false)...) + if diags.HasError() { + return nil, diags + } + fieldSecurity.Except = excepts + } + + newIndex.FieldSecurity = &fieldSecurity + } + } + + if !idx.AllowRestrictedIndices.IsNull() { + allowRestrictedIndices := idx.AllowRestrictedIndices.ValueBool() + newIndex.AllowRestrictedIndices = &allowRestrictedIndices + } + + indices[i] = newIndex + } + role.Indices = indices + } + + // Remote Indices + if !data.RemoteIndices.IsNull() { + var remoteIndicesList []RemoteIndexPermsData + diags.Append(data.RemoteIndices.ElementsAs(ctx, &remoteIndicesList, false)...) + if diags.HasError() { + return nil, diags + } + + remoteIndices := make([]models.RemoteIndexPerms, len(remoteIndicesList)) + for i, remoteIdx := range remoteIndicesList { + var names, clusters, privileges []string + diags.Append(remoteIdx.Names.ElementsAs(ctx, &names, false)...) + diags.Append(remoteIdx.Clusters.ElementsAs(ctx, &clusters, false)...) + diags.Append(remoteIdx.Privileges.ElementsAs(ctx, &privileges, false)...) + if diags.HasError() { + return nil, diags + } + + newRemoteIndex := models.RemoteIndexPerms{ + Names: names, + Clusters: clusters, + Privileges: privileges, + } + + if !remoteIdx.Query.IsNull() { + query := remoteIdx.Query.ValueString() + newRemoteIndex.Query = &query + } + + // Field Security + if !remoteIdx.FieldSecurity.IsNull() { + var fieldSecList []FieldSecurityData + diags.Append(remoteIdx.FieldSecurity.ElementsAs(ctx, &fieldSecList, false)...) + if diags.HasError() { + return nil, diags + } + + if len(fieldSecList) > 0 { + fieldSec := fieldSecList[0] + remoteFieldSecurity := models.FieldSecurity{} + + if !fieldSec.Grant.IsNull() { + var grants []string + diags.Append(fieldSec.Grant.ElementsAs(ctx, &grants, false)...) + if diags.HasError() { + return nil, diags + } + remoteFieldSecurity.Grant = grants + } + + if !fieldSec.Except.IsNull() { + var excepts []string + diags.Append(fieldSec.Except.ElementsAs(ctx, &excepts, false)...) + if diags.HasError() { + return nil, diags + } + remoteFieldSecurity.Except = excepts + } + + newRemoteIndex.FieldSecurity = &remoteFieldSecurity + } + } + + remoteIndices[i] = newRemoteIndex + } + role.RemoteIndices = remoteIndices + } + + // Metadata + if !data.Metadata.IsNull() { + var metadata map[string]interface{} + if err := json.Unmarshal([]byte(data.Metadata.ValueString()), &metadata); err != nil { + diags.AddError("Invalid JSON", fmt.Sprintf("Error parsing metadata JSON: %s", err)) + return nil, diags + } + role.Metadata = metadata + } + + // Run As + if !data.RunAs.IsNull() { + var runAs []string + diags.Append(data.RunAs.ElementsAs(ctx, &runAs, false)...) + if diags.HasError() { + return nil, diags + } + role.RusAs = runAs + } + + return &role, diags +} + +// fromAPIModel converts the API model to the Terraform model +func (data *RoleData) fromAPIModel(ctx context.Context, role *models.Role) diag.Diagnostics { + var diags diag.Diagnostics + + data.Name = types.StringValue(role.Name) + + // Description + data.Description = types.StringPointerValue(role.Description) + + // Applications + if len(role.Applications) > 0 { + appElements := make([]attr.Value, len(role.Applications)) + for i, app := range role.Applications { + privSet, d := types.SetValueFrom(ctx, types.StringType, app.Privileges) + diags.Append(d...) + if diags.HasError() { + return diags + } + + resSet, d := types.SetValueFrom(ctx, types.StringType, app.Resources) + diags.Append(d...) + if diags.HasError() { + return diags + } + + appObj, d := types.ObjectValue(getApplicationAttrTypes(), map[string]attr.Value{ + "application": types.StringValue(app.Name), + "privileges": privSet, + "resources": resSet, + }) + diags.Append(d...) + if diags.HasError() { + return diags + } + + appElements[i] = appObj + } + + appSet, d := types.SetValue(types.ObjectType{AttrTypes: getApplicationAttrTypes()}, appElements) + diags.Append(d...) + if diags.HasError() { + return diags + } + data.Applications = appSet + } else { + data.Applications = types.SetNull(types.ObjectType{AttrTypes: getApplicationAttrTypes()}) + } + + // Cluster + if len(role.Cluster) > 0 { + clusterSet, d := types.SetValueFrom(ctx, types.StringType, role.Cluster) + diags.Append(d...) + if diags.HasError() { + return diags + } + data.Cluster = clusterSet + } else { + data.Cluster = types.SetNull(types.StringType) + } + + // Global + if role.Global != nil { + global, err := json.Marshal(role.Global) + if err != nil { + diags.AddError("JSON Marshal Error", fmt.Sprintf("Error marshaling global JSON: %s", err)) + return diags + } + data.Global = jsontypes.NewNormalizedValue(string(global)) + } else { + data.Global = jsontypes.NewNormalizedNull() + } + + // Indices + if len(role.Indices) > 0 { + indicesElements := make([]attr.Value, len(role.Indices)) + for i, index := range role.Indices { + namesSet, d := types.SetValueFrom(ctx, types.StringType, index.Names) + diags.Append(d...) + if diags.HasError() { + return diags + } + + privSet, d := types.SetValueFrom(ctx, types.StringType, index.Privileges) + diags.Append(d...) + if diags.HasError() { + return diags + } + + var queryVal jsontypes.Normalized + if index.Query != nil { + queryVal = jsontypes.NewNormalizedValue(*index.Query) + } else { + queryVal = jsontypes.NewNormalizedNull() + } + + allowRestrictedVal := types.BoolPointerValue(index.AllowRestrictedIndices) + + var fieldSecList types.List + if index.FieldSecurity != nil { + grantSet, d := types.SetValueFrom(ctx, types.StringType, index.FieldSecurity.Grant) + diags.Append(d...) + if diags.HasError() { + return diags + } + + exceptSet, d := types.SetValueFrom(ctx, types.StringType, index.FieldSecurity.Except) + diags.Append(d...) + if diags.HasError() { + return diags + } + + fieldSecObj, d := types.ObjectValue(getFieldSecurityAttrTypes(), map[string]attr.Value{ + "grant": grantSet, + "except": exceptSet, + }) + diags.Append(d...) + if diags.HasError() { + return diags + } + + fieldSecList, d = types.ListValue(types.ObjectType{AttrTypes: getFieldSecurityAttrTypes()}, []attr.Value{fieldSecObj}) + diags.Append(d...) + if diags.HasError() { + return diags + } + } else { + fieldSecList = types.ListNull(types.ObjectType{AttrTypes: getFieldSecurityAttrTypes()}) + } + + indexObj, d := types.ObjectValue(getIndexPermsAttrTypes(), map[string]attr.Value{ + "field_security": fieldSecList, + "names": namesSet, + "privileges": privSet, + "query": queryVal, + "allow_restricted_indices": allowRestrictedVal, + }) + diags.Append(d...) + if diags.HasError() { + return diags + } + + indicesElements[i] = indexObj + } + + indicesSet, d := types.SetValue(types.ObjectType{AttrTypes: getIndexPermsAttrTypes()}, indicesElements) + diags.Append(d...) + if diags.HasError() { + return diags + } + data.Indices = indicesSet + } else { + data.Indices = types.SetNull(types.ObjectType{AttrTypes: getIndexPermsAttrTypes()}) + } + + // Remote Indices + if len(role.RemoteIndices) > 0 { + remoteIndicesElements := make([]attr.Value, len(role.RemoteIndices)) + for i, remoteIndex := range role.RemoteIndices { + clustersSet, d := types.SetValueFrom(ctx, types.StringType, remoteIndex.Clusters) + diags.Append(d...) + if diags.HasError() { + return diags + } + + namesSet, d := types.SetValueFrom(ctx, types.StringType, remoteIndex.Names) + diags.Append(d...) + if diags.HasError() { + return diags + } + + privSet, d := types.SetValueFrom(ctx, types.StringType, remoteIndex.Privileges) + diags.Append(d...) + if diags.HasError() { + return diags + } + + var queryVal jsontypes.Normalized + if remoteIndex.Query != nil { + queryVal = jsontypes.NewNormalizedValue(*remoteIndex.Query) + } else { + queryVal = jsontypes.NewNormalizedNull() + } + + var fieldSecList types.List + if remoteIndex.FieldSecurity != nil { + grantSet, d := types.SetValueFrom(ctx, types.StringType, remoteIndex.FieldSecurity.Grant) + diags.Append(d...) + if diags.HasError() { + return diags + } + + exceptSet, d := types.SetValueFrom(ctx, types.StringType, remoteIndex.FieldSecurity.Except) + diags.Append(d...) + if diags.HasError() { + return diags + } + + fieldSecObj, d := types.ObjectValue(getFieldSecurityAttrTypes(), map[string]attr.Value{ + "grant": grantSet, + "except": exceptSet, + }) + diags.Append(d...) + if diags.HasError() { + return diags + } + + fieldSecList, d = types.ListValue(types.ObjectType{AttrTypes: getFieldSecurityAttrTypes()}, []attr.Value{fieldSecObj}) + diags.Append(d...) + if diags.HasError() { + return diags + } + } else { + fieldSecList = types.ListNull(types.ObjectType{AttrTypes: getFieldSecurityAttrTypes()}) + } + + remoteIndexObj, d := types.ObjectValue(getRemoteIndexPermsAttrTypes(), map[string]attr.Value{ + "clusters": clustersSet, + "field_security": fieldSecList, + "query": queryVal, + "names": namesSet, + "privileges": privSet, + }) + diags.Append(d...) + if diags.HasError() { + return diags + } + + remoteIndicesElements[i] = remoteIndexObj + } + + remoteIndicesSet, d := types.SetValue(types.ObjectType{AttrTypes: getRemoteIndexPermsAttrTypes()}, remoteIndicesElements) + diags.Append(d...) + if diags.HasError() { + return diags + } + data.RemoteIndices = remoteIndicesSet + } else { + data.RemoteIndices = types.SetNull(types.ObjectType{AttrTypes: getRemoteIndexPermsAttrTypes()}) + } + + // Metadata + if role.Metadata != nil { + metadata, err := json.Marshal(role.Metadata) + if err != nil { + diags.AddError("JSON Marshal Error", fmt.Sprintf("Error marshaling metadata JSON: %s", err)) + return diags + } + data.Metadata = jsontypes.NewNormalizedValue(string(metadata)) + } else { + data.Metadata = jsontypes.NewNormalizedNull() + } + + // Run As + if len(role.RusAs) > 0 { + runAsSet, d := types.SetValueFrom(ctx, types.StringType, role.RusAs) + diags.Append(d...) + if diags.HasError() { + return diags + } + data.RunAs = runAsSet + } else { + data.RunAs = types.SetNull(types.StringType) + } + + return diags +} diff --git a/internal/elasticsearch/security/role/read.go b/internal/elasticsearch/security/role/read.go index b7da99217..b913400a7 100644 --- a/internal/elasticsearch/security/role/read.go +++ b/internal/elasticsearch/security/role/read.go @@ -2,15 +2,12 @@ package role import ( "context" - "encoding/json" "fmt" "github.com/elastic/terraform-provider-elasticstack/internal/clients" "github.com/elastic/terraform-provider-elasticstack/internal/clients/elasticsearch" "github.com/elastic/terraform-provider-elasticstack/internal/diagutil" - "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" ) @@ -46,360 +43,10 @@ func (r *roleResource) Read(ctx context.Context, req resource.ReadRequest, resp return } - // Set the fields - data.Name = types.StringValue(roleId) - - // Set the description if it exists - if role.Description != nil { - data.Description = types.StringValue(*role.Description) - } else { - data.Description = types.StringNull() - } - - // Applications - if len(role.Applications) > 0 { - appElements := make([]attr.Value, len(role.Applications)) - for i, app := range role.Applications { - privSet, diags := types.SetValueFrom(ctx, types.StringType, app.Privileges) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - resSet, diags := types.SetValueFrom(ctx, types.StringType, app.Resources) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - appObj, diags := types.ObjectValue(map[string]attr.Type{ - "application": types.StringType, - "privileges": types.SetType{ElemType: types.StringType}, - "resources": types.SetType{ElemType: types.StringType}, - }, map[string]attr.Value{ - "application": types.StringValue(app.Name), - "privileges": privSet, - "resources": resSet, - }) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - appElements[i] = appObj - } - - appSet, diags := types.SetValue(types.ObjectType{ - AttrTypes: map[string]attr.Type{ - "application": types.StringType, - "privileges": types.SetType{ElemType: types.StringType}, - "resources": types.SetType{ElemType: types.StringType}, - }, - }, appElements) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - data.Applications = appSet - } else { - data.Applications = types.SetNull(types.ObjectType{ - AttrTypes: map[string]attr.Type{ - "application": types.StringType, - "privileges": types.SetType{ElemType: types.StringType}, - "resources": types.SetType{ElemType: types.StringType}, - }, - }) - } - - // Cluster - if len(role.Cluster) > 0 { - clusterSet, diags := types.SetValueFrom(ctx, types.StringType, role.Cluster) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - data.Cluster = clusterSet - } else { - data.Cluster = types.SetNull(types.StringType) - } - - // Global - if role.Global != nil { - global, err := json.Marshal(role.Global) - if err != nil { - resp.Diagnostics.AddError("JSON Marshal Error", fmt.Sprintf("Error marshaling global JSON: %s", err)) - return - } - data.Global = types.StringValue(string(global)) - } else { - data.Global = types.StringNull() - } - - // Indices - if len(role.Indices) > 0 { - indicesElements := make([]attr.Value, len(role.Indices)) - for i, index := range role.Indices { - namesSet, diags := types.SetValueFrom(ctx, types.StringType, index.Names) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - privSet, diags := types.SetValueFrom(ctx, types.StringType, index.Privileges) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - var queryVal types.String - if index.Query != nil { - queryVal = types.StringValue(*index.Query) - } else { - queryVal = types.StringNull() - } - - var allowRestrictedVal types.Bool - if index.AllowRestrictedIndices != nil { - allowRestrictedVal = types.BoolValue(*index.AllowRestrictedIndices) - } else { - allowRestrictedVal = types.BoolNull() - } - - var fieldSecList types.List - if index.FieldSecurity != nil { - grantSet, diags := types.SetValueFrom(ctx, types.StringType, index.FieldSecurity.Grant) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - exceptSet, diags := types.SetValueFrom(ctx, types.StringType, index.FieldSecurity.Except) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - fieldSecObj, diags := types.ObjectValue(map[string]attr.Type{ - "grant": types.SetType{ElemType: types.StringType}, - "except": types.SetType{ElemType: types.StringType}, - }, map[string]attr.Value{ - "grant": grantSet, - "except": exceptSet, - }) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - fieldSecList, diags = types.ListValue(types.ObjectType{ - AttrTypes: map[string]attr.Type{ - "grant": types.SetType{ElemType: types.StringType}, - "except": types.SetType{ElemType: types.StringType}, - }, - }, []attr.Value{fieldSecObj}) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - } else { - fieldSecList = types.ListNull(types.ObjectType{ - AttrTypes: map[string]attr.Type{ - "grant": types.SetType{ElemType: types.StringType}, - "except": types.SetType{ElemType: types.StringType}, - }, - }) - } - - indexObj, diags := types.ObjectValue(map[string]attr.Type{ - "field_security": types.ListType{ElemType: types.ObjectType{AttrTypes: map[string]attr.Type{"grant": types.SetType{ElemType: types.StringType}, "except": types.SetType{ElemType: types.StringType}}}}, - "names": types.SetType{ElemType: types.StringType}, - "privileges": types.SetType{ElemType: types.StringType}, - "query": types.StringType, - "allow_restricted_indices": types.BoolType, - }, map[string]attr.Value{ - "field_security": fieldSecList, - "names": namesSet, - "privileges": privSet, - "query": queryVal, - "allow_restricted_indices": allowRestrictedVal, - }) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - indicesElements[i] = indexObj - } - - indicesSet, diags := types.SetValue(types.ObjectType{ - AttrTypes: map[string]attr.Type{ - "field_security": types.ListType{ElemType: types.ObjectType{AttrTypes: map[string]attr.Type{"grant": types.SetType{ElemType: types.StringType}, "except": types.SetType{ElemType: types.StringType}}}}, - "names": types.SetType{ElemType: types.StringType}, - "privileges": types.SetType{ElemType: types.StringType}, - "query": types.StringType, - "allow_restricted_indices": types.BoolType, - }, - }, indicesElements) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - data.Indices = indicesSet - } else { - data.Indices = types.SetNull(types.ObjectType{ - AttrTypes: map[string]attr.Type{ - "field_security": types.ListType{ElemType: types.ObjectType{AttrTypes: map[string]attr.Type{"grant": types.SetType{ElemType: types.StringType}, "except": types.SetType{ElemType: types.StringType}}}}, - "names": types.SetType{ElemType: types.StringType}, - "privileges": types.SetType{ElemType: types.StringType}, - "query": types.StringType, - "allow_restricted_indices": types.BoolType, - }, - }) - } - - // Remote Indices - if len(role.RemoteIndices) > 0 { - remoteIndicesElements := make([]attr.Value, len(role.RemoteIndices)) - for i, remoteIndex := range role.RemoteIndices { - clustersSet, diags := types.SetValueFrom(ctx, types.StringType, remoteIndex.Clusters) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - namesSet, diags := types.SetValueFrom(ctx, types.StringType, remoteIndex.Names) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - privSet, diags := types.SetValueFrom(ctx, types.StringType, remoteIndex.Privileges) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - var queryVal types.String - if remoteIndex.Query != nil { - queryVal = types.StringValue(*remoteIndex.Query) - } else { - queryVal = types.StringNull() - } - - var fieldSecList types.List - if remoteIndex.FieldSecurity != nil { - grantSet, diags := types.SetValueFrom(ctx, types.StringType, remoteIndex.FieldSecurity.Grant) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - exceptSet, diags := types.SetValueFrom(ctx, types.StringType, remoteIndex.FieldSecurity.Except) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - fieldSecObj, diags := types.ObjectValue(map[string]attr.Type{ - "grant": types.SetType{ElemType: types.StringType}, - "except": types.SetType{ElemType: types.StringType}, - }, map[string]attr.Value{ - "grant": grantSet, - "except": exceptSet, - }) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - fieldSecList, diags = types.ListValue(types.ObjectType{ - AttrTypes: map[string]attr.Type{ - "grant": types.SetType{ElemType: types.StringType}, - "except": types.SetType{ElemType: types.StringType}, - }, - }, []attr.Value{fieldSecObj}) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - } else { - fieldSecList = types.ListNull(types.ObjectType{ - AttrTypes: map[string]attr.Type{ - "grant": types.SetType{ElemType: types.StringType}, - "except": types.SetType{ElemType: types.StringType}, - }, - }) - } - - remoteIndexObj, diags := types.ObjectValue(map[string]attr.Type{ - "clusters": types.SetType{ElemType: types.StringType}, - "field_security": types.ListType{ElemType: types.ObjectType{AttrTypes: map[string]attr.Type{"grant": types.SetType{ElemType: types.StringType}, "except": types.SetType{ElemType: types.StringType}}}}, - "query": types.StringType, - "names": types.SetType{ElemType: types.StringType}, - "privileges": types.SetType{ElemType: types.StringType}, - }, map[string]attr.Value{ - "clusters": clustersSet, - "field_security": fieldSecList, - "query": queryVal, - "names": namesSet, - "privileges": privSet, - }) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - remoteIndicesElements[i] = remoteIndexObj - } - - remoteIndicesSet, diags := types.SetValue(types.ObjectType{ - AttrTypes: map[string]attr.Type{ - "clusters": types.SetType{ElemType: types.StringType}, - "field_security": types.ListType{ElemType: types.ObjectType{AttrTypes: map[string]attr.Type{"grant": types.SetType{ElemType: types.StringType}, "except": types.SetType{ElemType: types.StringType}}}}, - "query": types.StringType, - "names": types.SetType{ElemType: types.StringType}, - "privileges": types.SetType{ElemType: types.StringType}, - }, - }, remoteIndicesElements) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - data.RemoteIndices = remoteIndicesSet - } else { - data.RemoteIndices = types.SetNull(types.ObjectType{ - AttrTypes: map[string]attr.Type{ - "clusters": types.SetType{ElemType: types.StringType}, - "field_security": types.ListType{ElemType: types.ObjectType{AttrTypes: map[string]attr.Type{"grant": types.SetType{ElemType: types.StringType}, "except": types.SetType{ElemType: types.StringType}}}}, - "query": types.StringType, - "names": types.SetType{ElemType: types.StringType}, - "privileges": types.SetType{ElemType: types.StringType}, - }, - }) - } - - // Metadata - if role.Metadata != nil { - metadata, err := json.Marshal(role.Metadata) - if err != nil { - resp.Diagnostics.AddError("JSON Marshal Error", fmt.Sprintf("Error marshaling metadata JSON: %s", err)) - return - } - data.Metadata = types.StringValue(string(metadata)) - } else { - data.Metadata = types.StringNull() - } - - // Run As - if len(role.RusAs) > 0 { - runAsSet, diags := types.SetValueFrom(ctx, types.StringType, role.RusAs) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - data.RunAs = runAsSet - } else { - data.RunAs = types.SetNull(types.StringType) + // Convert from API model + resp.Diagnostics.Append(data.fromAPIModel(ctx, role)...) + if resp.Diagnostics.HasError() { + return } resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) diff --git a/internal/elasticsearch/security/role/resource-description.md b/internal/elasticsearch/security/role/resource-description.md index de9f7bb00..9c1d8ca10 100644 --- a/internal/elasticsearch/security/role/resource-description.md +++ b/internal/elasticsearch/security/role/resource-description.md @@ -1 +1 @@ -Adds and updates roles in the native realm. See the [security API put role documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-put-role.html) for more details. \ No newline at end of file +Adds and updates roles in the native realm. See the [role API documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-put-role.html) for more details. \ No newline at end of file diff --git a/internal/elasticsearch/security/role/schema.go b/internal/elasticsearch/security/role/schema.go index 5d864129a..d7080db1d 100644 --- a/internal/elasticsearch/security/role/schema.go +++ b/internal/elasticsearch/security/role/schema.go @@ -4,13 +4,13 @@ import ( "context" _ "embed" - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" providerschema "github.com/elastic/terraform-provider-elasticstack/internal/schema" @@ -70,9 +70,7 @@ func GetSchema() schema.Schema { "global": schema.StringAttribute{ MarkdownDescription: "An object defining global privileges.", Optional: true, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - }, + CustomType: jsontypes.NormalizedType{}, }, "cluster": schema.SetAttribute{ MarkdownDescription: "A list of cluster privileges. These privileges define the cluster level actions that users with this role are able to execute.", @@ -115,6 +113,7 @@ func GetSchema() schema.Schema { "query": schema.StringAttribute{ MarkdownDescription: "A search query that defines the documents the owners of the role have read access to.", Optional: true, + CustomType: jsontypes.NormalizedType{}, }, "allow_restricted_indices": schema.BoolAttribute{ MarkdownDescription: "Include matching restricted indices in names parameter. Usage is strongly discouraged as it can grant unrestricted operations on critical data, make the entire system unstable or leak sensitive information.", @@ -156,6 +155,7 @@ func GetSchema() schema.Schema { "query": schema.StringAttribute{ MarkdownDescription: "A search query that defines the documents the owners of the role have read access to.", Optional: true, + CustomType: jsontypes.NormalizedType{}, }, "names": schema.SetAttribute{ MarkdownDescription: "A list of indices (or index name patterns) to which the permissions in this entry apply.", @@ -174,6 +174,7 @@ func GetSchema() schema.Schema { MarkdownDescription: "Optional meta-data.", Optional: true, Computed: true, + CustomType: jsontypes.NormalizedType{}, }, "run_as": schema.SetAttribute{ MarkdownDescription: "A list of users that the owners of this role can impersonate.", @@ -183,3 +184,20 @@ func GetSchema() schema.Schema { }, } } + +// Helper functions to get attribute types from schema +func getApplicationAttrTypes() map[string]attr.Type { + return GetSchema().Attributes["applications"].GetType().(attr.TypeWithElementType).ElementType().(attr.TypeWithAttributeTypes).AttributeTypes() +} + +func getIndexPermsAttrTypes() map[string]attr.Type { + return GetSchema().Attributes["indices"].GetType().(attr.TypeWithElementType).ElementType().(attr.TypeWithAttributeTypes).AttributeTypes() +} + +func getFieldSecurityAttrTypes() map[string]attr.Type { + return getIndexPermsAttrTypes()["field_security"].(attr.TypeWithElementType).ElementType().(attr.TypeWithAttributeTypes).AttributeTypes() +} + +func getRemoteIndexPermsAttrTypes() map[string]attr.Type { + return GetSchema().Attributes["remote_indices"].GetType().(attr.TypeWithElementType).ElementType().(attr.TypeWithAttributeTypes).AttributeTypes() +} diff --git a/internal/elasticsearch/security/role/update.go b/internal/elasticsearch/security/role/update.go index 299973e69..89ef1ec0e 100644 --- a/internal/elasticsearch/security/role/update.go +++ b/internal/elasticsearch/security/role/update.go @@ -2,14 +2,11 @@ package role import ( "context" - "encoding/json" "fmt" - "strings" "github.com/elastic/terraform-provider-elasticstack/internal/clients" "github.com/elastic/terraform-provider-elasticstack/internal/clients/elasticsearch" "github.com/elastic/terraform-provider-elasticstack/internal/diagutil" - "github.com/elastic/terraform-provider-elasticstack/internal/models" "github.com/elastic/terraform-provider-elasticstack/internal/utils" "github.com/hashicorp/go-version" "github.com/hashicorp/terraform-plugin-framework/diag" @@ -50,233 +47,31 @@ func (r *roleResource) update(ctx context.Context, plan tfsdk.Plan, state *tfsdk return diags } - var role models.Role - role.Name = roleId - - // Add description to the role - if utils.IsKnown(data.Description) && !data.Description.IsNull() { - // Return an error if the server version is less than the minimum supported version + // Check version requirements + if utils.IsKnown(data.Description) { if serverVersion.LessThan(MinSupportedDescriptionVersion) { diags.AddError("Unsupported Feature", fmt.Sprintf("'description' is supported only for Elasticsearch v%s and above", MinSupportedDescriptionVersion.String())) return diags } - - description := data.Description.ValueString() - role.Description = &description - } - - // Applications - if utils.IsKnown(data.Applications) && !data.Applications.IsNull() { - var applicationsList []ApplicationData - diags.Append(data.Applications.ElementsAs(ctx, &applicationsList, false)...) - if diags.HasError() { - return diags - } - - applications := make([]models.Application, len(applicationsList)) - for i, app := range applicationsList { - var privileges, resources []string - diags.Append(app.Privileges.ElementsAs(ctx, &privileges, false)...) - diags.Append(app.Resources.ElementsAs(ctx, &resources, false)...) - if diags.HasError() { - return diags - } - - applications[i] = models.Application{ - Name: app.Application.ValueString(), - Privileges: privileges, - Resources: resources, - } - } - role.Applications = applications - } - - // Global - if utils.IsKnown(data.Global) && !data.Global.IsNull() { - global := make(map[string]interface{}) - if err := json.NewDecoder(strings.NewReader(data.Global.ValueString())).Decode(&global); err != nil { - diags.AddError("Invalid JSON", fmt.Sprintf("Error parsing global JSON: %s", err)) - return diags - } - role.Global = global - } - - // Cluster - if utils.IsKnown(data.Cluster) && !data.Cluster.IsNull() { - var cluster []string - diags.Append(data.Cluster.ElementsAs(ctx, &cluster, false)...) - if diags.HasError() { - return diags - } - role.Cluster = cluster - } - - // Indices - if utils.IsKnown(data.Indices) && !data.Indices.IsNull() { - var indicesList []IndexPermsData - diags.Append(data.Indices.ElementsAs(ctx, &indicesList, false)...) - if diags.HasError() { - return diags - } - - indices := make([]models.IndexPerms, len(indicesList)) - for i, idx := range indicesList { - var names, privileges []string - diags.Append(idx.Names.ElementsAs(ctx, &names, false)...) - diags.Append(idx.Privileges.ElementsAs(ctx, &privileges, false)...) - if diags.HasError() { - return diags - } - - newIndex := models.IndexPerms{ - Names: names, - Privileges: privileges, - } - - if utils.IsKnown(idx.Query) && !idx.Query.IsNull() { - query := idx.Query.ValueString() - newIndex.Query = &query - } - - // Field Security - if utils.IsKnown(idx.FieldSecurity) && !idx.FieldSecurity.IsNull() { - var fieldSecList []FieldSecurityData - diags.Append(idx.FieldSecurity.ElementsAs(ctx, &fieldSecList, false)...) - if diags.HasError() { - return diags - } - - if len(fieldSecList) > 0 { - fieldSec := fieldSecList[0] - fieldSecurity := models.FieldSecurity{} - - if utils.IsKnown(fieldSec.Grant) && !fieldSec.Grant.IsNull() { - var grants []string - diags.Append(fieldSec.Grant.ElementsAs(ctx, &grants, false)...) - if diags.HasError() { - return diags - } - fieldSecurity.Grant = grants - } - - if utils.IsKnown(fieldSec.Except) && !fieldSec.Except.IsNull() { - var excepts []string - diags.Append(fieldSec.Except.ElementsAs(ctx, &excepts, false)...) - if diags.HasError() { - return diags - } - fieldSecurity.Except = excepts - } - - newIndex.FieldSecurity = &fieldSecurity - } - } - - if utils.IsKnown(idx.AllowRestrictedIndices) && !idx.AllowRestrictedIndices.IsNull() { - allowRestrictedIndices := idx.AllowRestrictedIndices.ValueBool() - newIndex.AllowRestrictedIndices = &allowRestrictedIndices - } - - indices[i] = newIndex - } - role.Indices = indices } - // Remote Indices - if utils.IsKnown(data.RemoteIndices) && !data.RemoteIndices.IsNull() { + if utils.IsKnown(data.RemoteIndices) { var remoteIndicesList []RemoteIndexPermsData diags.Append(data.RemoteIndices.ElementsAs(ctx, &remoteIndicesList, false)...) - if diags.HasError() { - return diags - } - if len(remoteIndicesList) > 0 && serverVersion.LessThan(MinSupportedRemoteIndicesVersion) { diags.AddError("Unsupported Feature", fmt.Sprintf("'remote_indices' is supported only for Elasticsearch v%s and above", MinSupportedRemoteIndicesVersion.String())) return diags } - - remoteIndices := make([]models.RemoteIndexPerms, len(remoteIndicesList)) - for i, remoteIdx := range remoteIndicesList { - var names, clusters, privileges []string - diags.Append(remoteIdx.Names.ElementsAs(ctx, &names, false)...) - diags.Append(remoteIdx.Clusters.ElementsAs(ctx, &clusters, false)...) - diags.Append(remoteIdx.Privileges.ElementsAs(ctx, &privileges, false)...) - if diags.HasError() { - return diags - } - - newRemoteIndex := models.RemoteIndexPerms{ - Names: names, - Clusters: clusters, - Privileges: privileges, - } - - if utils.IsKnown(remoteIdx.Query) && !remoteIdx.Query.IsNull() { - query := remoteIdx.Query.ValueString() - newRemoteIndex.Query = &query - } - - // Field Security - if utils.IsKnown(remoteIdx.FieldSecurity) && !remoteIdx.FieldSecurity.IsNull() { - var fieldSecList []FieldSecurityData - diags.Append(remoteIdx.FieldSecurity.ElementsAs(ctx, &fieldSecList, false)...) - if diags.HasError() { - return diags - } - - if len(fieldSecList) > 0 { - fieldSec := fieldSecList[0] - remoteFieldSecurity := models.FieldSecurity{} - - if utils.IsKnown(fieldSec.Grant) && !fieldSec.Grant.IsNull() { - var grants []string - diags.Append(fieldSec.Grant.ElementsAs(ctx, &grants, false)...) - if diags.HasError() { - return diags - } - remoteFieldSecurity.Grant = grants - } - - if utils.IsKnown(fieldSec.Except) && !fieldSec.Except.IsNull() { - var excepts []string - diags.Append(fieldSec.Except.ElementsAs(ctx, &excepts, false)...) - if diags.HasError() { - return diags - } - remoteFieldSecurity.Except = excepts - } - - newRemoteIndex.FieldSecurity = &remoteFieldSecurity - } - } - - remoteIndices[i] = newRemoteIndex - } - role.RemoteIndices = remoteIndices } - // Metadata - if utils.IsKnown(data.Metadata) && !data.Metadata.IsNull() { - metadata := make(map[string]interface{}) - if err := json.NewDecoder(strings.NewReader(data.Metadata.ValueString())).Decode(&metadata); err != nil { - diags.AddError("Invalid JSON", fmt.Sprintf("Error parsing metadata JSON: %s", err)) - return diags - } - role.Metadata = metadata - } - - // Run As - if utils.IsKnown(data.RunAs) && !data.RunAs.IsNull() { - var runAs []string - diags.Append(data.RunAs.ElementsAs(ctx, &runAs, false)...) - if diags.HasError() { - return diags - } - role.RusAs = runAs + // Convert to API model + role, diags := data.toAPIModel(ctx) + if diags.HasError() { + return diags } // Put the role - sdkDiags = elasticsearch.PutRole(ctx, client, &role) + sdkDiags = elasticsearch.PutRole(ctx, client, role) diags.Append(diagutil.FrameworkDiagsFromSDK(sdkDiags)...) if diags.HasError() { return diags @@ -290,7 +85,4 @@ func (r *roleResource) update(ctx context.Context, plan tfsdk.Plan, state *tfsdk func (r *roleResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { diags := r.update(ctx, req.Plan, &resp.State) resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } } From ac636455006f371c345cc5b4d6b4cde100a3d124 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Sep 2025 07:34:09 +0000 Subject: [PATCH 5/8] Fix schema to use nested blocks and run acceptance tests successfully Co-authored-by: tobio <444668+tobio@users.noreply.github.com> --- .../elasticsearch/security/role/models.go | 65 ++++++++++---- internal/elasticsearch/security/role/read.go | 4 + .../elasticsearch/security/role/schema.go | 90 +++++++------------ 3 files changed, 86 insertions(+), 73 deletions(-) diff --git a/internal/elasticsearch/security/role/models.go b/internal/elasticsearch/security/role/models.go index e6b8b1dd9..504bd5da0 100644 --- a/internal/elasticsearch/security/role/models.go +++ b/internal/elasticsearch/security/role/models.go @@ -278,6 +278,34 @@ func (data *RoleData) toAPIModel(ctx context.Context) (*models.Role, diag.Diagno func (data *RoleData) fromAPIModel(ctx context.Context, role *models.Role) diag.Diagnostics { var diags diag.Diagnostics + // Define attribute type maps + applicationAttrTypes := map[string]attr.Type{ + "application": types.StringType, + "privileges": types.SetType{ElemType: types.StringType}, + "resources": types.SetType{ElemType: types.StringType}, + } + + fieldSecurityAttrTypes := map[string]attr.Type{ + "grant": types.SetType{ElemType: types.StringType}, + "except": types.SetType{ElemType: types.StringType}, + } + + indexPermsAttrTypes := map[string]attr.Type{ + "field_security": types.ListType{ElemType: types.ObjectType{AttrTypes: fieldSecurityAttrTypes}}, + "names": types.SetType{ElemType: types.StringType}, + "privileges": types.SetType{ElemType: types.StringType}, + "query": jsontypes.NormalizedType{}, + "allow_restricted_indices": types.BoolType, + } + + remoteIndexPermsAttrTypes := map[string]attr.Type{ + "clusters": types.SetType{ElemType: types.StringType}, + "field_security": types.ListType{ElemType: types.ObjectType{AttrTypes: fieldSecurityAttrTypes}}, + "query": jsontypes.NormalizedType{}, + "names": types.SetType{ElemType: types.StringType}, + "privileges": types.SetType{ElemType: types.StringType}, + } + data.Name = types.StringValue(role.Name) // Description @@ -299,7 +327,7 @@ func (data *RoleData) fromAPIModel(ctx context.Context, role *models.Role) diag. return diags } - appObj, d := types.ObjectValue(getApplicationAttrTypes(), map[string]attr.Value{ + appObj, d := types.ObjectValue(applicationAttrTypes, map[string]attr.Value{ "application": types.StringValue(app.Name), "privileges": privSet, "resources": resSet, @@ -312,14 +340,14 @@ func (data *RoleData) fromAPIModel(ctx context.Context, role *models.Role) diag. appElements[i] = appObj } - appSet, d := types.SetValue(types.ObjectType{AttrTypes: getApplicationAttrTypes()}, appElements) + appSet, d := types.SetValue(types.ObjectType{AttrTypes: applicationAttrTypes}, appElements) diags.Append(d...) if diags.HasError() { return diags } data.Applications = appSet } else { - data.Applications = types.SetNull(types.ObjectType{AttrTypes: getApplicationAttrTypes()}) + data.Applications = types.SetNull(types.ObjectType{AttrTypes: applicationAttrTypes}) } // Cluster @@ -369,7 +397,12 @@ func (data *RoleData) fromAPIModel(ctx context.Context, role *models.Role) diag. queryVal = jsontypes.NewNormalizedNull() } - allowRestrictedVal := types.BoolPointerValue(index.AllowRestrictedIndices) + var allowRestrictedVal types.Bool + if index.AllowRestrictedIndices != nil { + allowRestrictedVal = types.BoolValue(*index.AllowRestrictedIndices) + } else { + allowRestrictedVal = types.BoolNull() + } var fieldSecList types.List if index.FieldSecurity != nil { @@ -385,7 +418,7 @@ func (data *RoleData) fromAPIModel(ctx context.Context, role *models.Role) diag. return diags } - fieldSecObj, d := types.ObjectValue(getFieldSecurityAttrTypes(), map[string]attr.Value{ + fieldSecObj, d := types.ObjectValue(fieldSecurityAttrTypes, map[string]attr.Value{ "grant": grantSet, "except": exceptSet, }) @@ -394,16 +427,16 @@ func (data *RoleData) fromAPIModel(ctx context.Context, role *models.Role) diag. return diags } - fieldSecList, d = types.ListValue(types.ObjectType{AttrTypes: getFieldSecurityAttrTypes()}, []attr.Value{fieldSecObj}) + fieldSecList, d = types.ListValue(types.ObjectType{AttrTypes: fieldSecurityAttrTypes}, []attr.Value{fieldSecObj}) diags.Append(d...) if diags.HasError() { return diags } } else { - fieldSecList = types.ListNull(types.ObjectType{AttrTypes: getFieldSecurityAttrTypes()}) + fieldSecList = types.ListNull(types.ObjectType{AttrTypes: fieldSecurityAttrTypes}) } - indexObj, d := types.ObjectValue(getIndexPermsAttrTypes(), map[string]attr.Value{ + indexObj, d := types.ObjectValue(indexPermsAttrTypes, map[string]attr.Value{ "field_security": fieldSecList, "names": namesSet, "privileges": privSet, @@ -418,14 +451,14 @@ func (data *RoleData) fromAPIModel(ctx context.Context, role *models.Role) diag. indicesElements[i] = indexObj } - indicesSet, d := types.SetValue(types.ObjectType{AttrTypes: getIndexPermsAttrTypes()}, indicesElements) + indicesSet, d := types.SetValue(types.ObjectType{AttrTypes: indexPermsAttrTypes}, indicesElements) diags.Append(d...) if diags.HasError() { return diags } data.Indices = indicesSet } else { - data.Indices = types.SetNull(types.ObjectType{AttrTypes: getIndexPermsAttrTypes()}) + data.Indices = types.SetNull(types.ObjectType{AttrTypes: indexPermsAttrTypes}) } // Remote Indices @@ -471,7 +504,7 @@ func (data *RoleData) fromAPIModel(ctx context.Context, role *models.Role) diag. return diags } - fieldSecObj, d := types.ObjectValue(getFieldSecurityAttrTypes(), map[string]attr.Value{ + fieldSecObj, d := types.ObjectValue(fieldSecurityAttrTypes, map[string]attr.Value{ "grant": grantSet, "except": exceptSet, }) @@ -480,16 +513,16 @@ func (data *RoleData) fromAPIModel(ctx context.Context, role *models.Role) diag. return diags } - fieldSecList, d = types.ListValue(types.ObjectType{AttrTypes: getFieldSecurityAttrTypes()}, []attr.Value{fieldSecObj}) + fieldSecList, d = types.ListValue(types.ObjectType{AttrTypes: fieldSecurityAttrTypes}, []attr.Value{fieldSecObj}) diags.Append(d...) if diags.HasError() { return diags } } else { - fieldSecList = types.ListNull(types.ObjectType{AttrTypes: getFieldSecurityAttrTypes()}) + fieldSecList = types.ListNull(types.ObjectType{AttrTypes: fieldSecurityAttrTypes}) } - remoteIndexObj, d := types.ObjectValue(getRemoteIndexPermsAttrTypes(), map[string]attr.Value{ + remoteIndexObj, d := types.ObjectValue(remoteIndexPermsAttrTypes, map[string]attr.Value{ "clusters": clustersSet, "field_security": fieldSecList, "query": queryVal, @@ -504,14 +537,14 @@ func (data *RoleData) fromAPIModel(ctx context.Context, role *models.Role) diag. remoteIndicesElements[i] = remoteIndexObj } - remoteIndicesSet, d := types.SetValue(types.ObjectType{AttrTypes: getRemoteIndexPermsAttrTypes()}, remoteIndicesElements) + remoteIndicesSet, d := types.SetValue(types.ObjectType{AttrTypes: remoteIndexPermsAttrTypes}, remoteIndicesElements) diags.Append(d...) if diags.HasError() { return diags } data.RemoteIndices = remoteIndicesSet } else { - data.RemoteIndices = types.SetNull(types.ObjectType{AttrTypes: getRemoteIndexPermsAttrTypes()}) + data.RemoteIndices = types.SetNull(types.ObjectType{AttrTypes: remoteIndexPermsAttrTypes}) } // Metadata diff --git a/internal/elasticsearch/security/role/read.go b/internal/elasticsearch/security/role/read.go index b913400a7..1d4655a76 100644 --- a/internal/elasticsearch/security/role/read.go +++ b/internal/elasticsearch/security/role/read.go @@ -8,6 +8,7 @@ import ( "github.com/elastic/terraform-provider-elasticstack/internal/clients/elasticsearch" "github.com/elastic/terraform-provider-elasticstack/internal/diagutil" "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" ) @@ -49,5 +50,8 @@ func (r *roleResource) Read(ctx context.Context, req resource.ReadRequest, resp return } + // Set the name to the roleId we extracted to ensure consistency + data.Name = types.StringValue(roleId) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } diff --git a/internal/elasticsearch/security/role/schema.go b/internal/elasticsearch/security/role/schema.go index d7080db1d..ae1e816eb 100644 --- a/internal/elasticsearch/security/role/schema.go +++ b/internal/elasticsearch/security/role/schema.go @@ -5,10 +5,8 @@ import ( _ "embed" "github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes" - "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" "github.com/hashicorp/terraform-plugin-framework/types" @@ -28,27 +26,9 @@ func GetSchema() schema.Schema { MarkdownDescription: roleResourceDescription, Blocks: map[string]schema.Block{ "elasticsearch_connection": providerschema.GetEsFWConnectionBlock("elasticsearch_connection", false), - }, - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - MarkdownDescription: "Internal identifier of the resource", - Computed: true, - }, - "name": schema.StringAttribute{ - MarkdownDescription: "The name of the role.", - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - "description": schema.StringAttribute{ - MarkdownDescription: "The description of the role.", - Optional: true, - }, - "applications": schema.SetNestedAttribute{ + "applications": schema.SetNestedBlock{ MarkdownDescription: "A list of application privilege entries.", - Optional: true, - NestedObject: schema.NestedAttributeObject{ + NestedObject: schema.NestedBlockObject{ Attributes: map[string]schema.Attribute{ "application": schema.StringAttribute{ MarkdownDescription: "The name of the application to which this entry applies.", @@ -67,20 +47,9 @@ func GetSchema() schema.Schema { }, }, }, - "global": schema.StringAttribute{ - MarkdownDescription: "An object defining global privileges.", - Optional: true, - CustomType: jsontypes.NormalizedType{}, - }, - "cluster": schema.SetAttribute{ - MarkdownDescription: "A list of cluster privileges. These privileges define the cluster level actions that users with this role are able to execute.", - Optional: true, - ElementType: types.StringType, - }, - "indices": schema.SetNestedAttribute{ + "indices": schema.SetNestedBlock{ MarkdownDescription: "A list of indices permissions entries.", - Optional: true, - NestedObject: schema.NestedAttributeObject{ + NestedObject: schema.NestedBlockObject{ Attributes: map[string]schema.Attribute{ "field_security": schema.ListNestedAttribute{ MarkdownDescription: "The document fields that the owners of the role have read access to.", @@ -118,16 +87,13 @@ func GetSchema() schema.Schema { "allow_restricted_indices": schema.BoolAttribute{ MarkdownDescription: "Include matching restricted indices in names parameter. Usage is strongly discouraged as it can grant unrestricted operations on critical data, make the entire system unstable or leak sensitive information.", Optional: true, - Computed: true, - Default: booldefault.StaticBool(false), }, }, }, }, - "remote_indices": schema.SetNestedAttribute{ + "remote_indices": schema.SetNestedBlock{ MarkdownDescription: "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.", - Optional: true, - NestedObject: schema.NestedAttributeObject{ + NestedObject: schema.NestedBlockObject{ Attributes: map[string]schema.Attribute{ "clusters": schema.SetAttribute{ MarkdownDescription: "A list of cluster aliases to which the permissions in this entry apply.", @@ -170,6 +136,33 @@ func GetSchema() schema.Schema { }, }, }, + }, + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + MarkdownDescription: "Internal identifier of the resource", + Computed: true, + }, + "name": schema.StringAttribute{ + MarkdownDescription: "The name of the role.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "description": schema.StringAttribute{ + MarkdownDescription: "The description of the role.", + Optional: true, + }, + "global": schema.StringAttribute{ + MarkdownDescription: "An object defining global privileges.", + Optional: true, + CustomType: jsontypes.NormalizedType{}, + }, + "cluster": schema.SetAttribute{ + MarkdownDescription: "A list of cluster privileges. These privileges define the cluster level actions that users with this role are able to execute.", + Optional: true, + ElementType: types.StringType, + }, "metadata": schema.StringAttribute{ MarkdownDescription: "Optional meta-data.", Optional: true, @@ -184,20 +177,3 @@ func GetSchema() schema.Schema { }, } } - -// Helper functions to get attribute types from schema -func getApplicationAttrTypes() map[string]attr.Type { - return GetSchema().Attributes["applications"].GetType().(attr.TypeWithElementType).ElementType().(attr.TypeWithAttributeTypes).AttributeTypes() -} - -func getIndexPermsAttrTypes() map[string]attr.Type { - return GetSchema().Attributes["indices"].GetType().(attr.TypeWithElementType).ElementType().(attr.TypeWithAttributeTypes).AttributeTypes() -} - -func getFieldSecurityAttrTypes() map[string]attr.Type { - return getIndexPermsAttrTypes()["field_security"].(attr.TypeWithElementType).ElementType().(attr.TypeWithAttributeTypes).AttributeTypes() -} - -func getRemoteIndexPermsAttrTypes() map[string]attr.Type { - return GetSchema().Attributes["remote_indices"].GetType().(attr.TypeWithElementType).ElementType().(attr.TypeWithAttributeTypes).AttributeTypes() -} From a0c63363e4e378bd0715eaddad61b674b8a8af17 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Sep 2025 11:24:23 +0000 Subject: [PATCH 6/8] Fix IsNull vs IsKnown checks and reorganize attribute type definitions Co-authored-by: tobio <444668+tobio@users.noreply.github.com> --- docs/resources/elasticsearch_security_role.md | 12 +-- .../elasticsearch/security/role/models.go | 92 ++++++++++--------- 2 files changed, 53 insertions(+), 51 deletions(-) diff --git a/docs/resources/elasticsearch_security_role.md b/docs/resources/elasticsearch_security_role.md index 94f27e05d..083181c3c 100644 --- a/docs/resources/elasticsearch_security_role.md +++ b/docs/resources/elasticsearch_security_role.md @@ -55,21 +55,21 @@ output "role" { ### Optional -- `applications` (Attributes Set) A list of application privilege entries. (see [below for nested schema](#nestedatt--applications)) +- `applications` (Block Set) A list of application privilege entries. (see [below for nested schema](#nestedblock--applications)) - `cluster` (Set of String) A list of cluster privileges. These privileges define the cluster level actions that users with this role are able to execute. - `description` (String) The description of the role. - `elasticsearch_connection` (Block List, Deprecated) Elasticsearch connection configuration block. (see [below for nested schema](#nestedblock--elasticsearch_connection)) - `global` (String) An object defining global privileges. -- `indices` (Attributes Set) A list of indices permissions entries. (see [below for nested schema](#nestedatt--indices)) +- `indices` (Block Set) A list of indices permissions entries. (see [below for nested schema](#nestedblock--indices)) - `metadata` (String) Optional meta-data. -- `remote_indices` (Attributes Set) 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. (see [below for nested schema](#nestedatt--remote_indices)) +- `remote_indices` (Block Set) 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. (see [below for nested schema](#nestedblock--remote_indices)) - `run_as` (Set of String) A list of users that the owners of this role can impersonate. ### Read-Only - `id` (String) Internal identifier of the resource - + ### Nested Schema for `applications` Required: @@ -100,7 +100,7 @@ Optional: - `username` (String) Username to use for API authentication to Elasticsearch. - + ### Nested Schema for `indices` Required: @@ -124,7 +124,7 @@ Optional: - + ### Nested Schema for `remote_indices` Required: diff --git a/internal/elasticsearch/security/role/models.go b/internal/elasticsearch/security/role/models.go index 504bd5da0..2b4ceb561 100644 --- a/internal/elasticsearch/security/role/models.go +++ b/internal/elasticsearch/security/role/models.go @@ -6,12 +6,42 @@ import ( "fmt" "github.com/elastic/terraform-provider-elasticstack/internal/models" + "github.com/elastic/terraform-provider-elasticstack/internal/utils" "github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/types" ) +var ( + applicationAttrTypes = map[string]attr.Type{ + "application": types.StringType, + "privileges": types.SetType{ElemType: types.StringType}, + "resources": types.SetType{ElemType: types.StringType}, + } + + fieldSecurityAttrTypes = map[string]attr.Type{ + "grant": types.SetType{ElemType: types.StringType}, + "except": types.SetType{ElemType: types.StringType}, + } + + indexPermsAttrTypes = map[string]attr.Type{ + "field_security": types.ListType{ElemType: types.ObjectType{AttrTypes: fieldSecurityAttrTypes}}, + "names": types.SetType{ElemType: types.StringType}, + "privileges": types.SetType{ElemType: types.StringType}, + "query": jsontypes.NormalizedType{}, + "allow_restricted_indices": types.BoolType, + } + + remoteIndexPermsAttrTypes = map[string]attr.Type{ + "clusters": types.SetType{ElemType: types.StringType}, + "field_security": types.ListType{ElemType: types.ObjectType{AttrTypes: fieldSecurityAttrTypes}}, + "query": jsontypes.NormalizedType{}, + "names": types.SetType{ElemType: types.StringType}, + "privileges": types.SetType{ElemType: types.StringType}, + } +) + type RoleData struct { Id types.String `tfsdk:"id"` ElasticsearchConnection types.List `tfsdk:"elasticsearch_connection"` @@ -61,13 +91,13 @@ func (data *RoleData) toAPIModel(ctx context.Context) (*models.Role, diag.Diagno role.Name = data.Name.ValueString() // Description - if !data.Description.IsNull() { + if utils.IsKnown(data.Description) { description := data.Description.ValueString() role.Description = &description } // Applications - if !data.Applications.IsNull() { + if utils.IsKnown(data.Applications) { var applicationsList []ApplicationData diags.Append(data.Applications.ElementsAs(ctx, &applicationsList, false)...) if diags.HasError() { @@ -93,7 +123,7 @@ func (data *RoleData) toAPIModel(ctx context.Context) (*models.Role, diag.Diagno } // Global - if !data.Global.IsNull() { + if utils.IsKnown(data.Global) { var global map[string]interface{} if err := json.Unmarshal([]byte(data.Global.ValueString()), &global); err != nil { diags.AddError("Invalid JSON", fmt.Sprintf("Error parsing global JSON: %s", err)) @@ -103,7 +133,7 @@ func (data *RoleData) toAPIModel(ctx context.Context) (*models.Role, diag.Diagno } // Cluster - if !data.Cluster.IsNull() { + if utils.IsKnown(data.Cluster) { var cluster []string diags.Append(data.Cluster.ElementsAs(ctx, &cluster, false)...) if diags.HasError() { @@ -113,7 +143,7 @@ func (data *RoleData) toAPIModel(ctx context.Context) (*models.Role, diag.Diagno } // Indices - if !data.Indices.IsNull() { + if utils.IsKnown(data.Indices) { var indicesList []IndexPermsData diags.Append(data.Indices.ElementsAs(ctx, &indicesList, false)...) if diags.HasError() { @@ -134,13 +164,13 @@ func (data *RoleData) toAPIModel(ctx context.Context) (*models.Role, diag.Diagno Privileges: privileges, } - if !idx.Query.IsNull() { + if utils.IsKnown(idx.Query) { query := idx.Query.ValueString() newIndex.Query = &query } // Field Security - if !idx.FieldSecurity.IsNull() { + if utils.IsKnown(idx.FieldSecurity) { var fieldSecList []FieldSecurityData diags.Append(idx.FieldSecurity.ElementsAs(ctx, &fieldSecList, false)...) if diags.HasError() { @@ -151,7 +181,7 @@ func (data *RoleData) toAPIModel(ctx context.Context) (*models.Role, diag.Diagno fieldSec := fieldSecList[0] fieldSecurity := models.FieldSecurity{} - if !fieldSec.Grant.IsNull() { + if utils.IsKnown(fieldSec.Grant) { var grants []string diags.Append(fieldSec.Grant.ElementsAs(ctx, &grants, false)...) if diags.HasError() { @@ -160,7 +190,7 @@ func (data *RoleData) toAPIModel(ctx context.Context) (*models.Role, diag.Diagno fieldSecurity.Grant = grants } - if !fieldSec.Except.IsNull() { + if utils.IsKnown(fieldSec.Except) { var excepts []string diags.Append(fieldSec.Except.ElementsAs(ctx, &excepts, false)...) if diags.HasError() { @@ -173,7 +203,7 @@ func (data *RoleData) toAPIModel(ctx context.Context) (*models.Role, diag.Diagno } } - if !idx.AllowRestrictedIndices.IsNull() { + if utils.IsKnown(idx.AllowRestrictedIndices) { allowRestrictedIndices := idx.AllowRestrictedIndices.ValueBool() newIndex.AllowRestrictedIndices = &allowRestrictedIndices } @@ -184,7 +214,7 @@ func (data *RoleData) toAPIModel(ctx context.Context) (*models.Role, diag.Diagno } // Remote Indices - if !data.RemoteIndices.IsNull() { + if utils.IsKnown(data.RemoteIndices) { var remoteIndicesList []RemoteIndexPermsData diags.Append(data.RemoteIndices.ElementsAs(ctx, &remoteIndicesList, false)...) if diags.HasError() { @@ -207,13 +237,13 @@ func (data *RoleData) toAPIModel(ctx context.Context) (*models.Role, diag.Diagno Privileges: privileges, } - if !remoteIdx.Query.IsNull() { + if utils.IsKnown(remoteIdx.Query) { query := remoteIdx.Query.ValueString() newRemoteIndex.Query = &query } // Field Security - if !remoteIdx.FieldSecurity.IsNull() { + if utils.IsKnown(remoteIdx.FieldSecurity) { var fieldSecList []FieldSecurityData diags.Append(remoteIdx.FieldSecurity.ElementsAs(ctx, &fieldSecList, false)...) if diags.HasError() { @@ -224,7 +254,7 @@ func (data *RoleData) toAPIModel(ctx context.Context) (*models.Role, diag.Diagno fieldSec := fieldSecList[0] remoteFieldSecurity := models.FieldSecurity{} - if !fieldSec.Grant.IsNull() { + if utils.IsKnown(fieldSec.Grant) { var grants []string diags.Append(fieldSec.Grant.ElementsAs(ctx, &grants, false)...) if diags.HasError() { @@ -233,7 +263,7 @@ func (data *RoleData) toAPIModel(ctx context.Context) (*models.Role, diag.Diagno remoteFieldSecurity.Grant = grants } - if !fieldSec.Except.IsNull() { + if utils.IsKnown(fieldSec.Except) { var excepts []string diags.Append(fieldSec.Except.ElementsAs(ctx, &excepts, false)...) if diags.HasError() { @@ -252,7 +282,7 @@ func (data *RoleData) toAPIModel(ctx context.Context) (*models.Role, diag.Diagno } // Metadata - if !data.Metadata.IsNull() { + if utils.IsKnown(data.Metadata) { var metadata map[string]interface{} if err := json.Unmarshal([]byte(data.Metadata.ValueString()), &metadata); err != nil { diags.AddError("Invalid JSON", fmt.Sprintf("Error parsing metadata JSON: %s", err)) @@ -262,7 +292,7 @@ func (data *RoleData) toAPIModel(ctx context.Context) (*models.Role, diag.Diagno } // Run As - if !data.RunAs.IsNull() { + if utils.IsKnown(data.RunAs) { var runAs []string diags.Append(data.RunAs.ElementsAs(ctx, &runAs, false)...) if diags.HasError() { @@ -278,34 +308,6 @@ func (data *RoleData) toAPIModel(ctx context.Context) (*models.Role, diag.Diagno func (data *RoleData) fromAPIModel(ctx context.Context, role *models.Role) diag.Diagnostics { var diags diag.Diagnostics - // Define attribute type maps - applicationAttrTypes := map[string]attr.Type{ - "application": types.StringType, - "privileges": types.SetType{ElemType: types.StringType}, - "resources": types.SetType{ElemType: types.StringType}, - } - - fieldSecurityAttrTypes := map[string]attr.Type{ - "grant": types.SetType{ElemType: types.StringType}, - "except": types.SetType{ElemType: types.StringType}, - } - - indexPermsAttrTypes := map[string]attr.Type{ - "field_security": types.ListType{ElemType: types.ObjectType{AttrTypes: fieldSecurityAttrTypes}}, - "names": types.SetType{ElemType: types.StringType}, - "privileges": types.SetType{ElemType: types.StringType}, - "query": jsontypes.NormalizedType{}, - "allow_restricted_indices": types.BoolType, - } - - remoteIndexPermsAttrTypes := map[string]attr.Type{ - "clusters": types.SetType{ElemType: types.StringType}, - "field_security": types.ListType{ElemType: types.ObjectType{AttrTypes: fieldSecurityAttrTypes}}, - "query": jsontypes.NormalizedType{}, - "names": types.SetType{ElemType: types.StringType}, - "privileges": types.SetType{ElemType: types.StringType}, - } - data.Name = types.StringValue(role.Name) // Description From 78a783724b73f5bab955b515e5ff28e4798af6d2 Mon Sep 17 00:00:00 2001 From: Toby Brain Date: Mon, 29 Sep 2025 21:31:23 +1000 Subject: [PATCH 7/8] Fix tests --- docs/resources/elasticsearch_security_role.md | 4 +- .../elasticsearch/security/role/acc_test.go | 2 - .../elasticsearch/security/role/models.go | 63 +--- internal/elasticsearch/security/role/read.go | 43 ++- .../elasticsearch/security/role/resource.go | 71 ++++ .../security/role/resource_test.go | 348 ++++++++++++++++++ .../elasticsearch/security/role/schema.go | 96 ++++- .../elasticsearch/security/role/update.go | 13 +- 8 files changed, 564 insertions(+), 76 deletions(-) create mode 100644 internal/elasticsearch/security/role/resource_test.go diff --git a/docs/resources/elasticsearch_security_role.md b/docs/resources/elasticsearch_security_role.md index 083181c3c..bd5f83ea9 100644 --- a/docs/resources/elasticsearch_security_role.md +++ b/docs/resources/elasticsearch_security_role.md @@ -135,10 +135,10 @@ Required: Optional: -- `field_security` (Attributes List) The document fields that the owners of the role have read access to. (see [below for nested schema](#nestedatt--remote_indices--field_security)) +- `field_security` (Block List) The document fields that the owners of the role have read access to. (see [below for nested schema](#nestedblock--remote_indices--field_security)) - `query` (String) A search query that defines the documents the owners of the role have read access to. - + ### Nested Schema for `remote_indices.field_security` Optional: diff --git a/internal/elasticsearch/security/role/acc_test.go b/internal/elasticsearch/security/role/acc_test.go index 6d47ca776..3a4553dd5 100644 --- a/internal/elasticsearch/security/role/acc_test.go +++ b/internal/elasticsearch/security/role/acc_test.go @@ -46,7 +46,6 @@ func TestAccResourceSecurityRole(t *testing.T) { resource.TestCheckNoResourceAttr("elasticstack_elasticsearch_security_role.test", "run_as.#"), resource.TestCheckNoResourceAttr("elasticstack_elasticsearch_security_role.test", "global"), resource.TestCheckNoResourceAttr("elasticstack_elasticsearch_security_role.test", "applications.#"), - resource.TestCheckNoResourceAttr("elasticstack_elasticsearch_security_role.test", "indices.0.allow_restricted_indices"), ), }, { @@ -75,7 +74,6 @@ func TestAccResourceSecurityRole(t *testing.T) { resource.TestCheckNoResourceAttr("elasticstack_elasticsearch_security_role.test", "run_as.#"), resource.TestCheckNoResourceAttr("elasticstack_elasticsearch_security_role.test", "global"), resource.TestCheckNoResourceAttr("elasticstack_elasticsearch_security_role.test", "applications.#"), - resource.TestCheckNoResourceAttr("elasticstack_elasticsearch_security_role.test", "indices.0.allow_restricted_indices"), resource.TestCheckTypeSetElemAttr("elasticstack_elasticsearch_security_role.test", "remote_indices.*.clusters.*", "test-cluster2"), resource.TestCheckTypeSetElemAttr("elasticstack_elasticsearch_security_role.test", "remote_indices.*.names.*", "sample2"), ), diff --git a/internal/elasticsearch/security/role/models.go b/internal/elasticsearch/security/role/models.go index 2b4ceb561..04b4fbc5c 100644 --- a/internal/elasticsearch/security/role/models.go +++ b/internal/elasticsearch/security/role/models.go @@ -13,35 +13,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" ) -var ( - applicationAttrTypes = map[string]attr.Type{ - "application": types.StringType, - "privileges": types.SetType{ElemType: types.StringType}, - "resources": types.SetType{ElemType: types.StringType}, - } - - fieldSecurityAttrTypes = map[string]attr.Type{ - "grant": types.SetType{ElemType: types.StringType}, - "except": types.SetType{ElemType: types.StringType}, - } - - indexPermsAttrTypes = map[string]attr.Type{ - "field_security": types.ListType{ElemType: types.ObjectType{AttrTypes: fieldSecurityAttrTypes}}, - "names": types.SetType{ElemType: types.StringType}, - "privileges": types.SetType{ElemType: types.StringType}, - "query": jsontypes.NormalizedType{}, - "allow_restricted_indices": types.BoolType, - } - - remoteIndexPermsAttrTypes = map[string]attr.Type{ - "clusters": types.SetType{ElemType: types.StringType}, - "field_security": types.ListType{ElemType: types.ObjectType{AttrTypes: fieldSecurityAttrTypes}}, - "query": jsontypes.NormalizedType{}, - "names": types.SetType{ElemType: types.StringType}, - "privileges": types.SetType{ElemType: types.StringType}, - } -) - type RoleData struct { Id types.String `tfsdk:"id"` ElasticsearchConnection types.List `tfsdk:"elasticsearch_connection"` @@ -329,7 +300,7 @@ func (data *RoleData) fromAPIModel(ctx context.Context, role *models.Role) diag. return diags } - appObj, d := types.ObjectValue(applicationAttrTypes, map[string]attr.Value{ + appObj, d := types.ObjectValue(getApplicationAttrTypes(), map[string]attr.Value{ "application": types.StringValue(app.Name), "privileges": privSet, "resources": resSet, @@ -342,14 +313,14 @@ func (data *RoleData) fromAPIModel(ctx context.Context, role *models.Role) diag. appElements[i] = appObj } - appSet, d := types.SetValue(types.ObjectType{AttrTypes: applicationAttrTypes}, appElements) + appSet, d := types.SetValue(types.ObjectType{AttrTypes: getApplicationAttrTypes()}, appElements) diags.Append(d...) if diags.HasError() { return diags } data.Applications = appSet } else { - data.Applications = types.SetNull(types.ObjectType{AttrTypes: applicationAttrTypes}) + data.Applications = types.SetNull(types.ObjectType{AttrTypes: getApplicationAttrTypes()}) } // Cluster @@ -420,7 +391,7 @@ func (data *RoleData) fromAPIModel(ctx context.Context, role *models.Role) diag. return diags } - fieldSecObj, d := types.ObjectValue(fieldSecurityAttrTypes, map[string]attr.Value{ + fieldSecObj, d := types.ObjectValue(getFieldSecurityAttrTypes(), map[string]attr.Value{ "grant": grantSet, "except": exceptSet, }) @@ -429,16 +400,16 @@ func (data *RoleData) fromAPIModel(ctx context.Context, role *models.Role) diag. return diags } - fieldSecList, d = types.ListValue(types.ObjectType{AttrTypes: fieldSecurityAttrTypes}, []attr.Value{fieldSecObj}) + fieldSecList, d = types.ListValue(types.ObjectType{AttrTypes: getFieldSecurityAttrTypes()}, []attr.Value{fieldSecObj}) diags.Append(d...) if diags.HasError() { return diags } } else { - fieldSecList = types.ListNull(types.ObjectType{AttrTypes: fieldSecurityAttrTypes}) + fieldSecList = types.ListNull(types.ObjectType{AttrTypes: getFieldSecurityAttrTypes()}) } - indexObj, d := types.ObjectValue(indexPermsAttrTypes, map[string]attr.Value{ + indexObj, d := types.ObjectValue(getIndexPermsAttrTypes(), map[string]attr.Value{ "field_security": fieldSecList, "names": namesSet, "privileges": privSet, @@ -453,14 +424,14 @@ func (data *RoleData) fromAPIModel(ctx context.Context, role *models.Role) diag. indicesElements[i] = indexObj } - indicesSet, d := types.SetValue(types.ObjectType{AttrTypes: indexPermsAttrTypes}, indicesElements) + indicesSet, d := types.SetValue(types.ObjectType{AttrTypes: getIndexPermsAttrTypes()}, indicesElements) diags.Append(d...) if diags.HasError() { return diags } data.Indices = indicesSet } else { - data.Indices = types.SetNull(types.ObjectType{AttrTypes: indexPermsAttrTypes}) + data.Indices = types.SetNull(types.ObjectType{AttrTypes: getIndexPermsAttrTypes()}) } // Remote Indices @@ -494,19 +465,19 @@ func (data *RoleData) fromAPIModel(ctx context.Context, role *models.Role) diag. var fieldSecList types.List if remoteIndex.FieldSecurity != nil { - grantSet, d := types.SetValueFrom(ctx, types.StringType, remoteIndex.FieldSecurity.Grant) + grantSet, d := types.SetValueFrom(ctx, types.StringType, utils.NonNilSlice(remoteIndex.FieldSecurity.Grant)) diags.Append(d...) if diags.HasError() { return diags } - exceptSet, d := types.SetValueFrom(ctx, types.StringType, remoteIndex.FieldSecurity.Except) + exceptSet, d := types.SetValueFrom(ctx, types.StringType, utils.NonNilSlice(remoteIndex.FieldSecurity.Except)) diags.Append(d...) if diags.HasError() { return diags } - fieldSecObj, d := types.ObjectValue(fieldSecurityAttrTypes, map[string]attr.Value{ + fieldSecObj, d := types.ObjectValue(getRemoteFieldSecurityAttrTypes(), map[string]attr.Value{ "grant": grantSet, "except": exceptSet, }) @@ -515,16 +486,16 @@ func (data *RoleData) fromAPIModel(ctx context.Context, role *models.Role) diag. return diags } - fieldSecList, d = types.ListValue(types.ObjectType{AttrTypes: fieldSecurityAttrTypes}, []attr.Value{fieldSecObj}) + fieldSecList, d = types.ListValue(types.ObjectType{AttrTypes: getRemoteFieldSecurityAttrTypes()}, []attr.Value{fieldSecObj}) diags.Append(d...) if diags.HasError() { return diags } } else { - fieldSecList = types.ListNull(types.ObjectType{AttrTypes: fieldSecurityAttrTypes}) + fieldSecList = types.ListNull(types.ObjectType{AttrTypes: getRemoteFieldSecurityAttrTypes()}) } - remoteIndexObj, d := types.ObjectValue(remoteIndexPermsAttrTypes, map[string]attr.Value{ + remoteIndexObj, d := types.ObjectValue(getRemoteIndexPermsAttrTypes(), map[string]attr.Value{ "clusters": clustersSet, "field_security": fieldSecList, "query": queryVal, @@ -539,14 +510,14 @@ func (data *RoleData) fromAPIModel(ctx context.Context, role *models.Role) diag. remoteIndicesElements[i] = remoteIndexObj } - remoteIndicesSet, d := types.SetValue(types.ObjectType{AttrTypes: remoteIndexPermsAttrTypes}, remoteIndicesElements) + remoteIndicesSet, d := types.SetValue(types.ObjectType{AttrTypes: getRemoteIndexPermsAttrTypes()}, remoteIndicesElements) diags.Append(d...) if diags.HasError() { return diags } data.RemoteIndices = remoteIndicesSet } else { - data.RemoteIndices = types.SetNull(types.ObjectType{AttrTypes: remoteIndexPermsAttrTypes}) + data.RemoteIndices = types.SetNull(types.ObjectType{AttrTypes: getRemoteIndexPermsAttrTypes()}) } // Metadata diff --git a/internal/elasticsearch/security/role/read.go b/internal/elasticsearch/security/role/read.go index 1d4655a76..ecae5f723 100644 --- a/internal/elasticsearch/security/role/read.go +++ b/internal/elasticsearch/security/role/read.go @@ -7,6 +7,7 @@ import ( "github.com/elastic/terraform-provider-elasticstack/internal/clients" "github.com/elastic/terraform-provider-elasticstack/internal/clients/elasticsearch" "github.com/elastic/terraform-provider-elasticstack/internal/diagutil" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" @@ -19,39 +20,53 @@ func (r *roleResource) Read(ctx context.Context, req resource.ReadRequest, resp return } - compId, diags := clients.CompositeIdFromStrFw(data.Id.ValueString()) + readData, diags := r.read(ctx, data) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } + + if readData == nil { + resp.State.RemoveResource(ctx) + return + } + + resp.Diagnostics.Append(req.State.Set(ctx, readData)...) +} + +func (r *roleResource) read(ctx context.Context, data RoleData) (*RoleData, diag.Diagnostics) { + compId, diags := clients.CompositeIdFromStrFw(data.Id.ValueString()) + diags.Append(diags...) + if diags.HasError() { + return nil, diags + } roleId := compId.ResourceId client, diags := clients.MaybeNewApiClientFromFrameworkResource(ctx, data.ElasticsearchConnection, r.client) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return + diags.Append(diags...) + if diags.HasError() { + return nil, diags } role, sdkDiags := elasticsearch.GetRole(ctx, client, roleId) - resp.Diagnostics.Append(diagutil.FrameworkDiagsFromSDK(sdkDiags)...) - if resp.Diagnostics.HasError() { - return + diags.Append(diagutil.FrameworkDiagsFromSDK(sdkDiags)...) + if diags.HasError() { + return nil, diags } if role == nil { - tflog.Warn(ctx, fmt.Sprintf(`Role "%s" not found, removing from state`, roleId)) - resp.State.RemoveResource(ctx) - return + tflog.Warn(ctx, fmt.Sprintf(`Role "%s" not found`, roleId)) + return nil, diags } // Convert from API model - resp.Diagnostics.Append(data.fromAPIModel(ctx, role)...) - if resp.Diagnostics.HasError() { - return + diags.Append(data.fromAPIModel(ctx, role)...) + if diags.HasError() { + return nil, diags } // Set the name to the roleId we extracted to ensure consistency data.Name = types.StringValue(roleId) - resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + return &data, diags } diff --git a/internal/elasticsearch/security/role/resource.go b/internal/elasticsearch/security/role/resource.go index fc078b4ac..79543f336 100644 --- a/internal/elasticsearch/security/role/resource.go +++ b/internal/elasticsearch/security/role/resource.go @@ -2,16 +2,20 @@ package role import ( "context" + "encoding/json" "github.com/elastic/terraform-provider-elasticstack/internal/clients" + "github.com/elastic/terraform-provider-elasticstack/internal/utils" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" ) // Ensure provider defined types fully satisfy framework interfaces var _ resource.Resource = &roleResource{} var _ resource.ResourceWithConfigure = &roleResource{} var _ resource.ResourceWithImportState = &roleResource{} +var _ resource.ResourceWithUpgradeState = &roleResource{} func NewRoleResource() resource.Resource { return &roleResource{} @@ -34,3 +38,70 @@ func (r *roleResource) Configure(_ context.Context, req resource.ConfigureReques func (r *roleResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) } + +func (r *roleResource) UpgradeState(_ context.Context) map[int64]resource.StateUpgrader { + return map[int64]resource.StateUpgrader{ + 0: { + PriorSchema: utils.Pointer(GetSchema(0)), + StateUpgrader: v0ToV1, + }, + } +} + +func v0ToV1(ctx context.Context, req resource.UpgradeStateRequest, resp *resource.UpgradeStateResponse) { + var priorState map[string]interface{} + err := json.Unmarshal(req.RawState.JSON, &priorState) + if err != nil { + resp.Diagnostics.AddError("State Upgrade Error", "Could not unmarshal prior state: "+err.Error()) + return + } + + if priorState["global"] == "" { + delete(priorState, "global") + } + + if priorState["metadata"] == "" { + delete(priorState, "metadata") + } + + indices, ok := priorState["indices"] + if ok { + indicesSlice, ok := indices.([]interface{}) + if ok { + for i, index := range indicesSlice { + indexMap, ok := index.(map[string]interface{}) + if ok { + if indexMap["query"] == "" { + delete(indexMap, "query") + } + indicesSlice[i] = indexMap + } + } + } + } + + remoteIndices, ok := priorState["remote_indices"] + if ok { + remoteIndicesSlice, ok := remoteIndices.([]interface{}) + if ok { + for i, remoteIndex := range remoteIndicesSlice { + remoteIndexMap, ok := remoteIndex.(map[string]interface{}) + if ok { + if remoteIndexMap["query"] == "" { + delete(remoteIndexMap, "query") + } + remoteIndicesSlice[i] = remoteIndexMap + } + } + } + } + + stateJSON, err := json.Marshal(priorState) + if err != nil { + resp.Diagnostics.AddError("State Upgrade Error", "Could not marshal new state: "+err.Error()) + return + } + resp.DynamicValue = &tfprotov6.DynamicValue{ + JSON: stateJSON, + } +} diff --git a/internal/elasticsearch/security/role/resource_test.go b/internal/elasticsearch/security/role/resource_test.go new file mode 100644 index 000000000..d1d52a141 --- /dev/null +++ b/internal/elasticsearch/security/role/resource_test.go @@ -0,0 +1,348 @@ +package role + +import ( + "context" + "encoding/json" + "testing" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestV0ToV1(t *testing.T) { + tests := []struct { + name string + input map[string]interface{} + expected map[string]interface{} + expectError bool + errorContains string + }{ + { + name: "empty_global_and_metadata_removed", + input: map[string]interface{}{ + "name": "test-role", + "description": "Test role", + "global": "", + "metadata": "", + "cluster": []string{"all"}, + }, + expected: map[string]interface{}{ + "name": "test-role", + "description": "Test role", + "cluster": []interface{}{"all"}, + }, + }, + { + name: "non_empty_global_and_metadata_preserved", + input: map[string]interface{}{ + "name": "test-role", + "description": "Test role", + "global": `{"profile": {"privileges": ["manage"]}}`, + "metadata": `{"version": 1}`, + "cluster": []string{"all"}, + }, + expected: map[string]interface{}{ + "name": "test-role", + "description": "Test role", + "global": `{"profile": {"privileges": ["manage"]}}`, + "metadata": `{"version": 1}`, + "cluster": []interface{}{"all"}, + }, + }, + { + name: "empty_query_in_indices_removed", + input: map[string]interface{}{ + "name": "test-role", + "description": "Test role", + "indices": []interface{}{ + map[string]interface{}{ + "names": []string{"index1", "index2"}, + "privileges": []string{"read"}, + "query": "", + }, + map[string]interface{}{ + "names": []string{"index3"}, + "privileges": []string{"write"}, + "query": `{"match": {"field": "value"}}`, + }, + }, + }, + expected: map[string]interface{}{ + "name": "test-role", + "description": "Test role", + "indices": []interface{}{ + map[string]interface{}{ + "names": []interface{}{"index1", "index2"}, + "privileges": []interface{}{"read"}, + }, + map[string]interface{}{ + "names": []interface{}{"index3"}, + "privileges": []interface{}{"write"}, + "query": `{"match": {"field": "value"}}`, + }, + }, + }, + }, + { + name: "empty_query_in_remote_indices_removed", + input: map[string]interface{}{ + "name": "test-role", + "description": "Test role", + "remote_indices": []interface{}{ + map[string]interface{}{ + "clusters": []string{"cluster1"}, + "names": []string{"remote-index1"}, + "privileges": []string{"read"}, + "query": "", + }, + map[string]interface{}{ + "clusters": []string{"cluster2"}, + "names": []string{"remote-index2"}, + "privileges": []string{"write"}, + "query": `{"term": {"status": "active"}}`, + }, + }, + }, + expected: map[string]interface{}{ + "name": "test-role", + "description": "Test role", + "remote_indices": []interface{}{ + map[string]interface{}{ + "clusters": []interface{}{"cluster1"}, + "names": []interface{}{"remote-index1"}, + "privileges": []interface{}{"read"}, + }, + map[string]interface{}{ + "clusters": []interface{}{"cluster2"}, + "names": []interface{}{"remote-index2"}, + "privileges": []interface{}{"write"}, + "query": `{"term": {"status": "active"}}`, + }, + }, + }, + }, + { + name: "all_empty_fields_removed_comprehensive", + input: map[string]interface{}{ + "name": "test-role", + "description": "Test role", + "global": "", + "metadata": "", + "cluster": []string{"all"}, + "indices": []interface{}{ + map[string]interface{}{ + "names": []string{"index1"}, + "privileges": []string{"read"}, + "query": "", + }, + }, + "remote_indices": []interface{}{ + map[string]interface{}{ + "clusters": []string{"cluster1"}, + "names": []string{"remote-index1"}, + "privileges": []string{"read"}, + "query": "", + }, + }, + }, + expected: map[string]interface{}{ + "name": "test-role", + "description": "Test role", + "cluster": []interface{}{"all"}, + "indices": []interface{}{ + map[string]interface{}{ + "names": []interface{}{"index1"}, + "privileges": []interface{}{"read"}, + }, + }, + "remote_indices": []interface{}{ + map[string]interface{}{ + "clusters": []interface{}{"cluster1"}, + "names": []interface{}{"remote-index1"}, + "privileges": []interface{}{"read"}, + }, + }, + }, + }, + { + name: "no_indices_or_remote_indices", + input: map[string]interface{}{ + "name": "test-role", + "description": "Test role", + "global": "", + "metadata": "", + "cluster": []string{"all"}, + }, + expected: map[string]interface{}{ + "name": "test-role", + "description": "Test role", + "cluster": []interface{}{"all"}, + }, + }, + { + name: "indices_not_array", + input: map[string]interface{}{ + "name": "test-role", + "description": "Test role", + "indices": "not-an-array", + }, + expected: map[string]interface{}{ + "name": "test-role", + "description": "Test role", + "indices": "not-an-array", // Should be preserved as-is if not an array + }, + }, + { + name: "remote_indices_not_array", + input: map[string]interface{}{ + "name": "test-role", + "description": "Test role", + "remote_indices": "not-an-array", + }, + expected: map[string]interface{}{ + "name": "test-role", + "description": "Test role", + "remote_indices": "not-an-array", // Should be preserved as-is if not an array + }, + }, + { + name: "index_item_not_map", + input: map[string]interface{}{ + "name": "test-role", + "description": "Test role", + "indices": []interface{}{ + "not-a-map", + map[string]interface{}{ + "names": []string{"index1"}, + "privileges": []string{"read"}, + "query": "", + }, + }, + }, + expected: map[string]interface{}{ + "name": "test-role", + "description": "Test role", + "indices": []interface{}{ + "not-a-map", // Should be preserved as-is if not a map + map[string]interface{}{ + "names": []interface{}{"index1"}, + "privileges": []interface{}{"read"}, + }, + }, + }, + }, + { + name: "remote_index_item_not_map", + input: map[string]interface{}{ + "name": "test-role", + "description": "Test role", + "remote_indices": []interface{}{ + "not-a-map", + map[string]interface{}{ + "clusters": []string{"cluster1"}, + "names": []string{"remote-index1"}, + "privileges": []string{"read"}, + "query": "", + }, + }, + }, + expected: map[string]interface{}{ + "name": "test-role", + "description": "Test role", + "remote_indices": []interface{}{ + "not-a-map", // Should be preserved as-is if not a map + map[string]interface{}{ + "clusters": []interface{}{"cluster1"}, + "names": []interface{}{"remote-index1"}, + "privileges": []interface{}{"read"}, + }, + }, + }, + }, + { + name: "nil_global_and_metadata_preserved", + input: map[string]interface{}{ + "name": "test-role", + "description": "Test role", + "global": nil, + "metadata": nil, + "cluster": []string{"all"}, + }, + expected: map[string]interface{}{ + "name": "test-role", + "description": "Test role", + "global": nil, + "metadata": nil, + "cluster": []interface{}{"all"}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Prepare the raw state JSON + inputJSON, err := json.Marshal(tt.input) + require.NoError(t, err) + + // Create the request + req := resource.UpgradeStateRequest{ + RawState: &tfprotov6.RawState{ + JSON: inputJSON, + }, + } + + // Create the response + resp := &resource.UpgradeStateResponse{} + + // Call the function + v0ToV1(context.Background(), req, resp) + + if tt.expectError { + assert.True(t, resp.Diagnostics.HasError()) + if tt.errorContains != "" { + found := false + for _, diag := range resp.Diagnostics.Errors() { + if assert.Contains(t, diag.Detail(), tt.errorContains) { + found = true + break + } + } + assert.True(t, found, "Expected error message not found") + } + return + } + + // Should not have errors + assert.False(t, resp.Diagnostics.HasError(), "Unexpected errors: %v", resp.Diagnostics) + + // Parse the output + require.NotNil(t, resp.DynamicValue) + require.NotNil(t, resp.DynamicValue.JSON) + + var actualState map[string]interface{} + err = json.Unmarshal(resp.DynamicValue.JSON, &actualState) + require.NoError(t, err) + + // Compare the results + assert.Equal(t, tt.expected, actualState) + }) + } +} + +func TestV0ToV1_InvalidJSON(t *testing.T) { + req := resource.UpgradeStateRequest{ + RawState: &tfprotov6.RawState{ + JSON: []byte("invalid json"), + }, + } + + resp := &resource.UpgradeStateResponse{} + + v0ToV1(context.Background(), req, resp) + + assert.True(t, resp.Diagnostics.HasError()) + assert.Contains(t, resp.Diagnostics.Errors()[0].Summary(), "State Upgrade Error") + assert.Contains(t, resp.Diagnostics.Errors()[0].Detail(), "Could not unmarshal prior state") +} diff --git a/internal/elasticsearch/security/role/schema.go b/internal/elasticsearch/security/role/schema.go index ae1e816eb..2d6785bcf 100644 --- a/internal/elasticsearch/security/role/schema.go +++ b/internal/elasticsearch/security/role/schema.go @@ -5,24 +5,30 @@ import ( _ "embed" "github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/setplanmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" "github.com/hashicorp/terraform-plugin-framework/types" providerschema "github.com/elastic/terraform-provider-elasticstack/internal/schema" ) +const CurrentSchemaVersion = 1 + //go:embed resource-description.md var roleResourceDescription string func (r *roleResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - resp.Schema = GetSchema() + resp.Schema = GetSchema(CurrentSchemaVersion) } -func GetSchema() schema.Schema { +func GetSchema(version int64) schema.Schema { return schema.Schema{ + Version: version, MarkdownDescription: roleResourceDescription, Blocks: map[string]schema.Block{ "elasticsearch_connection": providerschema.GetEsFWConnectionBlock("elasticsearch_connection", false), @@ -87,6 +93,10 @@ func GetSchema() schema.Schema { "allow_restricted_indices": schema.BoolAttribute{ MarkdownDescription: "Include matching restricted indices in names parameter. Usage is strongly discouraged as it can grant unrestricted operations on critical data, make the entire system unstable or leak sensitive information.", Optional: true, + Computed: true, + PlanModifiers: []planmodifier.Bool{ + boolplanmodifier.UseStateForUnknown(), + }, }, }, }, @@ -94,16 +104,10 @@ func GetSchema() schema.Schema { "remote_indices": schema.SetNestedBlock{ MarkdownDescription: "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.", NestedObject: schema.NestedBlockObject{ - Attributes: map[string]schema.Attribute{ - "clusters": schema.SetAttribute{ - MarkdownDescription: "A list of cluster aliases to which the permissions in this entry apply.", - Required: true, - ElementType: types.StringType, - }, - "field_security": schema.ListNestedAttribute{ + Blocks: map[string]schema.Block{ + "field_security": schema.ListNestedBlock{ MarkdownDescription: "The document fields that the owners of the role have read access to.", - Optional: true, - NestedObject: schema.NestedAttributeObject{ + NestedObject: schema.NestedBlockObject{ Attributes: map[string]schema.Attribute{ "grant": schema.SetAttribute{ MarkdownDescription: "List of the fields to grant the access to.", @@ -113,11 +117,22 @@ func GetSchema() schema.Schema { "except": schema.SetAttribute{ MarkdownDescription: "List of the fields to which the grants will not be applied.", Optional: true, + Computed: true, ElementType: types.StringType, + PlanModifiers: []planmodifier.Set{ + setplanmodifier.UseStateForUnknown(), + }, }, }, }, }, + }, + Attributes: map[string]schema.Attribute{ + "clusters": schema.SetAttribute{ + MarkdownDescription: "A list of cluster aliases to which the permissions in this entry apply.", + Required: true, + ElementType: types.StringType, + }, "query": schema.StringAttribute{ MarkdownDescription: "A search query that defines the documents the owners of the role have read access to.", Optional: true, @@ -177,3 +192,62 @@ func GetSchema() schema.Schema { }, } } + +// Helper functions to get attribute types from the schema +func getApplicationAttrTypes() map[string]attr.Type { + attrs := GetSchema(CurrentSchemaVersion).Blocks["applications"].(schema.SetNestedBlock).NestedObject.Attributes + result := make(map[string]attr.Type) + for name, attr := range attrs { + result[name] = attr.GetType() + } + return result +} + +func getFieldSecurityAttrTypes() map[string]attr.Type { + attrs := GetSchema(CurrentSchemaVersion).Blocks["indices"].(schema.SetNestedBlock).NestedObject.Attributes["field_security"].(schema.ListNestedAttribute).NestedObject.Attributes + result := make(map[string]attr.Type) + for name, attr := range attrs { + result[name] = attr.GetType() + } + return result +} + +func getIndexPermsAttrTypes() map[string]attr.Type { + nestedObj := GetSchema(CurrentSchemaVersion).Blocks["indices"].(schema.SetNestedBlock).NestedObject + result := make(map[string]attr.Type) + for name, attr := range nestedObj.Attributes { + result[name] = attr.GetType() + } + return result +} + +func getRemoteIndexPermsAttrTypes() map[string]attr.Type { + nestedObj := GetSchema(CurrentSchemaVersion).Blocks["remote_indices"].(schema.SetNestedBlock).NestedObject + result := make(map[string]attr.Type) + // Add attributes + for name, attr := range nestedObj.Attributes { + result[name] = attr.GetType() + } + // Add blocks as attributes (field_security is a block in remote_indices) + for name, block := range nestedObj.Blocks { + switch b := block.(type) { + case schema.ListNestedBlock: + // For ListNestedBlock, the type is ListType with ObjectType element + blockAttrs := make(map[string]attr.Type) + for attrName, attr := range b.NestedObject.Attributes { + blockAttrs[attrName] = attr.GetType() + } + result[name] = types.ListType{ElemType: types.ObjectType{AttrTypes: blockAttrs}} + } + } + return result +} + +func getRemoteFieldSecurityAttrTypes() map[string]attr.Type { + attrs := GetSchema(CurrentSchemaVersion).Blocks["remote_indices"].(schema.SetNestedBlock).NestedObject.Blocks["field_security"].(schema.ListNestedBlock).NestedObject.Attributes + result := make(map[string]attr.Type) + for name, attr := range attrs { + result[name] = attr.GetType() + } + return result +} diff --git a/internal/elasticsearch/security/role/update.go b/internal/elasticsearch/security/role/update.go index 89ef1ec0e..78c7140c8 100644 --- a/internal/elasticsearch/security/role/update.go +++ b/internal/elasticsearch/security/role/update.go @@ -78,7 +78,18 @@ func (r *roleResource) update(ctx context.Context, plan tfsdk.Plan, state *tfsdk } data.Id = types.StringValue(id.String()) - diags.Append(state.Set(ctx, &data)...) + readData, readDiags := r.read(ctx, data) + diags.Append(readDiags...) + if diags.HasError() { + return diags + } + + if readData == nil { + diags.AddError("Not Found", fmt.Sprintf("Role %q was not found after update", roleId)) + return diags + } + + diags.Append(state.Set(ctx, readData)...) return diags } From cef94b5c9ab241d134c6992a449369932ee48262 Mon Sep 17 00:00:00 2001 From: Toby Brain Date: Tue, 30 Sep 2025 13:59:14 +1000 Subject: [PATCH 8/8] Remove old acceptance tests --- internal/elasticsearch/security/role_test.go | 284 ------------------- 1 file changed, 284 deletions(-) delete mode 100644 internal/elasticsearch/security/role_test.go diff --git a/internal/elasticsearch/security/role_test.go b/internal/elasticsearch/security/role_test.go deleted file mode 100644 index 6824dd960..000000000 --- a/internal/elasticsearch/security/role_test.go +++ /dev/null @@ -1,284 +0,0 @@ -package security_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/elasticsearch/security" - "github.com/elastic/terraform-provider-elasticstack/internal/versionutils" - sdkacctest "github.com/hashicorp/terraform-plugin-testing/helper/acctest" - "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/hashicorp/terraform-plugin-testing/terraform" -) - -func TestAccResourceSecurityRole(t *testing.T) { - // generate a random username - roleName := sdkacctest.RandStringFromCharSet(10, sdkacctest.CharSetAlphaNum) - roleNameRemoteIndices := sdkacctest.RandStringFromCharSet(10, sdkacctest.CharSetAlphaNum) - roleNameDescription := sdkacctest.RandStringFromCharSet(10, sdkacctest.CharSetAlphaNum) - - resource.Test(t, resource.TestCase{ - PreCheck: func() { acctest.PreCheck(t) }, - CheckDestroy: checkResourceSecurityRoleDestroy, - ProtoV6ProviderFactories: acctest.Providers, - Steps: []resource.TestStep{ - { - Config: testAccResourceSecurityRoleCreate(roleName), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("elasticstack_elasticsearch_security_role.test", "name", roleName), - resource.TestCheckResourceAttr("elasticstack_elasticsearch_security_role.test", "indices.0.allow_restricted_indices", "true"), - resource.TestCheckTypeSetElemAttr("elasticstack_elasticsearch_security_role.test", "indices.*.names.*", "index1"), - resource.TestCheckTypeSetElemAttr("elasticstack_elasticsearch_security_role.test", "indices.*.names.*", "index2"), - resource.TestCheckTypeSetElemAttr("elasticstack_elasticsearch_security_role.test", "cluster.*", "all"), - resource.TestCheckTypeSetElemAttr("elasticstack_elasticsearch_security_role.test", "run_as.*", "other_user"), - resource.TestCheckNoResourceAttr("elasticstack_elasticsearch_security_role.test", "global"), - ), - }, - { - Config: testAccResourceSecurityRoleUpdate(roleName), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("elasticstack_elasticsearch_security_role.test", "name", roleName), - resource.TestCheckTypeSetElemAttr("elasticstack_elasticsearch_security_role.test", "indices.*.names.*", "index1"), - resource.TestCheckTypeSetElemAttr("elasticstack_elasticsearch_security_role.test", "indices.*.names.*", "index2"), - resource.TestCheckTypeSetElemAttr("elasticstack_elasticsearch_security_role.test", "cluster.*", "all"), - resource.TestCheckNoResourceAttr("elasticstack_elasticsearch_security_role.test", "run_as.#"), - resource.TestCheckNoResourceAttr("elasticstack_elasticsearch_security_role.test", "global"), - resource.TestCheckNoResourceAttr("elasticstack_elasticsearch_security_role.test", "applications.#"), - resource.TestCheckNoResourceAttr("elasticstack_elasticsearch_security_role.test", "indices.0.allow_restricted_indices"), - ), - }, - { - SkipFunc: versionutils.CheckIfVersionIsUnsupported(security.MinSupportedRemoteIndicesVersion), - Config: testAccResourceSecurityRoleRemoteIndicesCreate(roleNameRemoteIndices), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("elasticstack_elasticsearch_security_role.test", "name", roleNameRemoteIndices), - resource.TestCheckResourceAttr("elasticstack_elasticsearch_security_role.test", "indices.0.allow_restricted_indices", "true"), - resource.TestCheckTypeSetElemAttr("elasticstack_elasticsearch_security_role.test", "indices.*.names.*", "index1"), - resource.TestCheckTypeSetElemAttr("elasticstack_elasticsearch_security_role.test", "indices.*.names.*", "index2"), - resource.TestCheckTypeSetElemAttr("elasticstack_elasticsearch_security_role.test", "cluster.*", "all"), - resource.TestCheckTypeSetElemAttr("elasticstack_elasticsearch_security_role.test", "run_as.*", "other_user"), - resource.TestCheckNoResourceAttr("elasticstack_elasticsearch_security_role.test", "global"), - resource.TestCheckTypeSetElemAttr("elasticstack_elasticsearch_security_role.test", "remote_indices.*.clusters.*", "test-cluster"), - resource.TestCheckTypeSetElemAttr("elasticstack_elasticsearch_security_role.test", "remote_indices.*.names.*", "sample"), - ), - }, - { - SkipFunc: versionutils.CheckIfVersionIsUnsupported(security.MinSupportedRemoteIndicesVersion), - Config: testAccResourceSecurityRoleRemoteIndicesUpdate(roleNameRemoteIndices), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("elasticstack_elasticsearch_security_role.test", "name", roleNameRemoteIndices), - resource.TestCheckTypeSetElemAttr("elasticstack_elasticsearch_security_role.test", "indices.*.names.*", "index1"), - resource.TestCheckTypeSetElemAttr("elasticstack_elasticsearch_security_role.test", "indices.*.names.*", "index2"), - resource.TestCheckTypeSetElemAttr("elasticstack_elasticsearch_security_role.test", "cluster.*", "all"), - resource.TestCheckNoResourceAttr("elasticstack_elasticsearch_security_role.test", "run_as.#"), - resource.TestCheckNoResourceAttr("elasticstack_elasticsearch_security_role.test", "global"), - resource.TestCheckNoResourceAttr("elasticstack_elasticsearch_security_role.test", "applications.#"), - resource.TestCheckNoResourceAttr("elasticstack_elasticsearch_security_role.test", "indices.0.allow_restricted_indices"), - resource.TestCheckTypeSetElemAttr("elasticstack_elasticsearch_security_role.test", "remote_indices.*.clusters.*", "test-cluster2"), - resource.TestCheckTypeSetElemAttr("elasticstack_elasticsearch_security_role.test", "remote_indices.*.names.*", "sample2"), - ), - }, - { - SkipFunc: versionutils.CheckIfVersionIsUnsupported(security.MinSupportedDescriptionVersion), - Config: testAccResourceSecurityRoleDescriptionCreate(roleNameDescription), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("elasticstack_elasticsearch_security_role.test", "name", roleNameDescription), - resource.TestCheckResourceAttr("elasticstack_elasticsearch_security_role.test", "description", "test description"), - ), - }, - { - SkipFunc: versionutils.CheckIfVersionIsUnsupported(security.MinSupportedDescriptionVersion), - Config: testAccResourceSecurityRoleDescriptionUpdate(roleNameDescription), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("elasticstack_elasticsearch_security_role.test", "name", roleNameDescription), - resource.TestCheckResourceAttr("elasticstack_elasticsearch_security_role.test", "description", "updated test description"), - ), - }, - }, - }) -} - -func testAccResourceSecurityRoleCreate(roleName string) string { - return fmt.Sprintf(` -provider "elasticstack" { - elasticsearch {} -} - -resource "elasticstack_elasticsearch_security_role" "test" { - name = "%s" - - cluster = ["all"] - - indices { - names = ["index1", "index2"] - privileges = ["all"] - allow_restricted_indices = true - } - - applications { - application = "myapp" - privileges = ["admin", "read"] - resources = ["*"] - } - - run_as = ["other_user"] - - metadata = jsonencode({ - version = 1 - }) -} - `, roleName) -} - -func testAccResourceSecurityRoleUpdate(roleName string) string { - return fmt.Sprintf(` -provider "elasticstack" { - elasticsearch {} -} - -resource "elasticstack_elasticsearch_security_role" "test" { - name = "%s" - - cluster = ["all"] - - indices { - names = ["index1", "index2"] - privileges = ["all"] - } - - metadata = jsonencode({ - version = 1 - }) -} - `, roleName) -} - -func testAccResourceSecurityRoleRemoteIndicesCreate(roleName string) string { - return fmt.Sprintf(` -provider "elasticstack" { - elasticsearch {} -} - -resource "elasticstack_elasticsearch_security_role" "test" { - name = "%s" - cluster = ["all"] - - indices { - names = ["index1", "index2"] - privileges = ["all"] - allow_restricted_indices = true - } - - remote_indices { - clusters = ["test-cluster"] - field_security { - grant = ["sample"] - except = [] - } - names = ["sample"] - privileges = ["create", "read", "write"] - } - - applications { - application = "myapp" - privileges = ["admin", "read"] - resources = ["*"] - } - - run_as = ["other_user"] - - metadata = jsonencode({ - version = 1 - }) -} - `, roleName) -} - -func testAccResourceSecurityRoleRemoteIndicesUpdate(roleName string) string { - return fmt.Sprintf(` -provider "elasticstack" { - elasticsearch {} -} - -resource "elasticstack_elasticsearch_security_role" "test" { - name = "%s" - cluster = ["all"] - - indices { - names = ["index1", "index2"] - privileges = ["all"] - } - - remote_indices { - clusters = ["test-cluster2"] - field_security { - grant = ["sample"] - except = [] - } - names = ["sample2"] - privileges = ["create", "read", "write"] - } - - metadata = jsonencode({ - version = 1 - }) -} - `, roleName) -} - -func testAccResourceSecurityRoleDescriptionCreate(roleName string) string { - return fmt.Sprintf(` -provider "elasticstack" { - elasticsearch {} -} - -resource "elasticstack_elasticsearch_security_role" "test" { - name = "%s" - description = "test description" -} - `, roleName) -} - -func testAccResourceSecurityRoleDescriptionUpdate(roleName string) string { - return fmt.Sprintf(` -provider "elasticstack" { - elasticsearch {} -} - -resource "elasticstack_elasticsearch_security_role" "test" { - name = "%s" - description = "updated test description" -} - `, 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_elasticsearch_security_role" { - continue - } - compId, _ := clients.CompositeIdFromStr(rs.Primary.ID) - - esClient, err := client.GetESClient() - if err != nil { - return err - } - req := esClient.Security.GetRole.WithName(compId.ResourceId) - res, err := esClient.Security.GetRole(req) - if err != nil { - return err - } - - if res.StatusCode != 404 { - return fmt.Errorf("role (%s) still exists", compId.ResourceId) - } - } - return nil -}