mirror of
				https://gitee.com/gitea/gitea
				synced 2025-11-04 08:30:25 +08:00 
			
		
		
		
	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>
This commit is contained in:
		@@ -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 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
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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 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")
 | 
			
		||||
 
 | 
			
		||||
@@ -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())
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
			}
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
 
 | 
			
		||||
@@ -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")
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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"
 | 
			
		||||
 
 | 
			
		||||
@@ -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"
 | 
			
		||||
 
 | 
			
		||||
@@ -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"
 | 
			
		||||
 
 | 
			
		||||
@@ -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"
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user