Skip to content

Commit aa1d953

Browse files
authored
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 <[email protected]>
1 parent 618c911 commit aa1d953

10 files changed

+598
-406
lines changed

cmd/admin.go

-406
Large diffs are not rendered by default.

cmd/admin_user.go

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Copyright 2023 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package cmd
5+
6+
import (
7+
"github.com/urfave/cli"
8+
)
9+
10+
var subcmdUser = cli.Command{
11+
Name: "user",
12+
Usage: "Modify users",
13+
Subcommands: []cli.Command{
14+
microcmdUserCreate,
15+
microcmdUserList,
16+
microcmdUserChangePassword,
17+
microcmdUserDelete,
18+
microcmdUserGenerateAccessToken,
19+
microcmdUserMustChangePassword,
20+
},
21+
}

cmd/admin_user_change_password.go

+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// Copyright 2023 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package cmd
5+
6+
import (
7+
"context"
8+
"errors"
9+
"fmt"
10+
11+
user_model "code.gitea.io/gitea/models/user"
12+
pwd "code.gitea.io/gitea/modules/password"
13+
"code.gitea.io/gitea/modules/setting"
14+
15+
"github.com/urfave/cli"
16+
)
17+
18+
var microcmdUserChangePassword = cli.Command{
19+
Name: "change-password",
20+
Usage: "Change a user's password",
21+
Action: runChangePassword,
22+
Flags: []cli.Flag{
23+
cli.StringFlag{
24+
Name: "username,u",
25+
Value: "",
26+
Usage: "The user to change password for",
27+
},
28+
cli.StringFlag{
29+
Name: "password,p",
30+
Value: "",
31+
Usage: "New password to set for user",
32+
},
33+
},
34+
}
35+
36+
func runChangePassword(c *cli.Context) error {
37+
if err := argsSet(c, "username", "password"); err != nil {
38+
return err
39+
}
40+
41+
ctx, cancel := installSignals()
42+
defer cancel()
43+
44+
if err := initDB(ctx); err != nil {
45+
return err
46+
}
47+
if len(c.String("password")) < setting.MinPasswordLength {
48+
return fmt.Errorf("Password is not long enough. Needs to be at least %d", setting.MinPasswordLength)
49+
}
50+
51+
if !pwd.IsComplexEnough(c.String("password")) {
52+
return errors.New("Password does not meet complexity requirements")
53+
}
54+
pwned, err := pwd.IsPwned(context.Background(), c.String("password"))
55+
if err != nil {
56+
return err
57+
}
58+
if pwned {
59+
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")
60+
}
61+
uname := c.String("username")
62+
user, err := user_model.GetUserByName(ctx, uname)
63+
if err != nil {
64+
return err
65+
}
66+
if err = user.SetPassword(c.String("password")); err != nil {
67+
return err
68+
}
69+
70+
if err = user_model.UpdateUserCols(ctx, user, "passwd", "passwd_hash_algo", "salt"); err != nil {
71+
return err
72+
}
73+
74+
fmt.Printf("%s's password has been successfully updated!\n", user.Name)
75+
return nil
76+
}

cmd/admin_user_create.go

