Skip to content

Commit 9f1bcc2

Browse files
authored
Merge pull request #279 from deviceinsight/feature/acl
more granular control when listing/deleting acls
2 parents d2dae46 + a8451f7 commit 9f1bcc2

File tree

8 files changed

+229
-0
lines changed

8 files changed

+229
-0
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

77
## [Unreleased]
8+
9+
### Added
10+
- [#276](https://github.com/deviceinsight/kafkactl/issues/276) More granular control when listing/deleting acls
11+
12+
### Fixed
813
- Do not commit offset for messages over the max-messages
914

1015
## 5.10.1 - 2025-07-02

README.adoc

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1036,6 +1036,10 @@ kafkactl get access-control-list
10361036
kafkactl get acl --topics
10371037
# filter only consumer group resources with operation read
10381038
kafkactl get acl --groups --operation read
1039+
# filter specific topic and user
1040+
kafkactl get acl --resource-name my-topic --principal User:myUser
1041+
# filter specific topic and host
1042+
kafkactl get acl --resource-name my-topic --host my-host
10391043
----
10401044

10411045
==== Delete ACLs
@@ -1050,6 +1054,10 @@ kafkactl delete acl --topics --operation any --pattern any
10501054
kafkactl delete acl --cluster --operation any --pattern any
10511055
# delete all consumer-group acls with operation describe, patternType prefixed and permissionType allow
10521056
kafkactl delete acl --groups --operation describe --pattern prefixed --allow
1057+
# delete all topic acls for a principal
1058+
kafkactl delete acl --topics --operation any --pattern any --prinicipal User:myUser
1059+
# delete all topic acls for a host
1060+
kafkactl delete acl --topics --operation any --pattern any --host my-host
10531061
----
10541062

10551063
=== Getting Brokers

cmd/deletion/delete-acl.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ func newDeleteACLCmd() *cobra.Command {
2727
cmdDeleteACL.Flags().StringVarP(&flags.Operation, "operation", "o", "", "operation of acl")
2828
cmdDeleteACL.Flags().StringVarP(&flags.PatternType, "pattern", "", "", "pattern type. one of (any, match, prefixed, literal)")
2929

30+
cmdDeleteACL.Flags().StringVarP(&flags.Principal, "principal", "p", "", "principal of acl")
31+
cmdDeleteACL.Flags().StringVarP(&flags.Host, "host", "", "", "host of acl")
32+
3033
// specify permissionType
3134
cmdDeleteACL.Flags().BoolVarP(&flags.Allow, "allow", "a", false, "acl of permissionType 'allow'")
3235
cmdDeleteACL.Flags().BoolVarP(&flags.Deny, "deny", "d", false, "acl of permissionType 'deny'")

cmd/deletion/delete-acl_test.go

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import (
44
"fmt"
55
"testing"
66

7+
"github.com/deviceinsight/kafkactl/v5/internal/acl"
8+
79
"github.com/deviceinsight/kafkactl/v5/internal/testutil"
810
)
911

@@ -62,3 +64,105 @@ func TestDeleteTopicReadAclIntegration(t *testing.T) {
6264

6365
testutil.AssertErrorContainsOneOf(t, []string{pre28ErrorMessage, errorMessage}, err)
6466
}
67+
68+
func TestDeleteAclByPrincipalIntegration(t *testing.T) {
69+
70+
testutil.StartIntegrationTestWithContext(t, "sasl-admin")
71+
72+
kafkaCtl := testutil.CreateKafkaCtlCommand()
73+
74+
topicName := testutil.CreateTopic(t, "acl-topic-principal")
75+
76+
// add read acl
77+
if _, err := kafkaCtl.Execute("create", "acl", "--topic", topicName, "--operation", "read", "--allow", "--principal", "User:user"); err != nil {
78+
t.Fatalf("failed to execute command: %v", err)
79+
}
80+
81+
// add write acl for other user
82+
if _, err := kafkaCtl.Execute("create", "acl", "--topic", topicName, "--operation", "read", "--allow", "--principal", "User:admin"); err != nil {
83+
t.Fatalf("failed to execute command: %v", err)
84+
}
85+
86+
// list acls
87+
if _, err := kafkaCtl.Execute("get", "acl", "--resource-name", topicName, "-o", "yaml"); err != nil {
88+
t.Fatalf("failed to execute command: %v", err)
89+
}
90+
91+
acls, err := acl.FromYaml(kafkaCtl.GetStdOut())
92+
if err != nil {
93+
t.Fatalf("failed to read yaml: %v", err)
94+
}
95+
testutil.AssertIntEquals(t, 1, len(acls))
96+
testutil.AssertIntEquals(t, 2, len(acls[0].Acls))
97+
testutil.AssertEquals(t, "User:user", acls[0].Acls[0].Principal)
98+
testutil.AssertEquals(t, "User:admin", acls[0].Acls[1].Principal)
99+
100+
// delete the acl
101+
if _, err := kafkaCtl.Execute("delete", "acl", "--topics", "--operation", "read", "--principal", "User:user", "--pattern", "literal"); err != nil {
102+
t.Fatalf("failed to execute command: %v", err)
103+
}
104+
105+
// list acls
106+
if _, err := kafkaCtl.Execute("get", "acl", "--resource-name", topicName, "-o", "yaml"); err != nil {
107+
t.Fatalf("failed to execute command: %v", err)
108+
}
109+
110+
acls, err = acl.FromYaml(kafkaCtl.GetStdOut())
111+
if err != nil {
112+
t.Fatalf("failed to read yaml: %v", err)
113+
}
114+
testutil.AssertIntEquals(t, 1, len(acls))
115+
testutil.AssertIntEquals(t, 1, len(acls[0].Acls))
116+
testutil.AssertEquals(t, "User:admin", acls[0].Acls[0].Principal)
117+
}
118+
119+
func TestDeleteAclByHostIntegration(t *testing.T) {
120+
121+
testutil.StartIntegrationTestWithContext(t, "sasl-admin")
122+
123+
kafkaCtl := testutil.CreateKafkaCtlCommand()
124+
125+
topicName := testutil.CreateTopic(t, "acl-topic-host")
126+
127+
// add acl for host-a
128+
if _, err := kafkaCtl.Execute("create", "acl", "--topic", topicName, "--operation", "read", "--allow", "--principal", "User:user", "--host", "host-a"); err != nil {
129+
t.Fatalf("failed to execute command: %v", err)
130+
}
131+
132+
// add acl for host-b
133+
if _, err := kafkaCtl.Execute("create", "acl", "--topic", topicName, "--operation", "read", "--allow", "--principal", "User:user", "--host", "host-b"); err != nil {
134+
t.Fatalf("failed to execute command: %v", err)
135+
}
136+
137+
// list acls
138+
if _, err := kafkaCtl.Execute("get", "acl", "--resource-name", topicName, "-o", "yaml"); err != nil {
139+
t.Fatalf("failed to execute command: %v", err)
140+
}
141+
142+
acls, err := acl.FromYaml(kafkaCtl.GetStdOut())
143+
if err != nil {
144+
t.Fatalf("failed to read yaml: %v", err)
145+
}
146+
testutil.AssertIntEquals(t, 1, len(acls))
147+
testutil.AssertIntEquals(t, 2, len(acls[0].Acls))
148+
testutil.AssertEquals(t, "host-a", acls[0].Acls[0].Host)
149+
testutil.AssertEquals(t, "host-b", acls[0].Acls[1].Host)
150+
151+
// delete the acl
152+
if _, err := kafkaCtl.Execute("delete", "acl", "--topics", "--operation", "read", "--host", "host-a", "--pattern", "literal"); err != nil {
153+
t.Fatalf("failed to execute command: %v", err)
154+
}
155+
156+
// list acls
157+
if _, err := kafkaCtl.Execute("get", "acl", "--resource-name", topicName, "-o", "yaml"); err != nil {
158+
t.Fatalf("failed to execute command: %v", err)
159+
}
160+
161+
acls, err = acl.FromYaml(kafkaCtl.GetStdOut())
162+
if err != nil {
163+
t.Fatalf("failed to read yaml: %v", err)
164+
}
165+
testutil.AssertIntEquals(t, 1, len(acls))
166+
testutil.AssertIntEquals(t, 1, len(acls[0].Acls))
167+
testutil.AssertEquals(t, "host-b", acls[0].Acls[0].Host)
168+
}

cmd/get/get-acl.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ func newGetACLCmd() *cobra.Command {
2727
cmdGetAcls.Flags().StringVarP(&flags.Operation, "operation", "", "any", "operation of acl")
2828
cmdGetAcls.Flags().StringVarP(&flags.PatternType, "pattern", "", "any", "pattern type. one of (any, match, prefixed, literal)")
2929

30+
cmdGetAcls.Flags().StringVarP(&flags.ResourceName, "resource-name", "r", "", "resource name of acl (e.g. topic name)")
31+
cmdGetAcls.Flags().StringVarP(&flags.Principal, "principal", "p", "", "principal of acl")
32+
cmdGetAcls.Flags().StringVarP(&flags.Host, "host", "", "", "host of acl")
33+
3034
// specify permissionType
3135
cmdGetAcls.Flags().BoolVarP(&flags.Allow, "allow", "a", false, "acl of permissionType 'allow'")
3236
cmdGetAcls.Flags().BoolVarP(&flags.Deny, "deny", "d", false, "acl of permissionType 'deny'")

cmd/get/get-acl_test.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import (
66
"strings"
77
"testing"
88

9+
"github.com/deviceinsight/kafkactl/v5/internal/acl"
10+
911
"github.com/deviceinsight/kafkactl/v5/internal/testutil"
1012
)
1113

@@ -37,3 +39,67 @@ func TestGetTopicReadAclIntegration(t *testing.T) {
3739

3840
testutil.AssertContains(t, fmt.Sprintf("Topic %s Literal User:user * Read Allow", topicName), outputLines)
3941
}
42+
43+
func TestGetAclByPrincipalIntegration(t *testing.T) {
44+
45+
testutil.StartIntegrationTestWithContext(t, "sasl-admin")
46+
47+
kafkaCtl := testutil.CreateKafkaCtlCommand()
48+
49+
topicName := testutil.CreateTopic(t, "acl-get-topic-principal")
50+
51+
// add read acl
52+
if _, err := kafkaCtl.Execute("create", "acl", "--topic", topicName, "--operation", "read", "--allow", "--principal", "User:user"); err != nil {
53+
t.Fatalf("failed to execute command: %v", err)
54+
}
55+
56+
// add write acl for other user
57+
if _, err := kafkaCtl.Execute("create", "acl", "--topic", topicName, "--operation", "read", "--allow", "--principal", "User:admin"); err != nil {
58+
t.Fatalf("failed to execute command: %v", err)
59+
}
60+
61+
// list acls
62+
if _, err := kafkaCtl.Execute("get", "acl", "--resource-name", topicName, "--principal", "User:user", "-o", "yaml"); err != nil {
63+
t.Fatalf("failed to execute command: %v", err)
64+
}
65+
66+
acls, err := acl.FromYaml(kafkaCtl.GetStdOut())
67+
if err != nil {
68+
t.Fatalf("failed to read yaml: %v", err)
69+
}
70+
testutil.AssertIntEquals(t, 1, len(acls))
71+
testutil.AssertIntEquals(t, 1, len(acls[0].Acls))
72+
testutil.AssertEquals(t, "User:user", acls[0].Acls[0].Principal)
73+
}
74+
75+
func TestGetAclByHostIntegration(t *testing.T) {
76+
77+
testutil.StartIntegrationTestWithContext(t, "sasl-admin")
78+
79+
kafkaCtl := testutil.CreateKafkaCtlCommand()
80+
81+
topicName := testutil.CreateTopic(t, "acl-get-topic-host")
82+
83+
// add acl for host-a
84+
if _, err := kafkaCtl.Execute("create", "acl", "--topic", topicName, "--operation", "read", "--allow", "--principal", "User:user", "--host", "host-a"); err != nil {
85+
t.Fatalf("failed to execute command: %v", err)
86+
}
87+
88+
// add acl for host-b
89+
if _, err := kafkaCtl.Execute("create", "acl", "--topic", topicName, "--operation", "read", "--allow", "--principal", "User:user", "--host", "host-b"); err != nil {
90+
t.Fatalf("failed to execute command: %v", err)
91+
}
92+
93+
// list acls
94+
if _, err := kafkaCtl.Execute("get", "acl", "--resource-name", topicName, "--host", "host-a", "-o", "yaml"); err != nil {
95+
t.Fatalf("failed to execute command: %v", err)
96+
}
97+
98+
acls, err := acl.FromYaml(kafkaCtl.GetStdOut())
99+
if err != nil {
100+
t.Fatalf("failed to read yaml: %v", err)
101+
}
102+
testutil.AssertIntEquals(t, 1, len(acls))
103+
testutil.AssertIntEquals(t, 1, len(acls[0].Acls))
104+
testutil.AssertEquals(t, "host-a", acls[0].Acls[0].Host)
105+
}

internal/acl/acl-operation.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"github.com/deviceinsight/kafkactl/v5/internal/output"
77
"github.com/pkg/errors"
88
"github.com/spf13/cobra"
9+
"gopkg.in/yaml.v2"
910
)
1011

1112
type ResourceACLEntry struct {
@@ -26,7 +27,10 @@ type GetACLFlags struct {
2627
OutputFormat string
2728
FilterTopic string
2829
Operation string
30+
ResourceName string
2931
PatternType string
32+
Principal string
33+
Host string
3034
Allow bool
3135
Deny bool
3236
Topics bool
@@ -54,6 +58,8 @@ type DeleteACLFlags struct {
5458
Cluster bool
5559
Allow bool
5660
Deny bool
61+
Principal string
62+
Host string
5763
Operation string
5864
PatternType string
5965
}
@@ -118,6 +124,18 @@ func (operation *Operation) GetACL(flags GetACLFlags) error {
118124
filter.ResourcePatternTypeFilter = patternTypeFromString(flags.PatternType)
119125
}
120126

127+
if flags.ResourceName != "" {
128+
filter.ResourceName = &flags.ResourceName
129+
}
130+
131+
if flags.Principal != "" {
132+
filter.Principal = &flags.Principal
133+
}
134+
135+
if flags.Host != "" {
136+
filter.Host = &flags.Host
137+
}
138+
121139
if acls, err = admin.ListAcls(filter); err != nil {
122140
return errors.Wrap(err, "failed to list acls")
123141
}
@@ -300,6 +318,14 @@ func (operation *Operation) DeleteACL(flags DeleteACLFlags) error {
300318
filter.ResourcePatternTypeFilter = patternTypeFromString(flags.PatternType)
301319
}
302320

321+
if flags.Principal != "" {
322+
filter.Principal = &flags.Principal
323+
}
324+
325+
if flags.Host != "" {
326+
filter.Host = &flags.Host
327+
}
328+
303329
if matchingACL, err = admin.DeleteACL(filter, flags.ValidateOnly); err != nil {
304330
return errors.Wrap(err, "failed to delete acl")
305331
}
@@ -370,3 +396,9 @@ func CompleteCreateACL(_ *cobra.Command, _ []string, _ string) ([]string, cobra.
370396
output.Infof("complete")
371397
return nil, cobra.ShellCompDirectiveError
372398
}
399+
400+
func FromYaml(yamlString string) ([]ResourceACLEntry, error) {
401+
var entries []ResourceACLEntry
402+
err := yaml.Unmarshal([]byte(yamlString), &entries)
403+
return entries, err
404+
}

internal/testutil/test_util.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,13 @@ func AssertEquals(t *testing.T, expected, actual string) {
183183
}
184184
}
185185

186+
func AssertIntEquals(t *testing.T, expected, actual int) {
187+
t.Helper()
188+
if actual != expected {
189+
t.Fatalf("unexpected output:\nexpected:\n--\n%d\n--\nactual:\n--\n%d\n--", expected, actual)
190+
}
191+
}
192+
186193
func AssertArraysEquals(t *testing.T, expected, actual []string) {
187194
t.Helper()
188195
sort.Strings(expected)

0 commit comments

Comments
 (0)