From 306c14550cab5f33ffb8ccf5e666f6b4ea06540c Mon Sep 17 00:00:00 2001 From: Toby Brain Date: Wed, 17 Sep 2025 23:04:56 +1000 Subject: [PATCH 1/2] Allow a default for `allow_restricted_indices` within an API Key role descriptor --- .../security/api_key/acc_test.go | 70 ++++++ .../elasticsearch/security/api_key/models.go | 6 +- .../security/api_key/role_descriptors_type.go | 71 ++++++ .../api_key/role_descriptors_type_test.go | 183 ++++++++++++++++ .../api_key/role_descriptors_value.go | 132 +++++++++++ .../api_key/role_descriptors_value_test.go | 207 ++++++++++++++++++ .../elasticsearch/security/api_key/schema.go | 2 +- 7 files changed, 667 insertions(+), 4 deletions(-) create mode 100644 internal/elasticsearch/security/api_key/role_descriptors_type.go create mode 100644 internal/elasticsearch/security/api_key/role_descriptors_type_test.go create mode 100644 internal/elasticsearch/security/api_key/role_descriptors_value.go create mode 100644 internal/elasticsearch/security/api_key/role_descriptors_value_test.go diff --git a/internal/elasticsearch/security/api_key/acc_test.go b/internal/elasticsearch/security/api_key/acc_test.go index be1d13152..9c57ea0e7 100644 --- a/internal/elasticsearch/security/api_key/acc_test.go +++ b/internal/elasticsearch/security/api_key/acc_test.go @@ -550,3 +550,73 @@ resource "elasticstack_elasticsearch_security_api_key" "test" { } `, apiKeyName) } + +func TestAccResourceSecurityApiKeyWithDefaultAllowRestrictedIndices(t *testing.T) { + // generate a random name + apiKeyName := sdkacctest.RandStringFromCharSet(10, sdkacctest.CharSetAlphaNum) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + CheckDestroy: checkResourceSecurityApiKeyDestroy, + ProtoV6ProviderFactories: acctest.Providers, + Steps: []resource.TestStep{ + { + SkipFunc: versionutils.CheckIfVersionIsUnsupported(api_key.MinVersion), + Config: testAccResourceSecurityApiKeyWithoutAllowRestrictedIndices(apiKeyName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("elasticstack_elasticsearch_security_api_key.test", "name", apiKeyName), + resource.TestCheckResourceAttrWith("elasticstack_elasticsearch_security_api_key.test", "role_descriptors", func(testValue string) error { + var testRoleDescriptor map[string]models.ApiKeyRoleDescriptor + if err := json.Unmarshal([]byte(testValue), &testRoleDescriptor); err != nil { + return err + } + + expectedRoleDescriptor := map[string]models.ApiKeyRoleDescriptor{ + "role-default": { + Cluster: []string{"monitor"}, + Indices: []models.IndexPerms{{ + Names: []string{"logs-*", "metrics-*"}, + Privileges: []string{"read", "view_index_metadata"}, + }}, + }, + } + + if !reflect.DeepEqual(testRoleDescriptor, expectedRoleDescriptor) { + return fmt.Errorf("role descriptor mismatch:\nexpected: %+v\nactual: %+v", expectedRoleDescriptor, testRoleDescriptor) + } + + return nil + }), + resource.TestCheckResourceAttrSet("elasticstack_elasticsearch_security_api_key.test", "api_key"), + resource.TestCheckResourceAttrSet("elasticstack_elasticsearch_security_api_key.test", "encoded"), + resource.TestCheckResourceAttrSet("elasticstack_elasticsearch_security_api_key.test", "id"), + ), + }, + }, + }) +} + +func testAccResourceSecurityApiKeyWithoutAllowRestrictedIndices(apiKeyName string) string { + return fmt.Sprintf(` +provider "elasticstack" { + elasticsearch {} +} + +resource "elasticstack_elasticsearch_security_api_key" "test" { + name = "%s" + + role_descriptors = jsonencode({ + role-default = { + cluster = ["monitor"] + indices = [{ + names = ["logs-*", "metrics-*"] + privileges = ["read", "view_index_metadata"] + # Note: allow_restricted_indices is NOT specified here - should default to false + }] + } + }) + + expiration = "2d" +} + `, apiKeyName) +} diff --git a/internal/elasticsearch/security/api_key/models.go b/internal/elasticsearch/security/api_key/models.go index 8847f68e9..f3da6e4ee 100644 --- a/internal/elasticsearch/security/api_key/models.go +++ b/internal/elasticsearch/security/api_key/models.go @@ -36,7 +36,7 @@ type tfModel struct { KeyID types.String `tfsdk:"key_id"` Name types.String `tfsdk:"name"` Type types.String `tfsdk:"type"` - RoleDescriptors jsontypes.Normalized `tfsdk:"role_descriptors"` + RoleDescriptors RoleDescriptorsValue `tfsdk:"role_descriptors"` Expiration types.String `tfsdk:"expiration"` ExpirationTimestamp types.Int64 `tfsdk:"expiration_timestamp"` Metadata jsontypes.Normalized `tfsdk:"metadata"` @@ -205,7 +205,7 @@ func (model *tfModel) populateFromAPI(apiKey models.ApiKeyResponse, serverVersio model.Metadata = jsontypes.NewNormalizedNull() if serverVersion.GreaterThanOrEqual(MinVersionReturningRoleDescriptors) { - model.RoleDescriptors = jsontypes.NewNormalizedNull() + model.RoleDescriptors = NewRoleDescriptorsNull() if apiKey.RolesDescriptors != nil { descriptors, diags := marshalNormalizedJsonValue(apiKey.RolesDescriptors) @@ -213,7 +213,7 @@ func (model *tfModel) populateFromAPI(apiKey models.ApiKeyResponse, serverVersio return diags } - model.RoleDescriptors = descriptors + model.RoleDescriptors = NewRoleDescriptorsValue(descriptors.ValueString()) } } diff --git a/internal/elasticsearch/security/api_key/role_descriptors_type.go b/internal/elasticsearch/security/api_key/role_descriptors_type.go new file mode 100644 index 000000000..a46bc7d99 --- /dev/null +++ b/internal/elasticsearch/security/api_key/role_descriptors_type.go @@ -0,0 +1,71 @@ +package api_key + +import ( + "context" + "fmt" + + "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/basetypes" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +var ( + _ basetypes.StringTypable = (*RoleDescriptorsType)(nil) +) + +type RoleDescriptorsType struct { + jsontypes.NormalizedType +} + +// String returns a human readable string of the type name. +func (t RoleDescriptorsType) String() string { + return "api_key.RoleDescriptorsType" +} + +// ValueType returns the Value type. +func (t RoleDescriptorsType) ValueType(ctx context.Context) attr.Value { + return RoleDescriptorsValue{} +} + +// Equal returns true if the given type is equivalent. +func (t RoleDescriptorsType) Equal(o attr.Type) bool { + other, ok := o.(RoleDescriptorsType) + + if !ok { + return false + } + + return t.StringType.Equal(other.StringType) +} + +// ValueFromString returns a StringValuable type given a StringValue. +func (t RoleDescriptorsType) ValueFromString(ctx context.Context, in basetypes.StringValue) (basetypes.StringValuable, diag.Diagnostics) { + return RoleDescriptorsValue{ + Normalized: jsontypes.Normalized{ + StringValue: in, + }, + }, nil +} + +// ValueFromTerraform returns a Value given a tftypes.Value. This is meant to convert the tftypes.Value into a more convenient Go type +// for the provider to consume the data with. +func (t RoleDescriptorsType) ValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Value, error) { + attrValue, err := t.StringType.ValueFromTerraform(ctx, in) + if err != nil { + return nil, err + } + + stringValue, ok := attrValue.(basetypes.StringValue) + if !ok { + return nil, fmt.Errorf("unexpected value type of %T", attrValue) + } + + stringValuable, diags := t.ValueFromString(ctx, stringValue) + if diags.HasError() { + return nil, fmt.Errorf("unexpected error converting StringValue to StringValuable: %v", diags) + } + + return stringValuable, nil +} diff --git a/internal/elasticsearch/security/api_key/role_descriptors_type_test.go b/internal/elasticsearch/security/api_key/role_descriptors_type_test.go new file mode 100644 index 000000000..351f679fc --- /dev/null +++ b/internal/elasticsearch/security/api_key/role_descriptors_type_test.go @@ -0,0 +1,183 @@ +package api_key + +import ( + "context" + "testing" + + "github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/stretchr/testify/assert" +) + +func TestRoleDescriptorsType_String(t *testing.T) { + roleDescriptorsType := RoleDescriptorsType{} + expected := "api_key.RoleDescriptorsType" + actual := roleDescriptorsType.String() + assert.Equal(t, expected, actual) +} + +func TestRoleDescriptorsType_ValueType(t *testing.T) { + roleDescriptorsType := RoleDescriptorsType{} + ctx := context.Background() + + value := roleDescriptorsType.ValueType(ctx) + + assert.IsType(t, RoleDescriptorsValue{}, value) +} + +func TestRoleDescriptorsType_Equal(t *testing.T) { + tests := []struct { + name string + thisType RoleDescriptorsType + other attr.Type + expected bool + }{ + { + name: "equal to same type", + thisType: RoleDescriptorsType{}, + other: RoleDescriptorsType{}, + expected: true, + }, + { + name: "not equal to different type", + thisType: RoleDescriptorsType{}, + other: basetypes.StringType{}, + expected: false, + }, + { + name: "not equal to nil", + thisType: RoleDescriptorsType{}, + other: nil, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := tt.thisType.Equal(tt.other) + assert.Equal(t, tt.expected, actual) + }) + } +} + +func TestRoleDescriptorsType_ValueFromString(t *testing.T) { + tests := []struct { + name string + input basetypes.StringValue + expectedValue RoleDescriptorsValue + expectedDiags bool + }{ + { + name: "valid string value", + input: basetypes.NewStringValue(`{"role1": {"cluster": ["read"]}}`), + expectedValue: RoleDescriptorsValue{ + Normalized: jsontypes.Normalized{ + StringValue: basetypes.NewStringValue(`{"role1": {"cluster": ["read"]}}`), + }, + }, + expectedDiags: false, + }, + { + name: "null string value", + input: basetypes.NewStringNull(), + expectedValue: RoleDescriptorsValue{ + Normalized: jsontypes.Normalized{ + StringValue: basetypes.NewStringNull(), + }, + }, + expectedDiags: false, + }, + { + name: "unknown string value", + input: basetypes.NewStringUnknown(), + expectedValue: RoleDescriptorsValue{ + Normalized: jsontypes.Normalized{ + StringValue: basetypes.NewStringUnknown(), + }, + }, + expectedDiags: false, + }, + { + name: "empty string value", + input: basetypes.NewStringValue(""), + expectedValue: RoleDescriptorsValue{ + Normalized: jsontypes.Normalized{ + StringValue: basetypes.NewStringValue(""), + }, + }, + expectedDiags: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + roleDescriptorsType := RoleDescriptorsType{} + ctx := context.Background() + + value, diags := roleDescriptorsType.ValueFromString(ctx, tt.input) + + if tt.expectedDiags { + assert.True(t, diags.HasError()) + } else { + assert.False(t, diags.HasError()) + } + + assert.Equal(t, tt.expectedValue, value) + }) + } +} + +func TestRoleDescriptorsType_ValueFromTerraform(t *testing.T) { + tests := []struct { + name string + input tftypes.Value + expectedError bool + expectedType interface{} + }{ + { + name: "valid string terraform value", + input: tftypes.NewValue(tftypes.String, `{"role1": {"cluster": ["read"]}}`), + expectedError: false, + expectedType: RoleDescriptorsValue{}, + }, + { + name: "null terraform value", + input: tftypes.NewValue(tftypes.String, nil), + expectedError: false, + expectedType: RoleDescriptorsValue{}, + }, + { + name: "unknown terraform value", + input: tftypes.NewValue(tftypes.String, tftypes.UnknownValue), + expectedError: false, + expectedType: RoleDescriptorsValue{}, + }, + { + name: "invalid terraform value type", + input: tftypes.NewValue(tftypes.Number, 123), + expectedError: true, + expectedType: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + roleDescriptorsType := RoleDescriptorsType{} + ctx := context.Background() + + value, err := roleDescriptorsType.ValueFromTerraform(ctx, tt.input) + + if tt.expectedError { + assert.Error(t, err) + assert.Nil(t, value) + } else { + assert.NoError(t, err) + if tt.expectedType != nil { + assert.IsType(t, tt.expectedType, value) + } + } + }) + } +} diff --git a/internal/elasticsearch/security/api_key/role_descriptors_value.go b/internal/elasticsearch/security/api_key/role_descriptors_value.go new file mode 100644 index 000000000..bf0684869 --- /dev/null +++ b/internal/elasticsearch/security/api_key/role_descriptors_value.go @@ -0,0 +1,132 @@ +package api_key + +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/attr/xattr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +var ( + _ basetypes.StringValuable = (*RoleDescriptorsValue)(nil) + _ basetypes.StringValuableWithSemanticEquals = (*RoleDescriptorsValue)(nil) + _ xattr.ValidateableAttribute = (*RoleDescriptorsValue)(nil) +) + +type RoleDescriptorsValue struct { + jsontypes.Normalized +} + +// Type returns a RoleDescriptorsType. +func (v RoleDescriptorsValue) Type(_ context.Context) attr.Type { + return RoleDescriptorsType{} +} + +func (v RoleDescriptorsValue) WithDefaults() (RoleDescriptorsValue, diag.Diagnostics) { + var diags diag.Diagnostics + + if v.IsNull() { + return v, diags + } + + if v.IsUnknown() { + return v, diags + } + + var parsedValue map[string]models.ApiKeyRoleDescriptor + err := json.Unmarshal([]byte(v.ValueString()), &parsedValue) + if err != nil { + diags.AddError("Failed to unmarshal role descriptors value", err.Error()) + return RoleDescriptorsValue{}, diags + } + + for role, descriptor := range parsedValue { + for i, index := range descriptor.Indices { + if index.AllowRestrictedIndices == nil { + descriptor.Indices[i].AllowRestrictedIndices = new(bool) + *descriptor.Indices[i].AllowRestrictedIndices = false + } + } + parsedValue[role] = descriptor + } + + valueWithDefaults, err := json.Marshal(parsedValue) + if err != nil { + diags.AddError("Failed to marshal sanitized config value", err.Error()) + return RoleDescriptorsValue{}, diags + } + + return NewRoleDescriptorsValue(string(valueWithDefaults)), diags +} + +// StringSemanticEquals returns true if the given config object value is semantically equal to the current config object value. +// The comparison will ignore any default values present in one value, but unset in the other. +func (v RoleDescriptorsValue) StringSemanticEquals(ctx context.Context, newValuable basetypes.StringValuable) (bool, diag.Diagnostics) { + var diags diag.Diagnostics + + newValue, ok := newValuable.(RoleDescriptorsValue) + if !ok { + diags.AddError( + "Semantic Equality Check Error", + "An unexpected value type was received while performing semantic equality checks. "+ + "Please report this to the provider developers.\n\n"+ + "Expected Value Type: "+fmt.Sprintf("%T", v)+"\n"+ + "Got Value Type: "+fmt.Sprintf("%T", newValuable), + ) + + return false, diags + } + + if v.IsNull() { + return newValue.IsNull(), diags + } + + if v.IsUnknown() { + return newValue.IsUnknown(), diags + } + + thisWithDefaults, d := v.WithDefaults() + diags.Append(d...) + if diags.HasError() { + return false, diags + } + + thatWithDefaults, d := newValue.WithDefaults() + diags.Append(d...) + if diags.HasError() { + return false, diags + } + + return thisWithDefaults.Normalized.StringSemanticEquals(ctx, thatWithDefaults.Normalized) +} + +// NewRoleDescriptorsNull creates a RoleDescriptorsValue with a null value. Determine whether the value is null via IsNull method. +func NewRoleDescriptorsNull() RoleDescriptorsValue { + return RoleDescriptorsValue{ + Normalized: jsontypes.NewNormalizedNull(), + } +} + +// NewRoleDescriptorsUnknown creates a RoleDescriptorsValue with an unknown value. Determine whether the value is unknown via IsUnknown method. +func NewRoleDescriptorsUnknown() RoleDescriptorsValue { + return RoleDescriptorsValue{ + Normalized: jsontypes.NewNormalizedUnknown(), + } +} + +// NewRoleDescriptorsValue creates a RoleDescriptorsValue with a known value. Access the value via ValueString method. +func NewRoleDescriptorsValue(value string) RoleDescriptorsValue { + if value == "" { + return NewRoleDescriptorsNull() + } + + return RoleDescriptorsValue{ + Normalized: jsontypes.NewNormalizedValue(value), + } +} diff --git a/internal/elasticsearch/security/api_key/role_descriptors_value_test.go b/internal/elasticsearch/security/api_key/role_descriptors_value_test.go new file mode 100644 index 000000000..cdb60fcfd --- /dev/null +++ b/internal/elasticsearch/security/api_key/role_descriptors_value_test.go @@ -0,0 +1,207 @@ +package api_key + +import ( + "context" + "testing" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/stretchr/testify/assert" +) + +func TestRoleDescriptorsValue_Type(t *testing.T) { + value := RoleDescriptorsValue{} + ctx := context.Background() + + attrType := value.Type(ctx) + + assert.IsType(t, RoleDescriptorsType{}, attrType) +} + +func TestRoleDescriptorsValue_WithDefaults(t *testing.T) { + tests := []struct { + name string + input RoleDescriptorsValue + expectedResult func(t *testing.T, result RoleDescriptorsValue, diags diag.Diagnostics) + expectError bool + }{ + { + name: "null value returns same value without error", + input: NewRoleDescriptorsNull(), + expectedResult: func(t *testing.T, result RoleDescriptorsValue, diags diag.Diagnostics) { + assert.True(t, result.IsNull()) + assert.False(t, diags.HasError()) + }, + expectError: false, + }, + { + name: "unknown value returns same value without error", + input: NewRoleDescriptorsUnknown(), + expectedResult: func(t *testing.T, result RoleDescriptorsValue, diags diag.Diagnostics) { + assert.True(t, result.IsUnknown()) + assert.False(t, diags.HasError()) + }, + expectError: false, + }, + { + name: "valid JSON with missing allow_restricted_indices sets default", + input: NewRoleDescriptorsValue(`{"admin":{"indices":[{"names":["index1"],"privileges":["read"]}]}}`), + expectedResult: func(t *testing.T, result RoleDescriptorsValue, diags diag.Diagnostics) { + assert.False(t, result.IsNull()) + assert.False(t, result.IsUnknown()) + assert.False(t, diags.HasError()) + assert.Contains(t, result.ValueString(), "allow_restricted_indices") + assert.Contains(t, result.ValueString(), "false") + }, + expectError: false, + }, + { + name: "valid JSON with existing allow_restricted_indices preserves value", + input: NewRoleDescriptorsValue(`{"admin":{"indices":[{"names":["index1"],"privileges":["read"],"allow_restricted_indices":true}]}}`), + expectedResult: func(t *testing.T, result RoleDescriptorsValue, diags diag.Diagnostics) { + assert.False(t, result.IsNull()) + assert.False(t, result.IsUnknown()) + assert.False(t, diags.HasError()) + assert.Contains(t, result.ValueString(), "allow_restricted_indices") + assert.Contains(t, result.ValueString(), "true") + }, + expectError: false, + }, + { + name: "empty role descriptor object", + input: NewRoleDescriptorsValue(`{"admin":{}}`), + expectedResult: func(t *testing.T, result RoleDescriptorsValue, diags diag.Diagnostics) { + assert.False(t, result.IsNull()) + assert.False(t, result.IsUnknown()) + assert.False(t, diags.HasError()) + }, + expectError: false, + }, + { + name: "invalid JSON returns error", + input: NewRoleDescriptorsValue(`{"invalid json"`), + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, diags := tt.input.WithDefaults() + + if tt.expectError { + assert.True(t, diags.HasError()) + } else { + if tt.expectedResult != nil { + tt.expectedResult(t, result, diags) + } + } + }) + } +} + +func TestRoleDescriptorsValue_StringSemanticEquals(t *testing.T) { + ctx := context.Background() + + tests := []struct { + name string + value1 RoleDescriptorsValue + value2 basetypes.StringValuable + expected bool + expectError bool + }{ + { + name: "both null values are equal", + value1: NewRoleDescriptorsNull(), + value2: NewRoleDescriptorsNull(), + expected: true, + expectError: false, + }, + { + name: "both unknown values are equal", + value1: NewRoleDescriptorsUnknown(), + value2: NewRoleDescriptorsUnknown(), + expected: true, + expectError: false, + }, + { + name: "null vs unknown are not equal", + value1: NewRoleDescriptorsNull(), + value2: NewRoleDescriptorsUnknown(), + expected: false, + expectError: false, + }, + { + name: "same JSON content are equal", + value1: NewRoleDescriptorsValue(`{"admin":{"cluster":["read"]}}`), + value2: NewRoleDescriptorsValue(`{"admin":{"cluster":["read"]}}`), + expected: true, + expectError: false, + }, + { + name: "different JSON content are not equal", + value1: NewRoleDescriptorsValue(`{"admin":{"cluster":["read"]}}`), + value2: NewRoleDescriptorsValue(`{"user":{"cluster":["write"]}}`), + expected: false, + expectError: false, + }, + { + name: "semantic equality with defaults - missing vs explicit false", + value1: NewRoleDescriptorsValue(`{"admin":{"indices":[{"names":["index1"],"privileges":["read"]}]}}`), + value2: NewRoleDescriptorsValue(`{"admin":{"indices":[{"names":["index1"],"privileges":["read"],"allow_restricted_indices":false}]}}`), + expected: true, + expectError: false, + }, + { + name: "wrong type returns error", + value1: NewRoleDescriptorsValue(`{"admin":{}}`), + value2: basetypes.NewStringValue("not a role descriptors value"), + expected: false, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, diags := tt.value1.StringSemanticEquals(ctx, tt.value2) + + if tt.expectError { + assert.True(t, diags.HasError()) + } else { + assert.False(t, diags.HasError()) + assert.Equal(t, tt.expected, result) + } + }) + } +} + +func TestRoleDescriptorsValue_WithDefaults_ComplexJSON(t *testing.T) { + // Test with complex role descriptor JSON that has multiple roles and indices + complexJSON := `{ + "admin": { + "cluster": ["all"], + "indices": [ + {"names": ["index1"], "privileges": ["read"]}, + {"names": ["index2"], "privileges": ["write"], "allow_restricted_indices": true} + ] + }, + "user": { + "indices": [ + {"names": ["public*"], "privileges": ["read"]} + ] + } + }` + + value := NewRoleDescriptorsValue(complexJSON) + result, diags := value.WithDefaults() + + assert.False(t, diags.HasError()) + assert.False(t, result.IsNull()) + assert.False(t, result.IsUnknown()) + + resultJSON := result.ValueString() + + // Should contain the original true value + assert.Contains(t, resultJSON, `"allow_restricted_indices":true`) + // Should contain default false values for indices without the field + assert.Contains(t, resultJSON, `"allow_restricted_indices":false`) +} diff --git a/internal/elasticsearch/security/api_key/schema.go b/internal/elasticsearch/security/api_key/schema.go index cdc4a3b97..0ea09de3d 100644 --- a/internal/elasticsearch/security/api_key/schema.go +++ b/internal/elasticsearch/security/api_key/schema.go @@ -77,7 +77,7 @@ func (r *Resource) getSchema(version int64) schema.Schema { }, "role_descriptors": schema.StringAttribute{ Description: "Role descriptors for this API key.", - CustomType: jsontypes.NormalizedType{}, + CustomType: RoleDescriptorsType{}, Optional: true, Computed: true, Validators: []validator.String{ From 7522c1bf79df66c15914b67e73100129b9feb5b9 Mon Sep 17 00:00:00 2001 From: Toby Brain Date: Wed, 17 Sep 2025 23:27:17 +1000 Subject: [PATCH 2/2] Changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f27ecf86..adfe1aa3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ - [Refactor] Regenerate the SLO client using the current OpenAPI spec ([#1303](https://github.com/elastic/terraform-provider-elasticstack/pull/1303)) - Add support for `data_view_id` in the `elasticstack_kibana_slo` resource ([#1305](https://github.com/elastic/terraform-provider-elasticstack/pull/1305)) - 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)) ## [0.11.17] - 2025-07-21