From aa1d95300ab1b34a3b4c9f5902ea821f2aa99f6e Mon Sep 17 00:00:00 2001 From: zeripath Date: Tue, 14 Feb 2023 22:12:19 +0000 Subject: [PATCH] Add command to bulk set must-change-password (#22823) As part of administration sometimes it is appropriate to forcibly tell users to update their passwords. This PR creates a new command `gitea admin user must-change-password` which will set the `MustChangePassword` flag on the provided users. Signed-off-by: Andrew Thornton --- cmd/admin.go | 406 ------------------- cmd/admin_user.go | 21 + cmd/admin_user_change_password.go | 76 ++++ cmd/admin_user_create.go | 169 ++++++++ cmd/admin_user_delete.go | 78 ++++ cmd/admin_user_generate_access_token.go | 80 ++++ cmd/admin_user_list.go | 60 +++ cmd/admin_user_must_change_password.go | 58 +++ docs/content/doc/usage/command-line.en-us.md | 7 + models/user/must_change_password.go | 49 +++ 10 files changed, 598 insertions(+), 406 deletions(-) create mode 100644 cmd/admin_user.go create mode 100644 cmd/admin_user_change_password.go create mode 100644 cmd/admin_user_create.go create mode 100644 cmd/admin_user_delete.go create mode 100644 cmd/admin_user_generate_access_token.go create mode 100644 cmd/admin_user_list.go create mode 100644 cmd/admin_user_must_change_password.go create mode 100644 models/user/must_change_password.go diff --git a/cmd/admin.go b/cmd/admin.go index 318c212d08..b913b817bd 100644 --- a/cmd/admin.go +++ b/cmd/admin.go @@ -5,7 +5,6 @@ package cmd import ( - "context" "errors" "fmt" "os" @@ -16,20 +15,15 @@ import ( auth_model "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" - user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/log" - pwd "code.gitea.io/gitea/modules/password" repo_module "code.gitea.io/gitea/modules/repository" - "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/storage" "code.gitea.io/gitea/modules/util" auth_service "code.gitea.io/gitea/services/auth" "code.gitea.io/gitea/services/auth/source/oauth2" "code.gitea.io/gitea/services/auth/source/smtp" repo_service "code.gitea.io/gitea/services/repository" - user_service "code.gitea.io/gitea/services/user" "github.com/urfave/cli" ) @@ -48,147 +42,6 @@ var ( }, } - subcmdUser = cli.Command{ - Name: "user", - Usage: "Modify users", - Subcommands: []cli.Command{ - microcmdUserCreate, - microcmdUserList, - microcmdUserChangePassword, - microcmdUserDelete, - microcmdUserGenerateAccessToken, - }, - } - - microcmdUserList = cli.Command{ - Name: "list", - Usage: "List users", - Action: runListUsers, - Flags: []cli.Flag{ - cli.BoolFlag{ - Name: "admin", - Usage: "List only admin users", - }, - }, - } - - microcmdUserCreate = cli.Command{ - Name: "create", - Usage: "Create a new user in database", - Action: runCreateUser, - Flags: []cli.Flag{ - cli.StringFlag{ - Name: "name", - Usage: "Username. DEPRECATED: use username instead", - }, - cli.StringFlag{ - Name: "username", - Usage: "Username", - }, - cli.StringFlag{ - Name: "password", - Usage: "User password", - }, - cli.StringFlag{ - Name: "email", - Usage: "User email address", - }, - cli.BoolFlag{ - Name: "admin", - Usage: "User is an admin", - }, - cli.BoolFlag{ - Name: "random-password", - Usage: "Generate a random password for the user", - }, - cli.BoolFlag{ - Name: "must-change-password", - Usage: "Set this option to false to prevent forcing the user to change their password after initial login, (Default: true)", - }, - cli.IntFlag{ - Name: "random-password-length", - Usage: "Length of the random password to be generated", - Value: 12, - }, - cli.BoolFlag{ - Name: "access-token", - Usage: "Generate access token for the user", - }, - cli.BoolFlag{ - Name: "restricted", - Usage: "Make a restricted user account", - }, - }, - } - - microcmdUserChangePassword = cli.Command{ - Name: "change-password", - Usage: "Change a user's password", - Action: runChangePassword, - Flags: []cli.Flag{ - cli.StringFlag{ - Name: "username,u", - Value: "", - Usage: "The user to change password for", - }, - cli.StringFlag{ - Name: "password,p", - Value: "", - Usage: "New password to set for user", - }, - }, - } - - microcmdUserDelete = cli.Command{ - Name: "delete", - Usage: "Delete specific user by id, name or email", - Flags: []cli.Flag{ - cli.Int64Flag{ - Name: "id", - Usage: "ID of user of the user to delete", - }, - cli.StringFlag{ - Name: "username,u", - Usage: "Username of the user to delete", - }, - cli.StringFlag{ - Name: "email,e", - Usage: "Email of the user to delete", - }, - cli.BoolFlag{ - Name: "purge", - Usage: "Purge user, all their repositories, organizations and comments", - }, - }, - Action: runDeleteUser, - } - - microcmdUserGenerateAccessToken = cli.Command{ - Name: "generate-access-token", - Usage: "Generate a access token for a specific user", - Flags: []cli.Flag{ - cli.StringFlag{ - Name: "username,u", - Usage: "Username", - }, - cli.StringFlag{ - Name: "token-name,t", - Usage: "Token name", - Value: "gitea-admin", - }, - cli.BoolFlag{ - Name: "raw", - Usage: "Display only the token value", - }, - cli.StringFlag{ - Name: "scopes", - Value: "", - Usage: "Comma separated list of scopes to apply to access token", - }, - }, - Action: runGenerateAccessToken, - } - subcmdRepoSyncReleases = cli.Command{ Name: "repo-sync-releases", Usage: "Synchronize repository releases with tags", @@ -486,265 +339,6 @@ var ( } ) -func runChangePassword(c *cli.Context) error { - if err := argsSet(c, "username", "password"); err != nil { - return err - } - - ctx, cancel := installSignals() - defer cancel() - - if err := initDB(ctx); err != nil { - return err - } - if len(c.String("password")) < setting.MinPasswordLength { - return fmt.Errorf("Password is not long enough. Needs to be at least %d", setting.MinPasswordLength) - } - - if !pwd.IsComplexEnough(c.String("password")) { - return errors.New("Password does not meet complexity requirements") - } - pwned, err := pwd.IsPwned(context.Background(), c.String("password")) - if err != nil { - return err - } - if pwned { - return errors.New("The password you chose is on a list of stolen passwords previously exposed in public data breaches. Please try again with a different password.\nFor more details, see https://haveibeenpwned.com/Passwords") - } - uname := c.String("username") - user, err := user_model.GetUserByName(ctx, uname) - if err != nil { - return err - } - if err = user.SetPassword(c.String("password")); err != nil { - return err - } - - if err = user_model.UpdateUserCols(ctx, user, "passwd", "passwd_hash_algo", "salt"); err != nil { - return err - } - - fmt.Printf("%s's password has been successfully updated!\n", user.Name) - return nil -} - -func runCreateUser(c *cli.Context) error { - if err := argsSet(c, "email"); err != nil { - return err - } - - if c.IsSet("name") && c.IsSet("username") { - return errors.New("Cannot set both --name and --username flags") - } - if !c.IsSet("name") && !c.IsSet("username") { - return errors.New("One of --name or --username flags must be set") - } - - if c.IsSet("password") && c.IsSet("random-password") { - return errors.New("cannot set both -random-password and -password flags") - } - - var username string - if c.IsSet("username") { - username = c.String("username") - } else { - username = c.String("name") - fmt.Fprintf(os.Stderr, "--name flag is deprecated. Use --username instead.\n") - } - - ctx, cancel := installSignals() - defer cancel() - - if err := initDB(ctx); err != nil { - return err - } - - var password string - if c.IsSet("password") { - password = c.String("password") - } else if c.IsSet("random-password") { - var err error - password, err = pwd.Generate(c.Int("random-password-length")) - if err != nil { - return err - } - fmt.Printf("generated random password is '%s'\n", password) - } else { - return errors.New("must set either password or random-password flag") - } - - // always default to true - changePassword := true - - // If this is the first user being created. - // Take it as the admin and don't force a password update. - if n := user_model.CountUsers(nil); n == 0 { - changePassword = false - } - - if c.IsSet("must-change-password") { - changePassword = c.Bool("must-change-password") - } - - restricted := util.OptionalBoolNone - - if c.IsSet("restricted") { - restricted = util.OptionalBoolOf(c.Bool("restricted")) - } - - // default user visibility in app.ini - visibility := setting.Service.DefaultUserVisibilityMode - - u := &user_model.User{ - Name: username, - Email: c.String("email"), - Passwd: password, - IsAdmin: c.Bool("admin"), - MustChangePassword: changePassword, - Visibility: visibility, - } - - overwriteDefault := &user_model.CreateUserOverwriteOptions{ - IsActive: util.OptionalBoolTrue, - IsRestricted: restricted, - } - - if err := user_model.CreateUser(u, overwriteDefault); err != nil { - return fmt.Errorf("CreateUser: %w", err) - } - - if c.Bool("access-token") { - t := &auth_model.AccessToken{ - Name: "gitea-admin", - UID: u.ID, - } - - if err := auth_model.NewAccessToken(t); err != nil { - return err - } - - fmt.Printf("Access token was successfully created... %s\n", t.Token) - } - - fmt.Printf("New user '%s' has been successfully created!\n", username) - return nil -} - -func runListUsers(c *cli.Context) error { - ctx, cancel := installSignals() - defer cancel() - - if err := initDB(ctx); err != nil { - return err - } - - users, err := user_model.GetAllUsers() - if err != nil { - return err - } - - w := tabwriter.NewWriter(os.Stdout, 5, 0, 1, ' ', 0) - - if c.IsSet("admin") { - fmt.Fprintf(w, "ID\tUsername\tEmail\tIsActive\n") - for _, u := range users { - if u.IsAdmin { - fmt.Fprintf(w, "%d\t%s\t%s\t%t\n", u.ID, u.Name, u.Email, u.IsActive) - } - } - } else { - twofa := user_model.UserList(users).GetTwoFaStatus() - fmt.Fprintf(w, "ID\tUsername\tEmail\tIsActive\tIsAdmin\t2FA\n") - for _, u := range users { - fmt.Fprintf(w, "%d\t%s\t%s\t%t\t%t\t%t\n", u.ID, u.Name, u.Email, u.IsActive, u.IsAdmin, twofa[u.ID]) - } - - } - - w.Flush() - return nil -} - -func runDeleteUser(c *cli.Context) error { - if !c.IsSet("id") && !c.IsSet("username") && !c.IsSet("email") { - return fmt.Errorf("You must provide the id, username or email of a user to delete") - } - - ctx, cancel := installSignals() - defer cancel() - - if err := initDB(ctx); err != nil { - return err - } - - if err := storage.Init(); err != nil { - return err - } - - var err error - var user *user_model.User - if c.IsSet("email") { - user, err = user_model.GetUserByEmail(c.String("email")) - } else if c.IsSet("username") { - user, err = user_model.GetUserByName(ctx, c.String("username")) - } else { - user, err = user_model.GetUserByID(ctx, c.Int64("id")) - } - if err != nil { - return err - } - if c.IsSet("username") && user.LowerName != strings.ToLower(strings.TrimSpace(c.String("username"))) { - return fmt.Errorf("The user %s who has email %s does not match the provided username %s", user.Name, c.String("email"), c.String("username")) - } - - if c.IsSet("id") && user.ID != c.Int64("id") { - return fmt.Errorf("The user %s does not match the provided id %d", user.Name, c.Int64("id")) - } - - return user_service.DeleteUser(ctx, user, c.Bool("purge")) -} - -func runGenerateAccessToken(c *cli.Context) error { - if !c.IsSet("username") { - return fmt.Errorf("You must provide the username to generate a token for them") - } - - ctx, cancel := installSignals() - defer cancel() - - if err := initDB(ctx); err != nil { - return err - } - - user, err := user_model.GetUserByName(ctx, c.String("username")) - if err != nil { - return err - } - - accessTokenScope, err := auth_model.AccessTokenScope(c.String("scopes")).Normalize() - if err != nil { - return err - } - - t := &auth_model.AccessToken{ - Name: c.String("token-name"), - UID: user.ID, - Scope: accessTokenScope, - } - - if err := auth_model.NewAccessToken(t); err != nil { - return err - } - - if c.Bool("raw") { - fmt.Printf("%s\n", t.Token) - } else { - fmt.Printf("Access token was successfully created: %s\n", t.Token) - } - - return nil -} - func runRepoSyncReleases(_ *cli.Context) error { ctx, cancel := installSignals() defer cancel() diff --git a/cmd/admin_user.go b/cmd/admin_user.go new file mode 100644 index 0000000000..a442b8fe9c --- /dev/null +++ b/cmd/admin_user.go @@ -0,0 +1,21 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package cmd + +import ( + "github.com/urfave/cli" +) + +var subcmdUser = cli.Command{ + Name: "user", + Usage: "Modify users", + Subcommands: []cli.Command{ + microcmdUserCreate, + microcmdUserList, + microcmdUserChangePassword, + microcmdUserDelete, + microcmdUserGenerateAccessToken, + microcmdUserMustChangePassword, + }, +} diff --git a/cmd/admin_user_change_password.go b/cmd/admin_user_change_password.go new file mode 100644 index 0000000000..1b7c73370d --- /dev/null +++ b/cmd/admin_user_change_password.go @@ -0,0 +1,76 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package cmd + +import ( + "context" + "errors" + "fmt" + + user_model "code.gitea.io/gitea/models/user" + pwd "code.gitea.io/gitea/modules/password" + "code.gitea.io/gitea/modules/setting" + + "github.com/urfave/cli" +) + +var microcmdUserChangePassword = cli.Command{ + Name: "change-password", + Usage: "Change a user's password", + Action: runChangePassword, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "username,u", + Value: "", + Usage: "The user to change password for", + }, + cli.StringFlag{ + Name: "password,p", + Value: "", + Usage: "New password to set for user", + }, + }, +} + +func runChangePassword(c *cli.Context) error { + if err := argsSet(c, "username", "password"); err != nil { + return err + } + + ctx, cancel := installSignals() + defer cancel() + + if err := initDB(ctx); err != nil { + return err + } + if len(c.String("password")) < setting.MinPasswordLength { + return fmt.Errorf("Password is not long enough. Needs to be at least %d", setting.MinPasswordLength) + } + + if !pwd.IsComplexEnough(c.String("password")) { + return errors.New("Password does not meet complexity requirements") + } + pwned, err := pwd.IsPwned(context.Background(), c.String("password")) + if err != nil { + return err + } + if pwned { + return errors.New("The password you chose is on a list of stolen passwords previously exposed in public data breaches. Please try again with a different password.\nFor more details, see https://haveibeenpwned.com/Passwords") + } + uname := c.String("username") + user, err := user_model.GetUserByName(ctx, uname) + if err != nil { + return err + } + if err = user.SetPassword(c.String("password")); err != nil { + return err + } + + if err = user_model.UpdateUserCols(ctx, user, "passwd", "passwd_hash_algo", "salt"); err != nil { + return err + } + + fmt.Printf("%s's password has been successfully updated!\n", user.Name) + return nil +} diff --git a/cmd/admin_user_create.go b/cmd/admin_user_create.go new file mode 100644 index 0000000000..579c6f2f62 --- /dev/null +++ b/cmd/admin_user_create.go @@ -0,0 +1,169 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package cmd + +import ( + "errors" + "fmt" + "os" + + auth_model "code.gitea.io/gitea/models/auth" + user_model "code.gitea.io/gitea/models/user" + pwd "code.gitea.io/gitea/modules/password" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" + + "github.com/urfave/cli" +) + +var microcmdUserCreate = cli.Command{ + Name: "create", + Usage: "Create a new user in database", + Action: runCreateUser, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "name", + Usage: "Username. DEPRECATED: use username instead", + }, + cli.StringFlag{ + Name: "username", + Usage: "Username", + }, + cli.StringFlag{ + Name: "password", + Usage: "User password", + }, + cli.StringFlag{ + Name: "email", + Usage: "User email address", + }, + cli.BoolFlag{ + Name: "admin", + Usage: "User is an admin", + }, + cli.BoolFlag{ + Name: "random-password", + Usage: "Generate a random password for the user", + }, + cli.BoolFlag{ + Name: "must-change-password", + Usage: "Set this option to false to prevent forcing the user to change their password after initial login, (Default: true)", + }, + cli.IntFlag{ + Name: "random-password-length", + Usage: "Length of the random password to be generated", + Value: 12, + }, + cli.BoolFlag{ + Name: "access-token", + Usage: "Generate access token for the user", + }, + cli.BoolFlag{ + Name: "restricted", + Usage: "Make a restricted user account", + }, + }, +} + +func runCreateUser(c *cli.Context) error { + if err := argsSet(c, "email"); err != nil { + return err + } + + if c.IsSet("name") && c.IsSet("username") { + return errors.New("Cannot set both --name and --username flags") + } + if !c.IsSet("name") && !c.IsSet("username") { + return errors.New("One of --name or --username flags must be set") + } + + if c.IsSet("password") && c.IsSet("random-password") { + return errors.New("cannot set both -random-password and -password flags") + } + + var username string + if c.IsSet("username") { + username = c.String("username") + } else { + username = c.String("name") + fmt.Fprintf(os.Stderr, "--name flag is deprecated. Use --username instead.\n") + } + + ctx, cancel := installSignals() + defer cancel() + + if err := initDB(ctx); err != nil { + return err + } + + var password string + if c.IsSet("password") { + password = c.String("password") + } else if c.IsSet("random-password") { + var err error + password, err = pwd.Generate(c.Int("random-password-length")) + if err != nil { + return err + } + fmt.Printf("generated random password is '%s'\n", password) + } else { + return errors.New("must set either password or random-password flag") + } + + // always default to true + changePassword := true + + // If this is the first user being created. + // Take it as the admin and don't force a password update. + if n := user_model.CountUsers(nil); n == 0 { + changePassword = false + } + + if c.IsSet("must-change-password") { + changePassword = c.Bool("must-change-password") + } + + restricted := util.OptionalBoolNone + + if c.IsSet("restricted") { + restricted = util.OptionalBoolOf(c.Bool("restricted")) + } + + // default user visibility in app.ini + visibility := setting.Service.DefaultUserVisibilityMode + + u := &user_model.User{ + Name: username, + Email: c.String("email"), + Passwd: password, + IsAdmin: c.Bool("admin"), + MustChangePassword: changePassword, + Visibility: visibility, + } + + overwriteDefault := &user_model.CreateUserOverwriteOptions{ + IsActive: util.OptionalBoolTrue, + IsRestricted: restricted, + } + + if err := user_model.CreateUser(u, overwriteDefault); err != nil { + return fmt.Errorf("CreateUser: %w", err) + } + + if c.Bool("access-token") { + t := &auth_model.AccessToken{ + Name: "gitea-admin", + UID: u.ID, + } + + if err := auth_model.NewAccessToken(t); err != nil { + return err + } + + fmt.Printf("Access token was successfully created... %s\n", t.Token) + } + + fmt.Printf("New user '%s' has been successfully created!\n", username) + return nil +} diff --git a/cmd/admin_user_delete.go b/cmd/admin_user_delete.go new file mode 100644 index 0000000000..328d5feb61 --- /dev/null +++ b/cmd/admin_user_delete.go @@ -0,0 +1,78 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package cmd + +import ( + "fmt" + "strings" + + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/storage" + user_service "code.gitea.io/gitea/services/user" + + "github.com/urfave/cli" +) + +var microcmdUserDelete = cli.Command{ + Name: "delete", + Usage: "Delete specific user by id, name or email", + Flags: []cli.Flag{ + cli.Int64Flag{ + Name: "id", + Usage: "ID of user of the user to delete", + }, + cli.StringFlag{ + Name: "username,u", + Usage: "Username of the user to delete", + }, + cli.StringFlag{ + Name: "email,e", + Usage: "Email of the user to delete", + }, + cli.BoolFlag{ + Name: "purge", + Usage: "Purge user, all their repositories, organizations and comments", + }, + }, + Action: runDeleteUser, +} + +func runDeleteUser(c *cli.Context) error { + if !c.IsSet("id") && !c.IsSet("username") && !c.IsSet("email") { + return fmt.Errorf("You must provide the id, username or email of a user to delete") + } + + ctx, cancel := installSignals() + defer cancel() + + if err := initDB(ctx); err != nil { + return err + } + + if err := storage.Init(); err != nil { + return err + } + + var err error + var user *user_model.User + if c.IsSet("email") { + user, err = user_model.GetUserByEmail(c.String("email")) + } else if c.IsSet("username") { + user, err = user_model.GetUserByName(ctx, c.String("username")) + } else { + user, err = user_model.GetUserByID(ctx, c.Int64("id")) + } + if err != nil { + return err + } + if c.IsSet("username") && user.LowerName != strings.ToLower(strings.TrimSpace(c.String("username"))) { + return fmt.Errorf("The user %s who has email %s does not match the provided username %s", user.Name, c.String("email"), c.String("username")) + } + + if c.IsSet("id") && user.ID != c.Int64("id") { + return fmt.Errorf("The user %s does not match the provided id %d", user.Name, c.Int64("id")) + } + + return user_service.DeleteUser(ctx, user, c.Bool("purge")) +} diff --git a/cmd/admin_user_generate_access_token.go b/cmd/admin_user_generate_access_token.go new file mode 100644 index 0000000000..822bc5c2bc --- /dev/null +++ b/cmd/admin_user_generate_access_token.go @@ -0,0 +1,80 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package cmd + +import ( + "fmt" + + auth_model "code.gitea.io/gitea/models/auth" + user_model "code.gitea.io/gitea/models/user" + + "github.com/urfave/cli" +) + +var microcmdUserGenerateAccessToken = cli.Command{ + Name: "generate-access-token", + Usage: "Generate an access token for a specific user", + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "username,u", + Usage: "Username", + }, + cli.StringFlag{ + Name: "token-name,t", + Usage: "Token name", + Value: "gitea-admin", + }, + cli.BoolFlag{ + Name: "raw", + Usage: "Display only the token value", + }, + cli.StringFlag{ + Name: "scopes", + Value: "", + Usage: "Comma separated list of scopes to apply to access token", + }, + }, + Action: runGenerateAccessToken, +} + +func runGenerateAccessToken(c *cli.Context) error { + if !c.IsSet("username") { + return fmt.Errorf("You must provide a username to generate a token for") + } + + ctx, cancel := installSignals() + defer cancel() + + if err := initDB(ctx); err != nil { + return err + } + + user, err := user_model.GetUserByName(ctx, c.String("username")) + if err != nil { + return err + } + + accessTokenScope, err := auth_model.AccessTokenScope(c.String("scopes")).Normalize() + if err != nil { + return err + } + + t := &auth_model.AccessToken{ + Name: c.String("token-name"), + UID: user.ID, + Scope: accessTokenScope, + } + + if err := auth_model.NewAccessToken(t); err != nil { + return err + } + + if c.Bool("raw") { + fmt.Printf("%s\n", t.Token) + } else { + fmt.Printf("Access token was successfully created: %s\n", t.Token) + } + + return nil +} diff --git a/cmd/admin_user_list.go b/cmd/admin_user_list.go new file mode 100644 index 0000000000..85490331ed --- /dev/null +++ b/cmd/admin_user_list.go @@ -0,0 +1,60 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package cmd + +import ( + "fmt" + "os" + "text/tabwriter" + + user_model "code.gitea.io/gitea/models/user" + + "github.com/urfave/cli" +) + +var microcmdUserList = cli.Command{ + Name: "list", + Usage: "List users", + Action: runListUsers, + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "admin", + Usage: "List only admin users", + }, + }, +} + +func runListUsers(c *cli.Context) error { + ctx, cancel := installSignals() + defer cancel() + + if err := initDB(ctx); err != nil { + return err + } + + users, err := user_model.GetAllUsers() + if err != nil { + return err + } + + w := tabwriter.NewWriter(os.Stdout, 5, 0, 1, ' ', 0) + + if c.IsSet("admin") { + fmt.Fprintf(w, "ID\tUsername\tEmail\tIsActive\n") + for _, u := range users { + if u.IsAdmin { + fmt.Fprintf(w, "%d\t%s\t%s\t%t\n", u.ID, u.Name, u.Email, u.IsActive) + } + } + } else { + twofa := user_model.UserList(users).GetTwoFaStatus() + fmt.Fprintf(w, "ID\tUsername\tEmail\tIsActive\tIsAdmin\t2FA\n") + for _, u := range users { + fmt.Fprintf(w, "%d\t%s\t%s\t%t\t%t\t%t\n", u.ID, u.Name, u.Email, u.IsActive, u.IsAdmin, twofa[u.ID]) + } + } + + w.Flush() + return nil +} diff --git a/cmd/admin_user_must_change_password.go b/cmd/admin_user_must_change_password.go new file mode 100644 index 0000000000..eb13fbcae5 --- /dev/null +++ b/cmd/admin_user_must_change_password.go @@ -0,0 +1,58 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package cmd + +import ( + "errors" + "fmt" + + user_model "code.gitea.io/gitea/models/user" + + "github.com/urfave/cli" +) + +var microcmdUserMustChangePassword = cli.Command{ + Name: "must-change-password", + Usage: "Set the must change password flag for the provided users or all users", + Action: runMustChangePassword, + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "all,A", + Usage: "All users must change password, except those explicitly excluded with --exclude", + }, + cli.StringSliceFlag{ + Name: "exclude,e", + Usage: "Do not change the must-change-password flag for these users", + }, + cli.BoolFlag{ + Name: "unset", + Usage: "Instead of setting the must-change-password flag, unset it", + }, + }, +} + +func runMustChangePassword(c *cli.Context) error { + ctx, cancel := installSignals() + defer cancel() + + if c.NArg() == 0 && !c.IsSet("all") { + return errors.New("either usernames or --all must be provided") + } + + mustChangePassword := !c.Bool("unset") + all := c.Bool("all") + exclude := c.StringSlice("exclude") + + if err := initDB(ctx); err != nil { + return err + } + + n, err := user_model.SetMustChangePassword(ctx, all, mustChangePassword, c.Args(), exclude) + if err != nil { + return err + } + + fmt.Printf("Updated %d users setting MustChangePassword to %t\n", n, mustChangePassword) + return nil +} diff --git a/docs/content/doc/usage/command-line.en-us.md b/docs/content/doc/usage/command-line.en-us.md index 9b861a9da3..70efebd203 100644 --- a/docs/content/doc/usage/command-line.en-us.md +++ b/docs/content/doc/usage/command-line.en-us.md @@ -99,6 +99,13 @@ Admin operations: - `--password value`, `-p value`: New password. Required. - Examples: - `gitea admin user change-password --username myname --password asecurepassword` + - `must-change-password`: + - Args: + - `[username...]`: Users that must change their passwords + - Options: + - `--all`, `-A`: Force a password change for all users + - `--exclude username`, `-e username`: Exclude the given user. Can be set multiple times. + - `--unset`: Revoke forced password change for the given users - `regenerate` - Options: - `hooks`: Regenerate Git Hooks for all repositories diff --git a/models/user/must_change_password.go b/models/user/must_change_password.go new file mode 100644 index 0000000000..7eab08de89 --- /dev/null +++ b/models/user/must_change_password.go @@ -0,0 +1,49 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package user + +import ( + "context" + "strings" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/util" + + "xorm.io/builder" +) + +func SetMustChangePassword(ctx context.Context, all, mustChangePassword bool, include, exclude []string) (int64, error) { + sliceTrimSpaceDropEmpty := func(input []string) []string { + output := make([]string, 0, len(input)) + for _, in := range input { + in = strings.ToLower(strings.TrimSpace(in)) + if in == "" { + continue + } + output = append(output, in) + } + return output + } + + var cond builder.Cond + + // Only include the users where something changes to get an accurate count + cond = builder.Neq{"must_change_password": mustChangePassword} + + if !all { + include = sliceTrimSpaceDropEmpty(include) + if len(include) == 0 { + return 0, util.NewSilentWrapErrorf(util.ErrInvalidArgument, "no users to include provided") + } + + cond = cond.And(builder.In("lower_name", include)) + } + + exclude = sliceTrimSpaceDropEmpty(exclude) + if len(exclude) > 0 { + cond = cond.And(builder.NotIn("lower_name", exclude)) + } + + return db.GetEngine(ctx).Where(cond).MustCols("must_change_password").Update(&User{MustChangePassword: mustChangePassword}) +}