Skip to content

Commit d2efd2b

Browse files
GiteaBotjolheiserlunny
authored
Require repo scope for PATs for private repos and basic authentication (#24362) (#24364)
Backport #24362 by @jolheiser > 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 <[email protected]> Co-authored-by: John Olheiser <[email protected]> Co-authored-by: Lunny Xiao <[email protected]>
1 parent 89297c9 commit d2efd2b

File tree

11 files changed

+117
-7
lines changed

11 files changed

+117
-7
lines changed

modules/context/permission.go

Lines changed: 33 additions & 0 deletions
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

Lines changed: 27 additions & 0 deletions
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"
@@ -35,6 +36,32 @@ import (
3536

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

routers/web/repo/attachment.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,11 @@ func GetAttachment(ctx *context.Context) {
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

Lines changed: 8 additions & 3 deletions
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"
@@ -164,13 +164,18 @@ func httpBase(ctx *context.Context) (h *serviceHandler) {
164164
return
165165
}
166166

167+
context.CheckRepoScopedToken(ctx, repo)
168+
if ctx.Written() {
169+
return
170+
}
171+
167172
if ctx.IsBasicAuth && ctx.Data["IsApiToken"] != true && ctx.Data["IsActionsToken"] != true {
168-
_, err = auth.GetTwoFactorByUID(ctx.Doer.ID)
173+
_, err = auth_model.GetTwoFactorByUID(ctx.Doer.ID)
169174
if err == nil {
170175
// 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
171176
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")
172177
return
173-
} else if !auth.IsErrTwoFactorNotEnrolled(err) {
178+
} else if !auth_model.IsErrTwoFactorNotEnrolled(err) {
174179
ctx.ServerError("IsErrTwoFactorNotEnrolled", err)
175180
return
176181
}

services/auth/basic.go

Lines changed: 1 addition & 0 deletions
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

Lines changed: 20 additions & 0 deletions
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

Lines changed: 15 additions & 0 deletions
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

Lines changed: 2 additions & 1 deletion
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"
@@ -26,7 +27,7 @@ func TestPackageNpm(t *testing.T) {
2627
defer tests.PrepareTestEnv(t)()
2728
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
2829

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

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

tests/integration/api_packages_nuget_test.go

Lines changed: 2 additions & 1 deletion
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

Lines changed: 2 additions & 1 deletion
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"
@@ -29,7 +30,7 @@ func TestPackagePub(t *testing.T) {
2930
defer tests.PrepareTestEnv(t)()
3031
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
3132

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

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

tests/integration/api_packages_vagrant_test.go

Lines changed: 2 additions & 1 deletion
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)