mirror of
https://github.com/go-gitea/gitea.git
synced 2025-02-20 11:43:57 +08:00
Add a option "--user-type bot" to admin user create, improve role display (#27885)
Partially solve #13044 Fix #33295 --------- Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
parent
1ec8d80fa3
commit
063c23e1bc
@ -31,6 +31,11 @@ var microcmdUserCreate = &cli.Command{
|
|||||||
Name: "username",
|
Name: "username",
|
||||||
Usage: "Username",
|
Usage: "Username",
|
||||||
},
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "user-type",
|
||||||
|
Usage: "Set user's type: individual or bot",
|
||||||
|
Value: "individual",
|
||||||
|
},
|
||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
Name: "password",
|
Name: "password",
|
||||||
Usage: "User password",
|
Usage: "User password",
|
||||||
@ -77,6 +82,22 @@ func runCreateUser(c *cli.Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
userTypes := map[string]user_model.UserType{
|
||||||
|
"individual": user_model.UserTypeIndividual,
|
||||||
|
"bot": user_model.UserTypeBot,
|
||||||
|
}
|
||||||
|
userType, ok := userTypes[c.String("user-type")]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("invalid user type: %s", c.String("user-type"))
|
||||||
|
}
|
||||||
|
if userType != user_model.UserTypeIndividual {
|
||||||
|
// Some other commands like "change-password" also only support individual users.
|
||||||
|
// It needs to clarify the "password" behavior for bot users in the future.
|
||||||
|
// At the moment, we do not allow setting password for bot users.
|
||||||
|
if c.IsSet("password") || c.IsSet("random-password") {
|
||||||
|
return errors.New("password can only be set for individual users")
|
||||||
|
}
|
||||||
|
}
|
||||||
if c.IsSet("name") && c.IsSet("username") {
|
if c.IsSet("name") && c.IsSet("username") {
|
||||||
return errors.New("cannot set both --name and --username flags")
|
return errors.New("cannot set both --name and --username flags")
|
||||||
}
|
}
|
||||||
@ -118,16 +139,19 @@ func runCreateUser(c *cli.Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
fmt.Printf("generated random password is '%s'\n", password)
|
fmt.Printf("generated random password is '%s'\n", password)
|
||||||
} else {
|
} else if userType == user_model.UserTypeIndividual {
|
||||||
return errors.New("must set either password or random-password flag")
|
return errors.New("must set either password or random-password flag")
|
||||||
}
|
}
|
||||||
|
|
||||||
isAdmin := c.Bool("admin")
|
isAdmin := c.Bool("admin")
|
||||||
mustChangePassword := true // always default to true
|
mustChangePassword := true // always default to true
|
||||||
if c.IsSet("must-change-password") {
|
if c.IsSet("must-change-password") {
|
||||||
|
if userType != user_model.UserTypeIndividual {
|
||||||
|
return errors.New("must-change-password flag can only be set for individual users")
|
||||||
|
}
|
||||||
// if the flag is set, use the value provided by the user
|
// if the flag is set, use the value provided by the user
|
||||||
mustChangePassword = c.Bool("must-change-password")
|
mustChangePassword = c.Bool("must-change-password")
|
||||||
} else {
|
} else if userType == user_model.UserTypeIndividual {
|
||||||
// check whether there are users in the database
|
// check whether there are users in the database
|
||||||
hasUserRecord, err := db.IsTableNotEmpty(&user_model.User{})
|
hasUserRecord, err := db.IsTableNotEmpty(&user_model.User{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -151,8 +175,9 @@ func runCreateUser(c *cli.Context) error {
|
|||||||
u := &user_model.User{
|
u := &user_model.User{
|
||||||
Name: username,
|
Name: username,
|
||||||
Email: c.String("email"),
|
Email: c.String("email"),
|
||||||
Passwd: password,
|
|
||||||
IsAdmin: isAdmin,
|
IsAdmin: isAdmin,
|
||||||
|
Type: userType,
|
||||||
|
Passwd: password,
|
||||||
MustChangePassword: mustChangePassword,
|
MustChangePassword: mustChangePassword,
|
||||||
Visibility: visibility,
|
Visibility: visibility,
|
||||||
}
|
}
|
||||||
|
@ -13,32 +13,54 @@ import (
|
|||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAdminUserCreate(t *testing.T) {
|
func TestAdminUserCreate(t *testing.T) {
|
||||||
app := NewMainApp(AppVersion{})
|
app := NewMainApp(AppVersion{})
|
||||||
|
|
||||||
reset := func() {
|
reset := func() {
|
||||||
assert.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.User{}))
|
require.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.User{}))
|
||||||
assert.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.EmailAddress{}))
|
require.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.EmailAddress{}))
|
||||||
}
|
}
|
||||||
|
|
||||||
type createCheck struct{ IsAdmin, MustChangePassword bool }
|
t.Run("MustChangePassword", func(t *testing.T) {
|
||||||
createUser := func(name, args string) createCheck {
|
type check struct {
|
||||||
assert.NoError(t, app.Run(strings.Fields(fmt.Sprintf("./gitea admin user create --username %s --email %s@gitea.local %s --password foobar", name, name, args))))
|
IsAdmin bool
|
||||||
|
MustChangePassword bool
|
||||||
|
}
|
||||||
|
createCheck := func(name, args string) check {
|
||||||
|
require.NoError(t, app.Run(strings.Fields(fmt.Sprintf("./gitea admin user create --username %s --email %s@gitea.local %s --password foobar", name, name, args))))
|
||||||
u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: name})
|
u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: name})
|
||||||
return createCheck{u.IsAdmin, u.MustChangePassword}
|
return check{IsAdmin: u.IsAdmin, MustChangePassword: u.MustChangePassword}
|
||||||
}
|
}
|
||||||
reset()
|
reset()
|
||||||
assert.Equal(t, createCheck{IsAdmin: false, MustChangePassword: false}, createUser("u", ""), "first non-admin user doesn't need to change password")
|
assert.Equal(t, check{IsAdmin: false, MustChangePassword: false}, createCheck("u", ""), "first non-admin user doesn't need to change password")
|
||||||
|
|
||||||
reset()
|
reset()
|
||||||
assert.Equal(t, createCheck{IsAdmin: true, MustChangePassword: false}, createUser("u", "--admin"), "first admin user doesn't need to change password")
|
assert.Equal(t, check{IsAdmin: true, MustChangePassword: false}, createCheck("u", "--admin"), "first admin user doesn't need to change password")
|
||||||
|
|
||||||
reset()
|
reset()
|
||||||
assert.Equal(t, createCheck{IsAdmin: true, MustChangePassword: true}, createUser("u", "--admin --must-change-password"))
|
assert.Equal(t, check{IsAdmin: true, MustChangePassword: true}, createCheck("u", "--admin --must-change-password"))
|
||||||
assert.Equal(t, createCheck{IsAdmin: true, MustChangePassword: true}, createUser("u2", "--admin"))
|
assert.Equal(t, check{IsAdmin: true, MustChangePassword: true}, createCheck("u2", "--admin"))
|
||||||
assert.Equal(t, createCheck{IsAdmin: true, MustChangePassword: false}, createUser("u3", "--admin --must-change-password=false"))
|
assert.Equal(t, check{IsAdmin: true, MustChangePassword: false}, createCheck("u3", "--admin --must-change-password=false"))
|
||||||
assert.Equal(t, createCheck{IsAdmin: false, MustChangePassword: true}, createUser("u4", ""))
|
assert.Equal(t, check{IsAdmin: false, MustChangePassword: true}, createCheck("u4", ""))
|
||||||
assert.Equal(t, createCheck{IsAdmin: false, MustChangePassword: false}, createUser("u5", "--must-change-password=false"))
|
assert.Equal(t, check{IsAdmin: false, MustChangePassword: false}, createCheck("u5", "--must-change-password=false"))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("UserType", func(t *testing.T) {
|
||||||
|
createUser := func(name, args string) error {
|
||||||
|
return app.Run(strings.Fields(fmt.Sprintf("./gitea admin user create --username %s --email %s@gitea.local %s", name, name, args)))
|
||||||
|
}
|
||||||
|
|
||||||
|
reset()
|
||||||
|
assert.ErrorContains(t, createUser("u", "--user-type invalid"), "invalid user type")
|
||||||
|
assert.ErrorContains(t, createUser("u", "--user-type bot --password 123"), "can only be set for individual users")
|
||||||
|
assert.ErrorContains(t, createUser("u", "--user-type bot --must-change-password"), "can only be set for individual users")
|
||||||
|
|
||||||
|
assert.NoError(t, createUser("u", "--user-type bot"))
|
||||||
|
u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "u"})
|
||||||
|
assert.Equal(t, user_model.UserTypeBot, u.Type)
|
||||||
|
assert.Equal(t, "", u.Passwd)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
@ -385,11 +385,12 @@ func (u *User) ValidatePassword(passwd string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// IsPasswordSet checks if the password is set or left empty
|
// IsPasswordSet checks if the password is set or left empty
|
||||||
|
// TODO: It's better to clarify the "password" behavior for different types (individual, bot)
|
||||||
func (u *User) IsPasswordSet() bool {
|
func (u *User) IsPasswordSet() bool {
|
||||||
return len(u.Passwd) != 0
|
return u.Passwd != ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsOrganization returns true if user is actually a organization.
|
// IsOrganization returns true if user is actually an organization.
|
||||||
func (u *User) IsOrganization() bool {
|
func (u *User) IsOrganization() bool {
|
||||||
return u.Type == UserTypeOrganization
|
return u.Type == UserTypeOrganization
|
||||||
}
|
}
|
||||||
@ -399,13 +400,14 @@ func (u *User) IsIndividual() bool {
|
|||||||
return u.Type == UserTypeIndividual
|
return u.Type == UserTypeIndividual
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *User) IsUser() bool {
|
// IsTypeBot returns whether the user is of type bot
|
||||||
return u.Type == UserTypeIndividual || u.Type == UserTypeBot
|
func (u *User) IsTypeBot() bool {
|
||||||
|
return u.Type == UserTypeBot
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsBot returns whether or not the user is of type bot
|
// IsTokenAccessAllowed returns whether the user is an individual or a bot (which allows for token access)
|
||||||
func (u *User) IsBot() bool {
|
func (u *User) IsTokenAccessAllowed() bool {
|
||||||
return u.Type == UserTypeBot
|
return u.Type == UserTypeIndividual || u.Type == UserTypeBot
|
||||||
}
|
}
|
||||||
|
|
||||||
// DisplayName returns full name if it's not empty,
|
// DisplayName returns full name if it's not empty,
|
||||||
|
@ -56,7 +56,7 @@ func NewActionsUser() *User {
|
|||||||
Email: ActionsUserEmail,
|
Email: ActionsUserEmail,
|
||||||
KeepEmailPrivate: true,
|
KeepEmailPrivate: true,
|
||||||
LoginName: ActionsUserName,
|
LoginName: ActionsUserName,
|
||||||
Type: UserTypeIndividual,
|
Type: UserTypeBot,
|
||||||
AllowCreateOrganization: true,
|
AllowCreateOrganization: true,
|
||||||
Visibility: structs.VisibleTypePublic,
|
Visibility: structs.VisibleTypePublic,
|
||||||
}
|
}
|
||||||
|
@ -268,12 +268,12 @@ func checkTokenPublicOnly() func(ctx *context.APIContext) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryUser):
|
case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryUser):
|
||||||
if ctx.ContextUser != nil && ctx.ContextUser.IsUser() && ctx.ContextUser.Visibility != api.VisibleTypePublic {
|
if ctx.ContextUser != nil && ctx.ContextUser.IsTokenAccessAllowed() && ctx.ContextUser.Visibility != api.VisibleTypePublic {
|
||||||
ctx.Error(http.StatusForbidden, "reqToken", "token scope is limited to public users")
|
ctx.Error(http.StatusForbidden, "reqToken", "token scope is limited to public users")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryActivityPub):
|
case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryActivityPub):
|
||||||
if ctx.ContextUser != nil && ctx.ContextUser.IsUser() && ctx.ContextUser.Visibility != api.VisibleTypePublic {
|
if ctx.ContextUser != nil && ctx.ContextUser.IsTokenAccessAllowed() && ctx.ContextUser.Visibility != api.VisibleTypePublic {
|
||||||
ctx.Error(http.StatusForbidden, "reqToken", "token scope is limited to public activitypub")
|
ctx.Error(http.StatusForbidden, "reqToken", "token scope is limited to public activitypub")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,6 @@
|
|||||||
package repo
|
package repo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
stdCtx "context"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/big"
|
"math/big"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -40,86 +39,80 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// roleDescriptor returns the role descriptor for a comment in/with the given repo, poster and issue
|
// roleDescriptor returns the role descriptor for a comment in/with the given repo, poster and issue
|
||||||
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) {
|
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) {
|
||||||
roleDescriptor := issues_model.RoleDescriptor{}
|
|
||||||
|
|
||||||
if hasOriginalAuthor {
|
if hasOriginalAuthor {
|
||||||
return roleDescriptor, nil
|
// the poster is a migrated user, so no need to detect the role
|
||||||
|
return roleDesc, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var perm access_model.Permission
|
if poster.IsGhost() || !poster.IsIndividual() {
|
||||||
var err error
|
return roleDesc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
roleDesc.IsPoster = issue.IsPoster(poster.ID) // check whether the comment's poster is the issue's poster
|
||||||
|
|
||||||
|
// Guess the role of the poster in the repo by permission
|
||||||
|
perm, hasPermCache := permsCache[poster.ID]
|
||||||
|
if !hasPermCache {
|
||||||
|
perm, err = access_model.GetUserRepoPermission(ctx, repo, poster)
|
||||||
|
if err != nil {
|
||||||
|
return roleDesc, err
|
||||||
|
}
|
||||||
|
}
|
||||||
if permsCache != nil {
|
if permsCache != nil {
|
||||||
var ok bool
|
|
||||||
perm, ok = permsCache[poster.ID]
|
|
||||||
if !ok {
|
|
||||||
perm, err = access_model.GetUserRepoPermission(ctx, repo, poster)
|
|
||||||
if err != nil {
|
|
||||||
return roleDescriptor, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
permsCache[poster.ID] = perm
|
permsCache[poster.ID] = perm
|
||||||
} else {
|
|
||||||
perm, err = access_model.GetUserRepoPermission(ctx, repo, poster)
|
|
||||||
if err != nil {
|
|
||||||
return roleDescriptor, err
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// If the poster is the actual poster of the issue, enable Poster role.
|
|
||||||
roleDescriptor.IsPoster = issue.IsPoster(poster.ID)
|
|
||||||
|
|
||||||
// Check if the poster is owner of the repo.
|
// Check if the poster is owner of the repo.
|
||||||
if perm.IsOwner() {
|
if perm.IsOwner() {
|
||||||
// If the poster isn't an admin, enable the owner role.
|
// If the poster isn't a site admin, then is must be the repo's owner
|
||||||
if !poster.IsAdmin {
|
if !poster.IsAdmin {
|
||||||
roleDescriptor.RoleInRepo = issues_model.RoleRepoOwner
|
roleDesc.RoleInRepo = issues_model.RoleRepoOwner
|
||||||
return roleDescriptor, nil
|
return roleDesc, nil
|
||||||
}
|
}
|
||||||
|
// Otherwise (poster is site admin), check if poster is the real repo admin.
|
||||||
// Otherwise check if poster is the real repo admin.
|
isRealRepoAdmin, err := access_model.IsUserRealRepoAdmin(ctx, repo, poster)
|
||||||
ok, err := access_model.IsUserRealRepoAdmin(ctx, repo, poster)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return roleDescriptor, err
|
return roleDesc, err
|
||||||
}
|
}
|
||||||
if ok {
|
if isRealRepoAdmin {
|
||||||
roleDescriptor.RoleInRepo = issues_model.RoleRepoOwner
|
roleDesc.RoleInRepo = issues_model.RoleRepoOwner
|
||||||
return roleDescriptor, nil
|
return roleDesc, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If repo is organization, check Member role
|
// If repo is organization, check Member role
|
||||||
if err := repo.LoadOwner(ctx); err != nil {
|
if err = repo.LoadOwner(ctx); err != nil {
|
||||||
return roleDescriptor, err
|
return roleDesc, err
|
||||||
}
|
}
|
||||||
if repo.Owner.IsOrganization() {
|
if repo.Owner.IsOrganization() {
|
||||||
if isMember, err := organization.IsOrganizationMember(ctx, repo.Owner.ID, poster.ID); err != nil {
|
if isMember, err := organization.IsOrganizationMember(ctx, repo.Owner.ID, poster.ID); err != nil {
|
||||||
return roleDescriptor, err
|
return roleDesc, err
|
||||||
} else if isMember {
|
} else if isMember {
|
||||||
roleDescriptor.RoleInRepo = issues_model.RoleRepoMember
|
roleDesc.RoleInRepo = issues_model.RoleRepoMember
|
||||||
return roleDescriptor, nil
|
return roleDesc, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the poster is the collaborator of the repo
|
// If the poster is the collaborator of the repo
|
||||||
if isCollaborator, err := repo_model.IsCollaborator(ctx, repo.ID, poster.ID); err != nil {
|
if isCollaborator, err := repo_model.IsCollaborator(ctx, repo.ID, poster.ID); err != nil {
|
||||||
return roleDescriptor, err
|
return roleDesc, err
|
||||||
} else if isCollaborator {
|
} else if isCollaborator {
|
||||||
roleDescriptor.RoleInRepo = issues_model.RoleRepoCollaborator
|
roleDesc.RoleInRepo = issues_model.RoleRepoCollaborator
|
||||||
return roleDescriptor, nil
|
return roleDesc, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
hasMergedPR, err := issues_model.HasMergedPullRequestInRepo(ctx, repo.ID, poster.ID)
|
hasMergedPR, err := issues_model.HasMergedPullRequestInRepo(ctx, repo.ID, poster.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return roleDescriptor, err
|
return roleDesc, err
|
||||||
} else if hasMergedPR {
|
} else if hasMergedPR {
|
||||||
roleDescriptor.RoleInRepo = issues_model.RoleRepoContributor
|
roleDesc.RoleInRepo = issues_model.RoleRepoContributor
|
||||||
} else if issue.IsPull {
|
} else if issue.IsPull {
|
||||||
// only display first time contributor in the first opening pull request
|
// only display first time contributor in the first opening pull request
|
||||||
roleDescriptor.RoleInRepo = issues_model.RoleRepoFirstTimeContributor
|
roleDesc.RoleInRepo = issues_model.RoleRepoFirstTimeContributor
|
||||||
}
|
}
|
||||||
|
|
||||||
return roleDescriptor, nil
|
return roleDesc, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getBranchData(ctx *context.Context, issue *issues_model.Issue) {
|
func getBranchData(ctx *context.Context, issue *issues_model.Issue) {
|
||||||
|
@ -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}}
|
<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}}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user