mirror of https://github.com/go-gitea/gitea
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>pull/18165/merge
parent
618c9118c1
commit
aa1d95300a
@ -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, |
||||
}, |
||||
} |
@ -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 |
||||
} |
@ -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 |
||||
} |
@ -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")) |
||||
} |
@ -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 |
||||
} |
@ -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 |
||||
} |
@ -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 |
||||
} |
@ -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…
Reference in new issue