From 1cea2ade3d5cbb4ede06623dd0fb33039b646f09 Mon Sep 17 00:00:00 2001 From: Satyam Singh Date: Wed, 27 Sep 2023 19:13:30 +0530 Subject: [PATCH] Update rbac commands (#26) This PR adds fix for different APIs added to the server for role based access control --- cmd/role.go | 283 ++++++++++++++++++++++++++++++++++++++++++++++++++++ cmd/user.go | 117 +++++++--------------- main.go | 12 +++ 3 files changed, 331 insertions(+), 81 deletions(-) create mode 100644 cmd/role.go diff --git a/cmd/role.go b/cmd/role.go new file mode 100644 index 0000000..107701b --- /dev/null +++ b/cmd/role.go @@ -0,0 +1,283 @@ +// Copyright (c) 2023 Cloudnatively Services Pvt Ltd +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cmd + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "os" + "pb/pkg/model/role" + "strings" + "sync" + + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/spf13/cobra" +) + +type RoleResource struct { + Stream string `json:"stream,omitempty"` + Tag string `json:"tag,omitempty"` +} + +type RoleData struct { + Privilege string `json:"privilege"` + Resource *RoleResource `json:"resource,omitempty"` +} + +func (user *RoleData) Render() string { + var s strings.Builder + s.WriteString(standardStyle.Render("Privilege: ")) + s.WriteString(standardStyleAlt.Render(user.Privilege)) + s.WriteString("\n") + if user.Resource != nil { + if user.Resource.Stream != "" { + s.WriteString(standardStyle.Render("Stream: ")) + s.WriteString(standardStyleAlt.Render(user.Resource.Stream)) + s.WriteString("\n") + } + if user.Resource.Tag != "" { + s.WriteString(standardStyle.Render("Tag: ")) + s.WriteString(standardStyleAlt.Render(user.Resource.Tag)) + s.WriteString("\n") + } + } + + return s.String() +} + +var AddRoleCmd = &cobra.Command{ + Use: "upsert role-name", + Example: " pb role upsert ingestors", + Short: "Add a new role", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + name := args[0] + + _m, err := tea.NewProgram(role.New()).Run() + if err != nil { + fmt.Printf("Alas, there's been an error: %v", err) + os.Exit(1) + } + m := _m.(role.Model) + + privilege := m.Selection.Value() + stream := m.Stream.Value() + tag := m.Tag.Value() + + if !m.Success { + fmt.Println("aborted by user") + return nil + } + + var putBody io.Reader + + // set role + if privilege != "none" { + roleData := RoleData{ + Privilege: privilege, + } + switch privilege { + case "writer": + roleData.Resource = &RoleResource{ + Stream: stream, + } + case "reader": + roleData.Resource = &RoleResource{ + Stream: stream, + } + if tag != "" { + roleData.Resource.Tag = tag + } + } + roleDataJSON, _ := json.Marshal([]RoleData{roleData}) + putBody = bytes.NewBuffer(roleDataJSON) + } + + client := DefaultClient() + req, err := client.NewRequest("PUT", "role/"+name, putBody) + if err != nil { + return err + } + + resp, err := client.client.Do(req) + if err != nil { + return err + } + + bytes, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + body := string(bytes) + defer resp.Body.Close() + + if resp.StatusCode == 200 { + fmt.Printf("Added role %s", name) + } else { + fmt.Printf("Request Failed\nStatus Code: %s\nResponse: %s\n", resp.Status, body) + } + + return nil + }, +} + +var RemoveRoleCmd = &cobra.Command{ + Use: "remove role-name", + Aliases: []string{"rm"}, + Example: " pb role remove ingestor", + Short: "Delete a role", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + name := args[0] + client := DefaultClient() + req, err := client.NewRequest("DELETE", "role/"+name, nil) + if err != nil { + return err + } + + resp, err := client.client.Do(req) + if err != nil { + return err + } + + if resp.StatusCode == 200 { + fmt.Printf("Removed role %s\n", styleBold.Render(name)) + } else { + bytes, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + body := string(bytes) + defer resp.Body.Close() + + fmt.Printf("Request Failed\nStatus Code: %s\nResponse: %s\n", resp.Status, body) + } + + return nil + }, +} + +var ListRoleCmd = &cobra.Command{ + Use: "list", + Short: "List all roles", + Example: " pb role list", + RunE: func(cmd *cobra.Command, args []string) error { + var roles []string + client := DefaultClient() + err := fetchRoles(&client, &roles) + if err != nil { + return err + } + + roleResponses := make([]struct { + data []RoleData + err error + }, len(roles)) + + wsg := sync.WaitGroup{} + for idx, role := range roles { + wsg.Add(1) + out := &roleResponses[idx] + role := role + client := &client + go func() { + out.data, out.err = fetchSpecificRole(client, role) + wsg.Done() + }() + } + + wsg.Wait() + fmt.Println() + for idx, roleName := range roles { + fetchRes := roleResponses[idx] + fmt.Print("• ") + fmt.Println(standardStyleBold.Bold(true).Render(roleName)) + if fetchRes.err == nil { + for _, role := range fetchRes.data { + fmt.Println(lipgloss.NewStyle().PaddingLeft(3).Render(role.Render())) + } + } else { + fmt.Println(fetchRes.err) + } + } + + return nil + }, +} + +func fetchRoles(client *HTTPClient, data *[]string) error { + req, err := client.NewRequest("GET", "role", nil) + if err != nil { + return err + } + + resp, err := client.client.Do(req) + if err != nil { + return err + } + + bytes, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode == 200 { + err = json.Unmarshal(bytes, data) + if err != nil { + return err + } + } else { + body := string(bytes) + return fmt.Errorf("request failed\nstatus code: %s\nresponse: %s", resp.Status, body) + } + + return nil +} + +func fetchSpecificRole(client *HTTPClient, role string) (res []RoleData, err error) { + req, err := client.NewRequest("GET", fmt.Sprintf("role/%s", role), nil) + if err != nil { + return + } + + resp, err := client.client.Do(req) + if err != nil { + return + } + + bytes, err := io.ReadAll(resp.Body) + if err != nil { + return + } + defer resp.Body.Close() + + if resp.StatusCode == 200 { + err = json.Unmarshal(bytes, &res) + if err != nil { + return + } + } else { + body := string(bytes) + err = fmt.Errorf("request failed\nstatus code: %s\nresponse: %s", resp.Status, body) + return + } + + return +} diff --git a/cmd/user.go b/cmd/user.go index ec1afe7..73fd4b4 100644 --- a/cmd/user.go +++ b/cmd/user.go @@ -22,7 +22,6 @@ import ( "io" "os" "pb/pkg/model/role" - "strings" "sync" tea "github.com/charmbracelet/bubbletea" @@ -31,41 +30,12 @@ import ( "golang.org/x/exp/slices" ) -type RoleResource struct { - Stream string `json:"stream,omitempty"` - Tag string `json:"tag,omitempty"` +type UserData struct { + ID string `json:"id"` + Method string `json:"method"` } -type UserRoleData struct { - Privilege string `json:"privilege"` - Resource *RoleResource `json:"resource,omitempty"` -} - -func (user *UserRoleData) Render() string { - var s strings.Builder - s.WriteString(standardStyle.Render("Privilege: ")) - s.WriteString(standardStyleAlt.Render(user.Privilege)) - s.WriteString("\n") - if user.Resource != nil { - if user.Resource.Stream != "" { - s.WriteString(standardStyle.Render("Stream: ")) - s.WriteString(standardStyleAlt.Render(user.Resource.Stream)) - s.WriteString("\n") - } - if user.Resource.Tag != "" { - s.WriteString(standardStyle.Render("Tag: ")) - s.WriteString(standardStyleAlt.Render(user.Resource.Tag)) - s.WriteString("\n") - } - } - - return s.String() -} - -type FetchUserRoleRes struct { - data []UserRoleData - err error -} +type UserRoleData map[string][]RoleData var AddUserCmd = &cobra.Command{ Use: "add user-name", @@ -75,13 +45,15 @@ var AddUserCmd = &cobra.Command{ RunE: func(cmd *cobra.Command, args []string) error { name := args[0] - var users []string client := DefaultClient() - if err := fetchUsers(&client, &users); err != nil { + users, err := fetchUsers(&client) + if err != nil { return err } - if slices.Contains(users, name) { + if slices.ContainsFunc(users, func(user UserData) bool { + return user.ID == name + }) { fmt.Println("user already exists") return nil } @@ -93,9 +65,7 @@ var AddUserCmd = &cobra.Command{ } m := _m.(role.Model) - privilege := m.Selection.Value() - stream := m.Stream.Value() - tag := m.Tag.Value() + roles := m.Selection.Value() if !m.Success { fmt.Println("aborted by user") @@ -103,28 +73,9 @@ var AddUserCmd = &cobra.Command{ } var putBody io.Reader + roleDataJSON, _ := json.Marshal([]string{roles}) + putBody = bytes.NewBuffer(roleDataJSON) - // set role - if privilege != "none" { - roleData := UserRoleData{ - Privilege: privilege, - } - switch privilege { - case "writer": - roleData.Resource = &RoleResource{ - Stream: stream, - } - case "reader": - roleData.Resource = &RoleResource{ - Stream: stream, - } - if tag != "" { - roleData.Resource.Tag = tag - } - } - roleDataJSON, _ := json.Marshal([]UserRoleData{roleData}) - putBody = bytes.NewBuffer(roleDataJSON) - } req, err := client.NewRequest("PUT", "user/"+name, putBody) if err != nil { return err @@ -193,23 +144,26 @@ var ListUserCmd = &cobra.Command{ Short: "List all users", Example: " pb user list", RunE: func(cmd *cobra.Command, args []string) error { - var users []string client := DefaultClient() - err := fetchUsers(&client, &users) + users, err := fetchUsers(&client) if err != nil { return err } - roleResponses := make([]FetchUserRoleRes, len(users)) + roleResponses := make([]struct { + data UserRoleData + err error + }, len(users)) + wsg := sync.WaitGroup{} - wsg.Add(len(users)) for idx, user := range users { - idx := idx - user := user + wsg.Add(1) + out := &roleResponses[idx] + user := user.ID client := &client go func() { - roleResponses[idx] = fetchUserRoles(client, user) + out.data, out.err = fetchUserRoles(client, user) wsg.Done() }() } @@ -219,10 +173,10 @@ var ListUserCmd = &cobra.Command{ for idx, user := range users { roles := roleResponses[idx] fmt.Print("• ") - fmt.Println(standardStyleBold.Bold(true).Render(user)) + fmt.Println(standardStyleBold.Bold(true).Render(user.ID)) if roles.err == nil { - for _, role := range roles.data { - fmt.Println(lipgloss.NewStyle().PaddingLeft(3).Render(role.Render())) + for role := range roles.data { + fmt.Println(lipgloss.NewStyle().PaddingLeft(3).Render(role)) } } else { fmt.Println(roles.err) @@ -233,37 +187,38 @@ var ListUserCmd = &cobra.Command{ }, } -func fetchUsers(client *HTTPClient, data *[]string) error { +func fetchUsers(client *HTTPClient) (res []UserData, err error) { req, err := client.NewRequest("GET", "user", nil) if err != nil { - return err + return } resp, err := client.client.Do(req) if err != nil { - return err + return } bytes, err := io.ReadAll(resp.Body) if err != nil { - return err + return } defer resp.Body.Close() if resp.StatusCode == 200 { - err = json.Unmarshal(bytes, data) + err = json.Unmarshal(bytes, &res) if err != nil { - return err + return } } else { body := string(bytes) - return fmt.Errorf("request failed\nstatus code: %s\nresponse: %s", resp.Status, body) + err = fmt.Errorf("request failed\nstatus code: %s\nresponse: %s", resp.Status, body) + return } - return nil + return } -func fetchUserRoles(client *HTTPClient, user string) (res FetchUserRoleRes) { +func fetchUserRoles(client *HTTPClient, user string) (res UserRoleData, err error) { req, err := client.NewRequest("GET", fmt.Sprintf("user/%s/role", user), nil) if err != nil { return @@ -278,6 +233,6 @@ func fetchUserRoles(client *HTTPClient, user string) (res FetchUserRoleRes) { } defer resp.Body.Close() - res.err = json.Unmarshal(body, &res.data) + err = json.Unmarshal(body, &res) return } diff --git a/main.go b/main.go index 387a779..2487c1e 100644 --- a/main.go +++ b/main.go @@ -78,6 +78,13 @@ var user = &cobra.Command{ PersistentPreRunE: cmd.PreRunDefaultProfile, } +var role = &cobra.Command{ + Use: "role", + Short: "Manage roles", + Long: "\nuser command is used to manage roles.", + PersistentPreRunE: cmd.PreRunDefaultProfile, +} + var stream = &cobra.Command{ Use: "stream", Short: "Manage streams", @@ -121,6 +128,10 @@ func main() { user.AddCommand(cmd.RemoveUserCmd) user.AddCommand(cmd.ListUserCmd) + role.AddCommand(cmd.AddRoleCmd) + role.AddCommand(cmd.RemoveRoleCmd) + role.AddCommand(cmd.ListRoleCmd) + stream.AddCommand(cmd.AddStreamCmd) stream.AddCommand(cmd.RemoveStreamCmd) stream.AddCommand(cmd.ListStreamCmd) @@ -132,6 +143,7 @@ func main() { cli.AddCommand(query) cli.AddCommand(stream) cli.AddCommand(user) + cli.AddCommand(role) // Set as command cmd.VersionCmd.Run = func(_ *cobra.Command, args []string) {