Skip to content

Commit 582887a

Browse files
vuldingene-redpanda
authored andcommitted
fixes #275
The problem was that the id field was defined in the schema but never being set in the Create and Read methods. Changes: 1. Create method: Added composite ID generation using all the ACL fields that uniquely identify an ACL 2. Read method: Added the same composite ID generation to maintain consistency 3. Related unit tests The composite ID format is: resource_type:resource_name:resource_pattern_type:principal:host:operation:permission_type This ensures that: - The ID field will always have a valid string value after planning - Tools like upjet can properly cast the ID to a string - The ID is deterministic and consistent across Create/Read operations - The ID uniquely identifies each ACL resource combination The fix follows the same pattern used in other resources like role assignments (resource_roleassignment.go:127) that also generate composite IDs when the underlying API doesn't provide a single unique identifier.
1 parent c0562a3 commit 582887a

File tree

2 files changed

+159
-0
lines changed

2 files changed

+159
-0
lines changed

redpanda/resources/acl/acl_test.go

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package acl
22

33
import (
4+
"fmt"
5+
"strings"
46
"testing"
57

68
dataplanev1 "buf.build/gen/go/redpandadata/dataplane/protocolbuffers/go/redpanda/api/dataplane/v1"
@@ -187,3 +189,138 @@ func Test_stringToACLOperation(t *testing.T) {
187189
})
188190
}
189191
}
192+
193+
func Test_generateACLCompositeID(t *testing.T) {
194+
tests := []struct {
195+
name string
196+
resourceType string
197+
resourceName string
198+
patternType string
199+
principal string
200+
host string
201+
operation string
202+
permissionType string
203+
expected string
204+
}{
205+
{
206+
name: "basic topic ACL",
207+
resourceType: "TOPIC",
208+
resourceName: "test-topic",
209+
patternType: "LITERAL",
210+
principal: "User:testuser",
211+
host: "*",
212+
operation: "READ",
213+
permissionType: "ALLOW",
214+
expected: "TOPIC:test-topic:LITERAL:User:testuser:*:READ:ALLOW",
215+
},
216+
{
217+
name: "group ACL with specific host",
218+
resourceType: "GROUP",
219+
resourceName: "test-group",
220+
patternType: "PREFIXED",
221+
principal: "User:admin",
222+
host: "192.168.1.100",
223+
operation: "WRITE",
224+
permissionType: "DENY",
225+
expected: "GROUP:test-group:PREFIXED:User:admin:192.168.1.100:WRITE:DENY",
226+
},
227+
{
228+
name: "cluster ACL with colon in resource name",
229+
resourceType: "CLUSTER",
230+
resourceName: "my:cluster:name",
231+
patternType: "MATCH",
232+
principal: "ServiceAccount:service",
233+
host: "*",
234+
operation: "CLUSTER_ACTION",
235+
permissionType: "ALLOW",
236+
expected: "CLUSTER:my:cluster:name:MATCH:ServiceAccount:service:*:CLUSTER_ACTION:ALLOW",
237+
},
238+
}
239+
240+
for _, tt := range tests {
241+
t.Run(tt.name, func(t *testing.T) {
242+
// Test the composite ID format used in both Create and Read methods
243+
compositeID := fmt.Sprintf("%s:%s:%s:%s:%s:%s:%s",
244+
tt.resourceType,
245+
tt.resourceName,
246+
tt.patternType,
247+
tt.principal,
248+
tt.host,
249+
tt.operation,
250+
tt.permissionType)
251+
252+
if compositeID != tt.expected {
253+
t.Errorf("generateACLCompositeID() = %q, expected %q", compositeID, tt.expected)
254+
}
255+
})
256+
}
257+
}
258+
259+
func Test_ACLCompositeIDFormat(t *testing.T) {
260+
tests := []struct {
261+
name string
262+
resourceType string
263+
resourceName string
264+
patternType string
265+
principal string
266+
host string
267+
operation string
268+
permissionType string
269+
expectedFormat string
270+
}{
271+
{
272+
name: "simple format validation",
273+
resourceType: "TOPIC",
274+
resourceName: "test-topic",
275+
patternType: "LITERAL",
276+
principal: "User:testuser",
277+
host: "*",
278+
operation: "READ",
279+
permissionType: "ALLOW",
280+
expectedFormat: "TOPIC:test-topic:LITERAL:User:testuser:*:READ:ALLOW",
281+
},
282+
{
283+
name: "format with complex values",
284+
resourceType: "GROUP",
285+
resourceName: "my-consumer-group",
286+
patternType: "PREFIXED",
287+
principal: "ServiceAccount:my-service",
288+
host: "192.168.1.100",
289+
operation: "DESCRIBE",
290+
permissionType: "DENY",
291+
expectedFormat: "GROUP:my-consumer-group:PREFIXED:ServiceAccount:my-service:192.168.1.100:DESCRIBE:DENY",
292+
},
293+
}
294+
295+
for _, tt := range tests {
296+
t.Run(tt.name, func(t *testing.T) {
297+
// Test that the composite ID format matches what's used in the actual resource implementation
298+
compositeID := fmt.Sprintf("%s:%s:%s:%s:%s:%s:%s",
299+
tt.resourceType,
300+
tt.resourceName,
301+
tt.patternType,
302+
tt.principal,
303+
tt.host,
304+
tt.operation,
305+
tt.permissionType)
306+
307+
if compositeID != tt.expectedFormat {
308+
t.Errorf("Composite ID format = %q, expected %q", compositeID, tt.expectedFormat)
309+
}
310+
311+
// Verify that the ID contains all required fields
312+
parts := strings.Split(compositeID, ":")
313+
if len(parts) < 7 {
314+
t.Errorf("Composite ID should have at least 7 colon-separated parts, got %d", len(parts))
315+
}
316+
317+
// Verify first and last parts (most reliable for validation)
318+
if parts[0] != tt.resourceType {
319+
t.Errorf("First part should be resource type %q, got %q", tt.resourceType, parts[0])
320+
}
321+
if parts[len(parts)-1] != tt.permissionType {
322+
t.Errorf("Last part should be permission type %q, got %q", tt.permissionType, parts[len(parts)-1])
323+
}
324+
})
325+
}
326+
}

