diff --git a/go.mod b/go.mod index 8358c908..caf8bddb 100644 --- a/go.mod +++ b/go.mod @@ -25,7 +25,7 @@ require ( github.com/mattn/go-shellwords v1.0.12 github.com/mitchellh/go-homedir v1.1.0 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c - github.com/planetscale/planetscale-go v0.142.0 + github.com/planetscale/planetscale-go v0.143.0 github.com/planetscale/psdb v0.0.0-20250717190954-65c6661ab6e4 github.com/planetscale/psdbproxy v0.0.0-20250728082226-3f4ea3a74ec7 github.com/spf13/cobra v1.10.1 diff --git a/go.sum b/go.sum index 4194c172..b0548c4b 100644 --- a/go.sum +++ b/go.sum @@ -175,8 +175,8 @@ github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjL github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/planetscale/noglog v0.2.1-0.20210421230640-bea75fcd2e8e h1:MZ8D+Z3m2vvqGZLvoQfpaGg/j1fNDr4j03s3PRz4rVY= github.com/planetscale/noglog v0.2.1-0.20210421230640-bea75fcd2e8e/go.mod h1:hwAsSPQdvPa3WcfKfzTXxtEq/HlqwLjQasfO6QbGo4Q= -github.com/planetscale/planetscale-go v0.142.0 h1:luApvfbD2xcg8gS9TmUC7nFINl4t3T9qzX7Ed8rYlwQ= -github.com/planetscale/planetscale-go v0.142.0/go.mod h1:PheYDHAwF14wfCBak1M0J64AdPW8NUeyvgPgWqe7zpI= +github.com/planetscale/planetscale-go v0.143.0 h1:3LeJXrPYIkXdxUcGIK5rhitA44D6HMp9ZxmVTy7ozO0= +github.com/planetscale/planetscale-go v0.143.0/go.mod h1:PheYDHAwF14wfCBak1M0J64AdPW8NUeyvgPgWqe7zpI= github.com/planetscale/psdb v0.0.0-20250717190954-65c6661ab6e4 h1:Xv5pj20Rhfty1Tv0OVcidg4ez4PvGrpKvb6rvUwQgDs= github.com/planetscale/psdb v0.0.0-20250717190954-65c6661ab6e4/go.mod h1:M52h5IWxAcbdQ1hSZrLAGQC4ZXslxEsK/Wh9nu3wdWs= github.com/planetscale/psdbproxy v0.0.0-20250728082226-3f4ea3a74ec7 h1:aRd6vdE1fyuSI4RVj7oCr8lFmgqXvpnPUmN85VbZCp8= diff --git a/internal/cmd/role/reassign.go b/internal/cmd/role/reassign.go new file mode 100644 index 00000000..e7810f47 --- /dev/null +++ b/internal/cmd/role/reassign.go @@ -0,0 +1,120 @@ +package role + +import ( + "errors" + "fmt" + "os" + + "github.com/AlecAivazis/survey/v2" + "github.com/AlecAivazis/survey/v2/terminal" + "github.com/planetscale/cli/internal/cmdutil" + "github.com/planetscale/cli/internal/printer" + ps "github.com/planetscale/planetscale-go/planetscale" + + "github.com/spf13/cobra" +) + +func ReassignCmd(ch *cmdutil.Helper) *cobra.Command { + var flags struct { + force bool + successor string + } + + cmd := &cobra.Command{ + Use: "reassign ", + Short: "Reassign objects owned by a role to another role", + Args: cmdutil.RequiredArgs("database", "branch", "role-id"), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + database := args[0] + branch := args[1] + roleID := args[2] + + if flags.successor == "" { + return fmt.Errorf("--successor flag is required") + } + + client, err := ch.Client() + if err != nil { + return err + } + + if !flags.force { + if ch.Printer.Format() != printer.Human { + return fmt.Errorf("cannot reassign role objects with the output format %q (run with --force to override)", ch.Printer.Format()) + } + + confirmationName := fmt.Sprintf("%s/%s/%s", database, branch, roleID) + if !printer.IsTTY { + return fmt.Errorf("cannot confirm object reassignment for role %q (run with --force to override)", confirmationName) + } + + confirmationMessage := fmt.Sprintf("%s %s %s", printer.Bold("Please type"), + printer.BoldBlue(confirmationName), printer.Bold("to confirm:")) + + prompt := &survey.Input{ + Message: confirmationMessage, + } + + var userInput string + err := survey.AskOne(prompt, &userInput) + if err != nil { + if err == terminal.InterruptErr { + os.Exit(0) + } else { + return err + } + } + + // If the confirmations don't match up, let's return an error. + if userInput != confirmationName { + return errors.New("incorrect role identifier entered, skipping object reassignment") + } + } + + end := ch.Printer.PrintProgress(fmt.Sprintf("Reassigning objects from role %s to %s in %s/%s...", + printer.BoldBlue(roleID), printer.BoldBlue(flags.successor), printer.BoldBlue(database), printer.BoldBlue(branch))) + defer end() + + err = client.PostgresRoles.ReassignObjects(ctx, &ps.ReassignPostgresRoleObjectsRequest{ + Organization: ch.Config.Organization, + Database: database, + Branch: branch, + RoleId: roleID, + Successor: flags.successor, + }) + if err != nil { + switch cmdutil.ErrCode(err) { + case ps.ErrNotFound: + return fmt.Errorf("role %s does not exist in branch %s of database %s (organization: %s)", + printer.BoldBlue(roleID), printer.BoldBlue(branch), printer.BoldBlue(database), printer.BoldBlue(ch.Config.Organization)) + default: + return cmdutil.HandleError(err) + } + } + + end() + + if ch.Printer.Format() == printer.Human { + ch.Printer.Printf("Objects owned by role %s were successfully reassigned to %s in %s/%s.\n", + printer.BoldBlue(roleID), printer.BoldBlue(flags.successor), printer.BoldBlue(database), printer.BoldBlue(branch)) + return nil + } + + return ch.Printer.PrintResource( + map[string]string{ + "result": "objects reassigned", + "role_id": roleID, + "successor": flags.successor, + "branch": branch, + }, + ) + }, + } + + cmd.Flags().BoolVar(&flags.force, "force", false, "Reassign objects without confirmation") + cmd.Flags().StringVar(&flags.successor, "successor", "", "Role to transfer ownership to (required)") + cmd.MarkFlagRequired("successor") // nolint:errcheck + + return cmd +} diff --git a/internal/cmd/role/reset.go b/internal/cmd/role/reset.go new file mode 100644 index 00000000..e590306d --- /dev/null +++ b/internal/cmd/role/reset.go @@ -0,0 +1,104 @@ +package role + +import ( + "errors" + "fmt" + "os" + + "github.com/AlecAivazis/survey/v2" + "github.com/AlecAivazis/survey/v2/terminal" + "github.com/planetscale/cli/internal/cmdutil" + "github.com/planetscale/cli/internal/printer" + ps "github.com/planetscale/planetscale-go/planetscale" + + "github.com/spf13/cobra" +) + +func ResetCmd(ch *cmdutil.Helper) *cobra.Command { + var flags struct { + force bool + } + + cmd := &cobra.Command{ + Use: "reset ", + Short: "Reset a role's password", + Args: cmdutil.RequiredArgs("database", "branch", "role-id"), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + database := args[0] + branch := args[1] + roleID := args[2] + + client, err := ch.Client() + if err != nil { + return err + } + + if !flags.force { + if ch.Printer.Format() != printer.Human { + return fmt.Errorf("cannot reset role password with the output format %q (run with --force to override)", ch.Printer.Format()) + } + + confirmationName := fmt.Sprintf("%s/%s/%s", database, branch, roleID) + if !printer.IsTTY { + return fmt.Errorf("cannot confirm password reset for role %q (run with --force to override)", confirmationName) + } + + confirmationMessage := fmt.Sprintf("%s %s %s", printer.Bold("Please type"), + printer.BoldBlue(confirmationName), printer.Bold("to confirm:")) + + prompt := &survey.Input{ + Message: confirmationMessage, + } + + var userInput string + err := survey.AskOne(prompt, &userInput) + if err != nil { + if err == terminal.InterruptErr { + os.Exit(0) + } else { + return err + } + } + + // If the confirmations don't match up, let's return an error. + if userInput != confirmationName { + return errors.New("incorrect role identifier entered, skipping password reset") + } + } + + end := ch.Printer.PrintProgress(fmt.Sprintf("Resetting password for role %s in %s/%s...", + printer.BoldBlue(roleID), printer.BoldBlue(database), printer.BoldBlue(branch))) + defer end() + + role, err := client.PostgresRoles.ResetPassword(ctx, &ps.ResetPostgresRolePasswordRequest{ + Organization: ch.Config.Organization, + Database: database, + Branch: branch, + RoleId: roleID, + }) + if err != nil { + switch cmdutil.ErrCode(err) { + case ps.ErrNotFound: + return fmt.Errorf("role %s does not exist in branch %s of database %s (organization: %s)", + printer.BoldBlue(roleID), printer.BoldBlue(branch), printer.BoldBlue(database), printer.BoldBlue(ch.Config.Organization)) + default: + return cmdutil.HandleError(err) + } + } + + end() + + if ch.Printer.Format() == printer.Human { + ch.Printer.Printf("Password for role %s was successfully reset in %s/%s.\n", + printer.BoldBlue(roleID), printer.BoldBlue(database), printer.BoldBlue(branch)) + } + + return ch.Printer.PrintResource(toPostgresRole(role)) + }, + } + + cmd.Flags().BoolVar(&flags.force, "force", false, "Reset password without confirmation") + + return cmd +} diff --git a/internal/cmd/role/role.go b/internal/cmd/role/role.go index cdf7813b..b3e73b84 100644 --- a/internal/cmd/role/role.go +++ b/internal/cmd/role/role.go @@ -21,7 +21,9 @@ func RoleCmd(ch *cmdutil.Helper) *cobra.Command { DeleteCmd(ch), GetCmd(ch), ListCmd(ch), + ReassignCmd(ch), RenewCmd(ch), + ResetCmd(ch), ResetDefaultCmd(ch), UpdateCmd(ch), ) diff --git a/internal/mock/postgres_roles.go b/internal/mock/postgres_roles.go index 859cac41..c5795262 100644 --- a/internal/mock/postgres_roles.go +++ b/internal/mock/postgres_roles.go @@ -21,6 +21,10 @@ type PostgresRolesService struct { DeleteFnInvoked bool ResetDefaultRoleFn func(context.Context, *ps.ResetDefaultRoleRequest) (*ps.PostgresRole, error) ResetDefaultRoleFnInvoked bool + ResetPasswordFn func(context.Context, *ps.ResetPostgresRolePasswordRequest) (*ps.PostgresRole, error) + ResetPasswordFnInvoked bool + ReassignObjectsFn func(context.Context, *ps.ReassignPostgresRoleObjectsRequest) error + ReassignObjectsFnInvoked bool } func (s *PostgresRolesService) List(ctx context.Context, req *ps.ListPostgresRolesRequest, opts ...ps.ListOption) ([]*ps.PostgresRole, error) { @@ -57,3 +61,13 @@ func (s *PostgresRolesService) ResetDefaultRole(ctx context.Context, req *ps.Res s.ResetDefaultRoleFnInvoked = true return s.ResetDefaultRoleFn(ctx, req) } + +func (s *PostgresRolesService) ResetPassword(ctx context.Context, req *ps.ResetPostgresRolePasswordRequest) (*ps.PostgresRole, error) { + s.ResetPasswordFnInvoked = true + return s.ResetPasswordFn(ctx, req) +} + +func (s *PostgresRolesService) ReassignObjects(ctx context.Context, req *ps.ReassignPostgresRoleObjectsRequest) error { + s.ReassignObjectsFnInvoked = true + return s.ReassignObjectsFn(ctx, req) +}