Skip to content

Commit d400796

Browse files
Support restriction in elasticstack_elasticsearch_security_api_key (#577)
* Support restriction in elasticstack_elasticsearch_security_api_key * Update example in docs * Skip test for API key with restriction on any version below 8.9 * Update docs * Raise error when restriction is not supported for API keys * Appease the linter * Small refactor * Revert debug change * Booleans are hard * This is a finicky test * Format a terraform example file * Use the ServerVersion() instead of creating a whole new request * Update docs * Remove two unused structs * Changelog --------- Co-authored-by: Toby Brain <[email protected]>
1 parent 962fbb8 commit d400796

File tree

6 files changed

+240
-17
lines changed

6 files changed

+240
-17
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
- Fix setting `id` for Fleet outputs and servers ([#666](https://github.com/elastic/terraform-provider-elasticstack/pull/666))
44
- Fix `elasticstack_fleet_enrollment_tokens` returning empty tokens in some case ([#683](https://github.com/elastic/terraform-provider-elasticstack/pull/683))
55
- Add support for Kibana synthetics private locations ([#696](https://github.com/elastic/terraform-provider-elasticstack/pull/696))
6+
- Support setting `restriction` in `elasticstack_elasticsearch_security_api_key` role definitions ([#577](https://github.com/elastic/terraform-provider-elasticstack/pull/577))
67

78
## [0.11.4] - 2024-06-13
89

docs/resources/elasticsearch_security_api_key.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,37 @@ resource "elasticstack_elasticsearch_security_api_key" "api_key" {
4141
})
4242
}
4343
44+
# restriction on a role descriptor for an API key is supported since Elastic 8.9
45+
resource "elasticstack_elasticsearch_security_api_key" "api_key_with_restriction" {
46+
# Set the name
47+
name = "My API key"
48+
# Set the role descriptors
49+
role_descriptors = jsonencode({
50+
role-a = {
51+
cluster = ["all"],
52+
indices = [
53+
{
54+
names = ["index-a*"],
55+
privileges = ["read"]
56+
}
57+
],
58+
restriction = {
59+
workflows = ["search_application_query"]
60+
}
61+
}
62+
})
63+
64+
# Set the expiration for the API key
65+
expiration = "1d"
66+
67+
# Set the custom metadata for this user
68+
metadata = jsonencode({
69+
"env" = "testing"
70+
"open" = false
71+
"number" = 49
72+
})
73+
}
74+
4475
output "api_key" {
4576
value = elasticstack_elasticsearch_security_api_key.api_key
4677
sensitive = true

examples/resources/elasticstack_elasticsearch_security_api_key/resource.tf

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,37 @@ resource "elasticstack_elasticsearch_security_api_key" "api_key" {
2626
})
2727
}
2828

29+
# restriction on a role descriptor for an API key is supported since Elastic 8.9
30+
resource "elasticstack_elasticsearch_security_api_key" "api_key_with_restriction" {
31+
# Set the name
32+
name = "My API key"
33+
# Set the role descriptors
34+
role_descriptors = jsonencode({
35+
role-a = {
36+
cluster = ["all"],
37+
indices = [
38+
{
39+
names = ["index-a*"],
40+
privileges = ["read"]
41+
}
42+
],
43+
restriction = {
44+
workflows = ["search_application_query"]
45+
}
46+
}
47+
})
48+
49+
# Set the expiration for the API key
50+
expiration = "1d"
51+
52+
# Set the custom metadata for this user
53+
metadata = jsonencode({
54+
"env" = "testing"
55+
"open" = false
56+
"number" = 49
57+
})
58+
}
59+
2960
output "api_key" {
3061
value = elasticstack_elasticsearch_security_api_key.api_key
3162
sensitive = true

internal/elasticsearch/security/api_key.go

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ import (
1616
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
1717
)
1818

19-
var APIKeyMinVersion = version.Must(version.NewVersion("8.0.0")) // Enabled in 8.0
19+
var APIKeyMinVersion = version.Must(version.NewVersion("8.0.0")) // Enabled in 8.0
20+
var APIKeyWithRestrictionMinVersion = version.Must(version.NewVersion("8.9.0")) // Enabled in 8.0
2021

2122
func ResourceApiKey() *schema.Resource {
2223
apikeySchema := map[string]*schema.Schema{
@@ -107,11 +108,33 @@ func resourceSecurityApiKeyCreate(ctx context.Context, d *schema.ResourceData, m
107108
}
108109

109110
if v, ok := d.GetOk("role_descriptors"); ok {
110-
role_descriptors := map[string]models.Role{}
111+
role_descriptors := map[string]models.ApiKeyRoleDescriptor{}
111112
if err := json.NewDecoder(strings.NewReader(v.(string))).Decode(&role_descriptors); err != nil {
112113
return diag.FromErr(err)
113114
}
114115
apikey.RolesDescriptors = role_descriptors
116+
117+
var hasRestriction = false
118+
var keysWithRestrictions []string
119+
120+
for key, roleDescriptor := range role_descriptors {
121+
if roleDescriptor.Restriction != nil {
122+
hasRestriction = true
123+
keysWithRestrictions = append(keysWithRestrictions, key)
124+
}
125+
}
126+
127+
if hasRestriction {
128+
isSupported, diags := doesCurrentVersionSupportRestrictionOnApiKey(ctx, client)
129+
130+
if diags.HasError() {
131+
return diags
132+
}
133+
134+
if !isSupported {
135+
return diag.Errorf("Specifying `restriction` on an API key role description is not supported in this version of Elasticsearch. Role descriptor(s) %s", strings.Join(keysWithRestrictions, ", "))
136+
}
137+
}
115138
}
116139

117140
if v, ok := d.GetOk("metadata"); ok {
@@ -155,6 +178,16 @@ func resourceSecurityApiKeyCreate(ctx context.Context, d *schema.ResourceData, m
155178
return resourceSecurityApiKeyRead(ctx, d, meta)
156179
}
157180

181+
func doesCurrentVersionSupportRestrictionOnApiKey(ctx context.Context, client *clients.ApiClient) (bool, diag.Diagnostics) {
182+
currentVersion, diags := client.ServerVersion(ctx)
183+
184+
if diags.HasError() {
185+
return false, diags
186+
}
187+
188+
return currentVersion.GreaterThanOrEqual(APIKeyWithRestrictionMinVersion), nil
189+
}
190+
158191
func resourceSecurityApiKeyUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
159192
diags := diag.Diagnostics{diag.Diagnostic{
160193
Severity: diag.Error,

internal/elasticsearch/security/api_key_test.go

Lines changed: 117 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
package security_test
22

33
import (
4+
"context"
45
"encoding/json"
56
"fmt"
7+
"github.com/hashicorp/go-version"
68
"reflect"
9+
"regexp"
710
"testing"
811

912
"github.com/elastic/terraform-provider-elasticstack/internal/acctest"
@@ -17,7 +20,7 @@ import (
1720
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
1821
)
1922

20-
func TestAccResourceSecuritApiKey(t *testing.T) {
23+
func TestAccResourceSecurityApiKey(t *testing.T) {
2124
// generate a random name
2225
apiKeyName := sdkacctest.RandStringFromCharSet(10, sdkacctest.CharSetAlphaNum)
2326

@@ -28,17 +31,17 @@ func TestAccResourceSecuritApiKey(t *testing.T) {
2831
Steps: []resource.TestStep{
2932
{
3033
SkipFunc: versionutils.CheckIfVersionIsUnsupported(security.APIKeyMinVersion),
31-
Config: testAccResourceSecuritApiKeyCreate(apiKeyName),
34+
Config: testAccResourceSecurityApiKeyCreate(apiKeyName),
3235
Check: resource.ComposeTestCheckFunc(
3336
resource.TestCheckResourceAttr("elasticstack_elasticsearch_security_api_key.test", "name", apiKeyName),
3437
resource.TestCheckResourceAttrWith("elasticstack_elasticsearch_security_api_key.test", "role_descriptors", func(testValue string) error {
35-
var testRoleDescriptor map[string]models.Role
38+
var testRoleDescriptor map[string]models.ApiKeyRoleDescriptor
3639
if err := json.Unmarshal([]byte(testValue), &testRoleDescriptor); err != nil {
3740
return err
3841
}
3942

4043
allowRestrictedIndices := false
41-
expectedRoleDescriptor := map[string]models.Role{
44+
expectedRoleDescriptor := map[string]models.ApiKeyRoleDescriptor{
4245
"role-a": {
4346
Cluster: []string{"all"},
4447
Indices: []models.IndexPerms{{
@@ -64,7 +67,88 @@ func TestAccResourceSecuritApiKey(t *testing.T) {
6467
})
6568
}
6669

67-
func testAccResourceSecuritApiKeyCreate(apiKeyName string) string {
70+
func TestAccResourceSecurityApiKeyWithWorkflowRestriction(t *testing.T) {
71+
// generate a random name
72+
apiKeyName := sdkacctest.RandStringFromCharSet(10, sdkacctest.CharSetAlphaNum)
73+
74+
resource.Test(t, resource.TestCase{
75+
PreCheck: func() { acctest.PreCheck(t) },
76+
CheckDestroy: checkResourceSecurityApiKeyDestroy,
77+
ProtoV6ProviderFactories: acctest.Providers,
78+
Steps: []resource.TestStep{
79+
{
80+
SkipFunc: versionutils.CheckIfVersionIsUnsupported(security.APIKeyWithRestrictionMinVersion),
81+
Config: testAccResourceSecurityApiKeyCreateWithWorkflowRestriction(apiKeyName),
82+
Check: resource.ComposeTestCheckFunc(
83+
resource.TestCheckResourceAttr("elasticstack_elasticsearch_security_api_key.test", "name", apiKeyName),
84+
resource.TestCheckResourceAttrWith("elasticstack_elasticsearch_security_api_key.test", "role_descriptors", func(testValue string) error {
85+
var testRoleDescriptor map[string]models.ApiKeyRoleDescriptor
86+
if err := json.Unmarshal([]byte(testValue), &testRoleDescriptor); err != nil {
87+
return err
88+
}
89+
90+
allowRestrictedIndices := false
91+
expectedRoleDescriptor := map[string]models.ApiKeyRoleDescriptor{
92+
"role-a": {
93+
Cluster: []string{"all"},
94+
Indices: []models.IndexPerms{{
95+
Names: []string{"index-a*"},
96+
Privileges: []string{"read"},
97+
AllowRestrictedIndices: &allowRestrictedIndices,
98+
}},
99+
Restriction: &models.Restriction{Workflows: []string{"search_application_query"}},
100+
},
101+
}
102+
103+
if !reflect.DeepEqual(testRoleDescriptor, expectedRoleDescriptor) {
104+
return fmt.Errorf("%v doesn't match %v", testRoleDescriptor, expectedRoleDescriptor)
105+
}
106+
107+
return nil
108+
}),
109+
resource.TestCheckResourceAttrSet("elasticstack_elasticsearch_security_api_key.test", "expiration"),
110+
resource.TestCheckResourceAttrSet("elasticstack_elasticsearch_security_api_key.test", "api_key"),
111+
resource.TestCheckResourceAttrSet("elasticstack_elasticsearch_security_api_key.test", "encoded"),
112+
),
113+
},
114+
},
115+
})
116+
}
117+
118+
func TestAccResourceSecurityApiKeyWithWorkflowRestrictionOnElasticPre8_9_x(t *testing.T) {
119+
// generate a random name
120+
apiKeyName := sdkacctest.RandStringFromCharSet(10, sdkacctest.CharSetAlphaNum)
121+
122+
resource.Test(t, resource.TestCase{
123+
PreCheck: func() { acctest.PreCheck(t) },
124+
CheckDestroy: checkResourceSecurityApiKeyDestroy,
125+
ProtoV6ProviderFactories: acctest.Providers,
126+
Steps: []resource.TestStep{
127+
{
128+
SkipFunc: SkipWhenApiKeysAreNotSupportedOrRestrictionsAreSupported(security.APIKeyMinVersion, security.APIKeyWithRestrictionMinVersion),
129+
Config: testAccResourceSecurityApiKeyCreateWithWorkflowRestriction(apiKeyName),
130+
ExpectError: regexp.MustCompile(fmt.Sprintf(".*Error: Specifying `restriction` on an API key role description is not supported in this version of Elasticsearch. Role descriptor\\(s\\) %s.*", "role-a")),
131+
},
132+
},
133+
})
134+
}
135+
136+
func SkipWhenApiKeysAreNotSupportedOrRestrictionsAreSupported(minApiKeySupportedVersion *version.Version, minRestrictionSupportedVersion *version.Version) func() (bool, error) {
137+
return func() (b bool, err error) {
138+
client, err := clients.NewAcceptanceTestingClient()
139+
if err != nil {
140+
return false, err
141+
}
142+
serverVersion, diags := client.ServerVersion(context.Background())
143+
if diags.HasError() {
144+
return false, fmt.Errorf("failed to parse the elasticsearch version %v", diags)
145+
}
146+
147+
return serverVersion.LessThan(minApiKeySupportedVersion) || serverVersion.GreaterThanOrEqual(minRestrictionSupportedVersion), nil
148+
}
149+
}
150+
151+
func testAccResourceSecurityApiKeyCreate(apiKeyName string) string {
68152
return fmt.Sprintf(`
69153
provider "elasticstack" {
70154
elasticsearch {}
@@ -89,6 +173,34 @@ resource "elasticstack_elasticsearch_security_api_key" "test" {
89173
`, apiKeyName)
90174
}
91175

176+
func testAccResourceSecurityApiKeyCreateWithWorkflowRestriction(apiKeyName string) string {
177+
return fmt.Sprintf(`
178+
provider "elasticstack" {
179+
elasticsearch {}
180+
}
181+
182+
resource "elasticstack_elasticsearch_security_api_key" "test" {
183+
name = "%s"
184+
185+
role_descriptors = jsonencode({
186+
role-a = {
187+
cluster = ["all"]
188+
indices = [{
189+
names = ["index-a*"]
190+
privileges = ["read"]
191+
allow_restricted_indices = false
192+
}],
193+
restriction = {
194+
workflows = [ "search_application_query"]
195+
}
196+
}
197+
})
198+
199+
expiration = "1d"
200+
}
201+
`, apiKeyName)
202+
}
203+
92204
func checkResourceSecurityApiKeyDestroy(s *terraform.State) error {
93205
client, err := clients.NewAcceptanceTestingClient()
94206
if err != nil {

internal/models/models.go

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,21 @@ type Role struct {
8282
RusAs []string `json:"run_as,omitempty"`
8383
}
8484

85+
type ApiKeyRoleDescriptor struct {
86+
Name string `json:"-"`
87+
Applications []Application `json:"applications,omitempty"`
88+
Global map[string]interface{} `json:"global,omitempty"`
89+
Cluster []string `json:"cluster,omitempty"`
90+
Indices []IndexPerms `json:"indices,omitempty"`
91+
Metadata map[string]interface{} `json:"metadata,omitempty"`
92+
RusAs []string `json:"run_as,omitempty"`
93+
Restriction *Restriction `json:"restriction,omitempty"`
94+
}
95+
96+
type Restriction struct {
97+
Workflows []string `json:"workflows,omitempty"`
98+
}
99+
85100
type RoleMapping struct {
86101
Name string `json:"-"`
87102
Enabled bool `json:"enabled"`
@@ -92,20 +107,20 @@ type RoleMapping struct {
92107
}
93108

94109
type ApiKey struct {
95-
Name string `json:"name"`
96-
RolesDescriptors map[string]Role `json:"role_descriptors,omitempty"`
97-
Expiration string `json:"expiration,omitempty"`
98-
Metadata map[string]interface{} `json:"metadata,omitempty"`
110+
Name string `json:"name"`
111+
RolesDescriptors map[string]ApiKeyRoleDescriptor `json:"role_descriptors,omitempty"`
112+
Expiration string `json:"expiration,omitempty"`
113+
Metadata map[string]interface{} `json:"metadata,omitempty"`
99114
}
100115

101116
type ApiKeyResponse struct {
102117
ApiKey
103-
RolesDescriptors map[string]Role `json:"role_descriptors,omitempty"`
104-
Expiration int64 `json:"expiration,omitempty"`
105-
Id string `json:"id,omitempty"`
106-
Key string `json:"api_key,omitempty"`
107-
EncodedKey string `json:"encoded,omitempty"`
108-
Invalidated bool `json:"invalidated,omitempty"`
118+
RolesDescriptors map[string]ApiKeyRoleDescriptor `json:"role_descriptors,omitempty"`
119+
Expiration int64 `json:"expiration,omitempty"`
120+
Id string `json:"id,omitempty"`
121+
Key string `json:"api_key,omitempty"`
122+
EncodedKey string `json:"encoded,omitempty"`
123+
Invalidated bool `json:"invalidated,omitempty"`
109124
}
110125

111126
type IndexPerms struct {

0 commit comments

Comments
 (0)