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:
		
					parent
					
						
							
								618c9118c1
							
						
					
				
			
			
				commit
				
					
						aa1d95300a
					
				
			
		
					 10 changed files with 598 additions and 406 deletions
				
			
		
							
								
								
									
										406
									
								
								cmd/admin.go
									
										
									
									
									
								
							
							
						
						
									
										406
									
								
								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() | ||||
|  |  | |||
							
								
								
									
										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. | ||||
|       - 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 | ||||
|  |  | |||
							
								
								
									
										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}) | ||||
| } | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 zeripath
				zeripath