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>tags/v1.19.0-rc0
@@ -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() |
@@ -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 | |||
} |
@@ -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 |
@@ -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}) | |||
} |