Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for API Key management #193

Merged
merged 22 commits into from
Nov 21, 2022
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,5 @@ website/vendor

# Keep windows files with windows line endings
*.winfile eol=crlf

terraform-provider-elasticstack
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
- Add `elasticstack_elasticsearch_security_role` data source ([#177](https://github.com/elastic/terraform-provider-elasticstack/pull/177))
- 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))

### Fixed
- Remove unnecessary unsetting id on delete ([#174](https://github.com/elastic/terraform-provider-elasticstack/pull/174))
Expand Down
83 changes: 83 additions & 0 deletions docs/resources/elasticsearch_security_api_key.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
---
subcategory: "Security"
layout: ""
page_title: "Elasticstack: elasticstack_elasticsearch_security_api_key Resource"
description: |-
Creates an API key for access without requiring basic authentication. See, https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-create-api-key.html
---

# elasticstack_elasticsearch_security_api_key (Resource)

Creates an API key for access without requiring basic authentication. See, https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-create-api-key.html

## Example Usage

```terraform
resource "elasticstack_elasticsearch_security_api_key" "api_key" {
# Set the name
name = "My API key"

# Set the role descriptors
role_descriptors = jsonencode({
role-a = {
cluster = ["all"],
indices = [{
names = ["index-a*"],
privileges = ["read"]
}]
}
})

# 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
}
```

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

### Required

- `name` (String) Specifies the name for this API key.

### 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))
- `expiration` (String) Expiration time for the API key. By default, API keys never expire.
- `metadata` (String) Arbitrary metadata that you want to associate with the API key.
- `role_descriptors` (String) Role descriptors for this API key.

### Read-Only

- `api_key` (String) Generated API Key.
- `encoded` (String) API key credentials which is the Base64-encoding of the UTF-8 representation of the id and api_key joined by a colon (:).
- `expiration_timestamp` (Number) Expiration time in milliseconds for the API key. By default, API keys never expire.
- `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.

## Import

Import is not supported due to the generated API key only being visible on create.
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
resource "elasticstack_elasticsearch_security_api_key" "api_key" {
# Set the name
name = "My API key"

# Set the role descriptors
role_descriptors = jsonencode({
role-a = {
cluster = ["all"],
indices = [{
names = ["index-a*"],
privileges = ["read"]
}]
}
})

# 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
}
90 changes: 90 additions & 0 deletions internal/clients/security.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,3 +195,93 @@ func (a *ApiClient) DeleteElasticsearchRoleMapping(ctx context.Context, roleMapp

return nil
}

func (a *ApiClient) PutElasticsearchApiKey(apikey *models.ApiKey) (*models.ApiKeyResponse, diag.Diagnostics) {
var diags diag.Diagnostics
apikeyBytes, err := json.Marshal(apikey)
if err != nil {
return nil, diag.FromErr(err)
}

res, err := a.es.Security.CreateAPIKey(bytes.NewReader(apikeyBytes))
if err != nil {
return nil, diag.FromErr(err)
}
defer res.Body.Close()
if diags := utils.CheckError(res, "Unable to create apikey"); diags.HasError() {
return nil, diags
}

var apiKey models.ApiKeyResponse

if err := json.NewDecoder(res.Body).Decode(&apiKey); err != nil {
return nil, diag.FromErr(err)
}

return &apiKey, diags
}

func (a *ApiClient) GetElasticsearchApiKey(id string) (*models.ApiKeyResponse, diag.Diagnostics) {
var diags diag.Diagnostics
req := a.es.Security.GetAPIKey.WithID(id)
res, err := a.es.Security.GetAPIKey(req)
if err != nil {
return nil, diag.FromErr(err)
}
defer res.Body.Close()
if res.StatusCode == http.StatusNotFound {
diags := append(diags, diag.Diagnostic{
Severity: diag.Error,
Summary: "Unable to find an apikey in the cluster.",
Detail: fmt.Sprintf("Unable to get apikey: '%s' from the cluster.", id),
})
return nil, diags
}
if diags := utils.CheckError(res, "Unable to get an apikey."); diags.HasError() {
return nil, diags
}

// unmarshal our response to proper type
var apiKeys struct {
ApiKeys []models.ApiKeyResponse `json:"api_keys"`
}
if err := json.NewDecoder(res.Body).Decode(&apiKeys); err != nil {
return nil, diag.FromErr(err)
}

if len(apiKeys.ApiKeys) != 1 {
diags = append(diags, diag.Diagnostic{
Severity: diag.Error,
Summary: "Unable to find an apikey in the cluster",
Detail: fmt.Sprintf(`Unable to find "%s" apikey in the cluster`, id),
})
return nil, diags
}

apiKey := apiKeys.ApiKeys[0]
return &apiKey, diags
}

func (a *ApiClient) DeleteElasticsearchApiKey(id string) diag.Diagnostics {
var diags diag.Diagnostics

apiKeys := struct {
Ids []string `json:"ids"`
}{
[]string{id},
}

apikeyBytes, err := json.Marshal(apiKeys)
if err != nil {
return diag.FromErr(err)
}
res, err := a.es.Security.InvalidateAPIKey(bytes.NewReader(apikeyBytes))
if err != nil && res.IsError() {
return diag.FromErr(err)
}
defer res.Body.Close()
if diags := utils.CheckError(res, "Unable to delete an apikey"); diags.HasError() {
return diags
}
return diags
}
Loading