redpanda/resources/acl/resource_acl.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,16 @@ func (a *ACL) Create(ctx context.Context, request resource.CreateRequest, respon
180180
return
181181
}
182182

183+
// Generate a composite ID from the ACL fields
184+
aclID := fmt.Sprintf("%s:%s:%s:%s:%s:%s:%s",
185+
model.ResourceType.ValueString(),
186+
model.ResourceName.ValueString(),
187+
model.ResourcePatternType.ValueString(),
188+
model.Principal.ValueString(),
189+
model.Host.ValueString(),
190+
model.Operation.ValueString(),
191+
model.PermissionType.ValueString())
192+
183193
response.Diagnostics.Append(response.State.Set(ctx, &models.ACL{
184194
ResourceType: model.ResourceType,
185195
ResourceName: model.ResourceName,
@@ -189,6 +199,7 @@ func (a *ACL) Create(ctx context.Context, request resource.CreateRequest, respon
189199
Operation: model.Operation,
190200
PermissionType: model.PermissionType,
191201
ClusterAPIURL: model.ClusterAPIURL,
202+
ID: types.StringValue(aclID),
192203
})...)
193204
}
194205

@@ -245,6 +256,16 @@ func (a *ACL) Read(ctx context.Context, request resource.ReadRequest, response *
245256

246257
for _, res := range aclList.Resources {
247258
if res.ResourceName == model.ResourceName.ValueString() && res.ResourceType == resourceType && res.ResourcePatternType == resourcePatternType {
259+
// Generate the same composite ID as in Create
260+
aclID := fmt.Sprintf("%s:%s:%s:%s:%s:%s:%s",
261+
model.ResourceType.ValueString(),
262+
model.ResourceName.ValueString(),
263+
model.ResourcePatternType.ValueString(),
264+
model.Principal.ValueString(),
265+
model.Host.ValueString(),
266+
model.Operation.ValueString(),
267+
model.PermissionType.ValueString())
268+
248269
response.Diagnostics.Append(response.State.Set(ctx, &models.ACL{
249270
ResourceType: types.StringValue(aclResourceTypeToString(res.ResourceType)),
250271
ResourceName: types.StringValue(res.ResourceName),
@@ -254,6 +275,7 @@ func (a *ACL) Read(ctx context.Context, request resource.ReadRequest, response *
254275
Operation: model.Operation,
255276
PermissionType: model.PermissionType,
256277
ClusterAPIURL: model.ClusterAPIURL,
278+
ID: types.StringValue(aclID),
257279
})...)
258280
return
259281
}

0 commit comments

Comments
 (0)