mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 19:06:18 +01:00 
			
		
		
		
	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 <art27@cantab.net>
This commit is contained in:
		
							
								
								
									
										406
									
								
								cmd/admin.go
									
									
									
									
									
								
							
							
						
						
									
										406
									
								
								cmd/admin.go
									
									
									
									
									
								
							| @@ -5,7 +5,6 @@ | |||||||
| package cmd | package cmd | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"context" |  | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"os" | 	"os" | ||||||
| @@ -16,20 +15,15 @@ import ( | |||||||
| 	auth_model "code.gitea.io/gitea/models/auth" | 	auth_model "code.gitea.io/gitea/models/auth" | ||||||
| 	"code.gitea.io/gitea/models/db" | 	"code.gitea.io/gitea/models/db" | ||||||
| 	repo_model "code.gitea.io/gitea/models/repo" | 	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/git" | ||||||
| 	"code.gitea.io/gitea/modules/graceful" | 	"code.gitea.io/gitea/modules/graceful" | ||||||
| 	"code.gitea.io/gitea/modules/log" | 	"code.gitea.io/gitea/modules/log" | ||||||
| 	pwd "code.gitea.io/gitea/modules/password" |  | ||||||
| 	repo_module "code.gitea.io/gitea/modules/repository" | 	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" | 	"code.gitea.io/gitea/modules/util" | ||||||
| 	auth_service "code.gitea.io/gitea/services/auth" | 	auth_service "code.gitea.io/gitea/services/auth" | ||||||
| 	"code.gitea.io/gitea/services/auth/source/oauth2" | 	"code.gitea.io/gitea/services/auth/source/oauth2" | ||||||
| 	"code.gitea.io/gitea/services/auth/source/smtp" | 	"code.gitea.io/gitea/services/auth/source/smtp" | ||||||
| 	repo_service "code.gitea.io/gitea/services/repository" | 	repo_service "code.gitea.io/gitea/services/repository" | ||||||
| 	user_service "code.gitea.io/gitea/services/user" |  | ||||||
|  |  | ||||||
| 	"github.com/urfave/cli" | 	"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{ | 	subcmdRepoSyncReleases = cli.Command{ | ||||||
| 		Name:   "repo-sync-releases", | 		Name:   "repo-sync-releases", | ||||||
| 		Usage:  "Synchronize repository releases with tags", | 		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 { | func runRepoSyncReleases(_ *cli.Context) error { | ||||||
| 	ctx, cancel := installSignals() | 	ctx, cancel := installSignals() | ||||||
| 	defer cancel() | 	defer cancel() | ||||||
|   | |||||||
							
								
								
									
										21
									
								
								cmd/admin_user.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								cmd/admin_user.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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, | ||||||
|  | 	}, | ||||||
|  | } | ||||||
							
								
								
									
										76
									
								
								cmd/admin_user_change_password.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								cmd/admin_user_change_password.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||||
|  | } | ||||||
							
								
								
									
										169
									
								
								cmd/admin_user_create.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										169
									
								
								cmd/admin_user_create.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||||
|  | } | ||||||
							
								
								
									
										78
									
								
								cmd/admin_user_delete.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								cmd/admin_user_delete.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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")) | ||||||
|  | } | ||||||
							
								
								
									
										80
									
								
								cmd/admin_user_generate_access_token.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								cmd/admin_user_generate_access_token.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||||
|  | } | ||||||
							
								
								
									
										60
									
								
								cmd/admin_user_list.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								cmd/admin_user_list.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||||
|  | } | ||||||
							
								
								
									
										58
									
								
								cmd/admin_user_must_change_password.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								cmd/admin_user_must_change_password.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||||
|  | } | ||||||
| @@ -99,6 +99,13 @@ Admin operations: | |||||||
|         - `--password value`, `-p value`: New password. Required. |         - `--password value`, `-p value`: New password. Required. | ||||||
|       - Examples: |       - Examples: | ||||||
|         - `gitea admin user change-password --username myname --password asecurepassword` |         - `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` |   - `regenerate` | ||||||
|     - Options: |     - Options: | ||||||
|       - `hooks`: Regenerate Git Hooks for all repositories |       - `hooks`: Regenerate Git Hooks for all repositories | ||||||
|   | |||||||
							
								
								
									
										49
									
								
								models/user/must_change_password.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								models/user/must_change_password.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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}) | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user