+169
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
// Copyright 2023 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package cmd
5+
6+
import (
7+
"errors"
8+
"fmt"
9+
"os"
10+
11+
auth_model "code.gitea.io/gitea/models/auth"
12+
user_model "code.gitea.io/gitea/models/user"
13+
pwd "code.gitea.io/gitea/modules/password"
14+
"code.gitea.io/gitea/modules/setting"
15+
"code.gitea.io/gitea/modules/util"
16+
17+
"github.com/urfave/cli"
18+
)
19+
20+
var microcmdUserCreate = cli.Command{
21+
Name: "create",
22+
Usage: "Create a new user in database",
23+
Action: runCreateUser,
24+
Flags: []cli.Flag{
25+
cli.StringFlag{
26+
Name: "name",
27+
Usage: "Username. DEPRECATED: use username instead",
28+
},
29+
cli.StringFlag{
30+
Name: "username",
31+
Usage: "Username",
32+
},
33+
cli.StringFlag{
34+
Name: "password",
35+
Usage: "User password",
36+
},
37+
cli.StringFlag{
38+
Name: "email",
39+
Usage: "User email address",
40+
},
41+
cli.BoolFlag{
42+
Name: "admin",
43+
Usage: "User is an admin",
44+
},
45+
cli.BoolFlag{
46+
Name: "random-password",
47+
Usage: "Generate a random password for the user",
48+
},
49+
cli.BoolFlag{
50+
Name: "must-change-password",
51+
Usage: "Set this option to false to prevent forcing the user to change their password after initial login, (Default: true)",
52+
},
53+
cli.IntFlag{
54+
Name: "random-password-length",
55+
Usage: "Length of the random password to be generated",
56+
Value: 12,
57+
},
58+
cli.BoolFlag{
59+
Name: "access-token",
60+
Usage: "Generate access token for the user",
61+
},
62+
cli.BoolFlag{
63+
Name: "restricted",
64+
Usage: "Make a restricted user account",
65+
},
66+
},
67+
}
68+
69+
func runCreateUser(c *cli.Context) error {
70+
if err := argsSet(c, "email"); err != nil {
71+
return err
72+
}
73+
74+
if c.IsSet("name") && c.IsSet("username") {
75+
return errors.New("Cannot set both --name and --username flags")
76+
}
77+
if !c.IsSet("name") && !c.IsSet("username") {
78+
return errors.New("One of --name or --username flags must be set")
79+
}
80+
81+
if c.IsSet("password") && c.IsSet("random-password") {
82+
return errors.New("cannot set both -random-password and -password flags")
83+
}
84+
85+
var username string
86+
if c.IsSet("username") {
87+
username = c.String("username")
88+
} else {
89+
username = c.String("name")
90+
fmt.Fprintf(os.Stderr, "--name flag is deprecated. Use --username instead.\n")
91+
}
92+
93+
ctx, cancel := installSignals()
94+
defer cancel()
95+
96+
if err := initDB(ctx); err != nil {
97+
return err
98+
}
99+
100+
var password string
101+
if c.IsSet("password") {
102+
password = c.String("password")
103+
} else if c.IsSet("random-password") {
104+
var err error
105+
password, err = pwd.Generate(c.Int("random-password-length"))
106+
if err != nil {
107+
return err
108+
}
109+
fmt.Printf("generated random password is '%s'\n", password)
110+
} else {
111+
return errors.New("must set either password or random-password flag")
112+
}
113+
114+
// always default to true
115+
changePassword := true
116+
117+
// If this is the first user being created.
118+
// Take it as the admin and don't force a password update.
119+
if n := user_model.CountUsers(nil); n == 0 {
120+
changePassword = false
121+
}
122+
123+
if c.IsSet("must-change-password") {
124+
changePassword = c.Bool("must-change-password")
125+
}
126+
127+
restricted := util.OptionalBoolNone
128+
129+
if c.IsSet("restricted") {
130+
restricted = util.OptionalBoolOf(c.Bool("restricted"))
131+
}
132+
133+
// default user visibility in app.ini
134+
visibility := setting.Service.DefaultUserVisibilityMode
135+
136+
u := &user_model.User{
137+
Name: username,
138+
Email: c.String("email"),
139+
Passwd: password,
140+
IsAdmin: c.Bool("admin"),
141+
MustChangePassword: changePassword,
142+
Visibility: visibility,
143+
}
144+
145+
overwriteDefault := &user_model.CreateUserOverwriteOptions{
146+
IsActive: util.OptionalBoolTrue,
147+
IsRestricted: restricted,
148+
}
149+
150+
if err := user_model.CreateUser(u, overwriteDefault); err != nil {
151+
return fmt.Errorf("CreateUser: %w", err)
152+
}
153+
154+
if c.Bool("access-token") {
155+
t := &auth_model.AccessToken{
156+
Name: "gitea-admin",
157+
UID: u.ID,
158+
}
159+
160+
if err := auth_model.NewAccessToken(t); err != nil {
161+
return err
162+
}
163+
164+
fmt.Printf("Access token was successfully created... %s\n", t.Token)
165+
}
166+
167+
fmt.Printf("New user '%s' has been successfully created!\n", username)
168+
return nil
169+
}

cmd/admin_user_delete.go

+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// Copyright 2023 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package cmd
5+
6+
import (
7+
"fmt"
8+
"strings"
9+
10+
user_model "code.gitea.io/gitea/models/user"
11+
"code.gitea.io/gitea/modules/storage"
12+
user_service "code.gitea.io/gitea/services/user"
13+
14+
"github.com/urfave/cli"
15+
)
16+
17+
var microcmdUserDelete = cli.Command{
18+
Name: "delete",
19+
Usage: "Delete specific user by id, name or email",
20+
Flags: []cli.Flag{
21+
cli.Int64Flag{
22+
Name: "id",
23+
Usage: "ID of user of the user to delete",
24+
},
25+
cli.StringFlag{
26+
Name: "username,u",
27+
Usage: "Username of the user to delete",
28+
},
29+
cli.StringFlag{
30+
Name: "email,e",
31+
Usage: "Email of the user to delete",
32+
},
33+
cli.BoolFlag{
34+
Name: "purge",
35+
Usage: "Purge user, all their repositories, organizations and comments",
36+
},
37+
},
38+
Action: runDeleteUser,
39+
}
40+
41+
func runDeleteUser(c *cli.Context) error {
42+
if !c.IsSet("id") && !c.IsSet("username") && !c.IsSet("email") {
43+
return fmt.Errorf("You must provide the id, username or email of a user to delete")
44+
}
45+
46+
ctx, cancel := installSignals()
47+
defer cancel()
48+
49+
if err := initDB(ctx); err != nil {
50+
return err
51+
}
52+
53+
if err := storage.Init(); err != nil {
54+
return err
55+
}
56+
57+
var err error
58+
var user *user_model.User
59+
if c.IsSet("email") {
60+
user, err = user_model.GetUserByEmail(c.String("email"))
61+
} else if c.IsSet("username") {
62+
user, err = user_model.GetUserByName(ctx, c.String("username"))
63+
} else {
64+
user, err = user_model.GetUserByID(ctx, c.Int64("id"))
65+
}
66+
if err != nil {
67+
return err
68+
}
69+
if c.IsSet("username") && user.LowerName != strings.ToLower(strings.TrimSpace(c.String("username"))) {
70+
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"))
71+
}
72+
73+
if c.IsSet("id") && user.ID != c.Int64("id") {
74+
return fmt.Errorf("The user %s does not match the provided id %d", user.Name, c.Int64("id"))
75+
}
76+
77+
return user_service.DeleteUser(ctx, user, c.Bool("purge"))
78+
}

0 commit comments

Comments
 (0)