Skip to content

Commit 5e36024

Browse files
jolheiserlunny
andauthored
Require repo scope for PATs for private repos and basic authentication (#24362)
> The scoped token PR just checked all API routes but in fact, some web routes like `LFS`, git `HTTP`, container, and attachments supports basic auth. This PR added scoped token check for them. --------- Signed-off-by: jolheiser <john.olheiser@gmail.com> Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
1 parent 8f57aa0 commit 5e36024

File tree

11 files changed

+117
-7
lines changed

11 files changed

+117
-7
lines changed

modules/context/permission.go

+33
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
package context
55

66
import (
7+
"net/http"
8+
9+
auth_model "code.gitea.io/gitea/models/auth"
10+
repo_model "code.gitea.io/gitea/models/repo"
711
"code.gitea.io/gitea/models/unit"
812
"code.gitea.io/gitea/modules/log"
913
)
@@ -106,3 +110,32 @@ func RequireRepoReaderOr(unitTypes ...unit.Type) func(ctx *Context) {
106110
ctx.NotFound(ctx.Req.URL.RequestURI(), nil)
107111
}
108112
}
113+
114+
// RequireRepoScopedToken check whether personal access token has repo scope
115+
func CheckRepoScopedToken(ctx *Context, repo *repo_model.Repository) {
116+
if !ctx.IsBasicAuth || ctx.Data["IsApiToken"] != true {
117+
return
118+
}
119+
120+
var err error
121+
scope, ok := ctx.Data["ApiTokenScope"].(auth_model.AccessTokenScope)
122+
if ok { // it's a personal access token but not oauth2 token
123+
var scopeMatched bool
124+
scopeMatched, err = scope.HasScope(auth_model.AccessTokenScopeRepo)
125+
if err != nil {
126+
ctx.ServerError("HasScope", err)
127+
return
128+
}
129+
if !scopeMatched && !repo.IsPrivate {
130+
scopeMatched, err = scope.HasScope(auth_model.AccessTokenScopePublicRepo)
131+
if err != nil {
132+
ctx.ServerError("HasScope", err)
133+
return
134+
}
135+
}
136+
if !scopeMatched {
137+
ctx.Error(http.StatusForbidden)
138+
return
139+
}
140+
}
141+
}

routers/api/packages/api.go

+27
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"regexp"
1010
"strings"
1111

