feat: vendor gitea 1.16.2

This commit is contained in:
2026-06-02 08:36:01 +00:00
parent 247cd60f69
commit 54798b4615
2145 changed files with 162965 additions and 126447 deletions
@@ -8,39 +8,16 @@ import (
"errors"
"fmt"
"net/http"
"regexp"
"strings"
"sync"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/auth/webauthn"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/session"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web/middleware"
gitea_context "code.gitea.io/gitea/services/context"
user_service "code.gitea.io/gitea/services/user"
)
type globalVarsStruct struct {
gitRawOrAttachPathRe *regexp.Regexp
lfsPathRe *regexp.Regexp
archivePathRe *regexp.Regexp
feedPathRe *regexp.Regexp
feedRefPathRe *regexp.Regexp
}
var globalVars = sync.OnceValue(func() *globalVarsStruct {
return &globalVarsStruct{
gitRawOrAttachPathRe: regexp.MustCompile(`^/[-.\w]+/[-.\w]+/(?:(?:git-(?:(?:upload)|(?:receive))-pack$)|(?:info/refs$)|(?:HEAD$)|(?:objects/)|(?:raw/)|(?:releases/download/)|(?:attachments/))`),
lfsPathRe: regexp.MustCompile(`^/[-.\w]+/[-.\w]+/info/lfs/`),
archivePathRe: regexp.MustCompile(`^/[-.\w]+/[-.\w]+/archive/`),
feedPathRe: regexp.MustCompile(`^/[-.\w]+(/[-.\w]+)?\.(rss|atom)$`), // "/owner.rss" or "/owner/repo.atom"
feedRefPathRe: regexp.MustCompile(`^/[-.\w]+/[-.\w]+/(rss|atom)/`), // "/owner/repo/rss/branch/..."
}
})
type ErrUserAuthMessage string
func (e ErrUserAuthMessage) Error() string {
@@ -61,66 +38,6 @@ func Init() {
webauthn.Init()
}
type authPathDetector struct {
req *http.Request
vars *globalVarsStruct
}
func newAuthPathDetector(req *http.Request) *authPathDetector {
return &authPathDetector{req: req, vars: globalVars()}
}
// isAPIPath returns true if the specified URL is an API path
func (a *authPathDetector) isAPIPath() bool {
return strings.HasPrefix(a.req.URL.Path, "/api/")
}
// isAttachmentDownload check if request is a file download (GET) with URL to an attachment
func (a *authPathDetector) isAttachmentDownload() bool {
return strings.HasPrefix(a.req.URL.Path, "/attachments/") && a.req.Method == http.MethodGet
}
func (a *authPathDetector) isFeedRequest(req *http.Request) bool {
if !setting.Other.EnableFeed {
return false
}
if req.Method != http.MethodGet {
return false
}
return a.vars.feedPathRe.MatchString(req.URL.Path) || a.vars.feedRefPathRe.MatchString(req.URL.Path)
}
// isContainerPath checks if the request targets the container endpoint
func (a *authPathDetector) isContainerPath() bool {
return strings.HasPrefix(a.req.URL.Path, "/v2/")
}
func (a *authPathDetector) isGitRawOrAttachPath() bool {
return a.vars.gitRawOrAttachPathRe.MatchString(a.req.URL.Path)
}
func (a *authPathDetector) isGitRawOrAttachOrLFSPath() bool {
if a.isGitRawOrAttachPath() {
return true
}
if setting.LFS.StartServer {
return a.vars.lfsPathRe.MatchString(a.req.URL.Path)
}
return false
}
func (a *authPathDetector) isArchivePath() bool {
return a.vars.archivePathRe.MatchString(a.req.URL.Path)
}
func (a *authPathDetector) isAuthenticatedTokenRequest() bool {
switch a.req.URL.Path {
case "/login/oauth/userinfo", "/login/oauth/introspect":
return true
}
return false
}
// handleSignIn clears existing session variables and stores new ones for the specified user object
func handleSignIn(resp http.ResponseWriter, req *http.Request, sess SessionStore, user *user_model.User) {
// We need to regenerate the session...
@@ -162,9 +79,4 @@ func handleSignIn(resp http.ResponseWriter, req *http.Request, sess SessionStore
}
middleware.SetLocaleCookie(resp, user.Language, 0)
// force to generate a new CSRF token
if ctx := gitea_context.GetWebContext(req.Context()); ctx != nil {
ctx.Csrf.PrepareForSessionUser(ctx)
}
}
@@ -1,155 +0,0 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package auth
import (
"net/http"
"testing"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/test"
"github.com/stretchr/testify/assert"
)
func Test_isGitRawOrLFSPath(t *testing.T) {
tests := []struct {
path string
want bool
}{
{
"/owner/repo/git-upload-pack",
true,
},
{
"/owner/repo/git-receive-pack",
true,
},
{
"/owner/repo/info/refs",
true,
},
{
"/owner/repo/HEAD",
true,
},
{
"/owner/repo/objects/info/alternates",
true,
},
{
"/owner/repo/objects/info/http-alternates",
true,
},
{
"/owner/repo/objects/info/packs",
true,
},
{
"/owner/repo/objects/info/blahahsdhsdkla",
true,
},
{
"/owner/repo/objects/01/23456789abcdef0123456789abcdef01234567",
true,
},
{
"/owner/repo/objects/pack/pack-123456789012345678921234567893124567894.pack",
true,
},
{
"/owner/repo/objects/pack/pack-0123456789abcdef0123456789abcdef0123456.idx",
true,
},
{
"/owner/repo/raw/branch/foo/fanaso",
true,
},
{
"/owner/repo/stars",
false,
},
{
"/notowner",
false,
},
{
"/owner/repo",
false,
},
{
"/owner/repo/commit/123456789012345678921234567893124567894",
false,
},
{
"/owner/repo/releases/download/tag/repo.tar.gz",
true,
},
{
"/owner/repo/attachments/6d92a9ee-5d8b-4993-97c9-6181bdaa8955",
true,
},
}
defer test.MockVariableValue(&setting.LFS.StartServer)()
for _, tt := range tests {
t.Run(tt.path, func(t *testing.T) {
req, _ := http.NewRequest(http.MethodPost, "http://localhost"+tt.path, nil)
setting.LFS.StartServer = false
assert.Equal(t, tt.want, newAuthPathDetector(req).isGitRawOrAttachOrLFSPath())
setting.LFS.StartServer = true
assert.Equal(t, tt.want, newAuthPathDetector(req).isGitRawOrAttachOrLFSPath())
})
}
lfsTests := []string{
"/owner/repo/info/lfs/",
"/owner/repo/info/lfs/objects/batch",
"/owner/repo/info/lfs/objects/oid/filename",
"/owner/repo/info/lfs/objects/oid",
"/owner/repo/info/lfs/objects",
"/owner/repo/info/lfs/verify",
"/owner/repo/info/lfs/locks",
"/owner/repo/info/lfs/locks/verify",
"/owner/repo/info/lfs/locks/123/unlock",
}
for _, tt := range lfsTests {
t.Run(tt, func(t *testing.T) {
req, _ := http.NewRequest(http.MethodPost, tt, nil)
setting.LFS.StartServer = false
got := newAuthPathDetector(req).isGitRawOrAttachOrLFSPath()
assert.Equalf(t, setting.LFS.StartServer, got, "isGitOrLFSPath(%q) = %v, want %v, %v", tt, got, setting.LFS.StartServer, globalVars().gitRawOrAttachPathRe.MatchString(tt))
setting.LFS.StartServer = true
got = newAuthPathDetector(req).isGitRawOrAttachOrLFSPath()
assert.Equalf(t, setting.LFS.StartServer, got, "isGitOrLFSPath(%q) = %v, want %v", tt, got, setting.LFS.StartServer)
})
}
}
func Test_isFeedRequest(t *testing.T) {
tests := []struct {
want bool
path string
}{
{true, "/user.rss"},
{true, "/user/repo.atom"},
{false, "/user/repo"},
{false, "/use/repo/file.rss"},
{true, "/org/repo/rss/branch/xxx"},
{true, "/org/repo/atom/tag/xxx"},
{false, "/org/repo/branch/main/rss/any"},
{false, "/org/atom/any"},
}
for _, tt := range tests {
t.Run(tt.path, func(t *testing.T) {
req, _ := http.NewRequest(http.MethodGet, "http://localhost"+tt.path, nil)
assert.Equal(t, tt.want, newAuthPathDetector(req).isFeedRequest(req))
})
}
}
@@ -32,7 +32,7 @@ var (
func CheckAuthToken(ctx context.Context, value string) (*auth_model.AuthToken, error) {
if len(value) == 0 {
return nil, nil
return nil, nil //nolint:nilnil // the auth method is not applicable
}
parts := strings.SplitN(value, ":", 2)
+29 -21
View File
@@ -40,25 +40,14 @@ func (b *Basic) Name() string {
return BasicMethodName
}
// Verify extracts and validates Basic data (username and password/token) from the
// "Authorization" header of the request and returns the corresponding user object for that
// name/token on successful validation.
// Returns nil if header is empty or validation fails.
func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) (*user_model.User, error) {
// Basic authentication should only fire on API, Feed, Download, Archives or on Git or LFSPaths
// Not all feed (rss/atom) clients feature the ability to add cookies or headers, so we need to allow basic auth for feeds
detector := newAuthPathDetector(req)
if !detector.isAPIPath() && !detector.isFeedRequest(req) && !detector.isContainerPath() && !detector.isAttachmentDownload() && !detector.isArchivePath() && !detector.isGitRawOrAttachOrLFSPath() {
return nil, nil
}
func (b *Basic) parseAuthBasic(req *http.Request) (ret struct{ authToken, uname, passwd string }) {
authHeader := req.Header.Get("Authorization")
if authHeader == "" {
return nil, nil
return ret
}
parsed, ok := httpauth.ParseAuthorizationHeader(authHeader)
if !ok || parsed.BasicAuth == nil {
return nil, nil
return ret
}
uname, passwd := parsed.BasicAuth.Username, parsed.BasicAuth.Password
@@ -73,9 +62,14 @@ func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore
} else {
log.Trace("Basic Authorization: Attempting login with username as token")
}
ret.authToken, ret.uname, ret.passwd = authToken, uname, passwd
return ret
}
// get oauth2 token's user's ID
_, uid := GetOAuthAccessTokenScopeAndUserID(req.Context(), authToken)
// VerifyAuthToken only the access token provided as parameter, used by other auth methods that want to reuse access token verification logic
func (b *Basic) VerifyAuthToken(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore, authToken string) (*user_model.User, error) {
// get oauth2 token's user's ID and access scope
accessTokenScope, uid := GetOAuthAccessTokenScopeAndUserID(req.Context(), authToken)
if uid != 0 {
log.Trace("Basic Authorization: Valid OAuthAccessToken for user[%d]", uid)
@@ -87,6 +81,7 @@ func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore
store.GetData()["LoginMethod"] = OAuth2TokenMethodName
store.GetData()["IsApiToken"] = true
store.GetData()["ApiTokenScope"] = accessTokenScope
return u, nil
}
@@ -117,16 +112,29 @@ func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore
task, err := actions_model.GetRunningTaskByToken(req.Context(), authToken)
if err == nil && task != nil {
log.Trace("Basic Authorization: Valid AccessToken for task[%d]", task.ID)
store.GetData()["LoginMethod"] = ActionTokenMethodName
store.GetData()["IsActionsToken"] = true
store.GetData()["ActionsTaskID"] = task.ID
return user_model.NewActionsUserWithTaskID(task.ID), nil
}
return nil, nil //nolint:nilnil // the auth method is not applicable
}
return user_model.NewActionsUser(), nil
// Verify extracts and validates Basic data (username and password/token) from the
// "Authorization" header of the request and returns the corresponding user object for that
// name/token on successful validation.
// Returns nil if header is empty or validation fails.
func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) (*user_model.User, error) {
parseBasicRet := b.parseAuthBasic(req)
authToken, uname, passwd := parseBasicRet.authToken, parseBasicRet.uname, parseBasicRet.passwd
if authToken == "" && uname == "" {
return nil, nil //nolint:nilnil // the auth method is not applicable
}
u, err := b.VerifyAuthToken(req, w, store, sess, authToken)
if u != nil || err != nil {
return u, err
}
if !setting.Service.EnableBasicAuth {
return nil, nil
return nil, nil //nolint:nilnil // the auth method is not applicable
}
log.Trace("Basic Authorization: Attempting SignIn for %s", uname)
@@ -42,7 +42,7 @@ func (h *HTTPSign) Name() string {
func (h *HTTPSign) Verify(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) (*user_model.User, error) {
sigHead := req.Header.Get("Signature")
if len(sigHead) == 0 {
return nil, nil
return nil, nil //nolint:nilnil // the auth method is not applicable
}
var (
@@ -53,14 +53,14 @@ func (h *HTTPSign) Verify(req *http.Request, w http.ResponseWriter, store DataSt
if len(req.Header.Get("X-Ssh-Certificate")) != 0 {
// Handle Signature signed by SSH certificates
if len(setting.SSH.TrustedUserCAKeys) == 0 {
return nil, nil
return nil, nil //nolint:nilnil // the auth method is not applicable
}
publicKey, err = VerifyCert(req)
if err != nil {
log.Debug("VerifyCert on request from %s: failed: %v", req.RemoteAddr, err)
log.Warn("Failed authentication attempt from %s", req.RemoteAddr)
return nil, nil
return nil, nil //nolint:nilnil // the auth method is not applicable
}
} else {
// Handle Signature signed by Public Key
@@ -68,7 +68,7 @@ func (h *HTTPSign) Verify(req *http.Request, w http.ResponseWriter, store DataSt
if err != nil {
log.Debug("VerifyPubKey on request from %s: failed: %v", req.RemoteAddr, err)
log.Warn("Failed authentication attempt from %s", req.RemoteAddr)
return nil, nil
return nil, nil //nolint:nilnil // the auth method is not applicable
}
}
+18 -46
View File
@@ -6,6 +6,7 @@ package auth
import (
"context"
"errors"
"net/http"
"strings"
"time"
@@ -17,14 +18,12 @@ import (
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/actions"
"code.gitea.io/gitea/services/oauth2_provider"
)
// Ensure the struct implements the interface.
var (
_ Method = &OAuth2{}
)
var _ Method = &OAuth2{}
// GetOAuthAccessTokenScopeAndUserID returns access token scope and user id
func GetOAuthAccessTokenScopeAndUserID(ctx context.Context, accessToken string) (auth_model.AccessTokenScope, int64) {
@@ -106,18 +105,16 @@ func parseToken(req *http.Request) (string, bool) {
return "", false
}
// userIDFromToken returns the user id corresponding to the OAuth token.
// userFromToken returns the user corresponding to the OAuth token.
// It will set 'IsApiToken' to true if the token is an API token and
// set 'ApiTokenScope' to the scope of the access token
func (o *OAuth2) userIDFromToken(ctx context.Context, tokenSHA string, store DataStore) int64 {
// set 'ApiTokenScope' to the scope of the access token (TODO: this behavior should be fixed, don't set ctx.Data)
func (o *OAuth2) userFromToken(ctx context.Context, tokenSHA string, store DataStore) (*user_model.User, error) {
// Let's see if token is valid.
if strings.Contains(tokenSHA, ".") {
// First attempt to decode an actions JWT, returning the actions user
if taskID, err := actions.TokenToTaskID(tokenSHA); err == nil {
if CheckTaskIsRunning(ctx, taskID) {
store.GetData()["IsActionsToken"] = true
store.GetData()["ActionsTaskID"] = taskID
return user_model.ActionsUserID
return user_model.NewActionsUserWithTaskID(taskID), nil
}
}
@@ -127,33 +124,27 @@ func (o *OAuth2) userIDFromToken(ctx context.Context, tokenSHA string, store Dat
store.GetData()["IsApiToken"] = true
store.GetData()["ApiTokenScope"] = accessTokenScope
}
return uid
return user_model.GetUserByID(ctx, uid)
}
t, err := auth_model.GetAccessTokenBySHA(ctx, tokenSHA)
if err != nil {
if auth_model.IsErrAccessTokenNotExist(err) {
// check task token
task, err := actions_model.GetRunningTaskByToken(ctx, tokenSHA)
if err == nil && task != nil {
if task, err := actions_model.GetRunningTaskByToken(ctx, tokenSHA); err == nil {
log.Trace("Basic Authorization: Valid AccessToken for task[%d]", task.ID)
store.GetData()["IsActionsToken"] = true
store.GetData()["ActionsTaskID"] = task.ID
return user_model.ActionsUserID
return user_model.NewActionsUserWithTaskID(task.ID), nil
}
} else if !auth_model.IsErrAccessTokenNotExist(err) && !auth_model.IsErrAccessTokenEmpty(err) {
log.Error("GetAccessTokenBySHA: %v", err)
}
return 0
return nil, err
}
t.UpdatedUnix = timeutil.TimeStampNow()
if err = auth_model.UpdateAccessToken(ctx, t); err != nil {
log.Error("UpdateAccessToken: %v", err)
}
store.GetData()["IsApiToken"] = true
store.GetData()["ApiTokenScope"] = t.Scope
return t.UID
return user_model.GetUserByID(ctx, t.UID)
}
// Verify extracts the user ID from the OAuth token in the query parameters
@@ -161,33 +152,14 @@ func (o *OAuth2) userIDFromToken(ctx context.Context, tokenSHA string, store Dat
// If verification is successful returns an existing user object.
// Returns nil if verification fails.
func (o *OAuth2) Verify(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) (*user_model.User, error) {
// These paths are not API paths, but we still want to check for tokens because they maybe in the API returned URLs
detector := newAuthPathDetector(req)
if !detector.isAPIPath() && !detector.isAttachmentDownload() && !detector.isAuthenticatedTokenRequest() &&
!detector.isGitRawOrAttachPath() && !detector.isArchivePath() {
return nil, nil
}
token, ok := parseToken(req)
if !ok {
return nil, nil
return nil, nil //nolint:nilnil // the auth method is not applicable
}
id := o.userIDFromToken(req.Context(), token, store)
if id <= 0 && id != -2 { // -2 means actions, so we need to allow it.
return nil, user_model.ErrUserNotExist{}
user, err := o.userFromToken(req.Context(), token, store)
if err != nil && !errors.Is(err, util.ErrNotExist) {
log.Error("userFromToken: %v", err) // the callers might ignore the error, so log it here
}
log.Trace("OAuth2 Authorization: Found token for user[%d]", id)
user, err := user_model.GetPossibleUserByID(req.Context(), id)
if err != nil {
if !user_model.IsErrUserNotExist(err) {
log.Error("GetUserByName: %v", err)
}
return nil, err
}
log.Trace("OAuth2 Authorization: Logged in user %-v", user)
return user, nil
return user, err
}
@@ -12,23 +12,26 @@ import (
"code.gitea.io/gitea/services/actions"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestUserIDFromToken(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
t.Run("Actions JWT", func(t *testing.T) {
const RunningTaskID = 47
const RunningTaskID int64 = 47
token, err := actions.CreateAuthorizationToken(RunningTaskID, 1, 2)
assert.NoError(t, err)
ds := make(reqctx.ContextData)
o := OAuth2{}
uid := o.userIDFromToken(t.Context(), token, ds)
assert.Equal(t, user_model.ActionsUserID, uid)
assert.Equal(t, true, ds["IsActionsToken"])
assert.Equal(t, ds["ActionsTaskID"], int64(RunningTaskID))
u, err := o.userFromToken(t.Context(), token, ds)
require.NoError(t, err)
assert.Equal(t, user_model.ActionsUserID, u.ID)
taskID, ok := user_model.GetActionsUserTaskID(u)
assert.True(t, ok)
assert.Equal(t, RunningTaskID, taskID)
})
}
@@ -29,7 +29,9 @@ const ReverseProxyMethodName = "reverse_proxy"
// On successful authentication the proxy is expected to populate the username in the
// "setting.ReverseProxyAuthUser" header. Optionally it can also populate the email of the
// user in the "setting.ReverseProxyAuthEmail" header.
type ReverseProxy struct{}
type ReverseProxy struct {
CreateSession bool
}
// getUserName extracts the username from the "setting.ReverseProxyAuthUser" header
func (r *ReverseProxy) getUserName(req *http.Request) string {
@@ -51,7 +53,7 @@ func (r *ReverseProxy) Name() string {
func (r *ReverseProxy) getUserFromAuthUser(req *http.Request) (*user_model.User, error) {
username := r.getUserName(req)
if len(username) == 0 {
return nil, nil
return nil, nil //nolint:nilnil // the auth method is not applicable
}
log.Trace("ReverseProxy Authorization: Found username: %s", username)
@@ -111,13 +113,11 @@ 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
return nil, nil //nolint:nilnil // the auth method is not applicable
}
}
// Make sure requests to API paths, attachment downloads, git and LFS do not create a new session
detector := newAuthPathDetector(req)
if !detector.isAPIPath() && !detector.isAttachmentDownload() && !detector.isGitRawOrAttachOrLFSPath() {
if r.CreateSession {
if sess != nil && (sess.Get("uid") == nil || sess.Get("uid").(int64) != user.ID) {
handleSignIn(w, req, sess, user)
}
@@ -29,19 +29,19 @@ func (s *Session) Name() string {
// Returns nil if there is no user uid stored in the session.
func (s *Session) Verify(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) (*user_model.User, error) {
if sess == nil {
return nil, nil
return nil, nil //nolint:nilnil // the auth method is not applicable
}
// Get user ID
uid := sess.Get("uid")
if uid == nil {
return nil, nil
return nil, nil //nolint:nilnil // the auth method is not applicable
}
log.Trace("Session Authorization: Found user[%d]", uid)
id, ok := uid.(int64)
if !ok {
return nil, nil
return nil, nil //nolint:nilnil // the auth method is not applicable
}
// Get user object
@@ -52,7 +52,7 @@ func (s *Session) Verify(req *http.Request, w http.ResponseWriter, store DataSto
// Return the err as-is to keep current signed-in session, in case the err is something like context.Canceled. Otherwise non-existing user (nil, nil) will make the caller clear the signed-in session.
return nil, err
}
return nil, nil
return nil, nil //nolint:nilnil // the auth method is not applicable
}
log.Trace("Session Authorization: Logged in user %-v", user)
+1 -1
View File
@@ -51,7 +51,7 @@ func UserSignIn(ctx context.Context, username, password string) (*user_model.Use
}
if user != nil {
hasUser, err := user_model.GetUser(ctx, user)
hasUser, err := user_model.GetIndividualUser(ctx, user)
if err != nil {
return nil, nil, err
}
@@ -44,6 +44,7 @@ type Source struct {
AttributesInBind bool // fetch attributes in bind context (not user)
AttributeSSHPublicKey string // LDAP SSH Public Key attribute
AttributeAvatar string
SSHKeysAreVerified bool // true if SSH keys in LDAP are verified
SearchPageSize uint32 // Search with paging page size
Filter string // Query filter to validate entry
AdminFilter string // Query filter to check if user is admin
@@ -73,7 +73,7 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u
}
if user != nil {
if isAttributeSSHPublicKeySet && asymkey_model.SynchronizePublicKeys(ctx, user, source.AuthSource, sr.SSHPublicKey) {
if isAttributeSSHPublicKeySet && asymkey_model.SynchronizePublicKeys(ctx, user, source.AuthSource, sr.SSHPublicKey, source.SSHKeysAreVerified) {
if err := asymkey_service.RewriteAllPublicKeys(ctx); err != nil {
return user, err
}
@@ -99,7 +99,7 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u
return user, err
}
if isAttributeSSHPublicKeySet && asymkey_model.AddPublicKeysBySource(ctx, user, source.AuthSource, sr.SSHPublicKey) {
if isAttributeSSHPublicKeySet && asymkey_model.AddPublicKeysBySource(ctx, user, source.AuthSource, sr.SSHPublicKey, source.SSHKeysAreVerified) {
if err := asymkey_service.RewriteAllPublicKeys(ctx); err != nil {
return user, err
}
@@ -135,7 +135,7 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
if err == nil && isAttributeSSHPublicKeySet {
log.Trace("SyncExternalUsers[%s]: Adding LDAP Public SSH Keys for user %s", source.AuthSource.Name, usr.Name)
if asymkey_model.AddPublicKeysBySource(ctx, usr, source.AuthSource, su.SSHPublicKey) {
if asymkey_model.AddPublicKeysBySource(ctx, usr, source.AuthSource, su.SSHPublicKey, source.SSHKeysAreVerified) {
sshKeysNeedUpdate = true
}
}
@@ -145,7 +145,7 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
}
} else if updateExisting {
// Synchronize SSH Public Key if that attribute is set
if isAttributeSSHPublicKeySet && asymkey_model.SynchronizePublicKeys(ctx, usr, source.AuthSource, su.SSHPublicKey) {
if isAttributeSSHPublicKeySet && asymkey_model.SynchronizePublicKeys(ctx, usr, source.AuthSource, su.SSHPublicKey, source.SSHKeysAreVerified) {
sshKeysNeedUpdate = true
}
@@ -22,9 +22,6 @@ import (
var gothRWMutex = sync.RWMutex{}
// UsersStoreKey is the key for the store
const UsersStoreKey = "gitea-oauth2-sessions"
// ProviderHeaderKey is the HTTP header key
const ProviderHeaderKey = "gitea-oauth2-provider"
@@ -33,7 +30,7 @@ func Init(ctx context.Context) error {
// Lock our mutex
gothRWMutex.Lock()
gob.Register(&sessions.Session{})
gob.Register(&sessions.Session{}) // TODO: CHI-SESSION-GOB-REGISTER. FIXME: it seems to be an abuse, why the Session struct itself is stored in session store again?
gothic.Store = &SessionsStore{
maxLength: int64(setting.OAuth2.MaxTokenLength),
@@ -20,6 +20,7 @@ import (
"code.gitea.io/gitea/modules/setting"
"github.com/markbates/goth"
"github.com/markbates/goth/providers/openidConnect"
)
// Provider is an interface for describing a single OAuth2 provider
@@ -61,7 +62,7 @@ func (p *AuthSourceProvider) DisplayName() string {
func (p *AuthSourceProvider) IconHTML(size int) template.HTML {
if p.iconURL != "" {
img := fmt.Sprintf(`<img class="tw-object-contain tw-mr-2" width="%d" height="%d" src="%s" alt="%s">`,
img := fmt.Sprintf(`<img class="tw-object-contain" width="%d" height="%d" src="%s" alt="%s">`,
size,
size,
html.EscapeString(p.iconURL), html.EscapeString(p.DisplayName()),
@@ -197,6 +198,26 @@ func ClearProviders() {
goth.ClearProviders()
}
// GetOIDCEndSessionEndpoint returns the OIDC end_session_endpoint for the
// given provider name. Returns "" if the provider is not OIDC or doesn't
// advertise an end_session_endpoint in its discovery document.
func GetOIDCEndSessionEndpoint(providerName string) string {
gothRWMutex.RLock()
defer gothRWMutex.RUnlock()
provider, ok := goth.GetProviders()[providerName]
if !ok {
return ""
}
oidcProvider, ok := provider.(*openidConnect.Provider)
if !ok || oidcProvider.OpenIDConfig == nil {
return ""
}
return oidcProvider.OpenIDConfig.EndSessionEndpoint
}
var ErrAuthSourceNotActivated = errors.New("auth source is not activated")
// used to create different types of goth providers
@@ -42,10 +42,10 @@ func (b *BaseProvider) IconHTML(size int) template.HTML {
case "github":
svgName = "octicon-mark-github"
}
svgHTML := svg.RenderHTML(svgName, size, "tw-mr-2")
svgHTML := svg.RenderHTML(svgName, size)
if svgHTML == "" {
log.Error("No SVG icon for oauth2 provider %q", b.name)
svgHTML = svg.RenderHTML("gitea-openid", size, "tw-mr-2")
svgHTML = svg.RenderHTML("gitea-openid", size)
}
return svgHTML
}
@@ -33,7 +33,7 @@ func (o *OpenIDProvider) DisplayName() string {
// IconHTML returns icon HTML for this provider
func (o *OpenIDProvider) IconHTML(size int) template.HTML {
return svg.RenderHTML("gitea-openid", size, "tw-mr-2")
return svg.RenderHTML("gitea-openid", size)
}
// CreateGothProvider creates a GothProvider from this Provider
@@ -19,11 +19,11 @@ func (p *fakeProvider) Name() string {
func (p *fakeProvider) SetName(name string) {}
func (p *fakeProvider) BeginAuth(state string) (goth.Session, error) {
return nil, nil
return nil, nil //nolint:nilnil // the auth method is not applicable
}
func (p *fakeProvider) UnmarshalSession(string) (goth.Session, error) {
return nil, nil
return nil, nil //nolint:nilnil // the auth method is not applicable
}
func (p *fakeProvider) FetchUser(goth.Session) (goth.User, error) {
@@ -67,7 +67,7 @@ func (source *Source) refresh(ctx context.Context, provider goth.Provider, u *us
LoginSource: u.LoginSourceID,
}
hasUser, err := user_model.GetUser(ctx, user)
hasUser, err := user_model.GetIndividualUser(ctx, user)
if err != nil {
return err
}
@@ -35,9 +35,9 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u
// Allow PAM sources with `@` in their name, like from Active Directory
username := pamLogin
email := pamLogin
idx := strings.Index(pamLogin, "@")
if idx > -1 {
username = pamLogin[:idx]
before, _, ok := strings.Cut(pamLogin, "@")
if ok {
username = before
}
if user_model.ValidateEmail(email) != nil {
if source.EmailDomain != "" {
@@ -21,10 +21,10 @@ import (
func (source *Source) Authenticate(ctx context.Context, user *user_model.User, userName, password string) (*user_model.User, error) {
// Verify allowed domains.
if len(source.AllowedDomains) > 0 {
idx := strings.Index(userName, "@")
if idx == -1 {
_, after, ok := strings.Cut(userName, "@")
if !ok {
return nil, user_model.ErrUserNotExist{Name: userName}
} else if !util.SliceContainsString(strings.Split(source.AllowedDomains, ","), userName[idx+1:], true) {
} else if !util.SliceContainsString(strings.Split(source.AllowedDomains, ","), after, true) {
return nil, user_model.ErrUserNotExist{Name: userName}
}
}
@@ -61,9 +61,9 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u
}
username := userName
idx := strings.Index(userName, "@")
if idx > -1 {
username = userName[:idx]
before, _, ok := strings.Cut(userName, "@")
if ok {
username = before
}
user = &user_model.User{
+10 -19
View File
@@ -46,7 +46,9 @@ var (
// The SSPI plugin is expected to be executed last, as it returns 401 status code if negotiation
// fails (or if negotiation should continue), which would prevent other authentication methods
// to execute at all.
type SSPI struct{}
type SSPI struct {
CreateSession bool
}
// Name represents the name of auth method
func (s *SSPI) Name() string {
@@ -63,7 +65,7 @@ func (s *SSPI) Verify(req *http.Request, w http.ResponseWriter, store DataStore,
return nil, sspiAuthErrInit
}
if !s.shouldAuthenticate(req) {
return nil, nil
return nil, nil //nolint:nilnil // the auth method is not applicable
}
cfg, err := s.getConfig(req.Context())
@@ -97,7 +99,7 @@ func (s *SSPI) Verify(req *http.Request, w http.ResponseWriter, store DataStore,
username := sanitizeUsername(userInfo.Username, cfg)
if len(username) == 0 {
return nil, nil
return nil, nil //nolint:nilnil // the auth method is not applicable
}
log.Info("Authenticated as %s\n", username)
@@ -109,7 +111,7 @@ func (s *SSPI) Verify(req *http.Request, w http.ResponseWriter, store DataStore,
}
if !cfg.AutoCreateUsers {
log.Error("User '%s' not found", username)
return nil, nil
return nil, nil //nolint:nilnil // the auth method is not applicable
}
user, err = s.newUser(req.Context(), username, cfg)
if err != nil {
@@ -118,9 +120,7 @@ func (s *SSPI) Verify(req *http.Request, w http.ResponseWriter, store DataStore,
}
}
// Make sure requests to API paths and PWA resources do not create a new session
detector := newAuthPathDetector(req)
if !detector.isAPIPath() && !detector.isAttachmentDownload() {
if s.CreateSession {
handleSignIn(w, req, sess, user)
}
@@ -147,18 +147,9 @@ func (s *SSPI) getConfig(ctx context.Context) (*sspi.Source, error) {
}
func (s *SSPI) shouldAuthenticate(req *http.Request) (shouldAuth bool) {
shouldAuth = false
path := strings.TrimSuffix(req.URL.Path, "/")
if path == "/user/login" {
if req.FormValue("user_name") != "" && req.FormValue("password") != "" {
shouldAuth = false
} else if req.FormValue("auth_with_sspi") == "1" {
shouldAuth = true
}
} else {
detector := newAuthPathDetector(req)
shouldAuth = detector.isAPIPath() || detector.isAttachmentDownload()
}
// SSPI is only applicable for login requests with "auth_with_sspi" form value set to "1"
// See the template code with "auth_with_sspi"
shouldAuth = req.URL.Path == "/user/login" && req.FormValue("auth_with_sspi") == "1"
return shouldAuth
}