mirror of
https://github.com/go-gitea/gitea.git
synced 2026-04-18 07:48:48 +01:00
Refactor auth middleware (#36848)
Principles: let the caller decide what it needs, but not let the framework (middleware) guess what it should do. Then a lot of hacky code can be removed. And some FIXMEs can be fixed. This PR introduces a new kind of middleware: "PreMiddleware", it will be executed before all other middlewares on the same routing level, then a route can declare its options for other middlewares. By the way, allow the workflow badge to be accessed by Basic or OAuth2 auth. Fixes: https://github.com/go-gitea/gitea/pull/36830 Fixes: https://github.com/go-gitea/gitea/issues/36859
This commit is contained in:
@@ -8,38 +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"
|
||||
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 {
|
||||
@@ -60,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...
|
||||
|
||||
@@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -41,13 +41,6 @@ func (b *Basic) Name() string {
|
||||
}
|
||||
|
||||
func (b *Basic) parseAuthBasic(req *http.Request) (ret struct{ authToken, uname, passwd string }) {
|
||||
// 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 ret
|
||||
}
|
||||
|
||||
authHeader := req.Header.Get("Authorization")
|
||||
if authHeader == "" {
|
||||
return ret
|
||||
|
||||
@@ -152,13 +152,6 @@ func (o *OAuth2) userFromToken(ctx context.Context, tokenSHA string, store DataS
|
||||
// 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 //nolint:nilnil // the auth method is not applicable
|
||||
}
|
||||
|
||||
token, ok := parseToken(req)
|
||||
if !ok {
|
||||
return nil, nil //nolint:nilnil // the auth method is not applicable
|
||||
|
||||
@@ -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 {
|
||||
@@ -115,9 +117,7 @@ func (r *ReverseProxy) Verify(req *http.Request, w http.ResponseWriter, store Da
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user