From dffc44c082b126a30a0767bd911aa8cded3b9dbf 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 +++++++ client/client.go | 4 +- cmd/yorkie/config/config.go | 2 +- cmd/yorkie/delete_account.go | 136 +++++++++++++++++++++++++++++++++++ cmd/yorkie/login.go | 29 ++++++-- cmd/yorkie/passwd.go | 116 ++++++++++++++++++++++++++++++ cmd/yorkie/version.go | 4 +- go.mod | 5 +- go.sum | 6 +- 9 files changed, 316 insertions(+), 13 deletions(-) create mode 100644 cmd/yorkie/delete_account.go create mode 100644 cmd/yorkie/passwd.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/client/client.go b/client/client.go index e3b8c30ed..19bc29549 100644 --- a/client/client.go +++ b/client/client.go @@ -781,11 +781,11 @@ func (c *Client) broadcast(ctx context.Context, doc *document.Document, topic st func newTLSConfigFromFile(certFile, serverNameOverride string) (*tls.Config, error) { b, err := os.ReadFile(filepath.Clean(certFile)) if err != nil { - return nil, fmt.Errorf("credentials: failed to read TLS config file %q: %w", certFile, err) + return nil, fmt.Errorf("read TLS config file %q: %w", certFile, err) } cp := x509.NewCertPool() if !cp.AppendCertsFromPEM(b) { - return nil, fmt.Errorf("credentials: failed to append certificates") + return nil, fmt.Errorf("failure to append certs from PEM") } return &tls.Config{ServerName: serverNameOverride, RootCAs: cp, MinVersion: tls.VersionTLS12}, nil diff --git a/cmd/yorkie/config/config.go b/cmd/yorkie/config/config.go index f60cee070..687763fa9 100644 --- a/cmd/yorkie/config/config.go +++ b/cmd/yorkie/config/config.go @@ -170,7 +170,7 @@ func Preload(_ *cobra.Command, _ []string) error { } if err := viper.ReadInConfig(); err != nil { - return fmt.Errorf("failed to read in config: %w", err) + return fmt.Errorf("read in config: %w", err) } return nil } diff --git a/cmd/yorkie/delete_account.go b/cmd/yorkie/delete_account.go new file mode 100644 index 000000000..6e1f61273 --- /dev/null +++ b/cmd/yorkie/delete_account.go @@ -0,0 +1,136 @@ +/* + * 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 main + +import ( + "context" + "fmt" + "time" + + "github.com/spf13/cobra" + "github.com/spf13/viper" + + "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 account", + PreRunE: config.Preload, + RunE: func(_ *cobra.Command, args []string) error { + rpcAddr := viper.GetString("rpcAddr") + auth, err := config.LoadAuth(rpcAddr) + if err != nil { + return err + } + + if err := readPassword(); 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 := deleteAccount(conf, auth, rpcAddr, username, password); err != nil { + fmt.Println("Failed to delete account: ", err) + } + + return nil + }, + } +} + +func makeConfirmation() (bool, error) { + fmt.Println("Warning: This action cannot be undone. Type 'DELETE' to confirm: ") + var confirmation string + if _, err := fmt.Scanln(&confirmation); err != nil { + return false, fmt.Errorf("read confirmation from user: %w", err) + } + + if confirmation != "DELETE" { + return false, fmt.Errorf("account deletion aborted") + } + + return true, nil +} + +func deleteAccount(conf *config.Config, auth config.Auth, rpcAddr, username, password string) error { + cli, err := admin.Dial(rpcAddr, admin.WithToken(auth.Token), admin.WithInsecure(auth.Insecure)) + if err != nil { + return err + } + defer func() { + 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("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", + ) + cmd.Flags().StringVarP( + &password, + "password", + "p", + "", + "Password (optional)", + ) + + _ = cmd.MarkFlagRequired("username") + rootCmd.AddCommand(cmd) +} diff --git a/cmd/yorkie/login.go b/cmd/yorkie/login.go index da636f0c8..d2626bd15 100644 --- a/cmd/yorkie/login.go +++ b/cmd/yorkie/login.go @@ -18,8 +18,11 @@ package main 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" @@ -35,9 +38,13 @@ var ( func newLoginCmd() *cobra.Command { return &cobra.Command{ Use: "login", - Short: "Log in to Yorkie server", + Short: "Log in to the Yorkie server", PreRunE: config.Preload, RunE: func(cmd *cobra.Command, args []string) error { + if err := readPassword(); err != nil { + return err + } + cli, err := admin.Dial(rpcAddr, admin.WithInsecure(insecure)) if err != nil { return err @@ -74,6 +81,20 @@ func newLoginCmd() *cobra.Command { } } +// readPassword reads the password from the user. +func readPassword() error { + if password == "" { + fmt.Print("Enter Password: ") + bytePassword, err := term.ReadPassword(int(os.Stdin.Fd())) + if err != nil { + return fmt.Errorf("read password: %w", err) + } + password = string(bytePassword) + fmt.Println() + } + return nil +} + func init() { cmd := newLoginCmd() cmd.Flags().StringVarP( @@ -81,14 +102,14 @@ func init() { "username", "u", "", - "Username (required if password is set)", + "Username", ) cmd.Flags().StringVarP( &password, "password", "p", "", - "Password (required if username is set)", + "Password (optional)", ) cmd.Flags().StringVar( &rpcAddr, @@ -102,6 +123,6 @@ func init() { false, "Skip the TLS connection of the client", ) - cmd.MarkFlagsRequiredTogether("username", "password") + _ = cmd.MarkFlagRequired("username") rootCmd.AddCommand(cmd) } diff --git a/cmd/yorkie/passwd.go b/cmd/yorkie/passwd.go new file mode 100644 index 000000000..0c1200317 --- /dev/null +++ b/cmd/yorkie/passwd.go @@ -0,0 +1,116 @@ +/* + * 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 main + +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" +) + +func passwdCmd() *cobra.Command { + return &cobra.Command{ + Use: "passwd", + Short: "Change password", + PreRunE: config.Preload, + RunE: func(cmd *cobra.Command, args []string) error { + rpcAddr := viper.GetString("rpcAddr") + auth, err := config.LoadAuth(rpcAddr) + if err != nil { + return err + } + + password, newPassword, err := readPasswords() + if err != nil { + return err + } + + cli, err := admin.Dial(rpcAddr, admin.WithToken(auth.Token), admin.WithInsecure(auth.Insecure)) + if err != nil { + return 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 readPasswords() (string, string, error) { + fmt.Print("Enter Password: ") + bytePassword, err := term.ReadPassword(int(os.Stdin.Fd())) + if err != nil { + return "", "", fmt.Errorf("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("read new 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 := passwdCmd() + cmd.Flags().StringVarP( + &username, + "username", + "u", + "", + "Username", + ) + _ = cmd.MarkFlagRequired("username") + rootCmd.AddCommand(cmd) +} diff --git a/cmd/yorkie/version.go b/cmd/yorkie/version.go index 3255a25ad..7207f76db 100644 --- a/cmd/yorkie/version.go +++ b/cmd/yorkie/version.go @@ -131,13 +131,13 @@ func printVersionInfo(cmd *cobra.Command, output string, versionInfo *types.Vers case "yaml": marshalled, err := yaml.Marshal(versionInfo) if err != nil { - return errors.New("failed to marshal YAML") + return errors.New("marshal YAML") } cmd.Println(string(marshalled)) case "json": marshalled, err := json.MarshalIndent(versionInfo, "", " ") if err != nil { - return errors.New("failed to marshal JSON") + return errors.New("marshal JSON") } cmd.Println(string(marshalled)) default: 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=