diff --git a/cmd/harbor/root/project/robot.go b/cmd/harbor/root/project/robot.go index 12a8a619..368ed7cf 100644 --- a/cmd/harbor/root/project/robot.go +++ b/cmd/harbor/root/project/robot.go @@ -17,6 +17,7 @@ func Robot() *cobra.Command { robot.ViewRobotCommand(), robot.CreateRobotCommand(), robot.UpdateRobotCommand(), + robot.RefreshSecretCommand(), ) return cmd diff --git a/cmd/harbor/root/project/robot/create.go b/cmd/harbor/root/project/robot/create.go index 87b6f9a0..7840e815 100644 --- a/cmd/harbor/root/project/robot/create.go +++ b/cmd/harbor/root/project/robot/create.go @@ -1,6 +1,7 @@ package robot import ( + "fmt" "os" "github.com/atotto/clipboard" @@ -14,7 +15,6 @@ import ( "github.com/spf13/viper" ) -// to-do add json file as input and getting json file as output from input. func CreateRobotCommand() *cobra.Command { var ( opts create.CreateView @@ -88,12 +88,16 @@ func CreateRobotCommand() *cobra.Command { FormatFlag := viper.GetString("output-format") if FormatFlag != "" { - utils.PrintPayloadInJSONFormat(response.Payload) + name := response.Payload.Name + res, _ := api.GetRobot(response.Payload.ID) + utils.SavePayloadJSON(name, res.Payload) return } - create.CreateRobotSecretView(response.Payload) + name, secret := response.Payload.Name, response.Payload.Secret + create.CreateRobotSecretView(name, secret) err = clipboard.WriteAll(response.Payload.Secret) + fmt.Println("secret copied to clipboard.") }, } flags := cmd.Flags() diff --git a/cmd/harbor/root/project/robot/refresh.go b/cmd/harbor/root/project/robot/refresh.go new file mode 100644 index 00000000..baafb6b7 --- /dev/null +++ b/cmd/harbor/root/project/robot/refresh.go @@ -0,0 +1,84 @@ +package robot + +import ( + "fmt" + "os" + "strconv" + + "github.com/atotto/clipboard" + "github.com/goharbor/harbor-cli/pkg/api" + "github.com/goharbor/harbor-cli/pkg/prompt" + "github.com/goharbor/harbor-cli/pkg/utils" + "github.com/goharbor/harbor-cli/pkg/views/robot/create" + log "github.com/sirupsen/logrus" + + "github.com/spf13/cobra" +) + +// handle robot view with interactive like in list command. +func RefreshSecretCommand() *cobra.Command { + var ( + robotID int64 + secret string + secretStdin bool + ) + cmd := &cobra.Command{ + Use: "refresh [robotID]", + Short: "refresh robot secret by id", + Args: cobra.MaximumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + var err error + if len(args) == 1 { + robotID, err = strconv.ParseInt(args[0], 10, 64) + if err != nil { + log.Errorf("failed to parse robot ID: %v", err) + } + } else { + projectID := prompt.GetProjectIDFromUser() + robotID = prompt.GetRobotIDFromUser(projectID) + } + + if secret != "" { + utils.ValidatePassword(secret) + } + if secretStdin { + secret = getSecret() + } + + response, err := api.RefreshSecret(secret, robotID) + if err != nil { + log.Errorf("failed to refresh robot secret.") + os.Exit(1) + } + + log.Info("Secret updated successfully.") + + secret = response.Payload.Secret + create.CreateRobotSecretView("", secret) + + err = clipboard.WriteAll(response.Payload.Secret) + fmt.Println("secret copied to clipboard.") + }, + } + + flags := cmd.Flags() + flags.StringVarP(&secret, "secret", "", "", "secret") + flags.BoolVarP(&secretStdin, "secret-stdin", "", false, "Take the robot secret from stdin") + + return cmd +} + +// getSecret from commandline +func getSecret() string { + secret, err := utils.GetSecretStdin("Enter your secret: ") + if err != nil { + log.Errorf("Error reading secret: %v\n", err) + os.Exit(1) + } + + if err := utils.ValidatePassword(secret); err != nil { + log.Errorf("Invalid secret: %v\n", err) + os.Exit(1) + } + return secret +} diff --git a/pkg/api/robot_handler.go b/pkg/api/robot_handler.go index 669bbe01..ad44db6e 100644 --- a/pkg/api/robot_handler.go +++ b/pkg/api/robot_handler.go @@ -119,9 +119,9 @@ func UpdateRobot(opts *update.UpdateView) error { // Loop through original permissions and convert them for _, perm := range permissions { convertedPerm := &models.RobotPermission{ - Access: perm.Access, - Kind: kind, - Namespace: opts.Permissions[0].Namespace, + Access: perm.Access, + Kind: kind, + Namespace: opts.Permissions[0].Namespace, } convertedPerms = append(convertedPerms, convertedPerm) } @@ -165,3 +165,24 @@ func GetPermissions() (*permissions.GetPermissionsOK, error) { return response, nil } + +func RefreshSecret(secret string, robotID int64) (*robot.RefreshSecOK, error) { + ctx, client, err := utils.ContextWithClient() + if err != nil { + return nil, err + } + + robotSec := &models.RobotSec{ + Secret: secret, + } + + response, err := client.Robot.RefreshSec(ctx, &robot.RefreshSecParams{ + RobotSec: robotSec, + RobotID: robotID, + }) + if err != nil { + return nil, err + } + + return response, nil +} diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index a64a67c7..d622a1f1 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -3,9 +3,13 @@ package utils import ( "encoding/json" "fmt" + "os" "strings" + "syscall" + "unicode" log "github.com/sirupsen/logrus" + "golang.org/x/term" ) // Returns Harbor v2 client for given clientConfig @@ -38,3 +42,61 @@ func ParseProjectRepoReference(projectRepoReference string) (string, string, str } return split[0], split[1], split[2] } + +func SavePayloadJSON(filename string, payload any) { + // Marshal the payload into a JSON string with indentation + jsonStr, err := json.MarshalIndent(payload, "", " ") + if err != nil { + panic(err) + } + // Define the filename + filename = filename + ".json" + err = os.WriteFile(filename, jsonStr, 0644) + if err != nil { + panic(err) + } + fmt.Printf("JSON data has been written to %s\n", filename) +} + +// Validate the secret based on the provided guidelines +func ValidatePassword(s string) error { + const ( + minLength = 8 + maxLength = 128 + ) + + var ( + hasLen = false + hasUpper = false + hasLower = false + hasNumber = false + ) + if len(s) >= minLength && len(s) <= maxLength { + hasLen = true + } + for _, char := range s { + switch { + case unicode.IsUpper(char): + hasUpper = true + case unicode.IsLower(char): + hasLower = true + case unicode.IsNumber(char): + hasNumber = true + } + } + if hasLen && hasUpper && hasLower && hasNumber { + return nil + } + return fmt.Errorf("secret should contain at least 1 uppercase, 1 lowercase and 1 number.") +} + +// Get Password as Stdin +func GetSecretStdin(prompt string) (string, error) { + fmt.Print(prompt) + bytePassword, err := term.ReadPassword(int(syscall.Stdin)) + if err != nil { + return "", err + } + fmt.Println() // move to the next line after input + return strings.TrimSpace(string(bytePassword)), nil +} diff --git a/pkg/views/base/multiselect/model.go b/pkg/views/base/multiselect/model.go index fe1b61eb..0d31b8de 100644 --- a/pkg/views/base/multiselect/model.go +++ b/pkg/views/base/multiselect/model.go @@ -2,8 +2,6 @@ package multiselect import ( "fmt" - "log" - "os" "strings" "github.com/charmbracelet/bubbles/viewport" diff --git a/pkg/views/robot/create/view.go b/pkg/views/robot/create/view.go index bb8404a6..9012c87d 100644 --- a/pkg/views/robot/create/view.go +++ b/pkg/views/robot/create/view.go @@ -72,21 +72,13 @@ func CreateRobotView(createView *CreateView) { } } -func CreateRobotSecretView(response *models.RobotCreated) { - name := response.Name - secret := response.Secret +func CreateRobotSecretView(name string, secret string) { theme := huh.ThemeCharm() err := huh.NewForm( huh.NewGroup( huh.NewInput(). Title("Robot Name"). - Value(&name). - Validate(func(str string) error { - if str == "" { - return errors.New("Name cannot be empty") - } - return nil - }), + Value(&name), huh.NewInput(). Title("Secret"). Value(&secret), diff --git a/pkg/views/robot/select/view.go b/pkg/views/robot/select/view.go index 01d9f186..c99afc92 100644 --- a/pkg/views/robot/select/view.go +++ b/pkg/views/robot/select/view.go @@ -2,7 +2,6 @@ package robot import ( "fmt" - "log" "os" "github.com/charmbracelet/bubbles/list"