Skip to content

Commit a3c2efd

Browse files
committed
Add validations for security exceptions
1 parent 084d79f commit a3c2efd

File tree

3 files changed

+230
-29
lines changed

3 files changed

+230
-29
lines changed

internal/kibana/security/exception_item/resource.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@ import (
1010
)
1111

1212
var (
13-
_ resource.Resource = &ExceptionItemResource{}
14-
_ resource.ResourceWithConfigure = &ExceptionItemResource{}
15-
_ resource.ResourceWithImportState = &ExceptionItemResource{}
13+
_ resource.Resource = &ExceptionItemResource{}
14+
_ resource.ResourceWithConfigure = &ExceptionItemResource{}
15+
_ resource.ResourceWithImportState = &ExceptionItemResource{}
16+
_ resource.ResourceWithValidateConfig = &ExceptionItemResource{}
1617
)
1718

1819
// NewResource is a helper function to simplify the provider implementation.

internal/kibana/security/exception_item/schema.go

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -119,14 +119,6 @@ func (r *ExceptionItemResource) Schema(_ context.Context, _ resource.SchemaReque
119119
MarkdownDescription: "The operator to use. Valid values: `included`, `excluded`. Note: The operator field is not supported for nested entry types and will be ignored if specified.",
120120
Optional: true,
121121
Validators: []validator.String{
122-
validators.ForbiddenIfDependentPathOneOf(
123-
path.Root("type"),
124-
[]string{"nested"},
125-
),
126-
validators.RequiredIfDependentPathOneOf(
127-
path.Root("type"),
128-
[]string{"match", "match_any", "list", "exists", "wildcard"},
129-
),
130122
stringvalidator.OneOf("included", "excluded"),
131123
},
132124
},
@@ -144,22 +136,10 @@ func (r *ExceptionItemResource) Schema(_ context.Context, _ resource.SchemaReque
144136
ElementType: types.StringType,
145137
MarkdownDescription: "Array of values to match (for `match_any` type).",
146138
Optional: true,
147-
Validators: []validator.List{
148-
validators.RequiredIfDependentPathOneOf(
149-
path.Root("type"),
150-
[]string{"match_any"},
151-
),
152-
},
153139
},
154140
"list": schema.SingleNestedAttribute{
155141
MarkdownDescription: "Value list reference (for `list` type).",
156142
Optional: true,
157-
Validators: []validator.Object{
158-
validators.RequiredIfDependentPathOneOf(
159-
path.Root("type"),
160-
[]string{"list"},
161-
),
162-
},
163143
Attributes: map[string]schema.Attribute{
164144
"id": schema.StringAttribute{
165145
MarkdownDescription: "The value list ID.",
@@ -174,12 +154,6 @@ func (r *ExceptionItemResource) Schema(_ context.Context, _ resource.SchemaReque
174154
"entries": schema.ListNestedAttribute{
175155
MarkdownDescription: "Nested entries (for `nested` type). Only `match`, `match_any`, and `exists` entry types are allowed as nested entries.",
176156
Optional: true,
177-
Validators: []validator.List{
178-
validators.RequiredIfDependentPathOneOf(
179-
path.Root("type"),
180-
[]string{"nested"},
181-
),
182-
},
183157
NestedObject: schema.NestedAttributeObject{
184158
Attributes: map[string]schema.Attribute{
185159
"type": schema.StringAttribute{
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
package exception_item
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/elastic/terraform-provider-elasticstack/internal/utils"
8+
"github.com/hashicorp/terraform-plugin-framework/diag"
9+
"github.com/hashicorp/terraform-plugin-framework/resource"
10+
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
11+
)
12+
13+
// ValidateConfig validates the configuration for an exception item resource.
14+
// It ensures that entries are properly configured based on their type:
15+
//
16+
// - For "match" and "wildcard" types: 'value' must be set
17+
// - For "match_any" type: 'values' must be set
18+
// - For "list" type: 'list' object must be set with 'id' and 'type'
19+
// - For "exists" type: only 'field' and 'operator' are required
20+
// - For "nested" type: 'entries' must be set and validated recursively
21+
// - The 'operator' field is required for all types except "nested"
22+
//
23+
// Validation only runs on known values. Values that are unknown (e.g., references to
24+
// other resources that haven't been created yet) are skipped.
25+
//
26+
// The function adds appropriate error diagnostics if validation fails.
27+
func (r *ExceptionItemResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) {
28+
var data ExceptionItemModel
29+
30+
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
31+
32+
if resp.Diagnostics.HasError() {
33+
return
34+
}
35+
36+
// Validate entries
37+
if !utils.IsKnown(data.Entries) {
38+
return
39+
}
40+
41+
var entries []EntryModel
42+
resp.Diagnostics.Append(data.Entries.ElementsAs(ctx, &entries, false)...)
43+
if resp.Diagnostics.HasError() {
44+
return
45+
}
46+
47+
for i, entry := range entries {
48+
validateEntry(ctx, entry, i, &resp.Diagnostics, "entries")
49+
}
50+
}
51+
52+
// validateEntry validates a single entry based on its type
53+
func validateEntry(ctx context.Context, entry EntryModel, index int, diags *diag.Diagnostics, path string) {
54+
if !utils.IsKnown(entry.Type) {
55+
return
56+
}
57+
58+
entryType := entry.Type.ValueString()
59+
entryPath := fmt.Sprintf("%s[%d]", path, index)
60+
61+
switch entryType {
62+
case "match", "wildcard":
63+
// 'value' is required (only validate if not unknown)
64+
if entry.Value.IsNull() {
65+
diags.AddError(
66+
"Missing Required Field",
67+
fmt.Sprintf("Entry type '%s' requires 'value' to be set at %s.", entryType, entryPath),
68+
)
69+
}
70+
// 'operator' is required (only validate if not unknown)
71+
if entry.Operator.IsNull() {
72+
diags.AddError(
73+
"Missing Required Field",
74+
fmt.Sprintf("Entry type '%s' requires 'operator' to be set at %s.", entryType, entryPath),
75+
)
76+
}
77+
78+
case "match_any":
79+
// 'values' is required (only validate if not unknown)
80+
if entry.Values.IsNull() {
81+
diags.AddError(
82+
"Missing Required Field",
83+
fmt.Sprintf("Entry type 'match_any' requires 'values' to be set at %s.", entryPath),
84+
)
85+
}
86+
// 'operator' is required (only validate if not unknown)
87+
if entry.Operator.IsNull() {
88+
diags.AddError(
89+
"Missing Required Field",
90+
fmt.Sprintf("Entry type 'match_any' requires 'operator' to be set at %s.", entryPath),
91+
)
92+
}
93+
94+
case "list":
95+
// 'list' object is required (only validate if not unknown)
96+
if entry.List.IsNull() {
97+
diags.AddError(
98+
"Missing Required Field",
99+
fmt.Sprintf("Entry type 'list' requires 'list' object to be set at %s.", entryPath),
100+
)
101+
} else if !entry.List.IsUnknown() {
102+
// Only validate list contents if the list object itself is known
103+
var listModel EntryListModel
104+
d := entry.List.As(ctx, &listModel, basetypes.ObjectAsOptions{})
105+
if d.HasError() {
106+
diags.Append(d...)
107+
} else {
108+
// Only validate if the values are not unknown
109+
if listModel.ID.IsNull() {
110+
diags.AddError(
111+
"Missing Required Field",
112+
fmt.Sprintf("Entry type 'list' requires 'list.id' to be set at %s.", entryPath),
113+
)
114+
}
115+
116+
if listModel.Type.IsNull() {
117+
diags.AddError(
118+
"Missing Required Field",
119+
fmt.Sprintf("Entry type 'list' requires 'list.type' to be set at %s.", entryPath),
120+
)
121+
}
122+
}
123+
}
124+
// 'operator' is required (only validate if not unknown)
125+
if entry.Operator.IsNull() {
126+
diags.AddError(
127+
"Missing Required Field",
128+
fmt.Sprintf("Entry type 'list' requires 'operator' to be set at %s.", entryPath),
129+
)
130+
}
131+
132+
case "exists":
133+
// Only 'field' and 'operator' are required (already handled by schema)
134+
// 'operator' is required (only validate if not unknown)
135+
if entry.Operator.IsNull() {
136+
diags.AddError(
137+
"Missing Required Field",
138+
fmt.Sprintf("Entry type 'exists' requires 'operator' to be set at %s.", entryPath),
139+
)
140+
}
141+
142+
case "nested":
143+
// 'entries' is required for nested type (only validate if not unknown)
144+
if entry.Entries.IsNull() {
145+
diags.AddError(
146+
"Missing Required Field",
147+
fmt.Sprintf("Entry type 'nested' requires 'entries' to be set at %s.", entryPath),
148+
)
149+
return
150+
}
151+
152+
// Skip validation if entries are unknown
153+
if entry.Entries.IsUnknown() {
154+
return
155+
}
156+
157+
// 'operator' should NOT be set for nested type
158+
if utils.IsKnown(entry.Operator) {
159+
diags.AddWarning(
160+
"Ignored Field",
161+
fmt.Sprintf("Entry type 'nested' does not support 'operator'. This field will be ignored at %s.", entryPath),
162+
)
163+
}
164+
165+
// Validate nested entries
166+
var nestedEntries []NestedEntryModel
167+
d := entry.Entries.ElementsAs(ctx, &nestedEntries, false)
168+
if d.HasError() {
169+
diags.Append(d...)
170+
return
171+
}
172+
173+
for j, nestedEntry := range nestedEntries {
174+
validateNestedEntry(ctx, nestedEntry, j, diags, fmt.Sprintf("%s.entries", entryPath))
175+
}
176+
}
177+
}
178+
179+
// validateNestedEntry validates a nested entry within a "nested" type entry
180+
func validateNestedEntry(ctx context.Context, entry NestedEntryModel, index int, diags *diag.Diagnostics, path string) {
181+
if !utils.IsKnown(entry.Type) {
182+
return
183+
}
184+
185+
entryType := entry.Type.ValueString()
186+
entryPath := fmt.Sprintf("%s[%d]", path, index)
187+
188+
// Nested entries can only be: match, match_any, or exists
189+
switch entryType {
190+
case "match":
191+
// 'value' is required (only validate if not unknown)
192+
if entry.Value.IsNull() {
193+
diags.AddError(
194+
"Missing Required Field",
195+
fmt.Sprintf("Nested entry type 'match' requires 'value' to be set at %s.", entryPath),
196+
)
197+
}
198+
199+
case "match_any":
200+
// 'values' is required (only validate if not unknown)
201+
if entry.Values.IsNull() {
202+
diags.AddError(
203+
"Missing Required Field",
204+
fmt.Sprintf("Nested entry type 'match_any' requires 'values' to be set at %s.", entryPath),
205+
)
206+
}
207+
208+
case "exists":
209+
// Only 'field' and 'operator' are required (already handled by schema)
210+
// Nothing additional to validate
211+
212+
default:
213+
diags.AddError(
214+
"Invalid Entry Type",
215+
fmt.Sprintf("Nested entry at %s has invalid type '%s'. Only 'match', 'match_any', and 'exists' are allowed for nested entries.", entryPath, entryType),
216+
)
217+
}
218+
219+
// 'operator' is always required for nested entries (only validate if not unknown)
220+
if entry.Operator.IsNull() {
221+
diags.AddError(
222+
"Missing Required Field",
223+
fmt.Sprintf("Nested entry requires 'operator' to be set at %s.", entryPath),
224+
)
225+
}
226+
}

0 commit comments

Comments
 (0)