Skip to content

Commit 063c23e

Browse files
mschererwxiaoguang
andauthored
Add a option "--user-type bot" to admin user create, improve role display (#27885)
Partially solve #13044 Fix #33295 --------- Co-authored-by: wxiaoguang <[email protected]>
1 parent 1ec8d80 commit 063c23e

File tree

7 files changed

+120
-78
lines changed

7 files changed

+120
-78
lines changed

cmd/admin_user_create.go

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ var microcmdUserCreate = &cli.Command{
3131
Name: "username",
3232
Usage: "Username",
3333
},
34+
&cli.StringFlag{
35+
Name: "user-type",
36+
Usage: "Set user's type: individual or bot",
37+
Value: "individual",
38+
},
3439
&cli.StringFlag{
3540
Name: "password",
3641
Usage: "User password",
@@ -77,6 +82,22 @@ func runCreateUser(c *cli.Context) error {
7782
return err
7883
}
7984

85+
userTypes := map[string]user_model.UserType{
86+
"individual": user_model.UserTypeIndividual,
87+
"bot": user_model.UserTypeBot,
88+
}
89+
userType, ok := userTypes[c.String("user-type")]
90+
if !ok {
91+
return fmt.Errorf("invalid user type: %s", c.String("user-type"))
92+
}
93+
if userType != user_model.UserTypeIndividual {
94+
// Some other commands like "change-password" also only support individual users.
95+
// It needs to clarify the "password" behavior for bot users in the future.
96+
// At the moment, we do not allow setting password for bot users.
97+
if c.IsSet("password") || c.IsSet("random-password") {
98+
return errors.New("password can only be set for individual users")
99+
}
100+
}
80101
if c.IsSet("name") && c.IsSet("username") {
81102
return errors.New("cannot set both --name and --username flags")
82103
}
@@ -118,16 +139,19 @@ func runCreateUser(c *cli.Context) error {
118139
return err
119140
}
120141
fmt.Printf("generated random password is '%s'\n", password)
121-
} else {
142+
} else if userType == user_model.UserTypeIndividual {
122143
return errors.New("must set either password or random-password flag")
123144
}
124145

125146
isAdmin := c.Bool("admin")
126147
mustChangePassword := true // always default to true
127148
if c.IsSet("must-change-password") {
149+
if userType != user_model.UserTypeIndividual {
150+
return errors.New("must-change-password flag can only be set for individual users")
151+
}
128152
// if the flag is set, use the value provided by the user
129153
mustChangePassword = c.Bool("must-change-password")
130-
} else {
154+
} else if userType == user_model.UserTypeIndividual {
131155
// check whether there are users in the database
132156
hasUserRecord, err := db.IsTableNotEmpty(&user_model.User{})
133157
if err != nil {
@@ -151,8 +175,9 @@ func runCreateUser(c *cli.Context) error {
151175
u := &user_model.User{
152176
Name: username,
153177
Email: c.String("email"),
154-
Passwd: password,
155178
IsAdmin: isAdmin,
179+
Type: userType,
180+
Passwd: password,
156181
MustChangePassword: mustChangePassword,
157182
Visibility: visibility,
158183
}

cmd/admin_user_create_test.go

Lines changed: 42 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,32 +13,54 @@ import (
1313
user_model "code.gitea.io/gitea/models/user"
1414

1515
"github.com/stretchr/testify/assert"
16+
"github.com/stretchr/testify/require"
1617
)
1718

1819
func TestAdminUserCreate(t *testing.T) {
1920
app := NewMainApp(AppVersion{})
2021

2122
reset := func() {
22-
assert.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.User{}))
23-
assert.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.EmailAddress{}))
23+
require.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.User{}))
24+
require.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.EmailAddress{}))
2425
}
2526

