Skip to content

Commit 301ec6d

Browse files
zeripathAbdulrhmnGhanem
authored andcommitted
Prevent double-login for Git HTTP and LFS and simplify login (go-gitea#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>
1 parent 6abd9dd commit 301ec6d

File tree

10 files changed

+288
-217
lines changed

10 files changed

+288
-217
lines changed

integrations/api_repo_lfs_locks_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ func TestAPILFSLocksNotLogin(t *testing.T) {
4444
resp := MakeRequest(t, req, http.StatusUnauthorized)
4545
var lfsLockError api.LFSLockError
4646
DecodeJSON(t, resp, &lfsLockError)
47-
assert.Equal(t, "Unauthorized", lfsLockError.Message)
47+
assert.Equal(t, "You must have pull access to list locks", lfsLockError.Message)
4848
}
4949

5050
func TestAPILFSLocksLogged(t *testing.T) {

modules/auth/sso/basic.go

+27-18
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"code.gitea.io/gitea/modules/log"
1515
"code.gitea.io/gitea/modules/setting"
1616
"code.gitea.io/gitea/modules/timeutil"
17+
"code.gitea.io/gitea/modules/web/middleware"
1718
)
1819

1920
// Ensure the struct implements the interface.
@@ -40,25 +41,30 @@ func (b *Basic) Free() error {
4041
// IsEnabled returns true as this plugin is enabled by default and its not possible to disable
4142
// it from settings.
4243
func (b *Basic) IsEnabled() bool {
43-
return setting.Service.EnableBasicAuth
44+
return true
4445
}
4546

4647
// VerifyAuthData extracts and validates Basic data (username and password/token) from the
4748
// "Authorization" header of the request and returns the corresponding user object for that
4849
// name/token on successful validation.
4950
// Returns nil if header is empty or validation fails.
5051
func (b *Basic) VerifyAuthData(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) *models.User {
52+
53+
// Basic authentication should only fire on API, Download or on Git or LFSPaths
54+
if middleware.IsInternalPath(req) || !middleware.IsAPIPath(req) && !isAttachmentDownload(req) && !isGitOrLFSPath(req) {
55+
return nil
56+
}
57+
5158
baHead := req.Header.Get("Authorization")
5259
if len(baHead) == 0 {
5360
return nil
5461
}
5562

56-
auths := strings.Fields(baHead)
63+
auths := strings.SplitN(baHead, " ", 2)
5764
if len(auths) != 2 || (auths[0] != "Basic" && auths[0] != "basic") {
5865
return nil
5966
}
6067

61-
var u *models.User
6268
uname, passwd, _ := base.BasicAuthDecode(auths[1])
6369

6470
// Check if username or password is a token
@@ -76,20 +82,21 @@ func (b *Basic) VerifyAuthData(req *http.Request, w http.ResponseWriter, store D
7682
uid := CheckOAuthAccessToken(authToken)
7783
if uid != 0 {
7884
log.Trace("Basic Authorization: Valid OAuthAccessToken for user[%d]", uid)
79-
var err error
80-
store.GetData()["IsApiToken"] = true
8185

82-
u, err = models.GetUserByID(uid)
86+
u, err := models.GetUserByID(uid)
8387
if err != nil {
8488
log.Error("GetUserByID: %v", err)
8589
return nil
8690
}
91+
92+
store.GetData()["IsApiToken"] = true
93+
return u
8794
}
95+
8896
token, err := models.GetAccessTokenBySHA(authToken)
8997
if err == nil {
9098
log.Trace("Basic Authorization: Valid AccessToken for user[%d]", uid)
91-
92-
u, err = models.GetUserByID(token.UID)
99+
u, err := models.GetUserByID(token.UID)
93100
if err != nil {
94101
log.Error("GetUserByID: %v", err)
95102
return nil
@@ -99,22 +106,24 @@ func (b *Basic) VerifyAuthData(req *http.Request, w http.ResponseWriter, store D
99106
if err = models.UpdateAccessToken(token); err != nil {
100107
log.Error("UpdateAccessToken: %v", err)
101108
}
109+
110+
store.GetData()["IsApiToken"] = true
111+
return u
102112
} else if !models.IsErrAccessTokenNotExist(err) && !models.IsErrAccessTokenEmpty(err) {
103113
log.Error("GetAccessTokenBySha: %v", err)
104114
}
105115

106-
if u == nil {
107-
log.Trace("Basic Authorization: Attempting SignIn for %s", uname)
116+
if !setting.Service.EnableBasicAuth {
117+
return nil
118+
}
108119

109-
u, err = models.UserSignIn(uname, passwd)
110-
if err != nil {
111-
if !models.IsErrUserNotExist(err) {
112-
log.Error("UserSignIn: %v", err)
113-
}
114-
return nil
120+
log.Trace("Basic Authorization: Attempting SignIn for %s", uname)
121+
u, err := models.UserSignIn(uname, passwd)
122+
if err != nil {
123+
if !models.IsErrUserNotExist(err) {
124+
log.Error("UserSignIn: %v", err)
115125
}
116-
} else {
117-
store.GetData()["IsApiToken"] = true
126+
return nil
118127
}
119128

120129
log.Trace("Basic Authorization: Logged in user %-v", u)

modules/auth/sso/sso.go

+16-1
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@ import (
99
"fmt"
1010
"net/http"
1111
"reflect"
12+
"regexp"
1213
"strings"
1314

1415
"code.gitea.io/gitea/models"
1516
"code.gitea.io/gitea/modules/log"
17+
"code.gitea.io/gitea/modules/setting"
1618
"code.gitea.io/gitea/modules/web/middleware"
1719
)
1820

@@ -27,9 +29,9 @@ import (
2729
// for users that have already signed in.
2830
var ssoMethods = []SingleSignOn{
2931
&OAuth2{},
32+
&Basic{},
3033
&Session{},
3134
&ReverseProxy{},
32-
&Basic{},
3335
}
3436

3537
// 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 {
102104
return strings.HasPrefix(req.URL.Path, "/attachments/") && req.Method == "GET"
103105
}
104106

107+
var gitPathRe = regexp.MustCompile(`^/[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+/(?:(?:git-(?:(?:upload)|(?:receive))-pack$)|(?:info/refs$)|(?:HEAD$)|(?:objects/))`)
108+
var lfsPathRe = regexp.MustCompile(`^/[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+/info/lfs/`)
109+
110+
func isGitOrLFSPath(req *http.Request) bool {
111+
if gitPathRe.MatchString(req.URL.Path) {
112+
return true
113+
}
114+
if setting.LFS.StartServer {
115+
return lfsPathRe.MatchString(req.URL.Path)
116+
}
117+
return false
118+
}
119+
105120
// handleSignIn clears existing session variables and stores new ones for the specified user object
106121
func handleSignIn(resp http.ResponseWriter, req *http.Request, sess SessionStore, user *models.User) {
107122
_ = sess.Delete("openid_verified_uri")

modules/auth/sso/sso_test.go

+124
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
// Copyright 2014 The Gogs Authors. All rights reserved.
2+
// Copyright 2019 The Gitea Authors. All rights reserved.
3+
// Use of this source code is governed by a MIT-style
4+
// license that can be found in the LICENSE file.
5+
6+
package sso
7+
8+
import (
9+
"net/http"
10+
"testing"
11+
12+
"code.gitea.io/gitea/modules/setting"
13+
)
14+
15+
func Test_isGitOrLFSPath(t *testing.T) {
16+
17+
tests := []struct {
18+
path string
19+
20+
want bool
21+
}{
22+
{
23+
"/owner/repo/git-upload-pack",
24+
true,
25+
},
26+
{
27+
"/owner/repo/git-receive-pack",
28+
true,
29+
},
30+
{
31+
"/owner/repo/info/refs",
32+
true,
33+
},
34+
{
35+
"/owner/repo/HEAD",
36+
true,
37+
},
38+
{
39+
"/owner/repo/objects/info/alternates",
40+
true,
41+
},
42+
{
43+
"/owner/repo/objects/info/http-alternates",
44+
true,
45+
},
46+
{
47+
"/owner/repo/objects/info/packs",
48+
true,
49+
},
50+
{
51+
"/owner/repo/objects/info/blahahsdhsdkla",
52+
true,
53+
},
54+
{
55+
"/owner/repo/objects/01/23456789abcdef0123456789abcdef01234567",
56+
true,
57+
},
58+
{
59+
"/owner/repo/objects/pack/pack-123456789012345678921234567893124567894.pack",
60+
true,
61+
},
62+
{
63+
"/owner/repo/objects/pack/pack-0123456789abcdef0123456789abcdef0123456.idx",
64+
true,
65+
},
66+
{
67+
"/owner/repo/stars",
68+
false,
69+
},
70+
{
71+
"/notowner",
72+
false,
73+
},
74+
{
75+
"/owner/repo",
76+
false,
77+
},
78+
{
79+
"/owner/repo/commit/123456789012345678921234567893124567894",
80+
false,
81+
},
82+
}
83+
lfsTests := []string{
84+
"/owner/repo/info/lfs/",
85+
"/owner/repo/info/lfs/objects/batch",
86+
"/owner/repo/info/lfs/objects/oid/filename",
87+
"/owner/repo/info/lfs/objects/oid",
88+
"/owner/repo/info/lfs/objects",
89+
"/owner/repo/info/lfs/verify",
90+
"/owner/repo/info/lfs/locks",
91+
"/owner/repo/info/lfs/locks/verify",
92+
"/owner/repo/info/lfs/locks/123/unlock",
93+
}
94+
95+
origLFSStartServer := setting.LFS.StartServer
96+
97+
for _, tt := range tests {
98+
t.Run(tt.path, func(t *testing.T) {
99+
req, _ := http.NewRequest("POST", "http://localhost"+tt.path, nil)
100+
setting.LFS.StartServer = false
101+
if got := isGitOrLFSPath(req); got != tt.want {
102+
t.Errorf("isGitOrLFSPath() = %v, want %v", got, tt.want)
103+
}
104+
setting.LFS.StartServer = true
105+
if got := isGitOrLFSPath(req); got != tt.want {
106+
t.Errorf("isGitOrLFSPath() = %v, want %v", got, tt.want)
107+
}
108+
})
109+
}
110+
for _, tt := range lfsTests {
111+
t.Run(tt, func(t *testing.T) {
112+
req, _ := http.NewRequest("POST", tt, nil)
113+
setting.LFS.StartServer = false
114+
if got := isGitOrLFSPath(req); got != setting.LFS.StartServer {
115+
t.Errorf("isGitOrLFSPath(%q) = %v, want %v, %v", tt, got, setting.LFS.StartServer, gitPathRe.MatchString(tt))
116+
}
117+
setting.LFS.StartServer = true
118+
if got := isGitOrLFSPath(req); got != setting.LFS.StartServer {
119+
t.Errorf("isGitOrLFSPath(%q) = %v, want %v", tt, got, setting.LFS.StartServer)
120+
}
121+
})
122+
}
123+
setting.LFS.StartServer = origLFSStartServer
124+
}

modules/context/context.go

+3
Original file line numberDiff line numberDiff line change
@@ -683,6 +683,9 @@ func Contexter() func(next http.Handler) http.Handler {
683683
} else {
684684
ctx.Data["SignedUserID"] = int64(0)
685685
ctx.Data["SignedUserName"] = ""
686+
687+
// ensure the session uid is deleted
688+
_ = ctx.Session.Delete("uid")
686689
}
687690

688691
ctx.Resp.Header().Set(`X-Frame-Options`, `SAMEORIGIN`)

routers/private/internal.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import (
2323
func CheckInternalToken(next http.Handler) http.Handler {
2424
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
2525
tokens := req.Header.Get("Authorization")
26-
fields := strings.Fields(tokens)
26+
fields := strings.SplitN(tokens, " ", 2)
2727
if len(fields) != 2 || fields[0] != "Bearer" || fields[1] != setting.InternalToken {
2828
log.Debug("Forbidden attempt to access internal url: Authorization header: %s", tokens)
2929
http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)

0 commit comments

Comments
 (0)