Skip to content

Commit a7c3cb4

Browse files
Adrian Bruinhouttobio
andauthored
Add support for API Key management (#193)
* 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]>
1 parent d666d0d commit a7c3cb4

File tree

10 files changed

+599
-0
lines changed

10 files changed

+599
-0
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,5 @@ website/vendor
3535

3636
# Keep windows files with windows line endings
3737
*.winfile eol=crlf
38+
39+
terraform-provider-elasticstack

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
- Add `elasticstack_elasticsearch_security_role` data source ([#177](https://github.com/elastic/terraform-provider-elasticstack/pull/177))
77
- Add `elasticstack_elasticsearch_security_role_mapping` data source ([#178](https://github.com/elastic/terraform-provider-elasticstack/pull/178))
88
- 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))
9+
- Add `elasticstack_elasticsearch_security_api_key` resource ([#193](https://github.com/elastic/terraform-provider-elasticstack/pull/193))
910

1011
### Fixed
1112
- Remove unnecessary unsetting id on delete ([#174](https://github.com/elastic/terraform-provider-elasticstack/pull/174))
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
---
2+
subcategory: "Security"
3+
layout: ""
4+
page_title: "Elasticstack: elasticstack_elasticsearch_security_api_key Resource"
5+
description: |-
6+
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
7+
---
8+
9+
# elasticstack_elasticsearch_security_api_key (Resource)
10+
11+
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
12+
13+
## Example Usage
14+
15+
```terraform
16+
resource "elasticstack_elasticsearch_security_api_key" "api_key" {
17+
# Set the name
18+
name = "My API key"
19+
20+
# Set the role descriptors
21+
role_descriptors = jsonencode({
22+
role-a = {
23+
cluster = ["all"],
24+
indices = [{
25+
names = ["index-a*"],
26+
privileges = ["read"]
27+
}]
28+
}
29+
})
30+
31+
# Set the expiration for the API key
32+
expiration = "1d"
33+
34+
# Set the custom metadata for this user
35+
metadata = jsonencode({
36+
"env" = "testing"
37+
"open" = false
38+
"number" = 49
39+
})
40+
}
41+
42+
output "api_key" {
43+
value = elasticstack_elasticsearch_security_api_key.api_key
44+
}
45+
```
46+
47+
<!-- schema generated by tfplugindocs -->
48+
## Schema
49+
50+
### Required
51+
52+
- `name` (String) Specifies the name for this API key.
53+
54+
### Optional
55+
56+
- `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))
57+
- `expiration` (String) Expiration time for the API key. By default, API keys never expire.
58+
- `metadata` (String) Arbitrary metadata that you want to associate with the API key.
59+
- `role_descriptors` (String) Role descriptors for this API key.
60+
61+
### Read-Only
62+
63+
- `api_key` (String) Generated API Key.
64+
- `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 (:).
65+
- `expiration_timestamp` (Number) Expiration time in milliseconds for the API key. By default, API keys never expire.
66+
- `id` (String) Internal identifier of the resource.
67+
68+
<a id="nestedblock--elasticsearch_connection"></a>
69+
### Nested Schema for `elasticsearch_connection`
70+
71+
Optional:
72+
73+
- `api_key` (String, Sensitive) API Key to use for authentication to Elasticsearch
74+
- `ca_data` (String) PEM-encoded custom Certificate Authority certificate
75+
- `ca_file` (String) Path to a custom Certificate Authority certificate
76+
- `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.
77+
- `insecure` (Boolean) Disable TLS certificate validation
78+
- `password` (String, Sensitive) A password to use for API authentication to Elasticsearch.
79+
- `username` (String) A username to use for API authentication to Elasticsearch.
80+
81+
## Import
82+
83+
Import is not supported due to the generated API key only being visible on create.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
resource "elasticstack_elasticsearch_security_api_key" "api_key" {
2+
# Set the name
3+
name = "My API key"
4+
5+
# Set the role descriptors
6+
role_descriptors = jsonencode({
7+
role-a = {
8+
cluster = ["all"],
9+
indices = [{
10+
names = ["index-a*"],
11+
privileges = ["read"]
12+
}]
13+
}
14+
})
15+
16+
# Set the expiration for the API key
17+
expiration = "1d"
18+
19+
# Set the custom metadata for this user
20+
metadata = jsonencode({
21+
"env" = "testing"
22+
"open" = false
23+
"number" = 49
24+
})
25+
}
26+
27+
output "api_key" {
28+
value = elasticstack_elasticsearch_security_api_key.api_key
29+
}

internal/clients/security.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,3 +195,93 @@ func (a *ApiClient) DeleteElasticsearchRoleMapping(ctx context.Context, roleMapp
195195

196196
return nil
197197
}
198+
199+
func (a *ApiClient) PutElasticsearchApiKey(apikey *models.ApiKey) (*models.ApiKeyResponse, diag.Diagnostics) {
200+
var diags diag.Diagnostics
201+
apikeyBytes, err := json.Marshal(apikey)
202+
if err != nil {
203+
return nil, diag.FromErr(err)
204+
}
205+
206+
res, err := a.es.Security.CreateAPIKey(bytes.NewReader(apikeyBytes))
207+
if err != nil {
208+
return nil, diag.FromErr(err)
209+
}
210+
defer res.Body.Close()
211+
if diags := utils.CheckError(res, "Unable to create apikey"); diags.HasError() {
212+
return nil, diags
213+
}
214+
215+
var apiKey models.ApiKeyResponse
216+
217+
if err := json.NewDecoder(res.Body).Decode(&apiKey); err != nil {
218+
return nil, diag.FromErr(err)
219+
}
220+
221+
return &apiKey, diags
222+
}
223+
224+
func (a *ApiClient) GetElasticsearchApiKey(id string) (*models.ApiKeyResponse, diag.Diagnostics) {
225+
var diags diag.Diagnostics
226+
req := a.es.Security.GetAPIKey.WithID(id)
227+
res, err := a.es.Security.GetAPIKey(req)
228+
if err != nil {
229+
return nil, diag.FromErr(err)
230+
}
231+
defer res.Body.Close()
232+
if res.StatusCode == http.StatusNotFound {
233+
diags := append(diags, diag.Diagnostic{
234+
Severity: diag.Error,
235+
Summary: "Unable to find an apikey in the cluster.",
236+
Detail: fmt.Sprintf("Unable to get apikey: '%s' from the cluster.", id),
237+
})
238+
return nil, diags
239+
}
240+
if diags := utils.CheckError(res, "Unable to get an apikey."); diags.HasError() {
241+
return nil, diags
242+
}
243+
244+
// unmarshal our response to proper type
245+
var apiKeys struct {
246+
ApiKeys []models.ApiKeyResponse `json:"api_keys"`
247+
}
248+
if err := json.NewDecoder(res.Body).Decode(&apiKeys); err != nil {
249+
return nil, diag.FromErr(err)
250+
}
251+
252+
if len(apiKeys.ApiKeys) != 1 {
253+
diags = append(diags, diag.Diagnostic{
254+
Severity: diag.Error,
255+
Summary: "Unable to find an apikey in the cluster",
256+
Detail: fmt.Sprintf(`Unable to find "%s" apikey in the cluster`, id),
257+
})
258+
return nil, diags
259+
}
260+
261+
apiKey := apiKeys.ApiKeys[0]
262+
return &apiKey, diags
263+
}
264+
265+
func (a *ApiClient) DeleteElasticsearchApiKey(id string) diag.Diagnostics {
266+
var diags diag.Diagnostics
267+
268+
apiKeys := struct {
269+
Ids []string `json:"ids"`
270+
}{
271+
[]string{id},
272+
}
273+
274+
apikeyBytes, err := json.Marshal(apiKeys)
275+
if err != nil {
276+
return diag.FromErr(err)
277+
}
278+
res, err := a.es.Security.InvalidateAPIKey(bytes.NewReader(apikeyBytes))
279+
if err != nil && res.IsError() {
280+
return diag.FromErr(err)
281+
}
282+
defer res.Body.Close()
283+
if diags := utils.CheckError(res, "Unable to delete an apikey"); diags.HasError() {
284+
return diags
285+
}
286+
return diags
287+
}

0 commit comments

Comments
 (0)