feat: vendor gitea 1.16.2
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user