Skip to content

Commit 0e8ec75

Browse files
committed
Add security list resource
1 parent 15e62bf commit 0e8ec75

File tree

18 files changed

+1027
-1
lines changed

18 files changed

+1027
-1
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
resource "elasticstack_kibana_security_list" "ip_list" {
2+
space_id = "default"
3+
name = "Trusted IP Addresses"
4+
description = "List of trusted IP addresses for security rules"
5+
type = "ip"
6+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
resource "elasticstack_kibana_security_list" "keyword_list" {
2+
space_id = "security"
3+
list_id = "custom-keywords"
4+
name = "Custom Keywords"
5+
description = "Custom keyword list for detection rules"
6+
type = "keyword"
7+
}
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
package kibana_oapi
2+
3+
import (
4+
"context"
5+
"net/http"
6+
7+
"github.com/elastic/terraform-provider-elasticstack/generated/kbapi"
8+
"github.com/elastic/terraform-provider-elasticstack/internal/diagutil"
9+
"github.com/hashicorp/terraform-plugin-framework/diag"
10+
)
11+
12+
// CreateListIndex creates the .lists and .items data streams for a space if they don't exist.
13+
// This is required before any list operations can be performed.
14+
func CreateListIndex(ctx context.Context, client *Client, spaceId string) diag.Diagnostics {
15+
resp, err := client.API.CreateListIndexWithResponse(ctx, kbapi.SpaceId(spaceId))
16+
if err != nil {
17+
return diagutil.FrameworkDiagFromError(err)
18+
}
19+
20+
switch resp.StatusCode() {
21+
case http.StatusOK:
22+
return nil
23+
default:
24+
return reportUnknownError(resp.StatusCode(), resp.Body)
25+
}
26+
}
27+
28+
// GetList reads a security list from the API by ID
29+
func GetList(ctx context.Context, client *Client, spaceId string, params *kbapi.ReadListParams) (*kbapi.ReadListResponse, diag.Diagnostics) {
30+
resp, err := client.API.ReadListWithResponse(ctx, kbapi.SpaceId(spaceId), params)
31+
if err != nil {
32+
return nil, diagutil.FrameworkDiagFromError(err)
33+
}
34+
35+
switch resp.StatusCode() {
36+
case http.StatusOK:
37+
return resp, nil
38+
case http.StatusNotFound:
39+
return nil, nil
40+
default:
41+
return nil, reportUnknownError(resp.StatusCode(), resp.Body)
42+
}
43+
}
44+
45+
// CreateList creates a new security list.
46+
func CreateList(ctx context.Context, client *Client, spaceId string, body kbapi.CreateListJSONRequestBody) (*kbapi.CreateListResponse, diag.Diagnostics) {
47+
resp, err := client.API.CreateListWithResponse(ctx, kbapi.SpaceId(spaceId), body)
48+
if err != nil {
49+
return nil, diagutil.FrameworkDiagFromError(err)
50+
}
51+
52+
switch resp.StatusCode() {
53+
case http.StatusOK:
54+
return resp, nil
55+
default:
56+
return nil, reportUnknownError(resp.StatusCode(), resp.Body)
57+
}
58+
}
59+
60+
// UpdateList updates an existing security list.
61+
func UpdateList(ctx context.Context, client *Client, spaceId string, body kbapi.UpdateListJSONRequestBody) (*kbapi.UpdateListResponse, diag.Diagnostics) {
62+
resp, err := client.API.UpdateListWithResponse(ctx, kbapi.SpaceId(spaceId), body)
63+
if err != nil {
64+
return nil, diagutil.FrameworkDiagFromError(err)
65+
}
66+
67+
switch resp.StatusCode() {
68+
case http.StatusOK:
69+
return resp, nil
70+
default:
71+
return nil, reportUnknownError(resp.StatusCode(), resp.Body)
72+
}
73+
}
74+
75+
// DeleteList deletes an existing security list.
76+
func DeleteList(ctx context.Context, client *Client, spaceId string, params *kbapi.DeleteListParams) diag.Diagnostics {
77+
resp, err := client.API.DeleteListWithResponse(ctx, kbapi.SpaceId(spaceId), params)
78+
if err != nil {
79+
return diagutil.FrameworkDiagFromError(err)
80+
}
81+
82+
switch resp.StatusCode() {
83+
case http.StatusOK:
84+
return nil
85+
case http.StatusNotFound:
86+
return nil
87+
default:
88+
return reportUnknownError(resp.StatusCode(), resp.Body)
89+
}
90+
}
91+
92+
// GetListItem reads a security list item from the API by ID or list_id and value
93+
func GetListItem(ctx context.Context, client *Client, spaceId string, params *kbapi.ReadListItemParams) (*kbapi.ReadListItemResponse, diag.Diagnostics) {
94+
resp, err := client.API.ReadListItemWithResponse(ctx, kbapi.SpaceId(spaceId), params)
95+
if err != nil {
96+
return nil, diagutil.FrameworkDiagFromError(err)
97+
}
98+
99+
switch resp.StatusCode() {
100+
case http.StatusOK:
101+
return resp, nil
102+
case http.StatusNotFound:
103+
return nil, nil
104+
default:
105+
return nil, reportUnknownError(resp.StatusCode(), resp.Body)
106+
}
107+
}
108+
109+
// CreateListItem creates a new security list item.
110+
func CreateListItem(ctx context.Context, client *Client, spaceId string, body kbapi.CreateListItemJSONRequestBody) (*kbapi.CreateListItemResponse, diag.Diagnostics) {
111+
resp, err := client.API.CreateListItemWithResponse(ctx, kbapi.SpaceId(spaceId), body)
112+
if err != nil {
113+
return nil, diagutil.FrameworkDiagFromError(err)
114+
}
115+
116+
switch resp.StatusCode() {
117+
case http.StatusOK:
118+
return resp, nil
119+
default:
120+
return nil, reportUnknownError(resp.StatusCode(), resp.Body)
121+
}
122+
}
123+
124+
// UpdateListItem updates an existing security list item.
125+
func UpdateListItem(ctx context.Context, client *Client, spaceId string, body kbapi.UpdateListItemJSONRequestBody) (*kbapi.UpdateListItemResponse, diag.Diagnostics) {
126+
resp, err := client.API.UpdateListItemWithResponse(ctx, kbapi.SpaceId(spaceId), body)
127+
if err != nil {
128+
return nil, diagutil.FrameworkDiagFromError(err)
129+
}
130+
131+
switch resp.StatusCode() {
132+
case http.StatusOK:
133+
return resp, nil
134+
default:
135+
return nil, reportUnknownError(resp.StatusCode(), resp.Body)
136+
}
137+
}
138+
139+
// DeleteListItem deletes an existing security list item.
140+
func DeleteListItem(ctx context.Context, client *Client, spaceId string, params *kbapi.DeleteListItemParams) diag.Diagnostics {
141+
resp, err := client.API.DeleteListItemWithResponse(ctx, kbapi.SpaceId(spaceId), params)
142+
if err != nil {
143+
return diagutil.FrameworkDiagFromError(err)
144+
}
145+
146+
switch resp.StatusCode() {
147+
case http.StatusOK:
148+
return nil
149+
case http.StatusNotFound:
150+
return nil
151+
default:
152+
return reportUnknownError(resp.StatusCode(), resp.Body)
153+
}
154+
}
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
package security_list_test
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/elastic/terraform-provider-elasticstack/internal/acctest"
8+
"github.com/elastic/terraform-provider-elasticstack/internal/clients"
9+
"github.com/elastic/terraform-provider-elasticstack/internal/clients/kibana_oapi"
10+
"github.com/google/uuid"
11+
"github.com/hashicorp/terraform-plugin-testing/config"
12+
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
13+
)
14+
15+
func ensureListIndexExists(t *testing.T) {
16+
client, err := clients.NewAcceptanceTestingClient()
17+
if err != nil {
18+
t.Fatalf("Failed to create client: %v", err)
19+
}
20+
21+
kibanaClient, err := client.GetKibanaOapiClient()
22+
if err != nil {
23+
t.Fatalf("Failed to get Kibana client: %v", err)
24+
}
25+
26+
diags := kibana_oapi.CreateListIndex(context.Background(), kibanaClient, "default")
27+
if diags.HasError() {
28+
// It's OK if it already exists, we'll only fail on other errors
29+
for _, d := range diags {
30+
if d.Summary() != "Unexpected status code from server: got HTTP 409" {
31+
t.Fatalf("Failed to create list index: %v", d.Detail())
32+
}
33+
}
34+
}
35+
}
36+
37+
func TestAccResourceSecurityList(t *testing.T) {
38+
listID := "test-list-" + uuid.New().String()
39+
resource.Test(t, resource.TestCase{
40+
PreCheck: func() {
41+
acctest.PreCheck(t)
42+
ensureListIndexExists(t)
43+
},
44+
ProtoV6ProviderFactories: acctest.Providers,
45+
Steps: []resource.TestStep{
46+
{ // Create
47+
ConfigDirectory: acctest.NamedTestCaseDirectory("create"),
48+
ConfigVariables: config.Variables{
49+
"list_id": config.StringVariable(listID),
50+
"name": config.StringVariable("Test Security List"),
51+
"description": config.StringVariable("A test security list for IP addresses"),
52+
"type": config.StringVariable("ip"),
53+
},
54+
Check: resource.ComposeTestCheckFunc(
55+
resource.TestCheckResourceAttrSet("elasticstack_kibana_security_list.test", "id"),
56+
resource.TestCheckResourceAttr("elasticstack_kibana_security_list.test", "name", "Test Security List"),
57+
resource.TestCheckResourceAttr("elasticstack_kibana_security_list.test", "description", "A test security list for IP addresses"),
58+
resource.TestCheckResourceAttr("elasticstack_kibana_security_list.test", "type", "ip"),
59+
resource.TestCheckResourceAttrSet("elasticstack_kibana_security_list.test", "created_at"),
60+
resource.TestCheckResourceAttrSet("elasticstack_kibana_security_list.test", "created_by"),
61+
resource.TestCheckResourceAttrSet("elasticstack_kibana_security_list.test", "updated_at"),
62+
resource.TestCheckResourceAttrSet("elasticstack_kibana_security_list.test", "updated_by"),
63+
),
64+
},
65+
{ // Update
66+
ConfigDirectory: acctest.NamedTestCaseDirectory("update"),
67+
ConfigVariables: config.Variables{
68+
"list_id": config.StringVariable(listID),
69+
"name": config.StringVariable("Updated Security List"),
70+
"description": config.StringVariable("An updated test security list"),
71+
"type": config.StringVariable("ip"),
72+
},
73+
Check: resource.ComposeTestCheckFunc(
74+
resource.TestCheckResourceAttr("elasticstack_kibana_security_list.test", "name", "Updated Security List"),
75+
resource.TestCheckResourceAttr("elasticstack_kibana_security_list.test", "description", "An updated test security list"),
76+
),
77+
},
78+
},
79+
})
80+
}
81+
82+
func TestAccResourceSecurityList_KeywordType(t *testing.T) {
83+
listID := "keyword-list-" + uuid.New().String()
84+
resource.Test(t, resource.TestCase{
85+
PreCheck: func() {
86+
acctest.PreCheck(t)
87+
ensureListIndexExists(t)
88+
},
89+
ProtoV6ProviderFactories: acctest.Providers,
90+
Steps: []resource.TestStep{
91+
{
92+
ConfigDirectory: acctest.NamedTestCaseDirectory("keyword_type"),
93+
ConfigVariables: config.Variables{
94+
"list_id": config.StringVariable(listID),
95+
"name": config.StringVariable("Keyword Security List"),
96+
"description": config.StringVariable("A test security list for keywords"),
97+
"type": config.StringVariable("keyword"),
98+
},
99+
Check: resource.ComposeTestCheckFunc(
100+
resource.TestCheckResourceAttr("elasticstack_kibana_security_list.test", "type", "keyword"),
101+
),
102+
},
103+
},
104+
})
105+
}
106+
107+
func TestAccResourceSecurityList_SerializerDeserializer(t *testing.T) {
108+
listID := "serializer-list-" + uuid.New().String()
109+
resource.Test(t, resource.TestCase{
110+
PreCheck: func() {
111+
acctest.PreCheck(t)
112+
ensureListIndexExists(t)
113+
},
114+
ProtoV6ProviderFactories: acctest.Providers,
115+
Steps: []resource.TestStep{
116+
{ // Create with serializer and deserializer
117+
ConfigDirectory: acctest.NamedTestCaseDirectory("create"),
118+
ConfigVariables: config.Variables{
119+
"list_id": config.StringVariable(listID),
120+
"name": config.StringVariable("Custom Serializer List"),
121+
"description": config.StringVariable("A test list with custom serializer and deserializer"),
122+
"type": config.StringVariable("ip"),
123+
"serializer": config.StringVariable("{{ip}}"),
124+
"deserializer": config.StringVariable("{{ip}}"),
125+
},
126+
Check: resource.ComposeTestCheckFunc(
127+
resource.TestCheckResourceAttrSet("elasticstack_kibana_security_list.test", "id"),
128+
resource.TestCheckResourceAttr("elasticstack_kibana_security_list.test", "name", "Custom Serializer List"),
129+
resource.TestCheckResourceAttr("elasticstack_kibana_security_list.test", "type", "ip"),
130+
resource.TestCheckResourceAttr("elasticstack_kibana_security_list.test", "serializer", "{{ip}}"),
131+
resource.TestCheckResourceAttr("elasticstack_kibana_security_list.test", "deserializer", "{{ip}}"),
132+
),
133+
},
134+
{ // Update name and description (serializer/deserializer are immutable)
135+
ConfigDirectory: acctest.NamedTestCaseDirectory("update"),
136+
ConfigVariables: config.Variables{
137+
"list_id": config.StringVariable(listID),
138+
"name": config.StringVariable("Updated Serializer List"),
139+
"description": config.StringVariable("Updated test list description"),
140+
"type": config.StringVariable("ip"),
141+
"serializer": config.StringVariable("{{ip}}"),
142+
"deserializer": config.StringVariable("{{ip}}"),
143+
},
144+
Check: resource.ComposeTestCheckFunc(
145+
resource.TestCheckResourceAttr("elasticstack_kibana_security_list.test", "name", "Updated Serializer List"),
146+
resource.TestCheckResourceAttr("elasticstack_kibana_security_list.test", "description", "Updated test list description"),
147+
resource.TestCheckResourceAttr("elasticstack_kibana_security_list.test", "serializer", "{{ip}}"),
148+
resource.TestCheckResourceAttr("elasticstack_kibana_security_list.test", "deserializer", "{{ip}}"),
149+
),
150+
},
151+
},
152+
})
153+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package security_list
2+
3+
import (
4+
"context"
5+
6+
"github.com/elastic/terraform-provider-elasticstack/generated/kbapi"
7+
"github.com/elastic/terraform-provider-elasticstack/internal/clients/kibana_oapi"
8+
"github.com/hashicorp/terraform-plugin-framework/resource"
9+
)
10+
11+
func (r *securityListResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
12+
var plan SecurityListModel
13+
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
14+
if resp.Diagnostics.HasError() {
15+
return
16+
}
17+
18+
// Get Kibana client
19+
client, err := r.client.GetKibanaOapiClient()
20+
if err != nil {
21+
resp.Diagnostics.AddError("Failed to get Kibana client", err.Error())
22+
return
23+
}
24+
25+
// Convert plan to API request
26+
createReq, diags := plan.toCreateRequest()
27+
resp.Diagnostics.Append(diags...)
28+
if resp.Diagnostics.HasError() {
29+
return
30+
}
31+
32+
// Create the list
33+
spaceID := plan.SpaceID.ValueString()
34+
createResp, diags := kibana_oapi.CreateList(ctx, client, spaceID, *createReq)
35+
resp.Diagnostics.Append(diags...)
36+
if resp.Diagnostics.HasError() {
37+
return
38+
}
39+
40+
if createResp == nil || createResp.JSON200 == nil {
41+
resp.Diagnostics.AddError("Failed to create security list", "API returned empty response")
42+
return
43+
}
44+
45+
// Read the created list to populate state
46+
readParams := &kbapi.ReadListParams{
47+
Id: createResp.JSON200.Id,
48+
}
49+
50+
readResp, diags := kibana_oapi.GetList(ctx, client, spaceID, readParams)
51+
resp.Diagnostics.Append(diags...)
52+
if resp.Diagnostics.HasError() {
53+
return
54+
}
55+
56+
if readResp == nil || readResp.JSON200 == nil {
57+
resp.State.RemoveResource(ctx)
58+
return
59+
}
60+
61+
// Update state with read response
62+
diags = plan.fromAPI(ctx, readResp.JSON200)
63+
resp.Diagnostics.Append(diags...)
64+
if resp.Diagnostics.HasError() {
65+
return
66+
}
67+
68+
resp.Diagnostics.Append(resp.State.Set(ctx, plan)...)
69+
}

0 commit comments

Comments
 (0)