12+
auth_model "code.gitea.io/gitea/models/auth"
1213
"code.gitea.io/gitea/models/perm"
1314
"code.gitea.io/gitea/modules/context"
1415
"code.gitea.io/gitea/modules/log"
@@ -36,6 +37,32 @@ import (
3637

3738
func reqPackageAccess(accessMode perm.AccessMode) func(ctx *context.Context) {
3839
return func(ctx *context.Context) {
40+
if ctx.Data["IsApiToken"] == true {
41+
scope, ok := ctx.Data["ApiTokenScope"].(auth_model.AccessTokenScope)
42+
if ok { // it's a personal access token but not oauth2 token
43+
scopeMatched := false
44+
var err error
45+
if accessMode == perm.AccessModeRead {
46+
scopeMatched, err = scope.HasScope(auth_model.AccessTokenScopeReadPackage)
47+
if err != nil {
48+
ctx.Error(http.StatusInternalServerError, "HasScope", err.Error())
49+
return
50+
}
51+
} else if accessMode == perm.AccessModeWrite {
52+
scopeMatched, err = scope.HasScope(auth_model.AccessTokenScopeWritePackage)
53+
if err != nil {
54+
ctx.Error(http.StatusInternalServerError, "HasScope", err.Error())
55+
return
56+
}
57+
}
58+
if !scopeMatched {
59+
ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="Gitea Package API"`)
60+
ctx.Error(http.StatusUnauthorized, "reqPackageAccess", "user should have specific permission or be a site admin")
61+
return
62+
}
63+
}
64+
}
65+
3966
if ctx.Package.AccessMode < accessMode && !ctx.IsUserSiteAdmin() {
4067
ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="Gitea Package API"`)
4168
ctx.Error(http.StatusUnauthorized, "reqPackageAccess", "user should have specific permission or be a site admin")

routers/web/repo/attachment.go

+5
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,11 @@ func ServeAttachment(ctx *context.Context, uuid string) {
110110
return
111111
}
112112
} else { // If we have the repository we check access
113+
context.CheckRepoScopedToken(ctx, repository)
114+
if ctx.Written() {
115+
return
116+
}
117+
113118
perm, err := access_model.GetUserRepoPermission(ctx, repository, ctx.Doer)
114119
if err != nil {
115120
ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err.Error())

routers/web/repo/http.go

+8-3
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import (
1919
"time"
2020

2121
actions_model "code.gitea.io/gitea/models/actions"
22-
"code.gitea.io/gitea/models/auth"
22+
auth_model "code.gitea.io/gitea/models/auth"
2323
"code.gitea.io/gitea/models/perm"
2424
access_model "code.gitea.io/gitea/models/perm/access"
2525
repo_model "code.gitea.io/gitea/models/repo"
@@ -152,13 +152,18 @@ func httpBase(ctx *context.Context) (h *serviceHandler) {
152152
return
153153
}
154154

155+
context.CheckRepoScopedToken(ctx, repo)
156+
if ctx.Written() {
157+
return
158+
}
159+
155160
if ctx.IsBasicAuth && ctx.Data["IsApiToken"] != true && ctx.Data["IsActionsToken"] != true {
156-
_, err = auth.GetTwoFactorByUID(ctx.Doer.ID)
161+
_, err = auth_model.GetTwoFactorByUID(ctx.Doer.ID)
157162
if err == nil {
158163
// 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
159164
ctx.PlainText(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")
160165
return
161-
} else if !auth.IsErrTwoFactorNotEnrolled(err) {
166+
} else if !auth_model.IsErrTwoFactorNotEnrolled(err) {
162167
ctx.ServerError("IsErrTwoFactorNotEnrolled", err)
163168
return
164169
}

services/auth/basic.go

+1
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore
102102
}
103103

104104
store.GetData()["IsApiToken"] = true
105+
store.GetData()["ApiTokenScope"] = token.Scope
105106
return u, nil
106107
} else if !auth_model.IsErrAccessTokenNotExist(err) && !auth_model.IsErrAccessTokenEmpty(err) {
107108
log.Error("GetAccessTokenBySha: %v", err)

services/lfs/locks.go

+20
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ func GetListLockHandler(ctx *context.Context) {
5858
}
5959
repository.MustOwner(ctx)
6060

61+
context.CheckRepoScopedToken(ctx, repository)
62+
if ctx.Written() {
63+
return
64+
}
65+
6166
authenticated := authenticate(ctx, repository, rv.Authorization, true, false)
6267
if !authenticated {
6368
ctx.Resp.Header().Set("WWW-Authenticate", "Basic realm=gitea-lfs")
@@ -145,6 +150,11 @@ func PostLockHandler(ctx *context.Context) {
145150
}
146151
repository.MustOwner(ctx)
147152

153+
context.CheckRepoScopedToken(ctx, repository)
154+
if ctx.Written() {
155+
return
156+
}
157+
148158
authenticated := authenticate(ctx, repository, authorization, true, true)
149159
if !authenticated {
150160
ctx.Resp.Header().Set("WWW-Authenticate", "Basic realm=gitea-lfs")
@@ -212,6 +222,11 @@ func VerifyLockHandler(ctx *context.Context) {
212222
}
213223
repository.MustOwner(ctx)
214224

225+
context.CheckRepoScopedToken(ctx, repository)
226+
if ctx.Written() {
227+
return
228+
}
229+
215230
authenticated := authenticate(ctx, repository, authorization, true, true)
216231
if !authenticated {
217232
ctx.Resp.Header().Set("WWW-Authenticate", "Basic realm=gitea-lfs")
@@ -278,6 +293,11 @@ func UnLockHandler(ctx *context.Context) {
278293
}
279294
repository.MustOwner(ctx)
280295

296+
context.CheckRepoScopedToken(ctx, repository)
297+
if ctx.Written() {
298+
return
299+
}
300+
281301
authenticated := authenticate(ctx, repository, authorization, true, true)
282302
if !authenticated {
283303
ctx.Resp.Header().Set("WWW-Authenticate", "Basic realm=gitea-lfs")

services/lfs/server.go

+15
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,11 @@ func DownloadHandler(ctx *context.Context) {
8686
return
8787
}
8888

89+
repository := getAuthenticatedRepository(ctx, rc, true)
90+
if repository == nil {
91+
return
92+
}
93+
8994
// Support resume download using Range header
9095
var fromByte, toByte int64
9196
toByte = meta.Size - 1
@@ -360,6 +365,11 @@ func VerifyHandler(ctx *context.Context) {
360365
return
361366
}
362367

368+
repository := getAuthenticatedRepository(ctx, rc, true)
369+
if repository == nil {
370+
return
371+
}
372+
363373
contentStore := lfs_module.NewContentStore()
364374
ok, err := contentStore.Verify(meta.Pointer)
365375

@@ -423,6 +433,11 @@ func getAuthenticatedRepository(ctx *context.Context, rc *requestContext, requir
423433
return nil
424434
}
425435

436+
context.CheckRepoScopedToken(ctx, repository)
437+
if ctx.Written() {
438+
return nil
439+
}
440+
426441
return repository
427442
}
428443

tests/integration/api_packages_npm_test.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"strings"
1212
"testing"
1313

14+
auth_model "code.gitea.io/gitea/models/auth"
1415
"code.gitea.io/gitea/models/db"
1516
"code.gitea.io/gitea/models/packages"
1617
"code.gitea.io/gitea/models/unittest"
@@ -27,7 +28,7 @@ func TestPackageNpm(t *testing.T) {
2728

2829
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
2930

30-
token := fmt.Sprintf("Bearer %s", getTokenForLoggedInUser(t, loginUser(t, user.Name)))
31+
token := fmt.Sprintf("Bearer %s", getTokenForLoggedInUser(t, loginUser(t, user.Name), auth_model.AccessTokenScopePackage))
3132

3233
packageName := "@scope/test-package"
3334
packageVersion := "1.0.1-pre"

tests/integration/api_packages_nuget_test.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"testing"
1717
"time"
1818

19+
auth_model "code.gitea.io/gitea/models/auth"
1920
"code.gitea.io/gitea/models/db"
2021
"code.gitea.io/gitea/models/packages"
2122
"code.gitea.io/gitea/models/unittest"
@@ -74,7 +75,7 @@ func TestPackageNuGet(t *testing.T) {
7475
}
7576

7677
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
77-
token := getUserToken(t, user.Name)
78+
token := getUserToken(t, user.Name, auth_model.AccessTokenScopePackage)
7879

7980
packageName := "test.package"
8081
packageVersion := "1.0.3"

tests/integration/api_packages_pub_test.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"testing"
1616
"time"
1717

18+
auth_model "code.gitea.io/gitea/models/auth"
1819
"code.gitea.io/gitea/models/db"
1920
"code.gitea.io/gitea/models/packages"
2021
"code.gitea.io/gitea/models/unittest"
@@ -30,7 +31,7 @@ func TestPackagePub(t *testing.T) {
3031

3132
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
3233

33-
token := "Bearer " + getUserToken(t, user.Name)
34+
token := "Bearer " + getUserToken(t, user.Name, auth_model.AccessTokenScopePackage)
3435

3536
packageName := "test_package"
3637
packageVersion := "1.0.1"

tests/integration/api_packages_vagrant_test.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"strings"
1313
"testing"
1414

15+
auth_model "code.gitea.io/gitea/models/auth"
1516
"code.gitea.io/gitea/models/db"
1617
"code.gitea.io/gitea/models/packages"
1718
"code.gitea.io/gitea/models/unittest"
@@ -27,7 +28,7 @@ func TestPackageVagrant(t *testing.T) {
2728
defer tests.PrepareTestEnv(t)()
2829
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
2930

30-
token := "Bearer " + getUserToken(t, user.Name)
31+
token := "Bearer " + getUserToken(t, user.Name, auth_model.AccessTokenScopePackage)
3132

3233
packageName := "test_package"
3334
packageVersion := "1.0.1"

0 commit comments

Comments
 (0)