Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
70 changes: 70 additions & 0 deletions internal/elasticsearch/security/api_key/acc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
6 changes: 3 additions & 3 deletions internal/elasticsearch/security/api_key/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down Expand Up @@ -205,15 +205,15 @@ 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)
if diags.HasError() {
return diags
}

model.RoleDescriptors = descriptors
model.RoleDescriptors = NewRoleDescriptorsValue(descriptors.ValueString())
}
}

Expand Down
71 changes: 71 additions & 0 deletions internal/elasticsearch/security/api_key/role_descriptors_type.go
Original file line number Diff line number Diff line change
@@ -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
}
183 changes: 183 additions & 0 deletions internal/elasticsearch/security/api_key/role_descriptors_type_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
})
}
}
Loading
Loading