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
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,13 @@ pb status
pb version
```

User and role behavior:

- `pb user add` creates self-hosted users and requires at least one existing role. Parseable Cloud users must be invited from the Cloud dashboard instead.
- `pb user set-role` adds the requested roles without removing the user's existing role assignments.
- `pb role add` starts an interactive prompt that asks only for a privilege: `admin`, `editor`, `ingestor`, `reader`, or `writer`.
- `pb role remove` (or `pb role rm`) deletes an existing role. If the role name does not exist, the CLI shows the available role names and does not send a delete request.

Short aliases are available for common commands:

```bash
Expand Down
83 changes: 61 additions & 22 deletions cmd/role.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,13 @@ import (
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/spf13/cobra"
"golang.org/x/exp/slices"
)

type RoleResource struct {
Stream string `json:"stream,omitempty"`
Dataset string `json:"dataset,omitempty"`
Tag string `json:"tag,omitempty"`
Stream string `json:"stream,omitempty"` // Legacy response compatibility.
}

type RoleData struct {
Expand All @@ -47,9 +50,14 @@ func (user *RoleData) Render() string {
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))
if dataset := roleDataset(*user.Resource); dataset != "" {
s.WriteString(StandardStyle.Render("Dataset: "))
s.WriteString(StandardStyleAlt.Render(dataset))
s.WriteString("\n")
}
if user.Resource.Tag != "" {
s.WriteString(StandardStyle.Render("Tag: "))
s.WriteString(StandardStyleAlt.Render(user.Resource.Tag))
s.WriteString("\n")
}
}
Expand All @@ -61,6 +69,7 @@ var AddRoleCmd = &cobra.Command{
Use: "add role-name",
Example: " pb role add ingestors",
Short: "Add a new role",
Long: "Add a new role using an interactive prompt that asks only for its privilege.",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
startTime := time.Now()
Expand All @@ -78,7 +87,7 @@ var AddRoleCmd = &cobra.Command{
return err
}

if strings.Contains(strings.Join(roles, " "), name) {
if slices.Contains(roles, name) {
fmt.Println("role already exists, please use a different name")
return nil
}
Expand All @@ -91,25 +100,19 @@ var AddRoleCmd = &cobra.Command{

m := _m.(role.Model)
privilege := m.Selection.Value()
stream := m.Stream.Value()

if !m.Success {
fmt.Println("aborted by user")
return nil
}

var putBody io.Reader
if privilege != "none" {
roleData := RoleData{Privilege: privilege}
switch privilege {
case "writer", "ingestor":
roleData.Resource = &RoleResource{Stream: stream}
case "reader":
roleData.Resource = &RoleResource{Stream: stream}
}
roleDataJSON, _ := json.Marshal([]RoleData{roleData})
putBody = bytes.NewBuffer(roleDataJSON)
roleData := newRoleData(privilege)
roleDataJSON, err := json.Marshal([]RoleData{roleData})
if err != nil {
cmd.Annotations["errors"] = fmt.Sprintf("Error encoding role: %s", err.Error())
return err
}
var putBody io.Reader = bytes.NewBuffer(roleDataJSON)

req, err := client.NewRequest("PUT", "role/"+name, putBody)
if err != nil {
Expand Down Expand Up @@ -145,8 +148,9 @@ var AddRoleCmd = &cobra.Command{
var RemoveRoleCmd = &cobra.Command{
Use: "remove role-name",
Aliases: []string{"rm"},
Example: " pb role remove ingestor",
Example: " pb role remove ingestor\n pb role rm ingestor",
Short: "Delete a role",
Long: "Delete an existing role. If the role name is missing, show the available role names without sending a delete request.",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
startTime := time.Now()
Expand All @@ -157,6 +161,17 @@ var RemoveRoleCmd = &cobra.Command{

name := args[0]
client := internalHTTP.DefaultClient(&DefaultProfile)
var roles []string
if err := fetchRoles(&client, &roles); err != nil {
cmd.Annotations["errors"] = fmt.Sprintf("Error fetching roles: %s", err.Error())
return err
}
if !slices.Contains(roles, name) {
fmt.Println(missingRoleMessage(name, roles))
cmd.Annotations["errors"] = fmt.Sprintf("role %s does not exist", name)
return nil
}

req, err := client.NewRequest("DELETE", "role/"+name, nil)
if err != nil {
cmd.Annotations["errors"] = fmt.Sprintf("Error creating delete request: %s", err.Error())
Expand Down Expand Up @@ -272,7 +287,7 @@ func printRoleTable(roles []string, roleResponses []struct {

roleWidth := lipgloss.Width("ROLE")
privilegeWidth := lipgloss.Width("PRIVILEGE")
streamWidth := lipgloss.Width("STREAM")
streamWidth := lipgloss.Width("DATASET")

for idx, roleName := range roles {
roleW := lipgloss.Width(roleName)
Expand Down Expand Up @@ -337,7 +352,7 @@ func printRoleTable(roles []string, roleResponses []struct {
fmt.Printf("%s%s%s\n",
headerStyle.Render(padRight("ROLE", privilegeColumn+1)),
headerStyle.Render(padRight("PRIVILEGE", privilegeWidth+5)),
headerStyle.Render("STREAM"),
headerStyle.Render("DATASET"),
)
fmt.Printf("%s%s%s\n",
ruleStyle.Render(strings.Repeat("─", privilegeColumn+1)),
Expand Down Expand Up @@ -380,10 +395,34 @@ func printRoleTable(roles []string, roleResponses []struct {
}

func roleStream(action RoleData) string {
if action.Resource == nil || action.Resource.Stream == "" {
if action.Resource == nil {
return "-"
}
return action.Resource.Stream
if dataset := roleDataset(*action.Resource); dataset != "" {
return dataset
}
return "-"
}

func roleDataset(resource RoleResource) string {
if resource.Dataset != "" {
return resource.Dataset
}
return resource.Stream
}

func newRoleData(privilege string) RoleData {
return RoleData{Privilege: privilege}
}

func missingRoleMessage(name string, roles []string) string {
if len(roles) == 0 {
return fmt.Sprintf("role %s doesn't exist. No roles are available", name)
}

roleNames := append([]string(nil), roles...)
slices.Sort(roleNames)
return fmt.Sprintf("role %s doesn't exist. Available role names: %s", name, strings.Join(roleNames, ", "))
}

func orDash(value string) string {
Expand Down
69 changes: 52 additions & 17 deletions cmd/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import (
"encoding/json"
"fmt"
"io"
"net/http"
"sort"
"strings"
"sync"
"time"
Expand Down Expand Up @@ -59,8 +61,9 @@ var (

var addUser = &cobra.Command{
Use: "add user-name",
Example: " pb user add bob",
Example: " pb user add bob --role reader",
Short: "Add a new user",
Long: "Add a self-hosted user and assign at least one existing role. For Parseable Cloud, invite the user from the dashboard instead.",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
startTime := time.Now()
Expand All @@ -70,6 +73,11 @@ var addUser = &cobra.Command{
}()

name := args[0]
if DefaultProfile.Cloud {
fmt.Println(cloudAddUserMessage())
cmd.Annotations["error"] = "user creation is not supported for cloud profiles"
return nil
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

client := internalHTTP.DefaultClient(&DefaultProfile)
users, err := fetchUsers(&client)
Expand All @@ -87,22 +95,24 @@ var addUser = &cobra.Command{
}

// fetch all the roles to be applied to this user
rolesToSet := cmd.Flag(roleFlag).Value.String()
rolesToSetArr := strings.Split(rolesToSet, ",")

// fetch the role names on the server
rolesToSet := strings.TrimSpace(cmd.Flag(roleFlag).Value.String())
var rolesOnServer []string
if err := fetchRoles(&client, &rolesOnServer); err != nil {
cmd.Annotations["error"] = err.Error()
return err
}
rolesOnServerArr := strings.Join(rolesOnServer, " ")
if rolesToSet == "" {
fmt.Println(selfHostedAddUserRoleMessage(name, rolesOnServer))
cmd.Annotations["error"] = "at least one role is required"
return nil
}
rolesToSetArr := strings.Split(rolesToSet, ",")

// validate if roles to be applied are actually present on the server
for idx, role := range rolesToSetArr {
rolesToSetArr[idx] = strings.TrimSpace(role)
if !strings.Contains(rolesOnServerArr, rolesToSetArr[idx]) {
fmt.Printf("role %s doesn't exist, please create a role using pb role add %s\n", rolesToSetArr[idx], rolesToSetArr[idx])
if !slices.Contains(rolesOnServer, rolesToSetArr[idx]) {
fmt.Printf("role %s doesn't exist. Create it first with `pb role add %s`, or use an existing role with `pb user add %s --role <role>`\n", rolesToSetArr[idx], rolesToSetArr[idx], name)
cmd.Annotations["error"] = fmt.Sprintf("role %s doesn't exist", rolesToSetArr[idx])
return nil
}
Expand Down Expand Up @@ -190,7 +200,8 @@ var RemoveUserCmd = &cobra.Command{

var SetUserRoleCmd = &cobra.Command{
Use: "set-role user-name roles",
Short: "Set roles for a user",
Short: "Add roles to a user",
Long: "Add one or more roles to a user without removing their existing role assignments.",
Example: " pb user set-role bob admin,developer",
PreRunE: func(_ *cobra.Command, args []string) error {
if len(args) < 2 {
Expand All @@ -216,7 +227,7 @@ var SetUserRoleCmd = &cobra.Command{
if !slices.ContainsFunc(users, func(user UserData) bool {
return user.ID == name
}) {
fmt.Printf("user doesn't exist. Please create the user with `pb user add %s`\n", name)
fmt.Println(missingUserMessage(name, DefaultProfile.Cloud))
cmd.Annotations["error"] = "user does not exist"
return nil
}
Expand All @@ -228,21 +239,16 @@ var SetUserRoleCmd = &cobra.Command{
cmd.Annotations["error"] = err.Error()
return err
}
rolesOnServerArr := strings.Join(rolesOnServer, " ")

for idx, role := range rolesToSetArr {
rolesToSetArr[idx] = strings.TrimSpace(role)
if !strings.Contains(rolesOnServerArr, rolesToSetArr[idx]) {
if !slices.Contains(rolesOnServer, rolesToSetArr[idx]) {
fmt.Printf("role %s doesn't exist, please create a role using `pb role add %s`\n", rolesToSetArr[idx], rolesToSetArr[idx])
cmd.Annotations["error"] = fmt.Sprintf("role %s doesn't exist", rolesToSetArr[idx])
return nil
}
}

var putBody io.Reader
putBodyJSON, _ := json.Marshal(rolesToSetArr)
putBody = bytes.NewBuffer([]byte(putBodyJSON))
req, err := client.NewRequest("PUT", "user/"+name+"/role", putBody)
req, err := newAddUserRolesRequest(&client, name, rolesToSetArr)
if err != nil {
cmd.Annotations["error"] = err.Error()
return err
Expand Down Expand Up @@ -274,6 +280,35 @@ var SetUserRoleCmd = &cobra.Command{
},
}

func newAddUserRolesRequest(client *internalHTTP.HTTPClient, name string, roles []string) (*http.Request, error) {
body, err := json.Marshal(roles)
if err != nil {
return nil, err
}
return client.NewRequest(http.MethodPatch, "user/"+name+"/role/add", bytes.NewReader(body))
}

func missingUserMessage(name string, cloud bool) string {
if cloud {
return fmt.Sprintf("user doesn't exist. Please invite the user from the Parseable Cloud dashboard first, then set the role with `pb user set-role %s <role>`", name)
}
return fmt.Sprintf("user doesn't exist. Please create the user with `pb user add %s`", name)
}

func cloudAddUserMessage() string {
return "`pb user add` is not available for Parseable Cloud. Please invite the user from the Parseable Cloud dashboard"
}

func selfHostedAddUserRoleMessage(name string, roles []string) string {
if len(roles) == 0 {
return fmt.Sprintf("a role is required to create user %s. No roles are available. Create one first with `pb role add <role>`, then run `pb user add %s --role <role>`", name, name)
}

roleNames := append([]string(nil), roles...)
sort.Strings(roleNames)
return fmt.Sprintf("a role is required to create user %s.\nAvailable roles: %s\nAssign one, for example: `pb user add %s --role %s`", name, strings.Join(roleNames, ", "), name, roleNames[0])
}

var ListUserCmd = &cobra.Command{
Use: "list",
Aliases: []string{"ls"},
Expand Down
Loading
Loading