mirror of
https://github.com/go-gitea/gitea.git
synced 2026-09-21 04:03:38 +09:00
feat: manage bot accounts from the admin UI, API and CLI (#38966)
Adds first-class bot accounts (`UserTypeBot`): local, password-less
users for automation that authenticate only with access tokens.
1. Admin UI: create bots, filter users by type, manage a bot's access
tokens, convert between user and bot
2. API: `POST /admin/users/{username}/convert-type`, and user objects
gain a GitHub-compatible `type` (`User`, `Organization`, `Bot`)
3. CLI: `gitea admin user change-type`, `--user-type` accepts `User` or
`Bot` case-insensitively
4. Converting keeps the password, 2FA, OAuth2 grants and access tokens,
and since sign-in rejects bots, converting back restores the account.
Only local, non-admin accounts can be converted, and conversions are
audited
5. Session, reverse proxy, SSPI, external source and password reset
sign-in reject non-individual users, so a bot never gets an interactive
session
6. Bots receive no notifications or emails
Co-authored-by: Nicolas <bircni@icloud.com>
Co-authored-by: joestump <joe@joestump.net>
Co-authored-by: Joe Stump <joe@stu.mp>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
This commit is contained in:
co-authored by
Nicolas
joestump
Joe Stump
silverwind
Lunny Xiao
parent
db7dbd5a6b
commit
3bec08f998
@@ -82,6 +82,9 @@ func (b *Basic) VerifyAuthToken(req *http.Request, w http.ResponseWriter, store
|
||||
log.Error("GetUserByID: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
if !u.IsIndividual() {
|
||||
return nil, nil //nolint:nilnil // the auth method is not applicable
|
||||
}
|
||||
|
||||
store.GetData()["LoginMethod"] = OAuth2TokenMethodName
|
||||
store.GetData()["ApiTokenScope"] = accessTokenScope
|
||||
|
||||
@@ -120,11 +120,13 @@ func (o *OAuth2) userFromToken(ctx context.Context, tokenSHA string, store DataS
|
||||
|
||||
// Otherwise, check if this is an OAuth access token
|
||||
accessTokenScope, uid, grantID := GetOAuthAccessTokenScopeAndUserID(ctx, tokenSHA)
|
||||
if uid != 0 {
|
||||
store.GetData()["ApiTokenScope"] = accessTokenScope
|
||||
setAuthCredential(store, credentialOAuth2Grant, grantID)
|
||||
user, err := user_model.GetUserByID(ctx, uid)
|
||||
if err != nil || !user.IsIndividual() {
|
||||
return nil, err
|
||||
}
|
||||
return user_model.GetUserByID(ctx, uid)
|
||||
store.GetData()["ApiTokenScope"] = accessTokenScope
|
||||
setAuthCredential(store, credentialOAuth2Grant, grantID)
|
||||
return user, nil
|
||||
}
|
||||
t, err := auth_model.GetAccessTokenBySHA(ctx, tokenSHA)
|
||||
if err != nil {
|
||||
|
||||
@@ -114,9 +114,9 @@ func (r *ReverseProxy) Verify(req *http.Request, w http.ResponseWriter, store Da
|
||||
}
|
||||
if user == nil {
|
||||
user = r.getUserFromAuthEmail(req)
|
||||
if user == nil {
|
||||
return nil, nil //nolint:nilnil // the auth method is not applicable
|
||||
}
|
||||
}
|
||||
if user == nil || !user.IsIndividual() {
|
||||
return nil, nil //nolint:nilnil // the auth method is not applicable
|
||||
}
|
||||
|
||||
if r.CreateSession && sess != nil {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"gitea.dev/models/unittest"
|
||||
@@ -17,6 +18,22 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestReverseProxyIgnoresBot(t *testing.T) {
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
defer test.MockVariableValue(&setting.ReverseProxyAuthUser, "X-WEBAUTH-USER")()
|
||||
defer test.MockVariableValue(&setting.ReverseProxyAuthEmail, "X-WEBAUTH-EMAIL")()
|
||||
defer test.MockVariableValue(&setting.Service.EnableReverseProxyEmail, true)()
|
||||
require.NoError(t, user_model.UpdateUserCols(t.Context(), &user_model.User{ID: 2, Type: user_model.UserTypeBot}, "type"))
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, "/", nil)
|
||||
require.NoError(t, err)
|
||||
req.Header.Set(setting.ReverseProxyAuthUser, "user2")
|
||||
req.Header.Set(setting.ReverseProxyAuthEmail, "user2@example.com")
|
||||
user, err := (&ReverseProxy{}).Verify(req, nil, nil, nil)
|
||||
require.NoError(t, err)
|
||||
assert.Nil(t, user)
|
||||
}
|
||||
|
||||
func TestReverseProxyLastLogin(t *testing.T) {
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
defer test.MockVariableValue(&setting.ReverseProxyAuthUser, "X-WEBAUTH-USER")()
|
||||
|
||||
@@ -50,6 +50,12 @@ func (s *Session) Verify(req *http.Request, w http.ResponseWriter, store DataSto
|
||||
return nil, nil //nolint:nilnil // the auth method is not applicable
|
||||
}
|
||||
|
||||
// sessions can't be enumerated per user, so one opened before a conversion to bot is rejected here
|
||||
if !user.IsIndividual() {
|
||||
log.Trace("Session Authorization: user %-v is not an individual, ignoring the session", user)
|
||||
return nil, nil //nolint:nilnil // the auth method is not applicable
|
||||
}
|
||||
|
||||
log.Trace("Session Authorization: Logged in user %-v", user)
|
||||
return user, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/session"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSessionVerify(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, "/", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
sess := session.NewMockMemStore("dummy-sid")
|
||||
method := &Session{}
|
||||
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
require.NoError(t, sess.Set(session.KeyUID, user.ID))
|
||||
u, err := method.Verify(req, nil, nil, sess)
|
||||
assert.NoError(t, err)
|
||||
require.NotNil(t, u)
|
||||
assert.Equal(t, user.ID, u.ID)
|
||||
|
||||
require.NoError(t, user_model.UpdateUserCols(t.Context(), &user_model.User{ID: user.ID, Type: user_model.UserTypeBot}, "type"))
|
||||
u, err = method.Verify(req, nil, nil, sess)
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, u)
|
||||
}
|
||||
@@ -122,10 +122,14 @@ func UserSignIn(ctx context.Context, username, password string) (*user_model.Use
|
||||
authUser, err := authenticator.Authenticate(ctx, nil, username, password)
|
||||
|
||||
if err == nil {
|
||||
if !authUser.ProhibitLogin {
|
||||
switch {
|
||||
case !authUser.IsIndividual():
|
||||
err = user_model.ErrUserNotExist{Name: username}
|
||||
case authUser.ProhibitLogin:
|
||||
err = user_model.ErrUserProhibitLogin{UID: authUser.ID, Name: authUser.Name}
|
||||
default:
|
||||
return authUser, source, nil
|
||||
}
|
||||
err = user_model.ErrUserProhibitLogin{UID: authUser.ID, Name: authUser.Name}
|
||||
}
|
||||
|
||||
if user_model.IsErrUserNotExist(err) {
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
auth_model "gitea.dev/models/auth"
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type mockBotSource struct {
|
||||
auth_model.ConfigBase
|
||||
}
|
||||
|
||||
func (s *mockBotSource) FromDB(bs []byte) error { return nil }
|
||||
func (s *mockBotSource) ToDB() ([]byte, error) { return []byte("{}"), nil }
|
||||
|
||||
func (s *mockBotSource) Authenticate(ctx context.Context, _ *user_model.User, login, _ string) (*user_model.User, error) {
|
||||
return user_model.GetUserByName(ctx, login)
|
||||
}
|
||||
|
||||
const mockBotSourceType auth_model.Type = 100
|
||||
|
||||
func TestUserSignIn_BotCannotSignIn(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
auth_model.RegisterTypeConfig(mockBotSourceType, &mockBotSource{})
|
||||
|
||||
bot := &user_model.User{Name: "test-bot", Email: "test-bot@example.com", Type: user_model.UserTypeBot, IsActive: true}
|
||||
require.NoError(t, user_model.AdminCreateUser(t.Context(), bot, &user_model.Meta{}))
|
||||
require.NoError(t, db.Insert(t.Context(), &auth_model.Source{
|
||||
Type: mockBotSourceType,
|
||||
Name: "mock-bot-source",
|
||||
IsActive: true,
|
||||
Cfg: &mockBotSource{},
|
||||
}))
|
||||
|
||||
_, _, err := UserSignIn(t.Context(), "test-bot", "")
|
||||
assert.ErrorAs(t, err, &user_model.ErrUserNotExist{})
|
||||
}
|
||||
@@ -119,6 +119,9 @@ func (s *SSPI) Verify(req *http.Request, w http.ResponseWriter, store DataStore,
|
||||
log.Error("CreateUser: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
} else if !user.IsIndividual() {
|
||||
log.Trace("SSPI Authorization: user %q is not an individual, ignoring", username)
|
||||
return nil, nil //nolint:nilnil // the auth method is not applicable
|
||||
}
|
||||
|
||||
if s.CreateSession {
|
||||
|
||||
@@ -9,8 +9,32 @@ import (
|
||||
"gitea.dev/models/perm"
|
||||
user_model "gitea.dev/models/user"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/util"
|
||||
)
|
||||
|
||||
func userTypeToString(t user_model.UserType) api.UserTypeString {
|
||||
switch t {
|
||||
case user_model.UserTypeOrganization, user_model.UserTypeOrganizationReserved:
|
||||
return api.UserTypeStringOrganization
|
||||
case user_model.UserTypeBot:
|
||||
return api.UserTypeStringBot
|
||||
default:
|
||||
return api.UserTypeStringUser
|
||||
}
|
||||
}
|
||||
|
||||
// UserTypeFromString parses a user type an admin may create or convert to
|
||||
func UserTypeFromString(s api.UserTypeString) (user_model.UserType, error) {
|
||||
switch s {
|
||||
case api.UserTypeStringUser:
|
||||
return user_model.UserTypeIndividual, nil
|
||||
case api.UserTypeStringBot:
|
||||
return user_model.UserTypeBot, nil
|
||||
default:
|
||||
return 0, util.NewInvalidArgumentErrorf("invalid user type %q (expected %s, %s)", s, api.UserTypeStringUser, api.UserTypeStringBot)
|
||||
}
|
||||
}
|
||||
|
||||
// ToUser convert user_model.User to api.User
|
||||
// if doer is set, private information is added if the doer has the permission to see it
|
||||
func ToUser(ctx context.Context, user, doer *user_model.User) *api.User {
|
||||
@@ -50,6 +74,7 @@ func toUser(ctx context.Context, user *user_model.User, signed, authed bool) *ap
|
||||
result := &api.User{
|
||||
ID: user.ID,
|
||||
UserName: user.Name,
|
||||
Type: userTypeToString(user.Type),
|
||||
FullName: user.FullName,
|
||||
Email: user.GetPlaceholderEmail(),
|
||||
AvatarURL: user.AvatarLink(ctx),
|
||||
|
||||
@@ -13,9 +13,10 @@ type AdminCreateUserForm struct {
|
||||
middleware.FormDefaultValidator
|
||||
LoginType string `binding:"Required"`
|
||||
LoginName string
|
||||
UserName string `binding:"Required;Username;MaxSize(40)"`
|
||||
Email string `binding:"Required;Email;MaxSize(254)"`
|
||||
Password string `binding:"MaxSize(255)"`
|
||||
UserName string `binding:"Required;Username;MaxSize(40)"`
|
||||
UserType structs.UserTypeString `binding:"In(,User,Bot)"`
|
||||
Email string `binding:"Required;Email;MaxSize(254)"`
|
||||
Password string `binding:"MaxSize(255)"`
|
||||
SendNotify bool
|
||||
MustChangePassword bool
|
||||
Visibility structs.VisibleType
|
||||
@@ -39,8 +40,9 @@ type AdminEditBadgeForm struct {
|
||||
// AdminEditUserForm form for admin to create user
|
||||
type AdminEditUserForm struct {
|
||||
middleware.FormDefaultValidator
|
||||
LoginType string `binding:"Required"`
|
||||
UserName string `binding:"Username;MaxSize(40)"`
|
||||
LoginType string `binding:"Required"`
|
||||
UserType structs.UserTypeString `binding:"In(,User,Bot)"`
|
||||
UserName string `binding:"Username;MaxSize(40)"`
|
||||
LoginName string
|
||||
FullName string `binding:"MaxSize(100)"`
|
||||
Email string `binding:"Required;Email;MaxSize(254)"`
|
||||
|
||||
@@ -120,8 +120,7 @@ func mailIssueCommentBatch(ctx context.Context, comment *mailComment, users []*u
|
||||
|
||||
langMap := make(map[string][]*user_model.User)
|
||||
for _, user := range users {
|
||||
if !user.IsActive {
|
||||
// Exclude deactivated users
|
||||
if !user.IsMailable() {
|
||||
continue
|
||||
}
|
||||
// At this point we exclude:
|
||||
@@ -205,8 +204,7 @@ func SendIssueAssignedMail(ctx context.Context, issue *issues_model.Issue, doer
|
||||
|
||||
langMap := make(map[string][]*user_model.User)
|
||||
for _, user := range recipients {
|
||||
if !user.IsActive {
|
||||
// don't send emails to inactive users
|
||||
if !user.IsMailable() {
|
||||
continue
|
||||
}
|
||||
langMap[user.Language] = append(langMap[user.Language], user)
|
||||
|
||||
@@ -38,8 +38,7 @@ func SendRepoTransferNotifyMail(ctx context.Context, doer, newOwner *user_model.
|
||||
|
||||
langMap := make(map[string][]*user_model.User)
|
||||
for _, user := range users {
|
||||
if !user.IsActive {
|
||||
// don't send emails to inactive users
|
||||
if !user.IsMailable() {
|
||||
continue
|
||||
}
|
||||
langMap[user.Language] = append(langMap[user.Language], user)
|
||||
@@ -54,6 +53,9 @@ func SendRepoTransferNotifyMail(ctx context.Context, doer, newOwner *user_model.
|
||||
return nil
|
||||
}
|
||||
|
||||
if newOwner.IsTypeBot() {
|
||||
return nil
|
||||
}
|
||||
return sendRepoTransferNotifyMailPerLang(newOwner.Language, newOwner, doer, []*user_model.User{newOwner}, repo)
|
||||
}
|
||||
|
||||
@@ -98,7 +100,7 @@ func sendRepoTransferNotifyMailPerLang(lang string, newOwner, doer *user_model.U
|
||||
|
||||
// SendCollaboratorMail sends mail notification to new collaborator.
|
||||
func SendCollaboratorMail(u, doer *user_model.User, repo *repo_model.Repository) {
|
||||
if setting.MailService == nil || !u.IsActive {
|
||||
if setting.MailService == nil || !u.IsMailable() {
|
||||
return
|
||||
}
|
||||
locale := translation.NewLocale(u.Language)
|
||||
|
||||
@@ -168,6 +168,28 @@ func TestMailMentionsComment(t *testing.T) {
|
||||
assert.Equal(t, 3, mails)
|
||||
}
|
||||
|
||||
func TestMailsSkipBots(t *testing.T) {
|
||||
doer, repo, issue, comment := prepareMailerTest(t)
|
||||
comment.Poster = doer
|
||||
var recipients []string
|
||||
defer test.MockVariableValue(&SendAsync, func(msgs ...*sender_service.Message) {
|
||||
for _, msg := range msgs {
|
||||
recipients = append(recipients, msg.To)
|
||||
}
|
||||
})()
|
||||
|
||||
require.NoError(t, user_model.UpdateUserCols(t.Context(), &user_model.User{ID: 5, Type: user_model.UserTypeBot}, "type"))
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
|
||||
bot := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
|
||||
require.NoError(t, SendIssueAssignedMail(t.Context(), issue, doer, "", comment, []*user_model.User{user, bot}))
|
||||
require.NoError(t, MailParticipantsComment(t.Context(), comment, activities_model.ActionCommentIssue, issue, []*user_model.User{bot}))
|
||||
require.NoError(t, SendRepoTransferNotifyMail(t.Context(), doer, bot, repo))
|
||||
SendCollaboratorMail(bot, doer, repo)
|
||||
SendRegisterNotifyMail(bot)
|
||||
assert.Contains(t, recipients, user.Email)
|
||||
assert.NotContains(t, strings.Join(recipients, " "), bot.Email)
|
||||
}
|
||||
|
||||
func TestComposeIssueMessage(t *testing.T) {
|
||||
doer, _, issue, _ := prepareMailerTest(t)
|
||||
|
||||
|
||||
@@ -101,8 +101,7 @@ func SendActivateEmailMail(u *user_model.User, email string) {
|
||||
|
||||
// SendRegisterNotifyMail triggers a notify e-mail by admin created a account.
|
||||
func SendRegisterNotifyMail(u *user_model.User) {
|
||||
if setting.MailService == nil || !u.IsActive {
|
||||
// No mail service configured OR user is inactive
|
||||
if setting.MailService == nil || !u.IsMailable() {
|
||||
return
|
||||
}
|
||||
locale := translation.NewLocale(u.Language)
|
||||
|
||||
+29
-1
@@ -14,6 +14,7 @@ import (
|
||||
"gitea.dev/modules/optional"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/services/audit"
|
||||
)
|
||||
|
||||
@@ -55,6 +56,7 @@ type UpdateOptions struct {
|
||||
AllowCreateOrganization optional.Option[bool]
|
||||
IsActive optional.Option[bool]
|
||||
IsAdmin optional.Option[UpdateOptionField[bool]]
|
||||
UserType optional.Option[user_model.UserType]
|
||||
EmailNotificationsPreference optional.Option[string]
|
||||
SetLastLogin bool
|
||||
RepoAdminChangeTeamAccess optional.Option[bool]
|
||||
@@ -63,7 +65,7 @@ type UpdateOptions struct {
|
||||
func UpdateUser(ctx context.Context, u *user_model.User, opts *UpdateOptions) error {
|
||||
cols := make([]string, 0, 20)
|
||||
|
||||
oldIsActive, oldIsRestricted, oldIsAdmin, oldVisibility := u.IsActive, u.IsRestricted, u.IsAdmin, u.Visibility
|
||||
oldIsActive, oldIsRestricted, oldIsAdmin, oldVisibility, oldType := u.IsActive, u.IsRestricted, u.IsAdmin, u.Visibility, u.Type
|
||||
|
||||
if opts.KeepEmailPrivate.Has() {
|
||||
u.KeepEmailPrivate = opts.KeepEmailPrivate.Value()
|
||||
@@ -175,6 +177,14 @@ func UpdateUser(ctx context.Context, u *user_model.User, opts *UpdateOptions) er
|
||||
cols = append(cols, "repo_admin_change_team_access")
|
||||
}
|
||||
|
||||
if opts.UserType.Has() && opts.UserType.Value() != u.Type {
|
||||
if err := CheckConvertUserType(u); err != nil {
|
||||
return err
|
||||
}
|
||||
u.Type = opts.UserType.Value()
|
||||
cols = append(cols, "type")
|
||||
}
|
||||
|
||||
if opts.EmailNotificationsPreference.Has() {
|
||||
u.EmailNotificationsPreference = opts.EmailNotificationsPreference.Value()
|
||||
|
||||
@@ -203,6 +213,9 @@ func UpdateUser(ctx context.Context, u *user_model.User, opts *UpdateOptions) er
|
||||
if u.Visibility != oldVisibility {
|
||||
audit.Record(ctx, audit_model.UserVisibility, u, "old_visibility", oldVisibility.String(), "new_visibility", u.Visibility.String())
|
||||
}
|
||||
if u.Type != oldType {
|
||||
audit.Record(ctx, audit_model.UserType, u, "user_type", u.Type.DisplayName())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -216,6 +229,9 @@ type UpdateAuthOptions struct {
|
||||
}
|
||||
|
||||
func UpdateAuth(ctx context.Context, u *user_model.User, opts *UpdateAuthOptions) error {
|
||||
if u.IsTypeBot() && (opts.Password.Has() || opts.LoginSource.Value() != 0 || opts.LoginName.Value() != "") {
|
||||
return util.NewInvalidArgumentErrorf("a bot account cannot have a password or authentication source")
|
||||
}
|
||||
loginSourceChanged := false
|
||||
authSourceName := ""
|
||||
if opts.LoginSource.Has() {
|
||||
@@ -279,3 +295,15 @@ func UpdateAuth(ctx context.Context, u *user_model.User, opts *UpdateAuthOptions
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func CheckConvertUserType(u *user_model.User) error {
|
||||
switch {
|
||||
case u.IsAdmin:
|
||||
return user_model.ErrBotCanNotBeAdmin
|
||||
case !u.IsIndividual() && !u.IsTypeBot():
|
||||
return user_model.ErrUserTypeCanNotConvert
|
||||
case !u.IsLocal():
|
||||
return user_model.ErrBotMustBeLocal
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ package user
|
||||
import (
|
||||
"testing"
|
||||
|
||||
audit_model "gitea.dev/models/audit"
|
||||
auth_model "gitea.dev/models/auth"
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
password_module "gitea.dev/modules/auth/password"
|
||||
@@ -13,6 +15,7 @@ import (
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/test"
|
||||
"gitea.dev/modules/util"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@@ -153,3 +156,40 @@ func TestUpdateUserVisibility(t *testing.T) {
|
||||
Visibility: optional.Some(structs.VisibleTypePublic),
|
||||
}))
|
||||
}
|
||||
|
||||
func TestConvertUserType(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
before := *user
|
||||
tokensBefore := unittest.GetCount(t, &auth_model.AccessToken{UID: user.ID})
|
||||
assert.NotEmpty(t, before.Passwd)
|
||||
assert.Positive(t, tokensBefore)
|
||||
|
||||
defer test.MockVariableValue(&setting.Audit.RecordOutput, setting.AuditRecordOutputDatabase)()
|
||||
assert.NoError(t, UpdateUser(t.Context(), user, &UpdateOptions{UserType: optional.Some(user_model.UserTypeBot)}))
|
||||
assert.True(t, user.IsTypeBot())
|
||||
unittest.AssertExistsAndLoadBean(t, &audit_model.Event{Action: audit_model.UserType, ScopeType: audit_model.ScopeUser, ScopeID: user.ID})
|
||||
|
||||
user = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
before.Type, before.UpdatedUnix = user_model.UserTypeBot, user.UpdatedUnix
|
||||
assert.Equal(t, before, *user)
|
||||
assert.Equal(t, tokensBefore, unittest.GetCount(t, &auth_model.AccessToken{UID: user.ID}))
|
||||
|
||||
assert.ErrorIs(t, UpdateAuth(t.Context(), user, &UpdateAuthOptions{Password: optional.Some("%$DRZUVB576tfzgu")}), util.ErrInvalidArgument)
|
||||
assert.ErrorIs(t, UpdateAuth(t.Context(), user, &UpdateAuthOptions{LoginSource: optional.Some(int64(1))}), util.ErrInvalidArgument)
|
||||
assert.ErrorIs(t, UpdateAuth(t.Context(), user, &UpdateAuthOptions{LoginName: optional.Some("cn=bot")}), util.ErrInvalidArgument)
|
||||
assert.ErrorIs(t, UpdateUser(t.Context(), user, &UpdateOptions{IsAdmin: UpdateOptionFieldFromValue(true)}), user_model.ErrBotCanNotBeAdmin)
|
||||
assert.False(t, unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).IsAdmin)
|
||||
|
||||
user = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
assert.NoError(t, UpdateUser(t.Context(), user, &UpdateOptions{UserType: optional.Some(user_model.UserTypeIndividual)}))
|
||||
assert.True(t, unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).IsIndividual())
|
||||
|
||||
toBot := &UpdateOptions{UserType: optional.Some(user_model.UserTypeBot)}
|
||||
assert.ErrorIs(t, UpdateUser(t.Context(), unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}), toBot), user_model.ErrUserTypeCanNotConvert)
|
||||
assert.ErrorIs(t, UpdateUser(t.Context(), unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}), toBot), user_model.ErrBotCanNotBeAdmin)
|
||||
assert.True(t, unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}).IsIndividual())
|
||||
assert.NoError(t, user_model.UpdateUserCols(t.Context(), &user_model.User{ID: 4, LoginType: auth_model.LDAP}, "login_type"))
|
||||
assert.ErrorIs(t, UpdateUser(t.Context(), unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}), toBot), user_model.ErrBotMustBeLocal)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user