26-
type createCheck struct{ IsAdmin, MustChangePassword bool }
27-
createUser := func(name, args string) createCheck {
28-
assert.NoError(t, app.Run(strings.Fields(fmt.Sprintf("./gitea admin user create --username %s --email %[email protected] %s --password foobar", name, name, args))))
29-
u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: name})
30-
return createCheck{u.IsAdmin, u.MustChangePassword}
31-
}
32-
reset()
33-
assert.Equal(t, createCheck{IsAdmin: false, MustChangePassword: false}, createUser("u", ""), "first non-admin user doesn't need to change password")
34-
35-
reset()
36-
assert.Equal(t, createCheck{IsAdmin: true, MustChangePassword: false}, createUser("u", "--admin"), "first admin user doesn't need to change password")
37-
38-
reset()
39-
assert.Equal(t, createCheck{IsAdmin: true, MustChangePassword: true}, createUser("u", "--admin --must-change-password"))
40-
assert.Equal(t, createCheck{IsAdmin: true, MustChangePassword: true}, createUser("u2", "--admin"))
41-
assert.Equal(t, createCheck{IsAdmin: true, MustChangePassword: false}, createUser("u3", "--admin --must-change-password=false"))
42-
assert.Equal(t, createCheck{IsAdmin: false, MustChangePassword: true}, createUser("u4", ""))
43-
assert.Equal(t, createCheck{IsAdmin: false, MustChangePassword: false}, createUser("u5", "--must-change-password=false"))
27+
t.Run("MustChangePassword", func(t *testing.T) {
28+
type check struct {
29+
IsAdmin bool
30+
MustChangePassword bool
31+
}
32+
createCheck := func(name, args string) check {
33+
require.NoError(t, app.Run(strings.Fields(fmt.Sprintf("./gitea admin user create --username %s --email %[email protected] %s --password foobar", name, name, args))))
34+
u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: name})
35+
return check{IsAdmin: u.IsAdmin, MustChangePassword: u.MustChangePassword}
36+
}
37+
reset()
38+
assert.Equal(t, check{IsAdmin: false, MustChangePassword: false}, createCheck("u", ""), "first non-admin user doesn't need to change password")
39+
40+
reset()
41+
assert.Equal(t, check{IsAdmin: true, MustChangePassword: false}, createCheck("u", "--admin"), "first admin user doesn't need to change password")
42+
43+
reset()
44+
assert.Equal(t, check{IsAdmin: true, MustChangePassword: true}, createCheck("u", "--admin --must-change-password"))
45+
assert.Equal(t, check{IsAdmin: true, MustChangePassword: true}, createCheck("u2", "--admin"))
46+
assert.Equal(t, check{IsAdmin: true, MustChangePassword: false}, createCheck("u3", "--admin --must-change-password=false"))
47+
assert.Equal(t, check{IsAdmin: false, MustChangePassword: true}, createCheck("u4", ""))
48+
assert.Equal(t, check{IsAdmin: false, MustChangePassword: false}, createCheck("u5", "--must-change-password=false"))
49+
})
50+
51+
t.Run("UserType", func(t *testing.T) {
52+
createUser := func(name, args string) error {
53+
return app.Run(strings.Fields(fmt.Sprintf("./gitea admin user create --username %s --email %[email protected] %s", name, name, args)))
54+
}
55+
56+
reset()
57+
assert.ErrorContains(t, createUser("u", "--user-type invalid"), "invalid user type")
58+
assert.ErrorContains(t, createUser("u", "--user-type bot --password 123"), "can only be set for individual users")
59+
assert.ErrorContains(t, createUser("u", "--user-type bot --must-change-password"), "can only be set for individual users")
60+
61+
assert.NoError(t, createUser("u", "--user-type bot"))
62+
u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "u"})
63+
assert.Equal(t, user_model.UserTypeBot, u.Type)
64+
assert.Equal(t, "", u.Passwd)
65+
})
4466
}

models/user/user.go

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -385,11 +385,12 @@ func (u *User) ValidatePassword(passwd string) bool {
385385
}
386386

387387
// IsPasswordSet checks if the password is set or left empty
388+
// TODO: It's better to clarify the "password" behavior for different types (individual, bot)
388389
func (u *User) IsPasswordSet() bool {
389-
return len(u.Passwd) != 0
390+
return u.Passwd != ""
390391
}
391392

392-
// IsOrganization returns true if user is actually a organization.
393+
// IsOrganization returns true if user is actually an organization.
393394
func (u *User) IsOrganization() bool {
394395
return u.Type == UserTypeOrganization
395396
}
@@ -399,13 +400,14 @@ func (u *User) IsIndividual() bool {
399400
return u.Type == UserTypeIndividual
400401
}
401402

