Skip to content

Commit c640526

Browse files
committed
Add security exception list resource
1 parent 15e62bf commit c640526

File tree

13 files changed

+906
-0
lines changed

13 files changed

+906
-0
lines changed
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
package security_exception_list_test
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
7+
"github.com/elastic/terraform-provider-elasticstack/internal/acctest"
8+
"github.com/elastic/terraform-provider-elasticstack/internal/versionutils"
9+
"github.com/google/uuid"
10+
"github.com/hashicorp/go-version"
11+
"github.com/hashicorp/terraform-plugin-testing/config"
12+
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
13+
)
14+
15+
var minExceptionListAPISupport = version.Must(version.NewVersion("7.9.0"))
16+
17+
func TestAccResourceExceptionList(t *testing.T) {
18+
resource.Test(t, resource.TestCase{
19+
PreCheck: func() { acctest.PreCheck(t) },
20+
Steps: []resource.TestStep{
21+
{
22+
SkipFunc: versionutils.CheckIfVersionIsUnsupported(minExceptionListAPISupport),
23+
ProtoV6ProviderFactories: acctest.Providers,
24+
ConfigDirectory: acctest.NamedTestCaseDirectory("create"),
25+
ConfigVariables: config.Variables{
26+
"list_id": config.StringVariable("test-exception-list"),
27+
"name": config.StringVariable("Test Exception List"),
28+
"description": config.StringVariable("Test exception list for acceptance tests"),
29+
"type": config.StringVariable("detection"),
30+
"namespace_type": config.StringVariable("single"),
31+
"tags": config.ListVariable(config.StringVariable("test")),
32+
},
33+
Check: resource.ComposeTestCheckFunc(
34+
resource.TestCheckResourceAttr("elasticstack_kibana_security_exception_list.test", "list_id", "test-exception-list"),
35+
resource.TestCheckResourceAttr("elasticstack_kibana_security_exception_list.test", "name", "Test Exception List"),
36+
resource.TestCheckResourceAttr("elasticstack_kibana_security_exception_list.test", "description", "Test exception list for acceptance tests"),
37+
resource.TestCheckResourceAttr("elasticstack_kibana_security_exception_list.test", "type", "detection"),
38+
resource.TestCheckResourceAttr("elasticstack_kibana_security_exception_list.test", "namespace_type", "single"),
39+
resource.TestCheckResourceAttr("elasticstack_kibana_security_exception_list.test", "tags.0", "test"),
40+
resource.TestCheckResourceAttrSet("elasticstack_kibana_security_exception_list.test", "id"),
41+
resource.TestCheckResourceAttrSet("elasticstack_kibana_security_exception_list.test", "created_at"),
42+
resource.TestCheckResourceAttrSet("elasticstack_kibana_security_exception_list.test", "created_by"),
43+
),
44+
},
45+
{
46+
SkipFunc: versionutils.CheckIfVersionIsUnsupported(minExceptionListAPISupport),
47+
ProtoV6ProviderFactories: acctest.Providers,
48+
ConfigDirectory: acctest.NamedTestCaseDirectory("update"),
49+
ConfigVariables: config.Variables{
50+
"list_id": config.StringVariable("test-exception-list"),
51+
"name": config.StringVariable("Test Exception List Updated"),
52+
"description": config.StringVariable("Updated description"),
53+
"type": config.StringVariable("detection"),
54+
"namespace_type": config.StringVariable("single"),
55+
"tags": config.ListVariable(config.StringVariable("test"), config.StringVariable("updated")),
56+
},
57+
Check: resource.ComposeTestCheckFunc(
58+
resource.TestCheckResourceAttr("elasticstack_kibana_security_exception_list.test", "name", "Test Exception List Updated"),
59+
resource.TestCheckResourceAttr("elasticstack_kibana_security_exception_list.test", "description", "Updated description"),
60+
resource.TestCheckResourceAttr("elasticstack_kibana_security_exception_list.test", "tags.0", "test"),
61+
resource.TestCheckResourceAttr("elasticstack_kibana_security_exception_list.test", "tags.1", "updated"),
62+
),
63+
},
64+
},
65+
})
66+
}
67+
68+
func TestAccResourceExceptionListWithSpace(t *testing.T) {
69+
resourceName := "elasticstack_kibana_security_exception_list.test"
70+
spaceResourceName := "elasticstack_kibana_space.test"
71+
spaceID := fmt.Sprintf("test-space-%s", uuid.New().String()[:8])
72+
73+
resource.Test(t, resource.TestCase{
74+
PreCheck: func() { acctest.PreCheck(t) },
75+
ProtoV6ProviderFactories: acctest.Providers,
76+
Steps: []resource.TestStep{
77+
{
78+
SkipFunc: versionutils.CheckIfVersionIsUnsupported(minExceptionListAPISupport),
79+
ConfigDirectory: acctest.NamedTestCaseDirectory("create"),
80+
ConfigVariables: config.Variables{
81+
"space_id": config.StringVariable(spaceID),
82+
"list_id": config.StringVariable("test-exception-list-space"),
83+
"name": config.StringVariable("Test Exception List in Space"),
84+
"description": config.StringVariable("Test exception list in custom space"),
85+
"type": config.StringVariable("detection"),
86+
"namespace_type": config.StringVariable("single"),
87+
"tags": config.ListVariable(config.StringVariable("test"), config.StringVariable("space")),
88+
},
89+
Check: resource.ComposeTestCheckFunc(
90+
// Check space attributes
91+
resource.TestCheckResourceAttr(spaceResourceName, "space_id", spaceID),
92+
resource.TestCheckResourceAttr(spaceResourceName, "name", "Test Space for Exception Lists"),
93+
94+
// Check exception list attributes
95+
resource.TestCheckResourceAttr(resourceName, "space_id", spaceID),
96+
resource.TestCheckResourceAttr(resourceName, "list_id", "test-exception-list-space"),
97+
resource.TestCheckResourceAttr(resourceName, "name", "Test Exception List in Space"),
98+
resource.TestCheckResourceAttr(resourceName, "description", "Test exception list in custom space"),
99+
resource.TestCheckResourceAttr(resourceName, "type", "detection"),
100+
resource.TestCheckResourceAttr(resourceName, "namespace_type", "single"),
101+
resource.TestCheckResourceAttr(resourceName, "tags.0", "test"),
102+
resource.TestCheckResourceAttr(resourceName, "tags.1", "space"),
103+
resource.TestCheckResourceAttrSet(resourceName, "id"),
104+
resource.TestCheckResourceAttrSet(resourceName, "created_at"),
105+
resource.TestCheckResourceAttrSet(resourceName, "created_by"),
106+
),
107+
},
108+
{
109+
SkipFunc: versionutils.CheckIfVersionIsUnsupported(minExceptionListAPISupport),
110+
ConfigDirectory: acctest.NamedTestCaseDirectory("update"),
111+
ConfigVariables: config.Variables{
112+
"space_id": config.StringVariable(spaceID),
113+
"list_id": config.StringVariable("test-exception-list-space"),
114+
"name": config.StringVariable("Test Exception List in Space Updated"),
115+
"description": config.StringVariable("Updated description in space"),
116+
"type": config.StringVariable("detection"),
117+
"namespace_type": config.StringVariable("single"),
118+
"tags": config.ListVariable(config.StringVariable("test"), config.StringVariable("space"), config.StringVariable("updated")),
119+
},
120+
Check: resource.ComposeTestCheckFunc(
121+
// Check space attributes remain the same
122+
resource.TestCheckResourceAttr(spaceResourceName, "space_id", spaceID),
123+
resource.TestCheckResourceAttr(spaceResourceName, "name", "Test Space for Exception Lists"),
124+
125+
// Check updated exception list attributes
126+
resource.TestCheckResourceAttr(resourceName, "space_id", spaceID),
127+
resource.TestCheckResourceAttr(resourceName, "name", "Test Exception List in Space Updated"),
128+
resource.TestCheckResourceAttr(resourceName, "description", "Updated description in space"),
129+
resource.TestCheckResourceAttr(resourceName, "tags.0", "test"),
130+
resource.TestCheckResourceAttr(resourceName, "tags.1", "space"),
131+
resource.TestCheckResourceAttr(resourceName, "tags.2", "updated"),
132+
),
133+
},
134+
},
135+
})
136+
}
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
package security_exception_list
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
7+
"github.com/elastic/terraform-provider-elasticstack/generated/kbapi"
8+
"github.com/elastic/terraform-provider-elasticstack/internal/clients/kibana_oapi"
9+
"github.com/elastic/terraform-provider-elasticstack/internal/utils"
10+
"github.com/hashicorp/terraform-plugin-framework/diag"
11+
"github.com/hashicorp/terraform-plugin-framework/resource"
12+
"github.com/hashicorp/terraform-plugin-framework/types"
13+
)
14+
15+
func (r *ExceptionListResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
16+
var plan ExceptionListModel
17+
18+
diags := req.Plan.Get(ctx, &plan)
19+
resp.Diagnostics.Append(diags...)
20+
if resp.Diagnostics.HasError() {
21+
return
22+
}
23+
24+
client, err := r.client.GetKibanaOapiClient()
25+
if err != nil {
26+
resp.Diagnostics.AddError("Failed to get Kibana client", err.Error())
27+
return
28+
}
29+
30+
// Build the request body
31+
body := kbapi.CreateExceptionListJSONRequestBody{
32+
ListId: (*kbapi.SecurityExceptionsAPIExceptionListHumanId)(plan.ListID.ValueStringPointer()),
33+
Name: kbapi.SecurityExceptionsAPIExceptionListName(plan.Name.ValueString()),
34+
Description: kbapi.SecurityExceptionsAPIExceptionListDescription(plan.Description.ValueString()),
35+
Type: kbapi.SecurityExceptionsAPIExceptionListType(plan.Type.ValueString()),
36+
}
37+
38+
// Set optional namespace_type
39+
if utils.IsKnown(plan.NamespaceType) {
40+
nsType := kbapi.SecurityExceptionsAPIExceptionNamespaceType(plan.NamespaceType.ValueString())
41+
body.NamespaceType = &nsType
42+
}
43+
44+
// Set optional os_types
45+
if utils.IsKnown(plan.OsTypes) && !plan.OsTypes.IsNull() {
46+
var osTypes []string
47+
diags := plan.OsTypes.ElementsAs(ctx, &osTypes, false)
48+
resp.Diagnostics.Append(diags...)
49+
if resp.Diagnostics.HasError() {
50+
return
51+
}
52+
if len(osTypes) > 0 {
53+
osTypesArray := make(kbapi.SecurityExceptionsAPIExceptionListOsTypeArray, len(osTypes))
54+
for i, osType := range osTypes {
55+
osTypesArray[i] = kbapi.SecurityExceptionsAPIExceptionListOsType(osType)
56+
}
57+
body.OsTypes = &osTypesArray
58+
}
59+
}
60+
61+
// Set optional tags
62+
if utils.IsKnown(plan.Tags) && !plan.Tags.IsNull() {
63+
var tags []string
64+
diags := plan.Tags.ElementsAs(ctx, &tags, false)
65+
resp.Diagnostics.Append(diags...)
66+
if resp.Diagnostics.HasError() {
67+
return
68+
}
69+
if len(tags) > 0 {
70+
tagsArray := kbapi.SecurityExceptionsAPIExceptionListTags(tags)
71+
body.Tags = &tagsArray
72+
}
73+
}
74+
75+
// Set optional meta
76+
if utils.IsKnown(plan.Meta) && !plan.Meta.IsNull() {
77+
var meta kbapi.SecurityExceptionsAPIExceptionListMeta
78+
if err := json.Unmarshal([]byte(plan.Meta.ValueString()), &meta); err != nil {
79+
resp.Diagnostics.AddError("Failed to parse meta JSON", err.Error())
80+
return
81+
}
82+
body.Meta = &meta
83+
}
84+
85+
// Create the exception list
86+
createResp, diags := kibana_oapi.CreateExceptionList(ctx, client, plan.SpaceID.ValueString(), body)
87+
resp.Diagnostics.Append(diags...)
88+
if resp.Diagnostics.HasError() {
89+
return
90+
}
91+
92+
if createResp == nil || createResp.JSON200 == nil {
93+
resp.Diagnostics.AddError("Failed to create exception list", "API returned empty response")
94+
return
95+
}
96+
97+
/*
98+
* In create/update paths we typically follow the write operation with a read, and then set the state from the read.
99+
* We want to avoid a dirty plan immediately after an apply.
100+
*/
101+
// Read back the created resource to get the final state
102+
readParams := &kbapi.ReadExceptionListParams{
103+
Id: (*kbapi.SecurityExceptionsAPIExceptionListId)(&createResp.JSON200.Id),
104+
}
105+
106+
readResp, diags := kibana_oapi.GetExceptionList(ctx, client, plan.SpaceID.ValueString(), readParams)
107+
resp.Diagnostics.Append(diags...)
108+
if resp.Diagnostics.HasError() {
109+
return
110+
}
111+
112+
if readResp == nil || readResp.JSON200 == nil {
113+
resp.State.RemoveResource(ctx)
114+
return
115+
}
116+
117+
// Update state with read response
118+
diags = r.updateStateFromAPIResponse(ctx, &plan, readResp.JSON200)
119+
resp.Diagnostics.Append(diags...)
120+
if resp.Diagnostics.HasError() {
121+
return
122+
}
123+
124+
diags = resp.State.Set(ctx, plan)
125+
resp.Diagnostics.Append(diags...)
126+
}
127+
128+
func (r *ExceptionListResource) updateStateFromAPIResponse(ctx context.Context, model *ExceptionListModel, apiResp *kbapi.SecurityExceptionsAPIExceptionList) diag.Diagnostics {
129+
var diags diag.Diagnostics
130+
131+
model.ID = types.StringValue(string(apiResp.Id))
132+
model.ListID = types.StringValue(string(apiResp.ListId))
133+
model.Name = types.StringValue(string(apiResp.Name))
134+
model.Description = types.StringValue(string(apiResp.Description))
135+
model.Type = types.StringValue(string(apiResp.Type))
136+
model.NamespaceType = types.StringValue(string(apiResp.NamespaceType))
137+
model.CreatedAt = types.StringValue(apiResp.CreatedAt.Format("2006-01-02T15:04:05.000Z"))
138+
model.CreatedBy = types.StringValue(apiResp.CreatedBy)
139+
model.UpdatedAt = types.StringValue(apiResp.UpdatedAt.Format("2006-01-02T15:04:05.000Z"))
140+
model.UpdatedBy = types.StringValue(apiResp.UpdatedBy)
141+
model.Immutable = types.BoolValue(apiResp.Immutable)
142+
model.TieBreakerID = types.StringValue(apiResp.TieBreakerId)
143+
144+
// Set optional os_types
145+
if apiResp.OsTypes != nil && len(*apiResp.OsTypes) > 0 {
146+
// osTypes := make([]string, len(*apiResp.OsTypes))
147+
// for i, osType := range *apiResp.OsTypes {
148+
// osTypes[i] = string(osType)
149+
// }
150+
// list, d := types.ListValueFrom(ctx, types.StringType, osTypes)
151+
list, d := types.ListValueFrom(ctx, types.StringType, apiResp.OsTypes)
152+
diags.Append(d...)
153+
model.OsTypes = list
154+
} else {
155+
model.OsTypes = types.ListNull(types.StringType)
156+
}
157+
158+
// Set optional tags
159+
if apiResp.Tags != nil && len(*apiResp.Tags) > 0 {
160+
list, d := types.ListValueFrom(ctx, types.StringType, *apiResp.Tags)
161+
diags.Append(d...)
162+
model.Tags = list
163+
} else {
164+
model.Tags = types.ListNull(types.StringType)
165+
}
166+
167+
// Set optional meta
168+
if apiResp.Meta != nil {
169+
metaJSON, err := json.Marshal(apiResp.Meta)
170+
if err != nil {
171+
diags.AddError("Failed to serialize meta", err.Error())
172+
return diags
173+
}
174+
model.Meta = types.StringValue(string(metaJSON))
175+
} else {
176+
model.Meta = types.StringNull()
177+
}
178+
179+
return diags
180+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package security_exception_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 *ExceptionListResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
12+
var state ExceptionListModel
13+
14+
diags := req.State.Get(ctx, &state)
15+
resp.Diagnostics.Append(diags...)
16+
if resp.Diagnostics.HasError() {
17+
return
18+
}
19+
20+
client, err := r.client.GetKibanaOapiClient()
21+
if err != nil {
22+
resp.Diagnostics.AddError("Failed to get Kibana client", err.Error())
23+
return
24+
}
25+
26+
// Delete by ID
27+
id := kbapi.SecurityExceptionsAPIExceptionListId(state.ID.ValueString())
28+
params := &kbapi.DeleteExceptionListParams{
29+
Id: &id,
30+
}
31+
32+
diags = kibana_oapi.DeleteExceptionList(ctx, client, state.SpaceID.ValueString(), params)
33+
resp.Diagnostics.Append(diags...)
34+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package security_exception_list
2+
3+
import (
4+
"github.com/hashicorp/terraform-plugin-framework/types"
5+
)
6+
7+
type ExceptionListModel struct {
8+
ID types.String `tfsdk:"id"`
9+
SpaceID types.String `tfsdk:"space_id"`
10+
ListID types.String `tfsdk:"list_id"`
11+
Name types.String `tfsdk:"name"`
12+
Description types.String `tfsdk:"description"`
13+
Type types.String `tfsdk:"type"`
14+
NamespaceType types.String `tfsdk:"namespace_type"`
15+
OsTypes types.List `tfsdk:"os_types"`
16+
Tags types.List `tfsdk:"tags"`
17+
Meta types.String `tfsdk:"meta"`
18+
CreatedAt types.String `tfsdk:"created_at"`
19+
CreatedBy types.String `tfsdk:"created_by"`
20+
UpdatedAt types.String `tfsdk:"updated_at"`
21+
UpdatedBy types.String `tfsdk:"updated_by"`
22+
Immutable types.Bool `tfsdk:"immutable"`
23+
TieBreakerID types.String `tfsdk:"tie_breaker_id"`
24+
}

0 commit comments

Comments
 (0)