mirror of
				https://gitee.com/gitea/gitea
				synced 2025-11-04 08:30:25 +08:00 
			
		
		
		
	Backport #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> Co-authored-by: Jason Song <i@wolfogre.com>
This commit is contained in:
		
							
								
								
									
										396
									
								
								cmd/admin.go
									
									
									
									
									
								
							
							
						
						
									
										396
									
								
								cmd/admin.go
									
									
									
									
									
								
							@@ -6,7 +6,6 @@
 | 
			
		||||
package cmd
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"os"
 | 
			
		||||
@@ -17,20 +16,14 @@ 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"
 | 
			
		||||
)
 | 
			
		||||
@@ -49,142 +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",
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		Action: runGenerateAccessToken,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	subcmdRepoSyncReleases = cli.Command{
 | 
			
		||||
		Name:   "repo-sync-releases",
 | 
			
		||||
		Usage:  "Synchronize repository releases with tags",
 | 
			
		||||
@@ -468,259 +325,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(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
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	t := &auth_model.AccessToken{
 | 
			
		||||
		Name: c.String("token-name"),
 | 
			
		||||
		UID:  user.ID,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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(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"))
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										69
									
								
								cmd/admin_user_generate_access_token.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								cmd/admin_user_generate_access_token.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,69 @@
 | 
			
		||||
// 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",
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
	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
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	t := &auth_model.AccessToken{
 | 
			
		||||
		Name: c.String("token-name"),
 | 
			
		||||
		UID:  user.ID,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/models/db"
 | 
			
		||||
 | 
			
		||||
	"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, fmt.Errorf("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