From c6378c391a970fc19c1dbd664016f09c6a73ea8d Mon Sep 17 00:00:00 2001 From: Peter Dannemann <28637185+petedannemann@users.noreply.github.com> Date: Mon, 30 Oct 2023 13:43:23 -0400 Subject: [PATCH] add methods for parsing and stringifying acl related resources (#1218) * Add methods for marshalling/unmarshalling ACLOperationType and ACLPermissionType * Fix unknown types * Implement ResourceType * make function ordering more consistent * add patterntype and resourcetype * make capitalization consistent * add comment and test around ResourceTypeBroker and ResourceTypeCluster having same value --- createacls.go | 94 ++++++++++++++++++++++++++++++++++++++++++++++ createacls_test.go | 34 +++++++++++++++++ resource.go | 86 ++++++++++++++++++++++++++++++++++++++++++ resource_test.go | 58 ++++++++++++++++++++++++++++ 4 files changed, 272 insertions(+) create mode 100644 resource_test.go diff --git a/createacls.go b/createacls.go index 672f6fdce..601974171 100644 --- a/createacls.go +++ b/createacls.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "net" + "strings" "time" "github.com/segmentio/kafka-go/protocol/createacls" @@ -42,6 +43,43 @@ const ( ACLPermissionTypeAllow ACLPermissionType = 3 ) +func (apt ACLPermissionType) String() string { + mapping := map[ACLPermissionType]string{ + ACLPermissionTypeUnknown: "Unknown", + ACLPermissionTypeAny: "Any", + ACLPermissionTypeDeny: "Deny", + ACLPermissionTypeAllow: "Allow", + } + s, ok := mapping[apt] + if !ok { + s = mapping[ACLPermissionTypeUnknown] + } + return s +} + +// MarshalText transforms an ACLPermissionType into its string representation. +func (apt ACLPermissionType) MarshalText() ([]byte, error) { + return []byte(apt.String()), nil +} + +// UnmarshalText takes a string representation of the resource type and converts it to an ACLPermissionType. +func (apt *ACLPermissionType) UnmarshalText(text []byte) error { + normalized := strings.ToLower(string(text)) + mapping := map[string]ACLPermissionType{ + "unknown": ACLPermissionTypeUnknown, + "any": ACLPermissionTypeAny, + "deny": ACLPermissionTypeDeny, + "allow": ACLPermissionTypeAllow, + } + parsed, ok := mapping[normalized] + if !ok { + *apt = ACLPermissionTypeUnknown + return fmt.Errorf("cannot parse %s as an ACLPermissionType", normalized) + } + *apt = parsed + return nil +} + type ACLOperationType int8 const ( @@ -60,6 +98,62 @@ const ( ACLOperationTypeIdempotentWrite ACLOperationType = 12 ) +func (aot ACLOperationType) String() string { + mapping := map[ACLOperationType]string{ + ACLOperationTypeUnknown: "Unknown", + ACLOperationTypeAny: "Any", + ACLOperationTypeAll: "All", + ACLOperationTypeRead: "Read", + ACLOperationTypeWrite: "Write", + ACLOperationTypeCreate: "Create", + ACLOperationTypeDelete: "Delete", + ACLOperationTypeAlter: "Alter", + ACLOperationTypeDescribe: "Describe", + ACLOperationTypeClusterAction: "ClusterAction", + ACLOperationTypeDescribeConfigs: "DescribeConfigs", + ACLOperationTypeAlterConfigs: "AlterConfigs", + ACLOperationTypeIdempotentWrite: "IdempotentWrite", + } + s, ok := mapping[aot] + if !ok { + s = mapping[ACLOperationTypeUnknown] + } + return s +} + +// MarshalText transforms an ACLOperationType into its string representation. +func (aot ACLOperationType) MarshalText() ([]byte, error) { + return []byte(aot.String()), nil +} + +// UnmarshalText takes a string representation of the resource type and converts it to an ACLPermissionType. +func (aot *ACLOperationType) UnmarshalText(text []byte) error { + normalized := strings.ToLower(string(text)) + mapping := map[string]ACLOperationType{ + "unknown": ACLOperationTypeUnknown, + "any": ACLOperationTypeAny, + "all": ACLOperationTypeAll, + "read": ACLOperationTypeRead, + "write": ACLOperationTypeWrite, + "create": ACLOperationTypeCreate, + "delete": ACLOperationTypeDelete, + "alter": ACLOperationTypeAlter, + "describe": ACLOperationTypeDescribe, + "clusteraction": ACLOperationTypeClusterAction, + "describeconfigs": ACLOperationTypeDescribeConfigs, + "alterconfigs": ACLOperationTypeAlterConfigs, + "idempotentwrite": ACLOperationTypeIdempotentWrite, + } + parsed, ok := mapping[normalized] + if !ok { + *aot = ACLOperationTypeUnknown + return fmt.Errorf("cannot parse %s as an ACLOperationType", normalized) + } + *aot = parsed + return nil + +} + type ACLEntry struct { ResourceType ResourceType ResourceName string diff --git a/createacls_test.go b/createacls_test.go index ee04779ea..314427a57 100644 --- a/createacls_test.go +++ b/createacls_test.go @@ -50,3 +50,37 @@ func TestClientCreateACLs(t *testing.T) { } } } + +func TestACLPermissionTypeMarshal(t *testing.T) { + for i := ACLPermissionTypeUnknown; i <= ACLPermissionTypeAllow; i++ { + text, err := i.MarshalText() + if err != nil { + t.Errorf("couldn't marshal %d to text: %s", i, err) + } + var got ACLPermissionType + err = got.UnmarshalText(text) + if err != nil { + t.Errorf("couldn't unmarshal %s to ACLPermissionType: %s", text, err) + } + if got != i { + t.Errorf("got %d, want %d", got, i) + } + } +} + +func TestACLOperationTypeMarshal(t *testing.T) { + for i := ACLOperationTypeUnknown; i <= ACLOperationTypeIdempotentWrite; i++ { + text, err := i.MarshalText() + if err != nil { + t.Errorf("couldn't marshal %d to text: %s", i, err) + } + var got ACLOperationType + err = got.UnmarshalText(text) + if err != nil { + t.Errorf("couldn't unmarshal %s to ACLOperationType: %s", text, err) + } + if got != i { + t.Errorf("got %d, want %d", got, i) + } + } +} diff --git a/resource.go b/resource.go index f5c2e73a5..b9be107c2 100644 --- a/resource.go +++ b/resource.go @@ -1,5 +1,10 @@ package kafka +import ( + "fmt" + "strings" +) + // https://github.com/apache/kafka/blob/trunk/clients/src/main/java/org/apache/kafka/common/resource/ResourceType.java type ResourceType int8 @@ -15,6 +20,50 @@ const ( ResourceTypeDelegationToken ResourceType = 6 ) +func (rt ResourceType) String() string { + mapping := map[ResourceType]string{ + ResourceTypeUnknown: "Unknown", + ResourceTypeAny: "Any", + ResourceTypeTopic: "Topic", + ResourceTypeGroup: "Group", + // Note that ResourceTypeBroker and ResourceTypeCluster have the same value. + // A map cannot have duplicate values so we just use the same value for both. + ResourceTypeCluster: "Cluster", + ResourceTypeTransactionalID: "Transactionalid", + ResourceTypeDelegationToken: "Delegationtoken", + } + s, ok := mapping[rt] + if !ok { + s = mapping[ResourceTypeUnknown] + } + return s +} + +func (rt ResourceType) MarshalText() ([]byte, error) { + return []byte(rt.String()), nil +} + +func (rt *ResourceType) UnmarshalText(text []byte) error { + normalized := strings.ToLower(string(text)) + mapping := map[string]ResourceType{ + "unknown": ResourceTypeUnknown, + "any": ResourceTypeAny, + "topic": ResourceTypeTopic, + "group": ResourceTypeGroup, + "broker": ResourceTypeBroker, + "cluster": ResourceTypeCluster, + "transactionalid": ResourceTypeTransactionalID, + "delegationtoken": ResourceTypeDelegationToken, + } + parsed, ok := mapping[normalized] + if !ok { + *rt = ResourceTypeUnknown + return fmt.Errorf("cannot parse %s as a ResourceType", normalized) + } + *rt = parsed + return nil +} + // https://github.com/apache/kafka/blob/trunk/clients/src/main/java/org/apache/kafka/common/resource/PatternType.java type PatternType int8 @@ -35,3 +84,40 @@ const ( // that start with 'foo'. PatternTypePrefixed PatternType = 4 ) + +func (pt PatternType) String() string { + mapping := map[PatternType]string{ + PatternTypeUnknown: "Unknown", + PatternTypeAny: "Any", + PatternTypeMatch: "Match", + PatternTypeLiteral: "Literal", + PatternTypePrefixed: "Prefixed", + } + s, ok := mapping[pt] + if !ok { + s = mapping[PatternTypeUnknown] + } + return s +} + +func (pt PatternType) MarshalText() ([]byte, error) { + return []byte(pt.String()), nil +} + +func (pt *PatternType) UnmarshalText(text []byte) error { + normalized := strings.ToLower(string(text)) + mapping := map[string]PatternType{ + "unknown": PatternTypeUnknown, + "any": PatternTypeAny, + "match": PatternTypeMatch, + "literal": PatternTypeLiteral, + "prefixed": PatternTypePrefixed, + } + parsed, ok := mapping[normalized] + if !ok { + *pt = PatternTypeUnknown + return fmt.Errorf("cannot parse %s as a PatternType", normalized) + } + *pt = parsed + return nil +} diff --git a/resource_test.go b/resource_test.go new file mode 100644 index 000000000..b0175d2a3 --- /dev/null +++ b/resource_test.go @@ -0,0 +1,58 @@ +package kafka + +import "testing" + +func TestResourceTypeMarshal(t *testing.T) { + for i := ResourceTypeUnknown; i <= ResourceTypeDelegationToken; i++ { + text, err := i.MarshalText() + if err != nil { + t.Errorf("couldn't marshal %d to text: %s", i, err) + } + var got ResourceType + err = got.UnmarshalText(text) + if err != nil { + t.Errorf("couldn't unmarshal %s to ResourceType: %s", text, err) + } + if got != i { + t.Errorf("got %d, want %d", got, i) + } + } +} + +// Verify that the text version of ResourceTypeBroker is "Cluster". +// This is added since ResourceTypeBroker and ResourceTypeCluster +// have the same value. +func TestResourceTypeBroker(t *testing.T) { + text, err := ResourceTypeBroker.MarshalText() + if err != nil { + t.Errorf("couldn't marshal %d to text: %s", ResourceTypeBroker, err) + } + if string(text) != "Cluster" { + t.Errorf("got %s, want %s", string(text), "Cluster") + } + var got ResourceType + err = got.UnmarshalText(text) + if err != nil { + t.Errorf("couldn't unmarshal %s to ResourceType: %s", text, err) + } + if got != ResourceTypeBroker { + t.Errorf("got %d, want %d", got, ResourceTypeBroker) + } +} + +func TestPatternTypeMarshal(t *testing.T) { + for i := PatternTypeUnknown; i <= PatternTypePrefixed; i++ { + text, err := i.MarshalText() + if err != nil { + t.Errorf("couldn't marshal %d to text: %s", i, err) + } + var got PatternType + err = got.UnmarshalText(text) + if err != nil { + t.Errorf("couldn't unmarshal %s to PatternType: %s", text, err) + } + if got != i { + t.Errorf("got %d, want %d", got, i) + } + } +}