diff --git a/CHANGELOG.md b/CHANGELOG.md index 9201c9ce3..ff948f49b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ - Fix setting `id` for Fleet outputs and servers ([#666](https://github.com/elastic/terraform-provider-elasticstack/pull/666)) - Fix `elasticstack_fleet_enrollment_tokens` returning empty tokens in some case ([#683](https://github.com/elastic/terraform-provider-elasticstack/pull/683)) - Add support for Kibana synthetics private locations ([#696](https://github.com/elastic/terraform-provider-elasticstack/pull/696)) +- Support setting `restriction` in `elasticstack_elasticsearch_security_api_key` role definitions ([#577](https://github.com/elastic/terraform-provider-elasticstack/pull/577)) ## [0.11.4] - 2024-06-13 diff --git a/docs/resources/elasticsearch_security_api_key.md b/docs/resources/elasticsearch_security_api_key.md index 9d9c9b90e..972a3a558 100644 --- a/docs/resources/elasticsearch_security_api_key.md +++ b/docs/resources/elasticsearch_security_api_key.md @@ -41,6 +41,37 @@ resource "elasticstack_elasticsearch_security_api_key" "api_key" { }) } +# restriction on a role descriptor for an API key is supported since Elastic 8.9 +resource "elasticstack_elasticsearch_security_api_key" "api_key_with_restriction" { + # Set the name + name = "My API key" + # Set the role descriptors + role_descriptors = jsonencode({ + role-a = { + cluster = ["all"], + indices = [ + { + names = ["index-a*"], + privileges = ["read"] + } + ], + restriction = { + workflows = ["search_application_query"] + } + } + }) + + # Set the expiration for the API key + expiration = "1d" + + # Set the custom metadata for this user + metadata = jsonencode({ + "env" = "testing" + "open" = false + "number" = 49 + }) +} + output "api_key" { value = elasticstack_elasticsearch_security_api_key.api_key sensitive = true diff --git a/examples/resources/elasticstack_elasticsearch_security_api_key/resource.tf b/examples/resources/elasticstack_elasticsearch_security_api_key/resource.tf index 9ddad52ec..f974e130b 100644 --- a/examples/resources/elasticstack_elasticsearch_security_api_key/resource.tf +++ b/examples/resources/elasticstack_elasticsearch_security_api_key/resource.tf @@ -26,6 +26,37 @@ resource "elasticstack_elasticsearch_security_api_key" "api_key" { }) } +# restriction on a role descriptor for an API key is supported since Elastic 8.9 +resource "elasticstack_elasticsearch_security_api_key" "api_key_with_restriction" { + # Set the name + name = "My API key" + # Set the role descriptors + role_descriptors = jsonencode({ + role-a = { + cluster = ["all"], + indices = [ + { + names = ["index-a*"], + privileges = ["read"] + } + ], + restriction = { + workflows = ["search_application_query"] + } + } + }) + + # Set the expiration for the API key + expiration = "1d" + + # Set the custom metadata for this user + metadata = jsonencode({ + "env" = "testing" + "open" = false + "number" = 49 + }) +} + output "api_key" { value = elasticstack_elasticsearch_security_api_key.api_key sensitive = true diff --git a/internal/elasticsearch/security/api_key.go b/internal/elasticsearch/security/api_key.go index ef44aa1ad..fa8c54414 100644 --- a/internal/elasticsearch/security/api_key.go +++ b/internal/elasticsearch/security/api_key.go @@ -16,7 +16,8 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) -var APIKeyMinVersion = version.Must(version.NewVersion("8.0.0")) // Enabled in 8.0 +var APIKeyMinVersion = version.Must(version.NewVersion("8.0.0")) // Enabled in 8.0 +var APIKeyWithRestrictionMinVersion = version.Must(version.NewVersion("8.9.0")) // Enabled in 8.0 func ResourceApiKey() *schema.Resource { apikeySchema := map[string]*schema.Schema{ @@ -107,11 +108,33 @@ func resourceSecurityApiKeyCreate(ctx context.Context, d *schema.ResourceData, m } if v, ok := d.GetOk("role_descriptors"); ok { - role_descriptors := map[string]models.Role{} + role_descriptors := map[string]models.ApiKeyRoleDescriptor{} if err := json.NewDecoder(strings.NewReader(v.(string))).Decode(&role_descriptors); err != nil { return diag.FromErr(err) } apikey.RolesDescriptors = role_descriptors + + var hasRestriction = false + var keysWithRestrictions []string + + for key, roleDescriptor := range role_descriptors { + if roleDescriptor.Restriction != nil { + hasRestriction = true + keysWithRestrictions = append(keysWithRestrictions, key) + } + } + + if hasRestriction { + isSupported, diags := doesCurrentVersionSupportRestrictionOnApiKey(ctx, client) + + if diags.HasError() { + return diags + } + + if !isSupported { + 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, ", ")) + } + } } if v, ok := d.GetOk("metadata"); ok { @@ -155,6 +178,16 @@ func resourceSecurityApiKeyCreate(ctx context.Context, d *schema.ResourceData, m return resourceSecurityApiKeyRead(ctx, d, meta) } +func doesCurrentVersionSupportRestrictionOnApiKey(ctx context.Context, client *clients.ApiClient) (bool, diag.Diagnostics) { + currentVersion, diags := client.ServerVersion(ctx) + + if diags.HasError() { + return false, diags + } + + return currentVersion.GreaterThanOrEqual(APIKeyWithRestrictionMinVersion), nil +} + func resourceSecurityApiKeyUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { diags := diag.Diagnostics{diag.Diagnostic{ Severity: diag.Error, diff --git a/internal/elasticsearch/security/api_key_test.go b/internal/elasticsearch/security/api_key_test.go index 5bac86911..14b433927 100644 --- a/internal/elasticsearch/security/api_key_test.go +++ b/internal/elasticsearch/security/api_key_test.go @@ -1,9 +1,12 @@ package security_test import ( + "context" "encoding/json" "fmt" + "github.com/hashicorp/go-version" "reflect" + "regexp" "testing" "github.com/elastic/terraform-provider-elasticstack/internal/acctest" @@ -17,7 +20,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" ) -func TestAccResourceSecuritApiKey(t *testing.T) { +func TestAccResourceSecurityApiKey(t *testing.T) { // generate a random name apiKeyName := sdkacctest.RandStringFromCharSet(10, sdkacctest.CharSetAlphaNum) @@ -28,17 +31,17 @@ func TestAccResourceSecuritApiKey(t *testing.T) { Steps: []resource.TestStep{ { SkipFunc: versionutils.CheckIfVersionIsUnsupported(security.APIKeyMinVersion), - Config: testAccResourceSecuritApiKeyCreate(apiKeyName), + Config: testAccResourceSecurityApiKeyCreate(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.Role + var testRoleDescriptor map[string]models.ApiKeyRoleDescriptor if err := json.Unmarshal([]byte(testValue), &testRoleDescriptor); err != nil { return err } allowRestrictedIndices := false - expectedRoleDescriptor := map[string]models.Role{ + expectedRoleDescriptor := map[string]models.ApiKeyRoleDescriptor{ "role-a": { Cluster: []string{"all"}, Indices: []models.IndexPerms{{ @@ -64,7 +67,88 @@ func TestAccResourceSecuritApiKey(t *testing.T) { }) } -func testAccResourceSecuritApiKeyCreate(apiKeyName string) string { +func TestAccResourceSecurityApiKeyWithWorkflowRestriction(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(security.APIKeyWithRestrictionMinVersion), + Config: testAccResourceSecurityApiKeyCreateWithWorkflowRestriction(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 + } + + allowRestrictedIndices := false + expectedRoleDescriptor := map[string]models.ApiKeyRoleDescriptor{ + "role-a": { + Cluster: []string{"all"}, + Indices: []models.IndexPerms{{ + Names: []string{"index-a*"}, + Privileges: []string{"read"}, + AllowRestrictedIndices: &allowRestrictedIndices, + }}, + Restriction: &models.Restriction{Workflows: []string{"search_application_query"}}, + }, + } + + if !reflect.DeepEqual(testRoleDescriptor, expectedRoleDescriptor) { + return fmt.Errorf("%v doesn't match %v", testRoleDescriptor, expectedRoleDescriptor) + } + + return nil + }), + resource.TestCheckResourceAttrSet("elasticstack_elasticsearch_security_api_key.test", "expiration"), + resource.TestCheckResourceAttrSet("elasticstack_elasticsearch_security_api_key.test", "api_key"), + resource.TestCheckResourceAttrSet("elasticstack_elasticsearch_security_api_key.test", "encoded"), + ), + }, + }, + }) +} + +func TestAccResourceSecurityApiKeyWithWorkflowRestrictionOnElasticPre8_9_x(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: SkipWhenApiKeysAreNotSupportedOrRestrictionsAreSupported(security.APIKeyMinVersion, security.APIKeyWithRestrictionMinVersion), + Config: testAccResourceSecurityApiKeyCreateWithWorkflowRestriction(apiKeyName), + 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")), + }, + }, + }) +} + +func SkipWhenApiKeysAreNotSupportedOrRestrictionsAreSupported(minApiKeySupportedVersion *version.Version, minRestrictionSupportedVersion *version.Version) func() (bool, error) { + return func() (b bool, err error) { + client, err := clients.NewAcceptanceTestingClient() + if err != nil { + return false, err + } + serverVersion, diags := client.ServerVersion(context.Background()) + if diags.HasError() { + return false, fmt.Errorf("failed to parse the elasticsearch version %v", diags) + } + + return serverVersion.LessThan(minApiKeySupportedVersion) || serverVersion.GreaterThanOrEqual(minRestrictionSupportedVersion), nil + } +} + +func testAccResourceSecurityApiKeyCreate(apiKeyName string) string { return fmt.Sprintf(` provider "elasticstack" { elasticsearch {} @@ -89,6 +173,34 @@ resource "elasticstack_elasticsearch_security_api_key" "test" { `, apiKeyName) } +func testAccResourceSecurityApiKeyCreateWithWorkflowRestriction(apiKeyName string) string { + return fmt.Sprintf(` +provider "elasticstack" { + elasticsearch {} +} + +resource "elasticstack_elasticsearch_security_api_key" "test" { + name = "%s" + + role_descriptors = jsonencode({ + role-a = { + cluster = ["all"] + indices = [{ + names = ["index-a*"] + privileges = ["read"] + allow_restricted_indices = false + }], + restriction = { + workflows = [ "search_application_query"] + } + } + }) + + expiration = "1d" +} + `, apiKeyName) +} + func checkResourceSecurityApiKeyDestroy(s *terraform.State) error { client, err := clients.NewAcceptanceTestingClient() if err != nil { diff --git a/internal/models/models.go b/internal/models/models.go index 4f4918a14..dc8af8937 100644 --- a/internal/models/models.go +++ b/internal/models/models.go @@ -82,6 +82,21 @@ type Role struct { RusAs []string `json:"run_as,omitempty"` } +type ApiKeyRoleDescriptor struct { + Name string `json:"-"` + Applications []Application `json:"applications,omitempty"` + Global map[string]interface{} `json:"global,omitempty"` + Cluster []string `json:"cluster,omitempty"` + Indices []IndexPerms `json:"indices,omitempty"` + Metadata map[string]interface{} `json:"metadata,omitempty"` + RusAs []string `json:"run_as,omitempty"` + Restriction *Restriction `json:"restriction,omitempty"` +} + +type Restriction struct { + Workflows []string `json:"workflows,omitempty"` +} + type RoleMapping struct { Name string `json:"-"` Enabled bool `json:"enabled"` @@ -92,20 +107,20 @@ type RoleMapping struct { } type ApiKey struct { - Name string `json:"name"` - RolesDescriptors map[string]Role `json:"role_descriptors,omitempty"` - Expiration string `json:"expiration,omitempty"` - Metadata map[string]interface{} `json:"metadata,omitempty"` + Name string `json:"name"` + RolesDescriptors map[string]ApiKeyRoleDescriptor `json:"role_descriptors,omitempty"` + Expiration string `json:"expiration,omitempty"` + Metadata map[string]interface{} `json:"metadata,omitempty"` } type ApiKeyResponse struct { ApiKey - RolesDescriptors map[string]Role `json:"role_descriptors,omitempty"` - Expiration int64 `json:"expiration,omitempty"` - Id string `json:"id,omitempty"` - Key string `json:"api_key,omitempty"` - EncodedKey string `json:"encoded,omitempty"` - Invalidated bool `json:"invalidated,omitempty"` + RolesDescriptors map[string]ApiKeyRoleDescriptor `json:"role_descriptors,omitempty"` + Expiration int64 `json:"expiration,omitempty"` + Id string `json:"id,omitempty"` + Key string `json:"api_key,omitempty"` + EncodedKey string `json:"encoded,omitempty"` + Invalidated bool `json:"invalidated,omitempty"` } type IndexPerms struct {