Skip to content

Commit

Permalink
Merge pull request #2847 from gobitfly/NOBIDS/restricted-pw-reset
Browse files Browse the repository at this point in the history
(NOBIDS) prevent pw reset if not allowed
  • Loading branch information
recy21 committed Mar 12, 2024
2 parents 0794269 + 377a8e8 commit 442048c
Show file tree
Hide file tree
Showing 5 changed files with 136 additions and 11 deletions.
86 changes: 85 additions & 1 deletion cmd/misc/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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")

Expand Down Expand Up @@ -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)
}
Expand All @@ -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 {
Expand Down Expand Up @@ -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
}
Original file line number Diff line number Diff line change
@@ -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
25 changes: 20 additions & 5 deletions handlers/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.")
}
Expand Down Expand Up @@ -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 &types.PasswordResetNotAllowedError{}
}

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) {
Expand Down
18 changes: 13 additions & 5 deletions handlers/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(&currentUser, "SELECT id, email, password, email_confirmed FROM users WHERE id = $1", user.UserID)
err = db.FrontendWriterDB.Get(&currentUser, "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)
Expand All @@ -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 <a href='/resend'>resend link</a>!")
session.Save(r, w)
Expand Down
7 changes: 7 additions & 0 deletions types/templates.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down

0 comments on commit 442048c

Please sign in to comment.