From 5fd87564cc0f6ca75739ea363103eacb4924ee3b Mon Sep 17 00:00:00 2001 From: Patrick Pfeiffer Date: Mon, 11 Mar 2024 14:31:26 +0100 Subject: [PATCH 1/3] (NOBIDS) prevent pw reset if not allowed --- ...0_add_users_password_reset_not_allowed.sql | 11 ++++++++ handlers/auth.go | 25 +++++++++++++++---- handlers/user.go | 18 +++++++++---- types/templates.go | 7 ++++++ 4 files changed, 51 insertions(+), 10 deletions(-) create mode 100644 db/migrations/20240311140000_add_users_password_reset_not_allowed.sql diff --git a/db/migrations/20240311140000_add_users_password_reset_not_allowed.sql b/db/migrations/20240311140000_add_users_password_reset_not_allowed.sql new file mode 100644 index 0000000000..fd86bf5c6b --- /dev/null +++ b/db/migrations/20240311140000_add_users_password_reset_not_allowed.sql @@ -0,0 +1,11 @@ +-- +goose Up +-- +goose StatementBegin +SELECT 'up SQL query - add column users.password_reset_not_allowed'; +ALTER TABLE users ADD COLUMN IF NOT EXISTS password_reset_not_allowed BOOLEAN NOT NULL DEFAULT FALSE; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +SELECT 'down SQL query - drop column users.password_reset_not_allowed'; +ALTER TABLE users DROP COLUMN IF EXISTS password_reset_not_allowed; +-- +goose StatementEnd diff --git a/handlers/auth.go b/handlers/auth.go index 69350fa8cf..dac452d1c1 100644 --- a/handlers/auth.go +++ b/handlers/auth.go @@ -553,12 +553,18 @@ func RequestResetPasswordPost(w http.ResponseWriter, r *http.Request) { } var rateLimitError *types.RateLimitError + var passwordResetNotAllowedError *types.PasswordResetNotAllowedError err = sendPasswordResetEmail(email) - if err != nil && !errors.As(err, &rateLimitError) { - logger.Errorf("error sending reset-email: %v", err) - utils.SetFlash(w, r, authSessionName, authInternalServerErrorFlashMsg) - } else if err != nil && errors.As(err, &rateLimitError) { - utils.SetFlash(w, r, authSessionName, fmt.Sprintf("Error: The ratelimit for sending emails has been exceeded, please try again in %v.", err.(*types.RateLimitError).TimeLeft.Round(time.Second))) + if err != nil { + switch { + case errors.As(err, &passwordResetNotAllowedError): + utils.SetFlash(w, r, authSessionName, "Error: Password reset is not allowed for this user.") + case errors.As(err, &rateLimitError): + utils.SetFlash(w, r, authSessionName, fmt.Sprintf("Error: The ratelimit for sending emails has been exceeded, please try again in %v.", err.(*types.RateLimitError).TimeLeft.Round(time.Second))) + default: + logger.Errorf("error sending reset-email: %v", err) + utils.SetFlash(w, r, authSessionName, authInternalServerErrorFlashMsg) + } } else { utils.SetFlash(w, r, authSessionName, "An email has been sent which contains a link to reset your password.") } @@ -736,6 +742,15 @@ func sendPasswordResetEmail(email string) error { } defer tx.Rollback() + var passwordResetNotAllowed bool + err = tx.Get(&passwordResetNotAllowed, "SELECT COALESCE(password_reset_not_allowed, true) FROM users WHERE email = $1", email) + if err != nil { + return fmt.Errorf("error getting password_reset_not_allowed: %w", err) + } + if passwordResetNotAllowed { + return fmt.Errorf("password-reset not allowed for user: %v", email) + } + var lastTs *time.Time err = tx.Get(&lastTs, "SELECT password_reset_ts FROM users WHERE email = $1", email) if err != nil && !errors.Is(err, sql.ErrNoRows) { diff --git a/handlers/user.go b/handlers/user.go index d0363dcc1b..bb6ca2010f 100644 --- a/handlers/user.go +++ b/handlers/user.go @@ -1046,13 +1046,14 @@ func UserUpdatePasswordPost(w http.ResponseWriter, r *http.Request) { pwdOld := r.FormValue("old-password") currentUser := struct { - ID int64 `db:"id"` - Email string `db:"email"` - Password string `db:"password"` - Confirmed bool `db:"email_confirmed"` + ID int64 `db:"id"` + Email string `db:"email"` + Password string `db:"password"` + Confirmed bool `db:"email_confirmed"` + PasswordResetNotAllowed bool `db:"password_reset_not_allowed"` }{} - err = db.FrontendWriterDB.Get(¤tUser, "SELECT id, email, password, email_confirmed FROM users WHERE id = $1", user.UserID) + err = db.FrontendWriterDB.Get(¤tUser, "SELECT id, email, password, email_confirmed, password_reset_not_allowed FROM users WHERE id = $1", user.UserID) if err != nil { if err != sql.ErrNoRows { logger.Errorf("error retrieving password for user %v: %v", user.UserID, err) @@ -1063,6 +1064,13 @@ func UserUpdatePasswordPost(w http.ResponseWriter, r *http.Request) { return } + if currentUser.PasswordResetNotAllowed { + session.AddFlash("Error: Password reset is not allowed for this account!") + session.Save(r, w) + http.Redirect(w, r, "/user/settings", http.StatusSeeOther) + return + } + if !currentUser.Confirmed { session.AddFlash("Error: Email has not been confirmed, please click the link in the email we sent you or resend link!") session.Save(r, w) diff --git a/types/templates.go b/types/templates.go index 8818b6106e..f2f9acf876 100644 --- a/types/templates.go +++ b/types/templates.go @@ -1326,6 +1326,13 @@ type StakeWithUsPageData struct { FlashMessage string RecaptchaKey string } + +type PasswordResetNotAllowedError struct{} + +func (e *PasswordResetNotAllowedError) Error() string { + return "password reset not allowed for this account" +} + type RateLimitError struct { TimeLeft time.Duration } From f39f0c591e9c79158cec1dc8e184c1c96e571a5c Mon Sep 17 00:00:00 2001 From: Patrick Pfeiffer Date: Mon, 11 Mar 2024 16:29:11 +0100 Subject: [PATCH 2/3] (NOBIDS) use password-reset-not-allowed error --- handlers/auth.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/handlers/auth.go b/handlers/auth.go index dac452d1c1..8bd99f8b21 100644 --- a/handlers/auth.go +++ b/handlers/auth.go @@ -748,7 +748,7 @@ func sendPasswordResetEmail(email string) error { return fmt.Errorf("error getting password_reset_not_allowed: %w", err) } if passwordResetNotAllowed { - return fmt.Errorf("password-reset not allowed for user: %v", email) + return &types.PasswordResetNotAllowedError{} } var lastTs *time.Time From 377a8e89ed3522768803ea70eae20fc88c4645d0 Mon Sep 17 00:00:00 2001 From: Patrick Pfeiffer Date: Mon, 11 Mar 2024 17:13:39 +0100 Subject: [PATCH 3/3] (NOBIDS) misc: add command: disable-user-per-email --- cmd/misc/main.go | 86 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 85 insertions(+), 1 deletion(-) diff --git a/cmd/misc/main.go b/cmd/misc/main.go index a98962860a..b99d0b7b16 100644 --- a/cmd/misc/main.go +++ b/cmd/misc/main.go @@ -60,14 +60,16 @@ var opts = struct { Family string Key string ValidatorNameRanges string + Email string DryRun bool + Yes bool }{} func main() { statsPartitionCommand := commands.StatsMigratorCommand{} configPath := flag.String("config", "config/default.config.yml", "Path to the config file") - flag.StringVar(&opts.Command, "command", "", "command to run, available: updateAPIKey, applyDbSchema, initBigtableSchema, epoch-export, debug-rewards, debug-blocks, clear-bigtable, index-old-eth1-blocks, update-aggregation-bits, historic-prices-export, index-missing-blocks, export-epoch-missed-slots, migrate-last-attestation-slot-bigtable, export-genesis-validators, update-block-finalization-sequentially, nameValidatorsByRanges, export-stats-totals, export-sync-committee-periods, export-sync-committee-validator-stats, partition-validator-stats, migrate-app-purchases, update-ratelimits") + flag.StringVar(&opts.Command, "command", "", "command to run, available: updateAPIKey, applyDbSchema, initBigtableSchema, epoch-export, debug-rewards, debug-blocks, clear-bigtable, index-old-eth1-blocks, update-aggregation-bits, historic-prices-export, index-missing-blocks, export-epoch-missed-slots, migrate-last-attestation-slot-bigtable, export-genesis-validators, update-block-finalization-sequentially, nameValidatorsByRanges, export-stats-totals, export-sync-committee-periods, export-sync-committee-validator-stats, partition-validator-stats, migrate-app-purchases, update-ratelimits, disable-user-per-email") flag.Uint64Var(&opts.StartEpoch, "start-epoch", 0, "start epoch") flag.Uint64Var(&opts.EndEpoch, "end-epoch", 0, "end epoch") flag.Uint64Var(&opts.User, "user", 0, "user id") @@ -86,6 +88,8 @@ func main() { flag.StringVar(&opts.ValidatorNameRanges, "validator-name-ranges", "https://config.dencun-devnet-8.ethpandaops.io/api/v1/nodes/validator-ranges", "url to or json of validator-ranges (format must be: {'ranges':{'X-Y':'name'}})") flag.StringVar(&opts.Addresses, "addresses", "", "Comma separated list of addresses that should be processed by the command") flag.StringVar(&opts.Columns, "columns", "", "Comma separated list of columns that should be affected by the command") + flag.StringVar(&opts.Email, "email", "", "Email of the user") + flag.BoolVar(&opts.Yes, "yes", false, "Answer yes to all questions") dryRun := flag.String("dry-run", "true", "if 'false' it deletes all rows starting with the key, per default it only logs the rows that would be deleted, but does not really delete them") versionFlag := flag.Bool("version", false, "Show version and exit") @@ -394,6 +398,8 @@ func main() { err = fixEnsAddresses(erigonClient) case "update-ratelimits": ratelimit.DBUpdater() + case "disable-user-per-email": + err = disableUserPerEmail() default: utils.LogFatal(nil, fmt.Sprintf("unknown command %s", opts.Command), 0) } @@ -405,6 +411,60 @@ func main() { } } +func disableUserPerEmail() error { + if opts.Email == "" { + return errors.New("no email specified") + } + user := struct { + ID uint64 `db:"id"` + Email string `db:"email"` + }{} + err := db.FrontendWriterDB.Get(&user, `select id, email from users where email = $1`, opts.Email) + if err != nil { + return err + } + + if !askForConfirmation(fmt.Sprintf(`Do you want to disable the user with email: %v (id: %v)? + +- the user will get logged out +- the password will change +- the apikey will change +- password-reset will be disabled +`, user.Email, user.ID)) { + logrus.Warnf("aborted") + return nil + } + + _, err = db.FrontendWriterDB.Exec(`update users set password = $3, api_key = $4, password_reset_not_allowed = true where id = $1 and email = $2`, user.ID, user.Email, utils.RandomString(128), utils.RandomString(32)) + if err != nil { + return err + } + logrus.Infof("changed password and apikey and disallowed password-reset for user %v", user.ID) + + ctx := context.Background() + + // invalidate all sessions for this user + err = utils.SessionStore.SCS.Iterate(ctx, func(ctx context.Context) error { + sessionUserID, ok := utils.SessionStore.SCS.Get(ctx, "user_id").(uint64) + if !ok { + return nil + } + + if user.ID == sessionUserID { + logrus.Infof("destroying a session of user %v", user.ID) + return utils.SessionStore.SCS.Destroy(ctx) + } + + return nil + }) + + if err != nil { + return err + } + + return nil +} + func fixEns(erigonClient *rpc.ErigonClient) error { logrus.Infof("command: fix-ens") addrs := []struct { @@ -1936,3 +1996,27 @@ func reExportSyncCommittee(rpcClient rpc.Client, p uint64, dryRun bool) error { return tx.Commit() } } + +func askForConfirmation(q string) bool { + if opts.Yes { + return true + } + var s string + + fmt.Printf("%s (y/N): ", q) + _, err := fmt.Scanln(&s) + if err != nil { + if err.Error() == "unexpected newline" { + return false + } + panic(err) + } + + // s = strings.TrimSpace(s) + s = strings.ToLower(s) + + if s == "y" || s == "yes" { + return true + } + return false +}