Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions internal/kafka/command/flag/flag.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,14 @@ func (a *KafkaTopicGrantAccess) AutoComplete(context.Context, *naistrix.Argument
return []string{"read", "write", "readwrite"}, "Available access levels."
}

func (a *KafkaTopicGrantAccess) Validate() error {
func (a KafkaTopicGrantAccess) Validate() error {
valid := []string{"read", "write", "readwrite"}
if a == nil || *a == "" {
if a == "" {
return naistrix.Errorf("access level is required, must be one of: %s", strings.Join(valid, ", "))
}

if !slices.Contains(valid, string(*a)) {
return naistrix.Errorf("invalid access level: %q, must be one of: %s", *a, strings.Join(valid, ", "))
if !slices.Contains(valid, string(a)) {
return naistrix.Errorf("invalid access level: %q, must be one of: %s", a, strings.Join(valid, ", "))
}

return nil
Expand Down
4 changes: 2 additions & 2 deletions internal/kafka/command/list_grants_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,8 @@ func TestRevokeGrantCommand(t *testing.T) {
if command.Name != "revoke-grant" {
t.Errorf("Name = %q, want %q", command.Name, "revoke-grant")
}
if len(command.Args) != 3 || command.Args[0].Name != "topic" || command.Args[1].Name != "username" || command.Args[2].Name != "access" {
t.Errorf("Args = %#v, want topic, username, and access arguments", command.Args)
if len(command.Args) != 3 || command.Args[0].Name != "username" || command.Args[1].Name != "topic" || command.Args[2].Name != "access" {
t.Errorf("Args = %#v, want username, topic, and access arguments", command.Args)
}
if command.ValidateFunc == nil {
t.Error("ValidateFunc is nil")
Expand Down
98 changes: 62 additions & 36 deletions internal/kafka/command/revoke_grant.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,21 @@ func revokeGrant(parentFlags *flag.Kafka) *naistrix.Command {
Title: "Revoke a user's service-user access to a Kafka topic.",
Description: "Removes an ACL entry for a user on a Kafka topic with the specified access level.",
Args: []naistrix.Argument{
{Name: "topic"},
{Name: "username"},
{Name: "topic"},
{Name: "access"},
},
ValidateFunc: validation.RequireTeamAndEnvironment(parentFlags),
ValidateFunc: naistrix.ValidateFuncs(
validation.RequireTeamAndEnvironment(parentFlags),
func(_ context.Context, args *naistrix.Arguments) error {
return flag.KafkaTopicGrantAccess(args.Get("access")).Validate()
},
),
AutoCompleteFunc: autoCompleteKafkaGrantArguments(parentFlags),
RunFunc: func(ctx context.Context, args *naistrix.Arguments, out *naistrix.OutputWriter) error {
topicName := args.Get("topic")
subject := kafkaApplicationName(args.Get("username"))
topicName := args.Get("topic")
access := flag.KafkaTopicGrantAccess(args.Get("access"))
if err := access.Validate(); err != nil {
return err
}
grant := gql.KafkaTopicGrantInput{
Subject: subject,
TeamName: parentFlags.Team,
Expand All @@ -51,12 +53,7 @@ func revokeGrant(parentFlags *flag.Kafka) *naistrix.Command {
}

func autoCompleteKafkaGrantArguments(flags *flag.Kafka) naistrix.AutoCompleteFunc {
topic := autoCompleteKafkaTopicName(flags, 0)

return func(ctx context.Context, args *naistrix.Arguments, toComplete string) ([]string, string) {
if args.Len() == 0 {
return topic(ctx, args, toComplete)
}
if args.Len() > 2 {
return nil, ""
}
Expand All @@ -65,40 +62,69 @@ func autoCompleteKafkaGrantArguments(flags *flag.Kafka) naistrix.AutoCompleteFun
return nil, "Please provide team and environment to auto-complete Kafka grants."
}

grants, err := kafka.GetKafkaTopicGrants(ctx, args.Get("topic"), flags.Team, flags.Environment)
if err != nil {
return nil, "Unable to fetch Kafka topic grants."
}
switch args.Len() {
case 0, 1:
grants, err := kafka.GetTeamKafkaTopicGrants(ctx, flags.Team, flags.Environment)
if err != nil {
return nil, "Unable to fetch Kafka grants."
}

if args.Len() == 0 {
subjects := make([]string, 0, len(grants))
seen := make(map[string]struct{})
for _, grant := range grants {
if _, ok := seen[grant.WorkloadName]; ok {
continue
}
seen[grant.WorkloadName] = struct{}{}
subjects = append(subjects, grant.WorkloadName)
}
sort.Strings(subjects)
if len(subjects) == 0 {
return nil, "No Kafka grants found in the selected environment."
}
return subjects, "Select a subject with a Kafka grant."
}

if args.Len() == 1 {
subjects := make([]string, 0, len(grants))
subject := kafkaApplicationName(args.Get("username"))
topics := make([]string, 0, len(grants))
seen := make(map[string]struct{})
for _, grant := range grants {
if _, ok := seen[grant.WorkloadName]; ok {
if grant.WorkloadName != subject {
continue
}
if _, ok := seen[grant.TopicName]; ok {
continue
}
seen[grant.WorkloadName] = struct{}{}
subjects = append(subjects, grant.WorkloadName)
seen[grant.TopicName] = struct{}{}
topics = append(topics, grant.TopicName)
}
sort.Strings(subjects)
if len(subjects) == 0 {
return nil, "No access grants found for this Kafka topic."
sort.Strings(topics)
if len(topics) == 0 {
return nil, "No Kafka grants found for this subject."
}
return topics, "Select a Kafka topic with a grant for this subject."
case 2:
grants, err := kafka.GetKafkaTopicGrants(ctx, args.Get("topic"), flags.Team, flags.Environment)
if err != nil {
return nil, "Unable to fetch Kafka topic grants."
}
return subjects, "Select a subject with access to this Kafka topic."
}

subject := kafkaApplicationName(args.Get("username"))
accesses := make([]string, 0, len(grants))
for _, grant := range grants {
if grant.WorkloadName == subject {
accesses = append(accesses, strings.ToLower(grant.Access))
subject := kafkaApplicationName(args.Get("username"))
accesses := make([]string, 0, len(grants))
for _, grant := range grants {
if grant.WorkloadName == subject {
accesses = append(accesses, strings.ToLower(grant.Access))
}
}
sort.Strings(accesses)
if len(accesses) == 0 {
return nil, "No access grants found for this subject on the Kafka topic."
}
}
sort.Strings(accesses)
if len(accesses) == 0 {
return nil, "No access grants found for this subject on the Kafka topic."
}

return accesses, "Select an access level to revoke."
return accesses, "Select an access level to revoke."
default:
return nil, ""
}
}
}
52 changes: 52 additions & 0 deletions internal/kafka/kafka.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ type Grant struct {
Access string `heading:"Access level" json:"access"`
}

type TopicGrant struct {
TopicName string `json:"topicName"`
Grant
}

func GetTeamTopics(ctx context.Context, team string, environment string, labels []gql.LabelFilter) ([]Topic, error) {
_ = `# @genqlient
query GetTeamKafkaTopics($team: Slug!, $filter: KafkaTopicFilter) {
Expand Down Expand Up @@ -188,3 +193,50 @@ func GetKafkaTopicGrants(ctx context.Context, topicName, teamSlug string, enviro

return ret, nil
}

func GetTeamKafkaTopicGrants(ctx context.Context, teamSlug string, environmentName flags.Environment) ([]TopicGrant, error) {
_ = `# @genqlient
query GetTeamKafkaTopicGrants($teamSlug: Slug!, $environmentName: String!) {
team(slug: $teamSlug) {
kafkaTopics(first: 1000, filter: { environments: [$environmentName] }) {
nodes {
name
acl(first: 1000, filter: { team: $teamSlug }) {
nodes {
workloadName
teamName
access
}
}
}
}
}
}
`

client, err := naisapi.GraphqlClient(ctx)
if err != nil {
return nil, err
}

resp, err := gql.GetTeamKafkaTopicGrants(ctx, client, teamSlug, string(environmentName))
if err != nil {
return nil, err
}

var ret []TopicGrant
for _, topic := range resp.Team.KafkaTopics.Nodes {
for _, grant := range topic.Acl.Nodes {
ret = append(ret, TopicGrant{
TopicName: topic.Name,
Grant: Grant{
WorkloadName: grant.WorkloadName,
TeamName: grant.TeamName,
Access: string(grant.Access),
},
})
}
}

return ret, nil
}
144 changes: 144 additions & 0 deletions internal/naisapi/gql/generated.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading