Prevent double-login for Git HTTP and LFS and simplify login (#15303)
* Prevent double-login for Git HTTP and LFS and simplify login There are a number of inconsistencies with our current methods for logging in for git and lfs. The first is that there is a double login process. This is particularly evident in 1.13 where there are no less than 4 hash checks for basic authentication due to the previous IsPasswordSet behaviour. This duplicated code had individual inconsistencies that were not helpful and caused confusion. This PR does the following: * Remove the specific login code from the git and lfs handlers except for the lfs special bearer token * Simplify the meaning of DisableBasicAuthentication to allow Token and Oauth2 sign-in. * The removal of the specific code from git and lfs means that these both now have the same login semantics and can - if not DisableBasicAuthentication - login from external services. Further it allows Oauth2 token authentication as per our standard mechanisms. * The change in the recovery handler prevents the service from re-attempting to login - primarily because this could easily cause a further panic and it is wasteful. * add test Signed-off-by: Andrew Thornton <art27@cantab.net> Co-authored-by: Andrew Thornton <art27@cantab.net>
This commit is contained in:
parent
ba526ceffe
commit
17c5c654a5
10 changed files with 292 additions and 221 deletions
|
@ -44,7 +44,7 @@ func TestAPILFSLocksNotLogin(t *testing.T) {
|
|||
resp := MakeRequest(t, req, http.StatusUnauthorized)
|
||||
var lfsLockError api.LFSLockError
|
||||
DecodeJSON(t, resp, &lfsLockError)
|
||||
assert.Equal(t, "Unauthorized", lfsLockError.Message)
|
||||
assert.Equal(t, "You must have pull access to list locks", lfsLockError.Message)
|
||||
}
|
||||
|
||||
func TestAPILFSLocksLogged(t *testing.T) {
|
||||
|
|
|
@ -14,6 +14,7 @@ import (
|
|||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/web/middleware"
|
||||
)
|
||||
|
||||
// Ensure the struct implements the interface.
|
||||
|
@ -40,7 +41,7 @@ func (b *Basic) Free() error {
|
|||
// IsEnabled returns true as this plugin is enabled by default and its not possible to disable
|
||||
// it from settings.
|
||||
func (b *Basic) IsEnabled() bool {
|
||||
return setting.Service.EnableBasicAuth
|
||||
return true
|
||||
}
|
||||
|
||||
// VerifyAuthData extracts and validates Basic data (username and password/token) from the
|
||||
|
@ -48,17 +49,22 @@ func (b *Basic) IsEnabled() bool {
|
|||
// name/token on successful validation.
|
||||
// Returns nil if header is empty or validation fails.
|
||||
func (b *Basic) VerifyAuthData(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) *models.User {
|
||||
|
||||
// Basic authentication should only fire on API, Download or on Git or LFSPaths
|
||||
if middleware.IsInternalPath(req) || !middleware.IsAPIPath(req) && !isAttachmentDownload(req) && !isGitOrLFSPath(req) {
|
||||
return nil
|
||||
}
|
||||
|
||||
baHead := req.Header.Get("Authorization")
|
||||
if len(baHead) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
auths := strings.Fields(baHead)
|
||||
auths := strings.SplitN(baHead, " ", 2)
|
||||
if len(auths) != 2 || (auths[0] != "Basic" && auths[0] != "basic") {
|
||||
return nil
|
||||
}
|
||||
|
||||
var u *models.User
|
||||
uname, passwd, _ := base.BasicAuthDecode(auths[1])
|
||||
|
||||
// Check if username or password is a token
|
||||
|
@ -76,20 +82,21 @@ func (b *Basic) VerifyAuthData(req *http.Request, w http.ResponseWriter, store D
|
|||
uid := CheckOAuthAccessToken(authToken)
|
||||
if uid != 0 {
|
||||
log.Trace("Basic Authorization: Valid OAuthAccessToken for user[%d]", uid)
|
||||
var err error
|
||||
store.GetData()["IsApiToken"] = true
|
||||
|
||||
u, err = models.GetUserByID(uid)
|
||||
u, err := models.GetUserByID(uid)
|
||||
if err != nil {
|
||||
log.Error("GetUserByID: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
store.GetData()["IsApiToken"] = true
|
||||
return u
|
||||
}
|
||||
|
||||
token, err := models.GetAccessTokenBySHA(authToken)
|
||||
if err == nil {
|
||||
log.Trace("Basic Authorization: Valid AccessToken for user[%d]", uid)
|
||||
|
||||
u, err = models.GetUserByID(token.UID)
|
||||
u, err := models.GetUserByID(token.UID)
|
||||
if err != nil {
|
||||
log.Error("GetUserByID: %v", err)
|
||||
return nil
|
||||
|
@ -99,22 +106,24 @@ func (b *Basic) VerifyAuthData(req *http.Request, w http.ResponseWriter, store D
|
|||
if err = models.UpdateAccessToken(token); err != nil {
|
||||
log.Error("UpdateAccessToken: %v", err)
|
||||
}
|
||||
|
||||
store.GetData()["IsApiToken"] = true
|
||||
return u
|
||||
} else if !models.IsErrAccessTokenNotExist(err) && !models.IsErrAccessTokenEmpty(err) {
|
||||
log.Error("GetAccessTokenBySha: %v", err)
|
||||
}
|
||||
|
||||
if u == nil {
|
||||
log.Trace("Basic Authorization: Attempting SignIn for %s", uname)
|
||||
if !setting.Service.EnableBasicAuth {
|
||||
return nil
|
||||
}
|
||||
|
||||
u, err = models.UserSignIn(uname, passwd)
|
||||
if err != nil {
|
||||
if !models.IsErrUserNotExist(err) {
|
||||
log.Error("UserSignIn: %v", err)
|
||||
}
|
||||
return nil
|
||||
log.Trace("Basic Authorization: Attempting SignIn for %s", uname)
|
||||
u, err := models.UserSignIn(uname, passwd)
|
||||
if err != nil {
|
||||
if !models.IsErrUserNotExist(err) {
|
||||
log.Error("UserSignIn: %v", err)
|
||||
}
|
||||
} else {
|
||||
store.GetData()["IsApiToken"] = true
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Trace("Basic Authorization: Logged in user %-v", u)
|
||||
|
|
|
@ -9,10 +9,12 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/web/middleware"
|
||||
)
|
||||
|
||||
|
@ -27,9 +29,9 @@ import (
|
|||
// for users that have already signed in.
|
||||
var ssoMethods = []SingleSignOn{
|
||||
&OAuth2{},
|
||||
&Basic{},
|
||||
&Session{},
|
||||
&ReverseProxy{},
|
||||
&Basic{},
|
||||
}
|
||||
|
||||
// The purpose of the following three function variables is to let the linter know that
|
||||
|
@ -102,6 +104,19 @@ func isAttachmentDownload(req *http.Request) bool {
|
|||
return strings.HasPrefix(req.URL.Path, "/attachments/") && req.Method == "GET"
|
||||
}
|
||||
|
||||
var gitPathRe = regexp.MustCompile(`^/[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+/(?:(?:git-(?:(?:upload)|(?:receive))-pack$)|(?:info/refs$)|(?:HEAD$)|(?:objects/))`)
|
||||
var lfsPathRe = regexp.MustCompile(`^/[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+/info/lfs/`)
|
||||
|
||||
func isGitOrLFSPath(req *http.Request) bool {
|
||||
if gitPathRe.MatchString(req.URL.Path) {
|
||||
return true
|
||||
}
|
||||
if setting.LFS.StartServer {
|
||||
return lfsPathRe.MatchString(req.URL.Path)
|
||||
}
|
||||
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 *models.User) {
|
||||
_ = sess.Delete("openid_verified_uri")
|
||||
|
|
124
modules/auth/sso/sso_test.go
Normal file
124
modules/auth/sso/sso_test.go
Normal file
|
@ -0,0 +1,124 @@
|
|||
// Copyright 2014 The Gogs Authors. All rights reserved.
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package sso
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
)
|
||||
|
||||
func Test_isGitOrLFSPath(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/stars",
|
||||
false,
|
||||
},
|
||||
{
|
||||
"/notowner",
|
||||
false,
|
||||
},
|
||||
{
|
||||
"/owner/repo",
|
||||
false,
|
||||
},
|
||||
{
|
||||
"/owner/repo/commit/123456789012345678921234567893124567894",
|
||||
false,
|
||||
},
|
||||
}
|
||||
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",
|
||||
}
|
||||
|
||||
origLFSStartServer := setting.LFS.StartServer
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.path, func(t *testing.T) {
|
||||
req, _ := http.NewRequest("POST", "http://localhost"+tt.path, nil)
|
||||
setting.LFS.StartServer = false
|
||||
if got := isGitOrLFSPath(req); got != tt.want {
|
||||
t.Errorf("isGitOrLFSPath() = %v, want %v", got, tt.want)
|
||||
}
|
||||
setting.LFS.StartServer = true
|
||||
if got := isGitOrLFSPath(req); got != tt.want {
|
||||
t.Errorf("isGitOrLFSPath() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
for _, tt := range lfsTests {
|
||||
t.Run(tt, func(t *testing.T) {
|
||||
req, _ := http.NewRequest("POST", tt, nil)
|
||||
setting.LFS.StartServer = false
|
||||
if got := isGitOrLFSPath(req); got != setting.LFS.StartServer {
|
||||
t.Errorf("isGitOrLFSPath(%q) = %v, want %v, %v", tt, got, setting.LFS.StartServer, gitPathRe.MatchString(tt))
|
||||
}
|
||||
setting.LFS.StartServer = true
|
||||
if got := isGitOrLFSPath(req); got != setting.LFS.StartServer {
|
||||
t.Errorf("isGitOrLFSPath(%q) = %v, want %v", tt, got, setting.LFS.StartServer)
|
||||
}
|
||||
})
|
||||
}
|
||||
setting.LFS.StartServer = origLFSStartServer
|
||||
}
|
|
@ -683,6 +683,9 @@ func Contexter() func(next http.Handler) http.Handler {
|
|||
} else {
|
||||
ctx.Data["SignedUserID"] = int64(0)
|
||||
ctx.Data["SignedUserName"] = ""
|
||||
|
||||
// ensure the session uid is deleted
|
||||
_ = ctx.Session.Delete("uid")
|
||||
}
|
||||
|
||||
ctx.Resp.Header().Set(`X-Frame-Options`, `SAMEORIGIN`)
|
||||
|
|
|
@ -23,7 +23,7 @@ import (
|
|||
func CheckInternalToken(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
tokens := req.Header.Get("Authorization")
|
||||
fields := strings.Fields(tokens)
|
||||
fields := strings.SplitN(tokens, " ", 2)
|
||||
if len(fields) != 2 || fields[0] != "Bearer" || fields[1] != setting.InternalToken {
|
||||
log.Debug("Forbidden attempt to access internal url: Authorization header: %s", tokens)
|
||||
http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
|
||||
|
|
|
@ -22,15 +22,12 @@ import (
|
|||
"time"
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/modules/auth/sso"
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/process"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
repo_service "code.gitea.io/gitea/services/repository"
|
||||
)
|
||||
|
@ -153,11 +150,8 @@ func httpBase(ctx *context.Context) (h *serviceHandler) {
|
|||
// Only public pull don't need auth.
|
||||
isPublicPull := repoExist && !repo.IsPrivate && isPull
|
||||
var (
|
||||
askAuth = !isPublicPull || setting.Service.RequireSignInView
|
||||
authUser *models.User
|
||||
authUsername string
|
||||
authPasswd string
|
||||
environ []string
|
||||
askAuth = !isPublicPull || setting.Service.RequireSignInView
|
||||
environ []string
|
||||
)
|
||||
|
||||
// don't allow anonymous pulls if organization is not public
|
||||
|
@ -172,108 +166,33 @@ func httpBase(ctx *context.Context) (h *serviceHandler) {
|
|||
|
||||
// check access
|
||||
if askAuth {
|
||||
authUsername = ctx.Req.Header.Get(setting.ReverseProxyAuthUser)
|
||||
if setting.Service.EnableReverseProxyAuth && len(authUsername) > 0 {
|
||||
authUser, err = models.GetUserByName(authUsername)
|
||||
if err != nil {
|
||||
ctx.HandleText(401, "reverse proxy login error, got error while running GetUserByName")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
authHead := ctx.Req.Header.Get("Authorization")
|
||||
if len(authHead) == 0 {
|
||||
ctx.Resp.Header().Set("WWW-Authenticate", "Basic realm=\".\"")
|
||||
ctx.Error(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
// rely on the results of Contexter
|
||||
if !ctx.IsSigned {
|
||||
// TODO: support digit auth - which would be Authorization header with digit
|
||||
ctx.Resp.Header().Set("WWW-Authenticate", "Basic realm=\".\"")
|
||||
ctx.Error(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
auths := strings.Fields(authHead)
|
||||
// currently check basic auth
|
||||
// TODO: support digit auth
|
||||
// FIXME: middlewares/context.go did basic auth check already,
|
||||
// maybe could use that one.
|
||||
if len(auths) != 2 || auths[0] != "Basic" {
|
||||
ctx.HandleText(http.StatusUnauthorized, "no basic auth and digit auth")
|
||||
return
|
||||
}
|
||||
authUsername, authPasswd, err = base.BasicAuthDecode(auths[1])
|
||||
if err != nil {
|
||||
ctx.HandleText(http.StatusUnauthorized, "no basic auth and digit auth")
|
||||
return
|
||||
}
|
||||
|
||||
// Check if username or password is a token
|
||||
isUsernameToken := len(authPasswd) == 0 || authPasswd == "x-oauth-basic"
|
||||
// Assume username is token
|
||||
authToken := authUsername
|
||||
if !isUsernameToken {
|
||||
// Assume password is token
|
||||
authToken = authPasswd
|
||||
}
|
||||
uid := sso.CheckOAuthAccessToken(authToken)
|
||||
if uid != 0 {
|
||||
ctx.Data["IsApiToken"] = true
|
||||
|
||||
authUser, err = models.GetUserByID(uid)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetUserByID", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
// Assume password is a token.
|
||||
token, err := models.GetAccessTokenBySHA(authToken)
|
||||
if ctx.IsBasicAuth {
|
||||
_, err = models.GetTwoFactorByUID(ctx.User.ID)
|
||||
if err == nil {
|
||||
authUser, err = models.GetUserByID(token.UID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetUserByID", err)
|
||||
return
|
||||
}
|
||||
|
||||
token.UpdatedUnix = timeutil.TimeStampNow()
|
||||
if err = models.UpdateAccessToken(token); err != nil {
|
||||
ctx.ServerError("UpdateAccessToken", err)
|
||||
}
|
||||
} else if !models.IsErrAccessTokenNotExist(err) && !models.IsErrAccessTokenEmpty(err) {
|
||||
log.Error("GetAccessTokenBySha: %v", err)
|
||||
}
|
||||
|
||||
if authUser == nil {
|
||||
// Check username and password
|
||||
authUser, err = models.UserSignIn(authUsername, authPasswd)
|
||||
if err != nil {
|
||||
if models.IsErrUserProhibitLogin(err) {
|
||||
ctx.HandleText(http.StatusForbidden, "User is not permitted to login")
|
||||
return
|
||||
} else if !models.IsErrUserNotExist(err) {
|
||||
ctx.ServerError("UserSignIn error: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if authUser == nil {
|
||||
ctx.HandleText(http.StatusUnauthorized, fmt.Sprintf("invalid credentials from %s", ctx.RemoteAddr()))
|
||||
return
|
||||
}
|
||||
|
||||
_, err = models.GetTwoFactorByUID(authUser.ID)
|
||||
if err == nil {
|
||||
// TODO: This response should be changed to "invalid credentials" for security reasons once the expectation behind it (creating an app token to authenticate) is properly documented
|
||||
ctx.HandleText(http.StatusUnauthorized, "Users with two-factor authentication enabled cannot perform HTTP/HTTPS operations via plain username and password. Please create and use a personal access token on the user settings page")
|
||||
return
|
||||
} else if !models.IsErrTwoFactorNotEnrolled(err) {
|
||||
ctx.ServerError("IsErrTwoFactorNotEnrolled", err)
|
||||
return
|
||||
}
|
||||
// TODO: This response should be changed to "invalid credentials" for security reasons once the expectation behind it (creating an app token to authenticate) is properly documented
|
||||
ctx.HandleText(http.StatusUnauthorized, "Users with two-factor authentication enabled cannot perform HTTP/HTTPS operations via plain username and password. Please create and use a personal access token on the user settings page")
|
||||
return
|
||||
} else if !models.IsErrTwoFactorNotEnrolled(err) {
|
||||
ctx.ServerError("IsErrTwoFactorNotEnrolled", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if !authUser.IsActive || authUser.ProhibitLogin {
|
||||
if !ctx.User.IsActive || ctx.User.ProhibitLogin {
|
||||
ctx.HandleText(http.StatusForbidden, "Your account is disabled.")
|
||||
return
|
||||
}
|
||||
|
||||
if repoExist {
|
||||
perm, err := models.GetUserRepoPermission(repo, authUser)
|
||||
perm, err := models.GetUserRepoPermission(repo, ctx.User)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetUserRepoPermission", err)
|
||||
return
|
||||
|
@ -293,14 +212,14 @@ func httpBase(ctx *context.Context) (h *serviceHandler) {
|
|||
environ = []string{
|
||||
models.EnvRepoUsername + "=" + username,
|
||||
models.EnvRepoName + "=" + reponame,
|
||||
models.EnvPusherName + "=" + authUser.Name,
|
||||
models.EnvPusherID + fmt.Sprintf("=%d", authUser.ID),
|
||||
models.EnvPusherName + "=" + ctx.User.Name,
|
||||
models.EnvPusherID + fmt.Sprintf("=%d", ctx.User.ID),
|
||||
models.EnvIsDeployKey + "=false",
|
||||
models.EnvAppURL + "=" + setting.AppURL,
|
||||
}
|
||||
|
||||
if !authUser.KeepEmailPrivate {
|
||||
environ = append(environ, models.EnvPusherEmail+"="+authUser.Email)
|
||||
if !ctx.User.KeepEmailPrivate {
|
||||
environ = append(environ, models.EnvPusherEmail+"="+ctx.User.Email)
|
||||
}
|
||||
|
||||
if isWiki {
|
||||
|
@ -336,7 +255,7 @@ func httpBase(ctx *context.Context) (h *serviceHandler) {
|
|||
return
|
||||
}
|
||||
|
||||
repo, err = repo_service.PushCreateRepo(authUser, owner, reponame)
|
||||
repo, err = repo_service.PushCreateRepo(ctx.User, owner, reponame)
|
||||
if err != nil {
|
||||
log.Error("pushCreateRepo: %v", err)
|
||||
ctx.Status(http.StatusNotFound)
|
||||
|
|
|
@ -15,6 +15,7 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/modules/auth/sso"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/httpcache"
|
||||
|
@ -171,8 +172,19 @@ func Recovery() func(next http.Handler) http.Handler {
|
|||
},
|
||||
}
|
||||
|
||||
// Get user from session if logged in.
|
||||
user, _ := sso.SignedInUser(req, w, &store, sessionStore)
|
||||
var user *models.User
|
||||
if apiContext := context.GetAPIContext(req); apiContext != nil {
|
||||
user = apiContext.User
|
||||
}
|
||||
if user == nil {
|
||||
if ctx := context.GetContext(req); ctx != nil {
|
||||
user = ctx.User
|
||||
}
|
||||
}
|
||||
if user == nil {
|
||||
// Get user from session if logged in - do not attempt to sign-in
|
||||
user = sso.SessionUser(sessionStore)
|
||||
}
|
||||
if user != nil {
|
||||
store.Data["IsSigned"] = true
|
||||
store.Data["SignedUser"] = user
|
||||
|
|
|
@ -31,15 +31,6 @@ func checkIsValidRequest(ctx *context.Context) bool {
|
|||
writeStatus(ctx, http.StatusBadRequest)
|
||||
return false
|
||||
}
|
||||
if !ctx.IsSigned {
|
||||
user, _, _, err := parseToken(ctx.Req.Header.Get("Authorization"))
|
||||
if err != nil {
|
||||
ctx.Resp.Header().Set("WWW-Authenticate", "Basic realm=gitea-lfs")
|
||||
writeStatus(ctx, http.StatusUnauthorized)
|
||||
return false
|
||||
}
|
||||
ctx.User = user
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
|
@ -73,19 +64,21 @@ func GetListLockHandler(ctx *context.Context) {
|
|||
// Status is written in checkIsValidRequest
|
||||
return
|
||||
}
|
||||
ctx.Resp.Header().Set("Content-Type", lfs_module.MediaType)
|
||||
|
||||
rv, _ := unpack(ctx)
|
||||
|
||||
repository, err := models.GetRepositoryByOwnerAndName(rv.User, rv.Repo)
|
||||
if err != nil {
|
||||
log.Debug("Could not find repository: %s/%s - %s", rv.User, rv.Repo, err)
|
||||
writeStatus(ctx, 404)
|
||||
ctx.Resp.Header().Set("WWW-Authenticate", "Basic realm=gitea-lfs")
|
||||
ctx.JSON(401, api.LFSLockError{
|
||||
Message: "You must have pull access to list locks",
|
||||
})
|
||||
return
|
||||
}
|
||||
repository.MustOwner()
|
||||
|
||||
authenticated := authenticate(ctx, repository, rv.Authorization, false)
|
||||
authenticated := authenticate(ctx, repository, rv.Authorization, true, false)
|
||||
if !authenticated {
|
||||
ctx.Resp.Header().Set("WWW-Authenticate", "Basic realm=gitea-lfs")
|
||||
ctx.JSON(http.StatusUnauthorized, api.LFSLockError{
|
||||
|
@ -93,6 +86,7 @@ func GetListLockHandler(ctx *context.Context) {
|
|||
})
|
||||
return
|
||||
}
|
||||
ctx.Resp.Header().Set("Content-Type", lfs_module.MediaType)
|
||||
|
||||
cursor := ctx.QueryInt("cursor")
|
||||
if cursor < 0 {
|
||||
|
@ -160,7 +154,6 @@ func PostLockHandler(ctx *context.Context) {
|
|||
// Status is written in checkIsValidRequest
|
||||
return
|
||||
}
|
||||
ctx.Resp.Header().Set("Content-Type", lfs_module.MediaType)
|
||||
|
||||
userName := ctx.Params("username")
|
||||
repoName := strings.TrimSuffix(ctx.Params("reponame"), ".git")
|
||||
|
@ -169,12 +162,15 @@ func PostLockHandler(ctx *context.Context) {
|
|||
repository, err := models.GetRepositoryByOwnerAndName(userName, repoName)
|
||||
if err != nil {
|
||||
log.Error("Unable to get repository: %s/%s Error: %v", userName, repoName, err)
|
||||
writeStatus(ctx, 404)
|
||||
ctx.Resp.Header().Set("WWW-Authenticate", "Basic realm=gitea-lfs")
|
||||
ctx.JSON(401, api.LFSLockError{
|
||||
Message: "You must have push access to create locks",
|
||||
})
|
||||
return
|
||||
}
|
||||
repository.MustOwner()
|
||||
|
||||
authenticated := authenticate(ctx, repository, authorization, true)
|
||||
authenticated := authenticate(ctx, repository, authorization, true, true)
|
||||
if !authenticated {
|
||||
ctx.Resp.Header().Set("WWW-Authenticate", "Basic realm=gitea-lfs")
|
||||
ctx.JSON(http.StatusUnauthorized, api.LFSLockError{
|
||||
|
@ -183,6 +179,8 @@ func PostLockHandler(ctx *context.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
ctx.Resp.Header().Set("Content-Type", lfs_module.MediaType)
|
||||
|
||||
var req api.LFSLockRequest
|
||||
bodyReader := ctx.Req.Body
|
||||
defer bodyReader.Close()
|
||||
|
@ -229,7 +227,6 @@ func VerifyLockHandler(ctx *context.Context) {
|
|||
// Status is written in checkIsValidRequest
|
||||
return
|
||||
}
|
||||
ctx.Resp.Header().Set("Content-Type", lfs_module.MediaType)
|
||||
|
||||
userName := ctx.Params("username")
|
||||
repoName := strings.TrimSuffix(ctx.Params("reponame"), ".git")
|
||||
|
@ -238,12 +235,15 @@ func VerifyLockHandler(ctx *context.Context) {
|
|||
repository, err := models.GetRepositoryByOwnerAndName(userName, repoName)
|
||||
if err != nil {
|
||||
log.Error("Unable to get repository: %s/%s Error: %v", userName, repoName, err)
|
||||
writeStatus(ctx, 404)
|
||||
ctx.Resp.Header().Set("WWW-Authenticate", "Basic realm=gitea-lfs")
|
||||
ctx.JSON(401, api.LFSLockError{
|
||||
Message: "You must have push access to verify locks",
|
||||
})
|
||||
return
|
||||
}
|
||||
repository.MustOwner()
|
||||
|
||||
authenticated := authenticate(ctx, repository, authorization, true)
|
||||
authenticated := authenticate(ctx, repository, authorization, true, true)
|
||||
if !authenticated {
|
||||
ctx.Resp.Header().Set("WWW-Authenticate", "Basic realm=gitea-lfs")
|
||||
ctx.JSON(http.StatusUnauthorized, api.LFSLockError{
|
||||
|
@ -252,6 +252,8 @@ func VerifyLockHandler(ctx *context.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
ctx.Resp.Header().Set("Content-Type", lfs_module.MediaType)
|
||||
|
||||
cursor := ctx.QueryInt("cursor")
|
||||
if cursor < 0 {
|
||||
cursor = 0
|
||||
|
@ -296,7 +298,6 @@ func UnLockHandler(ctx *context.Context) {
|
|||
// Status is written in checkIsValidRequest
|
||||
return
|
||||
}
|
||||
ctx.Resp.Header().Set("Content-Type", lfs_module.MediaType)
|
||||
|
||||
userName := ctx.Params("username")
|
||||
repoName := strings.TrimSuffix(ctx.Params("reponame"), ".git")
|
||||
|
@ -305,12 +306,15 @@ func UnLockHandler(ctx *context.Context) {
|
|||
repository, err := models.GetRepositoryByOwnerAndName(userName, repoName)
|
||||
if err != nil {
|
||||
log.Error("Unable to get repository: %s/%s Error: %v", userName, repoName, err)
|
||||
writeStatus(ctx, 404)
|
||||
ctx.Resp.Header().Set("WWW-Authenticate", "Basic realm=gitea-lfs")
|
||||
ctx.JSON(401, api.LFSLockError{
|
||||
Message: "You must have push access to delete locks",
|
||||
})
|
||||
return
|
||||
}
|
||||
repository.MustOwner()
|
||||
|
||||
authenticated := authenticate(ctx, repository, authorization, true)
|
||||
authenticated := authenticate(ctx, repository, authorization, true, true)
|
||||
if !authenticated {
|
||||
ctx.Resp.Header().Set("WWW-Authenticate", "Basic realm=gitea-lfs")
|
||||
ctx.JSON(http.StatusUnauthorized, api.LFSLockError{
|
||||
|
@ -319,6 +323,8 @@ func UnLockHandler(ctx *context.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
ctx.Resp.Header().Set("Content-Type", lfs_module.MediaType)
|
||||
|
||||
var req api.LFSLockDeleteRequest
|
||||
bodyReader := ctx.Req.Body
|
||||
defer bodyReader.Close()
|
||||
|
|
|
@ -94,7 +94,7 @@ func getAuthenticatedRepoAndMeta(ctx *context.Context, rc *requestContext, p lfs
|
|||
return nil, nil
|
||||
}
|
||||
|
||||
if !authenticate(ctx, repository, rc.Authorization, requireWrite) {
|
||||
if !authenticate(ctx, repository, rc.Authorization, false, requireWrite) {
|
||||
requireAuth(ctx)
|
||||
return nil, nil
|
||||
}
|
||||
|
@ -232,7 +232,7 @@ func PostHandler(ctx *context.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
if !authenticate(ctx, repository, rc.Authorization, true) {
|
||||
if !authenticate(ctx, repository, rc.Authorization, false, true) {
|
||||
requireAuth(ctx)
|
||||
return
|
||||
}
|
||||
|
@ -322,7 +322,7 @@ func BatchHandler(ctx *context.Context) {
|
|||
requireWrite = true
|
||||
}
|
||||
|
||||
if !authenticate(ctx, repository, reqCtx.Authorization, requireWrite) {
|
||||
if !authenticate(ctx, repository, reqCtx.Authorization, false, requireWrite) {
|
||||
requireAuth(ctx)
|
||||
return
|
||||
}
|
||||
|
@ -561,7 +561,7 @@ func logRequest(r *http.Request, status int) {
|
|||
|
||||
// authenticate uses the authorization string to determine whether
|
||||
// or not to proceed. This server assumes an HTTP Basic auth format.
|
||||
func authenticate(ctx *context.Context, repository *models.Repository, authorization string, requireWrite bool) bool {
|
||||
func authenticate(ctx *context.Context, repository *models.Repository, authorization string, requireSigned, requireWrite bool) bool {
|
||||
accessMode := models.AccessModeRead
|
||||
if requireWrite {
|
||||
accessMode = models.AccessModeWrite
|
||||
|
@ -575,89 +575,72 @@ func authenticate(ctx *context.Context, repository *models.Repository, authoriza
|
|||
}
|
||||
|
||||
canRead := perm.CanAccess(accessMode, models.UnitTypeCode)
|
||||
if canRead {
|
||||
if canRead && (!requireSigned || ctx.IsSigned) {
|
||||
return true
|
||||
}
|
||||
|
||||
user, repo, opStr, err := parseToken(authorization)
|
||||
user, err := parseToken(authorization, repository, accessMode)
|
||||
if err != nil {
|
||||
// Most of these are Warn level - the true internal server errors are logged in parseToken already
|
||||
log.Warn("Authentication failure for provided token with Error: %v", err)
|
||||
return false
|
||||
}
|
||||
ctx.User = user
|
||||
if opStr == "basic" {
|
||||
perm, err = models.GetUserRepoPermission(repository, ctx.User)
|
||||
if err != nil {
|
||||
log.Error("Unable to GetUserRepoPermission for user %-v in repo %-v Error: %v", ctx.User, repository)
|
||||
return false
|
||||
}
|
||||
return perm.CanAccess(accessMode, models.UnitTypeCode)
|
||||
}
|
||||
if repository.ID == repo.ID {
|
||||
if requireWrite && opStr != "upload" {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
return true
|
||||
}
|
||||
|
||||
func parseToken(authorization string) (*models.User, *models.Repository, string, error) {
|
||||
func handleLFSToken(tokenSHA string, target *models.Repository, mode models.AccessMode) (*models.User, error) {
|
||||
if !strings.Contains(tokenSHA, ".") {
|
||||
return nil, nil
|
||||
}
|
||||
token, err := jwt.ParseWithClaims(tokenSHA, &Claims{}, func(t *jwt.Token) (interface{}, error) {
|
||||
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
|
||||
}
|
||||
return setting.LFS.JWTSecretBytes, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
claims, claimsOk := token.Claims.(*Claims)
|
||||
if !token.Valid || !claimsOk {
|
||||
return nil, fmt.Errorf("invalid token claim")
|
||||
}
|
||||
|
||||
if claims.RepoID != target.ID {
|
||||
return nil, fmt.Errorf("invalid token claim")
|
||||
}
|
||||
|
||||
if mode == models.AccessModeWrite && claims.Op != "upload" {
|
||||
return nil, fmt.Errorf("invalid token claim")
|
||||
}
|
||||
|
||||
u, err := models.GetUserByID(claims.UserID)
|
||||
if err != nil {
|
||||
log.Error("Unable to GetUserById[%d]: Error: %v", claims.UserID, err)
|
||||
return nil, err
|
||||
}
|
||||
return u, nil
|
||||
}
|
||||
|
||||
func parseToken(authorization string, target *models.Repository, mode models.AccessMode) (*models.User, error) {
|
||||
if authorization == "" {
|
||||
return nil, nil, "unknown", fmt.Errorf("No token")
|
||||
}
|
||||
if strings.HasPrefix(authorization, "Bearer ") {
|
||||
token, err := jwt.ParseWithClaims(authorization[7:], &Claims{}, func(t *jwt.Token) (interface{}, error) {
|
||||
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
|
||||
}
|
||||
return setting.LFS.JWTSecretBytes, nil
|
||||
})
|
||||
if err != nil {
|
||||
// The error here is WARN level because it is caused by bad authorization rather than an internal server error
|
||||
return nil, nil, "unknown", err
|
||||
}
|
||||
claims, claimsOk := token.Claims.(*Claims)
|
||||
if !token.Valid || !claimsOk {
|
||||
return nil, nil, "unknown", fmt.Errorf("Token claim invalid")
|
||||
}
|
||||
r, err := models.GetRepositoryByID(claims.RepoID)
|
||||
if err != nil {
|
||||
log.Error("Unable to GetRepositoryById[%d]: Error: %v", claims.RepoID, err)
|
||||
return nil, nil, claims.Op, err
|
||||
}
|
||||
u, err := models.GetUserByID(claims.UserID)
|
||||
if err != nil {
|
||||
log.Error("Unable to GetUserById[%d]: Error: %v", claims.UserID, err)
|
||||
return nil, r, claims.Op, err
|
||||
}
|
||||
return u, r, claims.Op, nil
|
||||
return nil, fmt.Errorf("no token")
|
||||
}
|
||||
|
||||
if strings.HasPrefix(authorization, "Basic ") {
|
||||
c, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(authorization, "Basic "))
|
||||
if err != nil {
|
||||
return nil, nil, "basic", err
|
||||
}
|
||||
cs := string(c)
|
||||
i := strings.IndexByte(cs, ':')
|
||||
if i < 0 {
|
||||
return nil, nil, "basic", fmt.Errorf("Basic auth invalid")
|
||||
}
|
||||
user, password := cs[:i], cs[i+1:]
|
||||
u, err := models.GetUserByName(user)
|
||||
if err != nil {
|
||||
log.Error("Unable to GetUserByName[%d]: Error: %v", user, err)
|
||||
return nil, nil, "basic", err
|
||||
}
|
||||
if !u.IsPasswordSet() || !u.ValidatePassword(password) {
|
||||
return nil, nil, "basic", fmt.Errorf("Basic auth failed")
|
||||
}
|
||||
return u, nil, "basic", nil
|
||||
parts := strings.SplitN(authorization, " ", 2)
|
||||
if len(parts) != 2 {
|
||||
return nil, fmt.Errorf("no token")
|
||||
}
|
||||
|
||||
return nil, nil, "unknown", fmt.Errorf("Token not found")
|
||||
tokenSHA := parts[1]
|
||||
switch strings.ToLower(parts[0]) {
|
||||
case "bearer":
|
||||
fallthrough
|
||||
case "token":
|
||||
return handleLFSToken(tokenSHA, target, mode)
|
||||
}
|
||||
return nil, fmt.Errorf("token not found")
|
||||
}
|
||||
|
||||
func requireAuth(ctx *context.Context) {
|
||||
|
|
Reference in a new issue