Skip to content

Commit

Permalink
Add support for API Key management (#193)
Browse files Browse the repository at this point in the history
* Impliment create/read/update

* Impliment delete (invalidate)

* Ignore binary

* Read only fields

* Docs

* Docs

* Acceptance test

* Updating models for differences between expiration and role types

* PR feedback

Co-authored-by: Toby Brain <[email protected]>

* Changelog

* Update client in test

* Replacing string comparion with DeepEqual when testing role_descriptors

* Removing ApiKeyRole and using indices instead of index field

* Skipping unsupported versions in test matrix

* fmt

* Removing now redundant comment

* Handling json.marshal in test

* docs

* Testing an open matrix

* Missed ref

* Re-enable version guard for test

* Bumping SkipFunc

Co-authored-by: Toby Brain <[email protected]>
  • Loading branch information
Adrian Bruinhout and tobio authored Nov 21, 2022
1 parent d666d0d commit a7c3cb4
Show file tree
Hide file tree
Showing 10 changed files with 599 additions and 0 deletions.
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

0 comments on commit a7c3cb4

Please sign in to comment.