From e36e51df7f46d443ba516946982433ea35cee05b Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Mon, 24 Apr 2023 13:45:58 +0800 Subject: [PATCH 1/6] Add scoped token check for some web routes which allow basic auth --- modules/context/permission.go | 33 +++++++++++++++++++++++++++++++++ routers/web/repo/attachment.go | 5 +++++ routers/web/repo/http.go | 11 ++++++++--- services/auth/basic.go | 1 + services/lfs/locks.go | 20 ++++++++++++++++++++ services/lfs/server.go | 15 +++++++++++++++ 6 files changed, 82 insertions(+), 3 deletions(-) diff --git a/modules/context/permission.go b/modules/context/permission.go index 8cb5d09eb946f..924e9ab10f855 100644 --- a/modules/context/permission.go +++ b/modules/context/permission.go @@ -4,6 +4,10 @@ package context import ( + "net/http" + + auth_model "code.gitea.io/gitea/models/auth" + repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/modules/log" ) @@ -106,3 +110,32 @@ func RequireRepoReaderOr(unitTypes ...unit.Type) func(ctx *Context) { ctx.NotFound(ctx.Req.URL.RequestURI(), nil) } } + +// RequireRepoScopedToken check whether personal access token has repo scope +func CheckRepoScopedToken(ctx *Context, repo *repo_model.Repository) { + if !ctx.IsBasicAuth || ctx.Data["IsApiToken"] != true { + return + } + + var err error + scope, ok := ctx.Data["ApiTokenScope"].(auth_model.AccessTokenScope) + if ok { // it's peronsall access token but not oauth2 token + scopeMatched := false + scopeMatched, err = scope.HasScope(auth_model.AccessTokenScopeRepo) + if err != nil { + ctx.ServerError("HasScope", err) + return + } + if !scopeMatched && !repo.IsPrivate { + scopeMatched, err = scope.HasScope(auth_model.AccessTokenScopePublicRepo) + if err != nil { + ctx.ServerError("HasScope", err) + return + } + } + if !scopeMatched { + ctx.Error(http.StatusForbidden) + return + } + } +} diff --git a/routers/web/repo/attachment.go b/routers/web/repo/attachment.go index 9fb9cb00bf90c..c6ea4e3cdb0d8 100644 --- a/routers/web/repo/attachment.go +++ b/routers/web/repo/attachment.go @@ -110,6 +110,11 @@ func ServeAttachment(ctx *context.Context, uuid string) { return } } else { // If we have the repository we check access + context.CheckRepoScopedToken(ctx, repository) + if ctx.Written() { + return + } + perm, err := access_model.GetUserRepoPermission(ctx, repository, ctx.Doer) if err != nil { ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err.Error()) diff --git a/routers/web/repo/http.go b/routers/web/repo/http.go index a01bb4f28eef9..4e45a9b6e21df 100644 --- a/routers/web/repo/http.go +++ b/routers/web/repo/http.go @@ -19,7 +19,7 @@ import ( "time" actions_model "code.gitea.io/gitea/models/actions" - "code.gitea.io/gitea/models/auth" + auth_model "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/perm" access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" @@ -152,13 +152,18 @@ func httpBase(ctx *context.Context) (h *serviceHandler) { return } + context.CheckRepoScopedToken(ctx, repo) + if ctx.Written() { + return + } + if ctx.IsBasicAuth && ctx.Data["IsApiToken"] != true && ctx.Data["IsActionsToken"] != true { - _, err = auth.GetTwoFactorByUID(ctx.Doer.ID) + _, err = auth_model.GetTwoFactorByUID(ctx.Doer.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.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") return - } else if !auth.IsErrTwoFactorNotEnrolled(err) { + } else if !auth_model.IsErrTwoFactorNotEnrolled(err) { ctx.ServerError("IsErrTwoFactorNotEnrolled", err) return } diff --git a/services/auth/basic.go b/services/auth/basic.go index dc03780905c96..36480568ff0aa 100644 --- a/services/auth/basic.go +++ b/services/auth/basic.go @@ -102,6 +102,7 @@ func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore } store.GetData()["IsApiToken"] = true + store.GetData()["ApiTokenScope"] = token.Scope return u, nil } else if !auth_model.IsErrAccessTokenNotExist(err) && !auth_model.IsErrAccessTokenEmpty(err) { log.Error("GetAccessTokenBySha: %v", err) diff --git a/services/lfs/locks.go b/services/lfs/locks.go index d963d9ab574fb..1e5db6bd2014a 100644 --- a/services/lfs/locks.go +++ b/services/lfs/locks.go @@ -58,6 +58,11 @@ func GetListLockHandler(ctx *context.Context) { } repository.MustOwner(ctx) + context.CheckRepoScopedToken(ctx, repository) + if ctx.Written() { + return + } + authenticated := authenticate(ctx, repository, rv.Authorization, true, false) if !authenticated { ctx.Resp.Header().Set("WWW-Authenticate", "Basic realm=gitea-lfs") @@ -145,6 +150,11 @@ func PostLockHandler(ctx *context.Context) { } repository.MustOwner(ctx) + context.CheckRepoScopedToken(ctx, repository) + if ctx.Written() { + return + } + authenticated := authenticate(ctx, repository, authorization, true, true) if !authenticated { ctx.Resp.Header().Set("WWW-Authenticate", "Basic realm=gitea-lfs") @@ -212,6 +222,11 @@ func VerifyLockHandler(ctx *context.Context) { } repository.MustOwner(ctx) + context.CheckRepoScopedToken(ctx, repository) + if ctx.Written() { + return + } + authenticated := authenticate(ctx, repository, authorization, true, true) if !authenticated { ctx.Resp.Header().Set("WWW-Authenticate", "Basic realm=gitea-lfs") @@ -278,6 +293,11 @@ func UnLockHandler(ctx *context.Context) { } repository.MustOwner(ctx) + context.CheckRepoScopedToken(ctx, repository) + if ctx.Written() { + return + } + authenticated := authenticate(ctx, repository, authorization, true, true) if !authenticated { ctx.Resp.Header().Set("WWW-Authenticate", "Basic realm=gitea-lfs") diff --git a/services/lfs/server.go b/services/lfs/server.go index 44de9ba74f2b4..4c69e47512b6c 100644 --- a/services/lfs/server.go +++ b/services/lfs/server.go @@ -86,6 +86,11 @@ func DownloadHandler(ctx *context.Context) { return } + repository := getAuthenticatedRepository(ctx, rc, true) + if repository == nil { + return + } + // Support resume download using Range header var fromByte, toByte int64 toByte = meta.Size - 1 @@ -360,6 +365,11 @@ func VerifyHandler(ctx *context.Context) { return } + repository := getAuthenticatedRepository(ctx, rc, true) + if repository == nil { + return + } + contentStore := lfs_module.NewContentStore() ok, err := contentStore.Verify(meta.Pointer) @@ -423,6 +433,11 @@ func getAuthenticatedRepository(ctx *context.Context, rc *requestContext, requir return nil } + context.CheckRepoScopedToken(ctx, repository) + if ctx.Written() { + return nil + } + return repository } From 5fce8f8f001c586d55d441ed9fed219aa2dd68e2 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Mon, 24 Apr 2023 14:27:41 +0800 Subject: [PATCH 2/6] Add check for container --- routers/api/packages/api.go | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/routers/api/packages/api.go b/routers/api/packages/api.go index 8bf5dbab35996..4d4c2da683de6 100644 --- a/routers/api/packages/api.go +++ b/routers/api/packages/api.go @@ -9,6 +9,7 @@ import ( "regexp" "strings" + auth_model "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/perm" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/log" @@ -36,6 +37,32 @@ import ( func reqPackageAccess(accessMode perm.AccessMode) func(ctx *context.Context) { return func(ctx *context.Context) { + if ctx.Data["IsApiToken"] == true { + scope, ok := ctx.Data["ApiTokenScope"].(auth_model.AccessTokenScope) + if ok { // it's peronsall access token but not oauth2 token + scopeMatched := false + var err error + if accessMode == perm.AccessModeRead { + scopeMatched, err = scope.HasScope(auth_model.AccessTokenScopeReadPackage) + if err != nil { + ctx.Error(http.StatusInternalServerError, "HasScope", err.Error()) + return + } + } else if accessMode == perm.AccessModeWrite { + scopeMatched, err = scope.HasScope(auth_model.AccessTokenScopeWritePackage) + if err != nil { + ctx.Error(http.StatusInternalServerError, "HasScope", err.Error()) + return + } + } + if !scopeMatched { + ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="Gitea Package API"`) + ctx.Error(http.StatusUnauthorized, "reqPackageAccess", "user should have specific permission or be a site admin") + return + } + } + } + if ctx.Package.AccessMode < accessMode && !ctx.IsUserSiteAdmin() { ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="Gitea Package API"`) ctx.Error(http.StatusUnauthorized, "reqPackageAccess", "user should have specific permission or be a site admin") From cee35b411424f3172a2a352d3ce09edb2dd1e306 Mon Sep 17 00:00:00 2001 From: jolheiser Date: Wed, 26 Apr 2023 14:04:32 -0500 Subject: [PATCH 3/6] chore: typo Signed-off-by: jolheiser --- modules/context/permission.go | 2 +- routers/api/packages/api.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/context/permission.go b/modules/context/permission.go index 924e9ab10f855..920ace35090d8 100644 --- a/modules/context/permission.go +++ b/modules/context/permission.go @@ -119,7 +119,7 @@ func CheckRepoScopedToken(ctx *Context, repo *repo_model.Repository) { var err error scope, ok := ctx.Data["ApiTokenScope"].(auth_model.AccessTokenScope) - if ok { // it's peronsall access token but not oauth2 token + if ok { // it's a personal access token but not oauth2 token scopeMatched := false scopeMatched, err = scope.HasScope(auth_model.AccessTokenScopeRepo) if err != nil { diff --git a/routers/api/packages/api.go b/routers/api/packages/api.go index 4d4c2da683de6..d5acd3d261165 100644 --- a/routers/api/packages/api.go +++ b/routers/api/packages/api.go @@ -39,7 +39,7 @@ func reqPackageAccess(accessMode perm.AccessMode) func(ctx *context.Context) { return func(ctx *context.Context) { if ctx.Data["IsApiToken"] == true { scope, ok := ctx.Data["ApiTokenScope"].(auth_model.AccessTokenScope) - if ok { // it's peronsall access token but not oauth2 token + if ok { // it's a personal access token but not oauth2 token scopeMatched := false var err error if accessMode == perm.AccessModeRead { From 3d9cefa89b720a94ae05671eceb45914e5fe7cb9 Mon Sep 17 00:00:00 2001 From: jolheiser Date: Wed, 26 Apr 2023 14:20:04 -0500 Subject: [PATCH 4/6] chore: placate linter Signed-off-by: jolheiser --- modules/context/permission.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/context/permission.go b/modules/context/permission.go index 920ace35090d8..cc53fb99ed747 100644 --- a/modules/context/permission.go +++ b/modules/context/permission.go @@ -120,7 +120,7 @@ func CheckRepoScopedToken(ctx *Context, repo *repo_model.Repository) { var err error scope, ok := ctx.Data["ApiTokenScope"].(auth_model.AccessTokenScope) if ok { // it's a personal access token but not oauth2 token - scopeMatched := false + var scopeMatched bool scopeMatched, err = scope.HasScope(auth_model.AccessTokenScopeRepo) if err != nil { ctx.ServerError("HasScope", err) From 1aadb787f9e0716605b09e504721628021d56492 Mon Sep 17 00:00:00 2001 From: jolheiser Date: Wed, 26 Apr 2023 15:08:03 -0500 Subject: [PATCH 5/6] chore: add token scope to npm Signed-off-by: jolheiser --- tests/integration/api_packages_npm_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/integration/api_packages_npm_test.go b/tests/integration/api_packages_npm_test.go index 28c14fb3b872a..78389b5740af3 100644 --- a/tests/integration/api_packages_npm_test.go +++ b/tests/integration/api_packages_npm_test.go @@ -11,6 +11,7 @@ import ( "strings" "testing" + auth_model "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/packages" "code.gitea.io/gitea/models/unittest" @@ -27,7 +28,7 @@ func TestPackageNpm(t *testing.T) { user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) - token := fmt.Sprintf("Bearer %s", getTokenForLoggedInUser(t, loginUser(t, user.Name))) + token := fmt.Sprintf("Bearer %s", getTokenForLoggedInUser(t, loginUser(t, user.Name), auth_model.AccessTokenScopePackage)) packageName := "@scope/test-package" packageVersion := "1.0.1-pre" From f27aaa228df378cffcf3594cf118881eb98f43aa Mon Sep 17 00:00:00 2001 From: jolheiser Date: Wed, 26 Apr 2023 15:31:33 -0500 Subject: [PATCH 6/6] chore: fix token scopes for other packages Signed-off-by: jolheiser --- tests/integration/api_packages_nuget_test.go | 3 ++- tests/integration/api_packages_pub_test.go | 3 ++- tests/integration/api_packages_vagrant_test.go | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/integration/api_packages_nuget_test.go b/tests/integration/api_packages_nuget_test.go index a74d696f03415..2240d2a5d4a67 100644 --- a/tests/integration/api_packages_nuget_test.go +++ b/tests/integration/api_packages_nuget_test.go @@ -16,6 +16,7 @@ import ( "testing" "time" + auth_model "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/packages" "code.gitea.io/gitea/models/unittest" @@ -74,7 +75,7 @@ func TestPackageNuGet(t *testing.T) { } user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) - token := getUserToken(t, user.Name) + token := getUserToken(t, user.Name, auth_model.AccessTokenScopePackage) packageName := "test.package" packageVersion := "1.0.3" diff --git a/tests/integration/api_packages_pub_test.go b/tests/integration/api_packages_pub_test.go index 4d4ce12402725..5c1cc6052f179 100644 --- a/tests/integration/api_packages_pub_test.go +++ b/tests/integration/api_packages_pub_test.go @@ -15,6 +15,7 @@ import ( "testing" "time" + auth_model "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/packages" "code.gitea.io/gitea/models/unittest" @@ -30,7 +31,7 @@ func TestPackagePub(t *testing.T) { user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) - token := "Bearer " + getUserToken(t, user.Name) + token := "Bearer " + getUserToken(t, user.Name, auth_model.AccessTokenScopePackage) packageName := "test_package" packageVersion := "1.0.1" diff --git a/tests/integration/api_packages_vagrant_test.go b/tests/integration/api_packages_vagrant_test.go index b4f04b0c89368..b28bfca6f08d9 100644 --- a/tests/integration/api_packages_vagrant_test.go +++ b/tests/integration/api_packages_vagrant_test.go @@ -12,6 +12,7 @@ import ( "strings" "testing" + auth_model "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/packages" "code.gitea.io/gitea/models/unittest" @@ -27,7 +28,7 @@ func TestPackageVagrant(t *testing.T) { defer tests.PrepareTestEnv(t)() user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) - token := "Bearer " + getUserToken(t, user.Name) + token := "Bearer " + getUserToken(t, user.Name, auth_model.AccessTokenScopePackage) packageName := "test_package" packageVersion := "1.0.1"