1// Copyright 2023 The Gitea Authors. All rights reserved.
2// SPDX-License-Identifier: MIT
3
4package cmd
5
6import (
7 "errors"
8 "fmt"
9 "strings"
10
11 auth_model "forgejo.org/models/auth"
12 "forgejo.org/models/db"
13 user_model "forgejo.org/models/user"
14 pwd "forgejo.org/modules/auth/password"
15 "forgejo.org/modules/optional"
16 "forgejo.org/modules/setting"
17
18 "github.com/urfave/cli/v2"
19)
20
21var microcmdUserCreate = &cli.Command{
22 Name: "create",
23 Usage: "Create a new user in database",
24 Action: runCreateUser,
25 Flags: []cli.Flag{
26 &cli.StringFlag{
27 Name: "name",
28 Usage: "Username. DEPRECATED: use username instead",
29 },
30 &cli.StringFlag{
31 Name: "username",
32 Usage: "Username",
33 },
34 &cli.StringFlag{
35 Name: "password",
36 Usage: "User password",
37 },
38 &cli.StringFlag{
39 Name: "email",
40 Usage: "User email address",
41 },
42 &cli.BoolFlag{
43 Name: "admin",
44 Usage: "User is an admin",
45 },
46 &cli.BoolFlag{
47 Name: "random-password",
48 Usage: "Generate a random password for the user",
49 },
50 &cli.BoolFlag{
51 Name: "must-change-password",
52 Usage: "Set this option to false to prevent forcing the user to change their password after initial login",
53 Value: true,
54 DisableDefaultText: true,
55 },
56 &cli.IntFlag{
57 Name: "random-password-length",
58 Usage: "Length of the random password to be generated",
59 Value: 12,
60 },
61 &cli.BoolFlag{
62 Name: "access-token",
63 Usage: "Generate access token for the user",
64 },
65 &cli.StringFlag{
66 Name: "access-token-name",
67 Usage: `Name of the generated access token`,
68 Value: "gitea-admin",
69 },
70 &cli.StringFlag{
71 Name: "access-token-scopes",
72 Usage: `Scopes of the generated access token, comma separated. Examples: "all", "public-only,read:issue", "write:repository,write:user"`,
73 Value: "all",
74 },
75 &cli.BoolFlag{
76 Name: "restricted",
77 Usage: "Make a restricted user account",
78 },
79 },
80}
81
82func runCreateUser(c *cli.Context) error {
83 // this command highly depends on the many setting options (create org, visibility, etc.), so it must have a full setting load first
84 // duplicate setting loading should be safe at the moment, but it should be refactored & improved in the future.
85 setting.LoadSettings()
86
87 if err := argsSet(c, "email"); err != nil {
88 return err
89 }
90
91 if c.IsSet("name") && c.IsSet("username") {
92 return errors.New("cannot set both --name and --username flags")
93 }
94 if !c.IsSet("name") && !c.IsSet("username") {
95 return errors.New("one of --name or --username flags must be set")
96 }
97
98 if c.IsSet("password") && c.IsSet("random-password") {
99 return errors.New("cannot set both -random-password and -password flags")
100 }
101
102 var username string
103 if c.IsSet("username") {
104 username = c.String("username")
105 } else {
106 username = c.String("name")
107 _, _ = fmt.Fprintf(c.App.ErrWriter, "--name flag is deprecated. Use --username instead.\n")
108 }
109
110 ctx, cancel := installSignals()
111 defer cancel()
112
113 if err := initDB(ctx); err != nil {
114 return err
115 }
116
117 var password string
118 if c.IsSet("password") {
119 password = c.String("password")
120 } else if c.IsSet("random-password") {
121 var err error
122 password, err = pwd.Generate(c.Int("random-password-length"))
123 if err != nil {
124 return err
125 }
126 fmt.Printf("generated random password is '%s'\n", password)
127 } else {
128 return errors.New("must set either password or random-password flag")
129 }
130
131 isAdmin := c.Bool("admin")
132 mustChangePassword := true // always default to true
133 if c.IsSet("must-change-password") {
134 // if the flag is set, use the value provided by the user
135 mustChangePassword = c.Bool("must-change-password")
136 } else {
137 // check whether there are users in the database
138 hasUserRecord, err := db.IsTableNotEmpty(&user_model.User{})
139 if err != nil {
140 return fmt.Errorf("IsTableNotEmpty: %w", err)
141 }
142 if !hasUserRecord {
143 // if this is the first admin being created, don't force to change password (keep the old behavior)
144 mustChangePassword = false
145 }
146 }
147
148 restricted := optional.None[bool]()
149
150 if c.IsSet("restricted") {
151 restricted = optional.Some(c.Bool("restricted"))
152 }
153
154 // default user visibility in app.ini
155 visibility := setting.Service.DefaultUserVisibilityMode
156
157 u := &user_model.User{
158 Name: username,
159 Email: c.String("email"),
160 Passwd: password,
161 IsAdmin: isAdmin,
162 MustChangePassword: mustChangePassword,
163 Visibility: visibility,
164 }
165
166 overwriteDefault := &user_model.CreateUserOverwriteOptions{
167 IsActive: optional.Some(true),
168 IsRestricted: restricted,
169 }
170
171 var accessTokenName string
172 var accessTokenScope auth_model.AccessTokenScope
173 if c.IsSet("access-token") {
174 accessTokenName = strings.TrimSpace(c.String("access-token-name"))
175 if accessTokenName == "" {
176 return errors.New("access-token-name cannot be empty")
177 }
178 var err error
179 accessTokenScope, err = auth_model.AccessTokenScope(c.String("access-token-scopes")).Normalize()
180 if err != nil {
181 return fmt.Errorf("invalid access token scope provided: %w", err)
182 }
183 if !accessTokenScope.HasPermissionScope() {
184 return errors.New("access token does not have any permission")
185 }
186 } else if c.IsSet("access-token-name") || c.IsSet("access-token-scopes") {
187 return errors.New("access-token-name and access-token-scopes flags are only valid when access-token flag is set")
188 }
189
190 // arguments should be prepared before creating the user & access token, in case there is anything wrong
191
192 // create the user
193 if err := user_model.CreateUser(ctx, u, overwriteDefault); err != nil {
194 return fmt.Errorf("CreateUser: %w", err)
195 }
196 fmt.Printf("New user '%s' has been successfully created!\n", username)
197
198 // create the access token
199 if accessTokenScope != "" {
200 t := &auth_model.AccessToken{Name: accessTokenName, UID: u.ID, Scope: accessTokenScope}
201 if err := auth_model.NewAccessToken(ctx, t); err != nil {
202 return err
203 }
204 fmt.Printf("Access token was successfully created... %s\n", t.Token)
205 }
206 return nil
207}