From 7a23cb1c19971ec7e9e3bb26f3a0cec61a5c9230 Mon Sep 17 00:00:00 2001 From: Sumi Jeong <125195487+sigmaith@users.noreply.github.com> Date: Tue, 3 Sep 2024 17:44:38 +0900 Subject: [PATCH] Add Account Deletion and Change Password to CLI Commands (#983) Added functionality allowing users to delete accounts and change passwords through the CLI to support recent development of admin ChangePassword and DeleteAccount APIs on the server side. --- admin/client.go | 27 +++++ cmd/yorkie/commands.go | 2 + cmd/yorkie/user/change_password.go | 130 ++++++++++++++++++++++++ cmd/yorkie/user/delete_account.go | 157 +++++++++++++++++++++++++++++ cmd/yorkie/{ => user}/login.go | 27 +++-- cmd/yorkie/{ => user}/logout.go | 4 +- cmd/yorkie/user/user.go | 28 +++++ go.mod | 5 +- go.sum | 6 +- 9 files changed, 369 insertions(+), 17 deletions(-) create mode 100644 cmd/yorkie/user/change_password.go create mode 100644 cmd/yorkie/user/delete_account.go rename cmd/yorkie/{ => user}/login.go (82%) rename cmd/yorkie/{ => user}/logout.go (97%) create mode 100644 cmd/yorkie/user/user.go diff --git a/admin/client.go b/admin/client.go index fddf21044..1883d032b 100644 --- a/admin/client.go +++ b/admin/client.go @@ -377,3 +377,30 @@ func withShardKey[T any](conn *connect.Request[T], keys ...string) *connect.Requ return conn } + +// DeleteAccount deletes the user's account. +func (c *Client) DeleteAccount(ctx context.Context, username, password string) error { + _, err := c.client.DeleteAccount(ctx, connect.NewRequest(&api.DeleteAccountRequest{ + Username: username, + Password: password, + })) + if err != nil { + return err + } + + return nil +} + +// ChangePassword changes the user's password. +func (c *Client) ChangePassword(ctx context.Context, username, password, newPassword string) error { + _, err := c.client.ChangePassword(ctx, connect.NewRequest(&api.ChangePasswordRequest{ + Username: username, + CurrentPassword: password, + NewPassword: newPassword, + })) + if err != nil { + return err + } + + return nil +} diff --git a/cmd/yorkie/commands.go b/cmd/yorkie/commands.go index 7e5418daa..c20fe6305 100644 --- a/cmd/yorkie/commands.go +++ b/cmd/yorkie/commands.go @@ -27,6 +27,7 @@ import ( "github.com/yorkie-team/yorkie/cmd/yorkie/context" "github.com/yorkie-team/yorkie/cmd/yorkie/document" "github.com/yorkie-team/yorkie/cmd/yorkie/project" + "github.com/yorkie-team/yorkie/cmd/yorkie/user" ) var rootCmd = &cobra.Command{ @@ -49,6 +50,7 @@ func init() { rootCmd.AddCommand(project.SubCmd) rootCmd.AddCommand(document.SubCmd) rootCmd.AddCommand(context.SubCmd) + rootCmd.AddCommand(user.SubCmd) viper.SetConfigName("config") viper.SetConfigType("json") viper.AddConfigPath(path.Join(os.Getenv("HOME"), ".yorkie")) diff --git a/cmd/yorkie/user/change_password.go b/cmd/yorkie/user/change_password.go new file mode 100644 index 000000000..ca4e51f6a --- /dev/null +++ b/cmd/yorkie/user/change_password.go @@ -0,0 +1,130 @@ +/* + * Copyright 2024 The Yorkie Authors. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package user + +import ( + "context" + "fmt" + "os" + + "github.com/spf13/cobra" + "github.com/spf13/viper" + "golang.org/x/term" + + "github.com/yorkie-team/yorkie/admin" + "github.com/yorkie-team/yorkie/cmd/yorkie/config" +) + +var ( + newPassword string +) + +func changePasswordCmd() *cobra.Command { + return &cobra.Command{ + Use: "change-password", + Short: "Change user password", + PreRunE: config.Preload, + RunE: func(cmd *cobra.Command, args []string) error { + password, newPassword, err := getPasswords() + if err != nil { + return err + } + + if rpcAddr == "" { + rpcAddr = viper.GetString("rpcAddr") + } + + cli, err := admin.Dial(rpcAddr, admin.WithInsecure(insecure)) + if err != nil { + return fmt.Errorf("failed to dial admin: %w", err) + } + defer func() { + cli.Close() + }() + + ctx := context.Background() + if err := cli.ChangePassword(ctx, username, password, newPassword); err != nil { + return err + } + + if err := deleteAuthSession(rpcAddr); err != nil { + return err + } + + return nil + }, + } +} + +func getPasswords() (string, string, error) { + fmt.Print("Enter Password: ") + bytePassword, err := term.ReadPassword(int(os.Stdin.Fd())) + if err != nil { + return "", "", fmt.Errorf("failed to read password: %w", err) + } + password := string(bytePassword) + fmt.Println() + + fmt.Print("Enter New Password: ") + bytePassword, err = term.ReadPassword(int(os.Stdin.Fd())) + if err != nil { + return "", "", fmt.Errorf("failed to read password: %w", err) + } + newPassword := string(bytePassword) + fmt.Println() + + return password, newPassword, nil +} + +func deleteAuthSession(rpcAddr string) error { + conf, err := config.Load() + if err != nil { + return err + } + + delete(conf.Auths, rpcAddr) + if err := config.Save(conf); err != nil { + return err + } + + return nil +} + +func init() { + cmd := changePasswordCmd() + cmd.Flags().StringVarP( + &username, + "username", + "u", + "", + "Username (required)", + ) + cmd.Flags().StringVar( + &rpcAddr, + "rpc-addr", + "", + "Address of the RPC server", + ) + cmd.Flags().BoolVar( + &insecure, + "insecure", + false, + "Skip the TLS connection of the client", + ) + _ = cmd.MarkFlagRequired("username") + SubCmd.AddCommand(cmd) +} diff --git a/cmd/yorkie/user/delete_account.go b/cmd/yorkie/user/delete_account.go new file mode 100644 index 000000000..80eeef454 --- /dev/null +++ b/cmd/yorkie/user/delete_account.go @@ -0,0 +1,157 @@ +/* + * Copyright 2024 The Yorkie Authors. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package user + +import ( + "context" + "fmt" + "os" + "time" + + "github.com/spf13/cobra" + "github.com/spf13/viper" + "golang.org/x/term" + + "github.com/yorkie-team/yorkie/admin" + "github.com/yorkie-team/yorkie/cmd/yorkie/config" +) + +func deleteAccountCmd() *cobra.Command { + return &cobra.Command{ + Use: "delete-account", + Short: "Delete user account", + PreRunE: config.Preload, + RunE: func(_ *cobra.Command, args []string) error { + password, err := getPassword() + if err != nil { + return err + } + + if confirmation, err := makeConfirmation(); !confirmation || err != nil { + if err != nil { + return err + } + return nil + } + + conf, err := config.Load() + if err != nil { + return err + } + + if rpcAddr == "" { + rpcAddr = viper.GetString("rpcAddr") + } + + if err := deleteAccountFromServer(conf, rpcAddr, insecure, username, password); err != nil { + fmt.Println("Failed to delete your account." + + "The account may not exist or the password might be incorrect. Please try again.") + } else { + fmt.Println("Your account has been successfully deleted.") + } + + return nil + }, + } +} + +func getPassword() (string, error) { + fmt.Print("Enter Password: ") + bytePassword, err := term.ReadPassword(int(os.Stdin.Fd())) + if err != nil { + return "", fmt.Errorf("failed to read password: %w", err) + } + password = string(bytePassword) + fmt.Println() + + return password, nil +} + +func makeConfirmation() (bool, error) { + fmt.Println( + "WARNING: This action is irreversible. Your account and all associated data will be permanently deleted.", + ) + + fmt.Print("Are you absolutely sure? Type 'DELETE' to confirm: ") + var confirmation string + if _, err := fmt.Scanln(&confirmation); err != nil { + return false, fmt.Errorf("failed to read confirmation from user: %w", err) + } + + if confirmation != "DELETE" { + return false, fmt.Errorf("account deletion aborted") + } + + return true, nil +} + +func deleteAccountFromServer(conf *config.Config, rpcAddr string, insecureFlag bool, username, password string) error { + cli, err := admin.Dial(rpcAddr, + admin.WithInsecure(insecureFlag), + admin.WithToken(conf.Auths[rpcAddr].Token), + ) + if err != nil { + return fmt.Errorf("failed to dial admin: %w", err) + } + defer cli.Close() + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + if err := cli.DeleteAccount(ctx, username, password); err != nil { + return fmt.Errorf("server failed to delete account: %w", err) + } + + delete(conf.Auths, rpcAddr) + if conf.RPCAddr == rpcAddr { + for addr := range conf.Auths { + conf.RPCAddr = addr + break + } + } + + if err := config.Save(conf); err != nil { + return err + } + + return nil +} + +func init() { + cmd := deleteAccountCmd() + cmd.Flags().StringVarP( + &username, + "username", + "u", + "", + "Username (required)", + ) + cmd.Flags().StringVar( + &rpcAddr, + "rpc-addr", + "", + "Address of the RPC server", + ) + cmd.Flags().BoolVar( + &insecure, + "insecure", + false, + "Skip the TLS connection of the client", + ) + _ = cmd.MarkFlagRequired("username") + SubCmd.AddCommand(cmd) +} diff --git a/cmd/yorkie/login.go b/cmd/yorkie/user/login.go similarity index 82% rename from cmd/yorkie/login.go rename to cmd/yorkie/user/login.go index da636f0c8..ac39a6b1b 100644 --- a/cmd/yorkie/login.go +++ b/cmd/yorkie/user/login.go @@ -14,12 +14,16 @@ * limitations under the License. */ -package main +// Package user provides the user command. +package user import ( "context" + "fmt" + "os" "github.com/spf13/cobra" + "golang.org/x/term" "github.com/yorkie-team/yorkie/admin" "github.com/yorkie-team/yorkie/cmd/yorkie/config" @@ -38,6 +42,14 @@ func newLoginCmd() *cobra.Command { Short: "Log in to Yorkie server", PreRunE: config.Preload, RunE: func(cmd *cobra.Command, args []string) error { + fmt.Print("Enter Password: ") + bytePassword, err := term.ReadPassword(int(os.Stdin.Fd())) + if err != nil { + return fmt.Errorf("failed to read password: %w", err) + } + password = string(bytePassword) + fmt.Println() + cli, err := admin.Dial(rpcAddr, admin.WithInsecure(insecure)) if err != nil { return err @@ -81,14 +93,7 @@ func init() { "username", "u", "", - "Username (required if password is set)", - ) - cmd.Flags().StringVarP( - &password, - "password", - "p", - "", - "Password (required if username is set)", + "Username (required)", ) cmd.Flags().StringVar( &rpcAddr, @@ -102,6 +107,6 @@ func init() { false, "Skip the TLS connection of the client", ) - cmd.MarkFlagsRequiredTogether("username", "password") - rootCmd.AddCommand(cmd) + _ = cmd.MarkFlagRequired("username") + SubCmd.AddCommand(cmd) } diff --git a/cmd/yorkie/logout.go b/cmd/yorkie/user/logout.go similarity index 97% rename from cmd/yorkie/logout.go rename to cmd/yorkie/user/logout.go index ab9047880..e3ecf6336 100644 --- a/cmd/yorkie/logout.go +++ b/cmd/yorkie/user/logout.go @@ -14,7 +14,7 @@ * limitations under the License. */ -package main +package user import ( "github.com/spf13/cobra" @@ -68,5 +68,5 @@ func init() { false, "force log out from all servers", ) - rootCmd.AddCommand(cmd) + SubCmd.AddCommand(cmd) } diff --git a/cmd/yorkie/user/user.go b/cmd/yorkie/user/user.go new file mode 100644 index 000000000..f572458cf --- /dev/null +++ b/cmd/yorkie/user/user.go @@ -0,0 +1,28 @@ +/* + * Copyright 2024 The Yorkie Authors. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Package user provides the user command. +package user + +import "github.com/spf13/cobra" + +var ( + // SubCmd represents the user command + SubCmd = &cobra.Command{ + Use: "user", + Short: "Manage user account", + } +) diff --git a/go.mod b/go.mod index 3174eb884..ec2fce562 100644 --- a/go.mod +++ b/go.mod @@ -27,6 +27,7 @@ require ( google.golang.org/grpc v1.58.3 google.golang.org/protobuf v1.31.0 gopkg.in/yaml.v2 v2.4.0 + gopkg.in/yaml.v3 v3.0.1 ) require ( @@ -72,10 +73,10 @@ require ( go.uber.org/multierr v1.9.0 // indirect golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect golang.org/x/sync v0.7.0 // indirect - golang.org/x/sys v0.15.0 // indirect + golang.org/x/sys v0.23.0 // indirect + golang.org/x/term v0.23.0 golang.org/x/text v0.14.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect ) replace github.com/hashicorp/go-memdb => github.com/hackerwins/go-memdb v1.3.3-0.20211225080334-513a74641622 diff --git a/go.sum b/go.sum index daf71193d..10b9cb034 100644 --- a/go.sum +++ b/go.sum @@ -524,10 +524,12 @@ golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= +golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= +golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=