fix(api): allow bots with pending password changes (#39551)

Allow bot accounts to use the API when a legacy password-change flag is
set, since bots cannot complete the interactive password-change flow.
Preserve password-change enforcement for human accounts and restrictions
for inactive or prohibited accounts.

Fixes: https://github.com/go-gitea/gitea/issues/39542
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
This commit is contained in:
authored and GitHub committed 2026-10-03 17:22:07 +00:00
1 parent 516a4883fa
commit cca466caae
11 files changed
+129 -100

No files matched your search

+15
View File
@@ -6,6 +6,8 @@ package common
import (
user_model "gitea.dev/models/user"
"gitea.dev/modules/log"
"gitea.dev/modules/session"
"gitea.dev/modules/setting"
"gitea.dev/modules/web/middleware"
auth_service "gitea.dev/services/auth"
"gitea.dev/services/context"
@@ -56,3 +58,16 @@ type VerifyOptions struct {
AdminRequired bool
DisableCrossOriginProtection bool
}
func CheckSignedInUser(doer *user_model.User, sess session.Store) (ret struct {
NeedActivateAccount bool
LoginIsProhibited bool
NeedChangePassword bool
},
) {
ret.NeedActivateAccount = !doer.IsActive && setting.Service.RegisterEmailConfirm
ret.LoginIsProhibited = !doer.IsActive || doer.ProhibitLogin
isImpersonated := sess != nil && context.IsDoerSessionImpersonated(sess)
ret.NeedChangePassword = doer.MustChangePassword && !isImpersonated && !doer.IsTypeBot()
return ret
}
+53
View File
@@ -0,0 +1,53 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package common
import (
"testing"
user_model "gitea.dev/models/user"
"gitea.dev/modules/session"
"gitea.dev/modules/setting"
"gitea.dev/modules/test"
"github.com/stretchr/testify/assert"
)
func TestCheckSignedInUser(t *testing.T) {
defer test.MockVariableValue(&setting.Service.RegisterEmailConfirm)()
sessNormal := session.NewMockMemStore("session-a")
sessImpersonated := session.NewMockMemStore("session-b")
_ = sessImpersonated.Set(session.KeyImpersonatorData, "any-value")
setting.Service.RegisterEmailConfirm = false
ret := CheckSignedInUser(&user_model.User{IsActive: false}, nil)
assert.False(t, ret.NeedActivateAccount)
assert.True(t, ret.LoginIsProhibited)
setting.Service.RegisterEmailConfirm = true
ret = CheckSignedInUser(&user_model.User{IsActive: false}, nil)
assert.True(t, ret.NeedActivateAccount)
assert.True(t, ret.LoginIsProhibited)
ret = CheckSignedInUser(&user_model.User{IsActive: true}, nil)
assert.False(t, ret.NeedActivateAccount)
assert.False(t, ret.LoginIsProhibited)
assert.False(t, ret.NeedChangePassword)
ret = CheckSignedInUser(&user_model.User{IsActive: true, ProhibitLogin: true}, nil)
assert.False(t, ret.NeedActivateAccount)
assert.True(t, ret.LoginIsProhibited)
ret = CheckSignedInUser(&user_model.User{MustChangePassword: true}, nil)
assert.True(t, ret.NeedChangePassword)
ret = CheckSignedInUser(&user_model.User{MustChangePassword: true}, sessNormal)
assert.True(t, ret.NeedChangePassword)
ret = CheckSignedInUser(&user_model.User{MustChangePassword: true, Type: user_model.UserTypeBot}, sessNormal)
assert.False(t, ret.NeedChangePassword)
ret = CheckSignedInUser(&user_model.User{MustChangePassword: true}, sessImpersonated)
assert.False(t, ret.NeedChangePassword)
}