402-
func (u *User) IsUser() bool {
403-
return u.Type == UserTypeIndividual || u.Type == UserTypeBot
403+
// IsTypeBot returns whether the user is of type bot
404+
func (u *User) IsTypeBot() bool {
405+
return u.Type == UserTypeBot
404406
}
405407

406-
// IsBot returns whether or not the user is of type bot
407-
func (u *User) IsBot() bool {
408-
return u.Type == UserTypeBot
408+
// IsTokenAccessAllowed returns whether the user is an individual or a bot (which allows for token access)
409+
func (u *User) IsTokenAccessAllowed() bool {
410+
return u.Type == UserTypeIndividual || u.Type == UserTypeBot
409411
}
410412

411413
// DisplayName returns full name if it's not empty,

models/user/user_system.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ func NewActionsUser() *User {
5656
Email: ActionsUserEmail,
5757
KeepEmailPrivate: true,
5858
LoginName: ActionsUserName,
59-
Type: UserTypeIndividual,
59+
Type: UserTypeBot,
6060
AllowCreateOrganization: true,
6161
Visibility: structs.VisibleTypePublic,
6262
}

routers/api/v1/api.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -268,12 +268,12 @@ func checkTokenPublicOnly() func(ctx *context.APIContext) {
268268
return
269269
}
270270
case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryUser):
271-
if ctx.ContextUser != nil && ctx.ContextUser.IsUser() && ctx.ContextUser.Visibility != api.VisibleTypePublic {
271+
if ctx.ContextUser != nil && ctx.ContextUser.IsTokenAccessAllowed() && ctx.ContextUser.Visibility != api.VisibleTypePublic {
272272
ctx.Error(http.StatusForbidden, "reqToken", "token scope is limited to public users")
273273
return
274274
}
275275
case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryActivityPub):
276-
if ctx.ContextUser != nil && ctx.ContextUser.IsUser() && ctx.ContextUser.Visibility != api.VisibleTypePublic {
276+
if ctx.ContextUser != nil && ctx.ContextUser.IsTokenAccessAllowed() && ctx.ContextUser.Visibility != api.VisibleTypePublic {
277277
ctx.Error(http.StatusForbidden, "reqToken", "token scope is limited to public activitypub")
278278
return
279279
}

routers/web/repo/issue_view.go

Lines changed: 37 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
package repo
55

66
import (
7-
stdCtx "context"
87
"fmt"
98
"math/big"
109
"net/http"
@@ -40,86 +39,80 @@ import (
4039
)
4140

4241
// roleDescriptor returns the role descriptor for a comment in/with the given repo, poster and issue
43-
func roleDescriptor(ctx stdCtx.Context, repo *repo_model.Repository, poster *user_model.User, permsCache map[int64]access_model.Permission, issue *issues_model.Issue, hasOriginalAuthor bool) (issues_model.RoleDescriptor, error) {
44-
roleDescriptor := issues_model.RoleDescriptor{}
45-
42+
func roleDescriptor(ctx *context.Context, repo *repo_model.Repository, poster *user_model.User, permsCache map[int64]access_model.Permission, issue *issues_model.Issue, hasOriginalAuthor bool) (roleDesc issues_model.RoleDescriptor, err error) {
4643
if hasOriginalAuthor {
47-
return roleDescriptor, nil
44+
// the poster is a migrated user, so no need to detect the role
45+
return roleDesc, nil
4846
}
4947

50-
var perm access_model.Permission
51-
var err error
52-
if permsCache != nil {
53-
var ok bool
54-
perm, ok = permsCache[poster.ID]
55-
if !ok {
56-
perm, err = access_model.GetUserRepoPermission(ctx, repo, poster)
57-
if err != nil {
58-
return roleDescriptor, err
59-
}
60-
}
61-
permsCache[poster.ID] = perm
62-
} else {
48+
if poster.IsGhost() || !poster.IsIndividual() {
49+
return roleDesc, nil
50+
}
51+
52+
roleDesc.IsPoster = issue.IsPoster(poster.ID) // check whether the comment's poster is the issue's poster
53+
54+
// Guess the role of the poster in the repo by permission
55+
perm, hasPermCache := permsCache[poster.ID]
56+
if !hasPermCache {
6357
perm, err = access_model.GetUserRepoPermission(ctx, repo, poster)
6458
if err != nil {
65-
return roleDescriptor, err
59+
return roleDesc, err
6660
}
6761
}
68-
69-
// If the poster is the actual poster of the issue, enable Poster role.
70-
roleDescriptor.IsPoster = issue.IsPoster(poster.ID)
62+
if permsCache != nil {
63+
permsCache[poster.ID] = perm
64+
}
7165

7266
// Check if the poster is owner of the repo.
7367
if perm.IsOwner() {
74-
// If the poster isn't an admin, enable the owner role.
68+
// If the poster isn't a site admin, then is must be the repo's owner
7569
if !poster.IsAdmin {
76-
roleDescriptor.RoleInRepo = issues_model.RoleRepoOwner
77-
return roleDescriptor, nil
70+
roleDesc.RoleInRepo = issues_model.RoleRepoOwner
71+
return roleDesc, nil
7872
}
79-
80-
// Otherwise check if poster is the real repo admin.
81-
ok, err := access_model.IsUserRealRepoAdmin(ctx, repo, poster)
73+
// Otherwise (poster is site admin), check if poster is the real repo admin.
74+
isRealRepoAdmin, err := access_model.IsUserRealRepoAdmin(ctx, repo, poster)
8275
if err != nil {
83-
return roleDescriptor, err
76+
return roleDesc, err
8477
}
85-
if ok {
86-
roleDescriptor.RoleInRepo = issues_model.RoleRepoOwner
87-
return roleDescriptor, nil
78+
if isRealRepoAdmin {
79+
roleDesc.RoleInRepo = issues_model.RoleRepoOwner
80+
return roleDesc, nil
8881
}
8982
}
9083

9184
// If repo is organization, check Member role
92-
if err := repo.LoadOwner(ctx); err != nil {
93-
return roleDescriptor, err
85+
if err = repo.LoadOwner(ctx); err != nil {
86+
return roleDesc, err
9487
}
9588
if repo.Owner.IsOrganization() {
9689
if isMember, err := organization.IsOrganizationMember(ctx, repo.Owner.ID, poster.ID); err != nil {
97-
return roleDescriptor, err
90+
return roleDesc, err
9891
} else if isMember {
99-
roleDescriptor.RoleInRepo = issues_model.RoleRepoMember
100-
return roleDescriptor, nil
92+
roleDesc.RoleInRepo = issues_model.RoleRepoMember
93+
return roleDesc, nil
10194
}
10295
}
10396

10497
// If the poster is the collaborator of the repo
10598
if isCollaborator, err := repo_model.IsCollaborator(ctx, repo.ID, poster.ID); err != nil {
106-
return roleDescriptor, err
99+
return roleDesc, err
107100
} else if isCollaborator {
108-
roleDescriptor.RoleInRepo = issues_model.RoleRepoCollaborator
109-
return roleDescriptor, nil
101+
roleDesc.RoleInRepo = issues_model.RoleRepoCollaborator
102+
return roleDesc, nil
110103
}
111104

112105
hasMergedPR, err := issues_model.HasMergedPullRequestInRepo(ctx, repo.ID, poster.ID)
113106
if err != nil {
114-
return roleDescriptor, err
107+
return roleDesc, err
115108
} else if hasMergedPR {
116-
roleDescriptor.RoleInRepo = issues_model.RoleRepoContributor
109+
roleDesc.RoleInRepo = issues_model.RoleRepoContributor
117110
} else if issue.IsPull {
118111
// only display first time contributor in the first opening pull request
119-
roleDescriptor.RoleInRepo = issues_model.RoleRepoFirstTimeContributor
112+
roleDesc.RoleInRepo = issues_model.RoleRepoFirstTimeContributor
120113
}
121114

122-
return roleDescriptor, nil
115+
return roleDesc, nil
123116
}
124117

125118
func getBranchData(ctx *context.Context, issue *issues_model.Issue) {

templates/shared/user/authorlink.tmpl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
<a class="author text black tw-font-semibold muted"{{if gt .ID 0}} href="{{.HomeLink}}"{{end}}>{{.GetDisplayName}}</a>{{if .IsBot}}<span class="ui basic label tw-p-1 tw-align-baseline">bot</span>{{end}}
1+
<a class="author text black tw-font-semibold muted"{{if gt .ID 0}} href="{{.HomeLink}}"{{end}}>{{.GetDisplayName}}</a>{{if .IsTypeBot}}<span class="ui basic label tw-p-1 tw-align-baseline">bot</span>{{end}}

0 commit comments

Comments
 (0)