Skip to content

Commit

Permalink
Require repo scope for PATs for private repos and basic authentication (
Browse files Browse the repository at this point in the history
#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]>
  • Loading branch information
3 people authored Apr 27, 2023
1 parent 89297c9 commit d2efd2b
Show file tree
Hide file tree
Showing 11 changed files with 117 additions and 7 deletions.
33 changes: 33 additions & 0 deletions modules/context/permission.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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 a personal access token but not oauth2 token
var scopeMatched bool
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
}
}
}
27 changes: 27 additions & 0 deletions routers/api/packages/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -35,6 +36,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 a personal 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")
Expand Down
5 changes: 5 additions & 0 deletions routers/web/repo/attachment.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,11 @@ func GetAttachment(ctx *context.Context) {
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())
Expand Down
11 changes: 8 additions & 3 deletions routers/web/repo/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -164,13 +164,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
}
Expand Down
1 change: 1 addition & 0 deletions services/auth/basic.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
20 changes: 20 additions & 0 deletions services/lfs/locks.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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")
Expand Down
15 changes: 15 additions & 0 deletions services/lfs/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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
}

Expand Down
3 changes: 2 additions & 1 deletion tests/integration/api_packages_npm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -26,7 +27,7 @@ func TestPackageNpm(t *testing.T) {
defer tests.PrepareTestEnv(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"
Expand Down
3 changes: 2 additions & 1 deletion tests/integration/api_packages_nuget_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down
3 changes: 2 additions & 1 deletion tests/integration/api_packages_pub_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -29,7 +30,7 @@ func TestPackagePub(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"
Expand Down
3 changes: 2 additions & 1 deletion tests/integration/api_packages_vagrant_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand Down

0 comments on commit d2efd2b

Please sign in to comment.