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 @@ -7,6 +7,7 @@
- Add `elasticstack_elasticsearch_security_role_mapping` data source ([#178](https://github.com/elastic/terraform-provider-elasticstack/pull/178))
- Apply `total_shards_per_node` setting in `allocate` action in ILM. Supported from Elasticsearch version **7.16** ([#112](https://github.com/elastic/terraform-provider-elasticstack/issues/112))
- Add `elasticstack_elasticsearch_security_api_key` resource ([#193](https://github.com/elastic/terraform-provider-elasticstack/pull/193))
- Add `elasticstack_elasticsearch_security_system_user` resource to manage built-in user ([#188](https://github.com/elastic/terraform-provider-elasticstack/pull/188))
- Add `unassigned_node_left_delayed_timeout` to index resource ([#196](https://github.com/elastic/terraform-provider-elasticstack/pull/196))

### Fixed
Expand Down
64 changes: 64 additions & 0 deletions docs/resources/elasticsearch_security_system_user.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
---
subcategory: "Security"
layout: ""
page_title: "Elasticstack: elasticstack_elasticsearch_security_system_user Resource"
description: |-
Updates system user's password and enablement.
---

# Resource: elasticstack_elasticsearch_security_system_user

Updates system user's password and enablement. See, https://www.elastic.co/guide/en/elasticsearch/reference/current/built-in-users.html
Since this resource is to manage built-in users, destroy will not delete the underlying Elasticsearch and will only remove it from Terraform state.

## Example Usage

```terraform
provider "elasticstack" {
elasticsearch {}
}

resource "elasticstack_elasticsearch_security_system_user" "kibana_system" {
username = "kibana_system"

// For details on how to generate the hashed password see https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-put-user.html#security-api-put-user-request-body
password_hash = "$2a$10$rMZe6TdsUwBX/TA8vRDz0OLwKAZeCzXM4jT3tfCjpSTB8HoFuq8xO"

elasticsearch_connection {
endpoints = ["http://localhost:9200"]
username = "elastic"
password = "changeme"
}
}
```

<!-- schema generated by tfplugindocs -->
## Schema

### Required

- `username` (String) An identifier for the system user (see https://www.elastic.co/guide/en/elasticsearch/reference/current/built-in-users.html).

### Optional

- `elasticsearch_connection` (Block List, Max: 1) Used to establish connection to Elasticsearch server. Overrides environment variables if present. (see [below for nested schema](#nestedblock--elasticsearch_connection))
- `enabled` (Boolean) Specifies whether the user is enabled. The default value is true.
- `password` (String, Sensitive) The user’s password. Passwords must be at least 6 characters long.
- `password_hash` (String, Sensitive) A hash of the user’s password. This must be produced using the same hashing algorithm as has been configured for password storage (see https://www.elastic.co/guide/en/elasticsearch/reference/current/security-settings.html#hashing-settings).

### Read-Only

- `id` (String) Internal identifier of the resource

<a id="nestedblock--elasticsearch_connection"></a>
### Nested Schema for `elasticsearch_connection`

Optional:

- `api_key` (String, Sensitive) API Key to use for authentication to Elasticsearch
- `ca_data` (String) PEM-encoded custom Certificate Authority certificate
- `ca_file` (String) Path to a custom Certificate Authority certificate
- `endpoints` (List of String, Sensitive) A list of endpoints the Terraform provider will point to. They must include the http(s) schema and port number.
- `insecure` (Boolean) Disable TLS certificate validation
- `password` (String, Sensitive) A password to use for API authentication to Elasticsearch.
- `username` (String) A username to use for API authentication to Elasticsearch.
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
provider "elasticstack" {
elasticsearch {}
}

resource "elasticstack_elasticsearch_security_system_user" "kibana_system" {
username = "kibana_system"

// For details on how to generate the hashed password see https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-put-user.html#security-api-put-user-request-body
password_hash = "$2a$10$rMZe6TdsUwBX/TA8vRDz0OLwKAZeCzXM4jT3tfCjpSTB8HoFuq8xO"

elasticsearch_connection {
endpoints = ["http://localhost:9200"]
username = "elastic"
password = "changeme"
}
}
47 changes: 47 additions & 0 deletions internal/clients/security.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,53 @@ func (a *ApiClient) DeleteElasticsearchUser(ctx context.Context, username string
return diags
}

func (a *ApiClient) EnableElasticsearchUser(ctx context.Context, username string) diag.Diagnostics {
var diags diag.Diagnostics
res, err := a.es.Security.EnableUser(username, a.es.Security.EnableUser.WithContext(ctx))
if err != nil {
return diag.FromErr(err)
}
defer res.Body.Close()
if diags := utils.CheckError(res, "Unable to enable system user"); diags.HasError() {
return diags
}
return diags
}

func (a *ApiClient) DisableElasticsearchUser(ctx context.Context, username string) diag.Diagnostics {
var diags diag.Diagnostics
res, err := a.es.Security.DisableUser(username, a.es.Security.DisableUser.WithContext(ctx))
if err != nil {
return diag.FromErr(err)
}
defer res.Body.Close()
if diags := utils.CheckError(res, "Unable to disable system user"); diags.HasError() {
return diags
}
return diags
}

func (a *ApiClient) ChangeElasticsearchUserPassword(ctx context.Context, username string, userPassword *models.UserPassword) diag.Diagnostics {
var diags diag.Diagnostics
userPasswordBytes, err := json.Marshal(userPassword)
if err != nil {
return diag.FromErr(err)
}
res, err := a.es.Security.ChangePassword(
bytes.NewReader(userPasswordBytes),
a.es.Security.ChangePassword.WithUsername(username),
a.es.Security.ChangePassword.WithContext(ctx),
)
if err != nil {
return diag.FromErr(err)
}
defer res.Body.Close()
if diags := utils.CheckError(res, "Unable to change user's password"); diags.HasError() {
return diags
}
return diags
}

func (a *ApiClient) PutElasticsearchRole(ctx context.Context, role *models.Role) diag.Diagnostics {
var diags diag.Diagnostics

Expand Down
161 changes: 161 additions & 0 deletions internal/elasticsearch/security/system_user.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
package security

import (
"context"
"fmt"
"regexp"

"github.com/elastic/terraform-provider-elasticstack/internal/clients"
"github.com/elastic/terraform-provider-elasticstack/internal/models"
"github.com/elastic/terraform-provider-elasticstack/internal/utils"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
)

func ResourceSystemUser() *schema.Resource {
userSchema := map[string]*schema.Schema{
"id": {
Description: "Internal identifier of the resource",
Type: schema.TypeString,
Computed: true,
},
"username": {
Description: "An identifier for the system user (see https://www.elastic.co/guide/en/elasticsearch/reference/current/built-in-users.html).",
Type: schema.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: validation.All(
validation.StringLenBetween(1, 1024),
validation.StringMatch(regexp.MustCompile(`^[[:graph:]]+$`), "must contain alphanumeric characters (a-z, A-Z, 0-9), spaces, punctuation, and printable symbols in the Basic Latin (ASCII) block. Leading or trailing whitespace is not allowed"),
),
},
"password": {
Description: "The user’s password. Passwords must be at least 6 characters long.",
Type: schema.TypeString,
Optional: true,
Sensitive: true,
ValidateFunc: validation.StringLenBetween(6, 128),
ConflictsWith: []string{"password_hash"},
},
"password_hash": {
Description: "A hash of the user’s password. This must be produced using the same hashing algorithm as has been configured for password storage (see https://www.elastic.co/guide/en/elasticsearch/reference/current/security-settings.html#hashing-settings).",
Type: schema.TypeString,
Optional: true,
Sensitive: true,
ValidateFunc: validation.StringLenBetween(6, 128),
ConflictsWith: []string{"password"},
},
"enabled": {
Description: "Specifies whether the user is enabled. The default value is true.",
Type: schema.TypeBool,
Optional: true,
Default: true,
},
}

utils.AddConnectionSchema(userSchema)

return &schema.Resource{
Description: "Updates system user's password and enablement. See, https://www.elastic.co/guide/en/elasticsearch/reference/current/built-in-users.html",

CreateContext: resourceSecuritySystemUserPut,
UpdateContext: resourceSecuritySystemUserPut,
ReadContext: resourceSecuritySystemUserRead,
DeleteContext: resourceSecuritySystemUserDelete,

Schema: userSchema,
}
}

func resourceSecuritySystemUserPut(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client, err := clients.NewApiClient(d, meta)
if err != nil {
return diag.FromErr(err)
}
usernameId := d.Get("username").(string)
id, diags := client.ID(ctx, usernameId)
if diags.HasError() {
return diags
}

user, diags := client.GetElasticsearchUser(ctx, usernameId)
if diags.HasError() {
return diags
}
if user == nil || !user.IsSystemUser() {
return diag.Errorf(`System user "%s" not found`, usernameId)
}

var userPassword models.UserPassword
if v, ok := d.GetOk("password"); ok && d.HasChange("password") {
password := v.(string)
userPassword.Password = &password
}
if v, ok := d.GetOk("password_hash"); ok && d.HasChange("password_hash") {
pass_hash := v.(string)
userPassword.PasswordHash = &pass_hash
}
if userPassword.Password != nil || userPassword.PasswordHash != nil {
if diags := client.ChangeElasticsearchUserPassword(ctx, usernameId, &userPassword); diags.HasError() {
return diags
}
}

if d.HasChange("enabled") {
if d.Get("enabled").(bool) {
if diags := client.EnableElasticsearchUser(ctx, usernameId); diags.HasError() {
return diags
}
} else {
if diags := client.DisableElasticsearchUser(ctx, usernameId); diags.HasError() {
return diags
}
}
}

d.SetId(id.String())
return resourceSecuritySystemUserRead(ctx, d, meta)
}

func resourceSecuritySystemUserRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
var diags diag.Diagnostics
client, err := clients.NewApiClient(d, meta)
if err != nil {
return diag.FromErr(err)
}
compId, diags := clients.CompositeIdFromStr(d.Id())
if diags.HasError() {
return diags
}
usernameId := compId.ResourceId

user, diags := client.GetElasticsearchUser(ctx, usernameId)
if diags == nil && (user == nil || !user.IsSystemUser()) {
tflog.Warn(ctx, fmt.Sprintf(`System user "%s" not found, removing from state`, compId.ResourceId))
d.SetId("")
return diags
}
if diags.HasError() {
return diags
}

if err := d.Set("username", usernameId); err != nil {
return diag.FromErr(err)
}
if err := d.Set("enabled", user.Enabled); err != nil {
return diag.FromErr(err)
}

return diags
}

func resourceSecuritySystemUserDelete(ctx context.Context, d *schema.ResourceData, _ interface{}) diag.Diagnostics {
compId, diags := clients.CompositeIdFromStr(d.Id())
if diags.HasError() {
return diags
}
tflog.Warn(ctx, fmt.Sprintf(`System user '%s' is not deletable, just removing from state`, compId.ResourceId))
return nil
}
77 changes: 77 additions & 0 deletions internal/elasticsearch/security/system_user_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package security_test

import (
"regexp"
"testing"

"github.com/elastic/terraform-provider-elasticstack/internal/acctest"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
)

func TestAccResourceSecuritySystemUser(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { acctest.PreCheck(t) },
ProviderFactories: acctest.Providers,
Steps: []resource.TestStep{
{
Config: testAccResourceSecuritySystemUserCreate,
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("elasticstack_elasticsearch_security_system_user.kibana_system", "username", "kibana_system"),
resource.TestCheckResourceAttr("elasticstack_elasticsearch_security_system_user.kibana_system", "enabled", "true"),
),
},
{
Config: testAccResourceSecuritySystemUserUpdate,
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("elasticstack_elasticsearch_security_system_user.kibana_system", "username", "kibana_system"),
resource.TestCheckResourceAttr("elasticstack_elasticsearch_security_system_user.kibana_system", "enabled", "false"),
),
},
},
})
}

func TestAccResourceSecuritySystemUserNotFound(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { acctest.PreCheck(t) },
ProviderFactories: acctest.Providers,
Steps: []resource.TestStep{
{
Config: testAccResourceSecuritySystemUserNotFound,
ExpectError: regexp.MustCompile(`System user "not_system_user" not found`),
},
},
})
}

const testAccResourceSecuritySystemUserCreate = `
provider "elasticstack" {
elasticsearch {}
}

resource "elasticstack_elasticsearch_security_system_user" "kibana_system" {
username = "kibana_system"
password = "new_password"
}
`
const testAccResourceSecuritySystemUserUpdate = `
provider "elasticstack" {
elasticsearch {}
}

resource "elasticstack_elasticsearch_security_system_user" "kibana_system" {
username = "kibana_system"
password = "new_password"
enabled = false
}
`
const testAccResourceSecuritySystemUserNotFound = `
provider "elasticstack" {
elasticsearch {}
}

resource "elasticstack_elasticsearch_security_system_user" "test" {
username = "not_system_user"
password = "new_password"
}
`
Loading