forked from gitea/gitea
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:
parent
8f57aa014b
commit
5e36024105
|
@ -4,6 +4,10 @@
|
||||||
package context
|
package context
|
||||||
|
|
||||||
import (
|
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/models/unit"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"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)
|
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"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
auth_model "code.gitea.io/gitea/models/auth"
|
||||||
"code.gitea.io/gitea/models/perm"
|
"code.gitea.io/gitea/models/perm"
|
||||||
"code.gitea.io/gitea/modules/context"
|
"code.gitea.io/gitea/modules/context"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
@ -36,6 +37,32 @@ import (
|
||||||
|
|
||||||
func reqPackageAccess(accessMode perm.AccessMode) func(ctx *context.Context) {
|
func reqPackageAccess(accessMode perm.AccessMode) func(ctx *context.Context) {
|
||||||
return 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() {
|
if ctx.Package.AccessMode < accessMode && !ctx.IsUserSiteAdmin() {
|
||||||
ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="Gitea Package API"`)
|
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")
|
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
|
return
|
||||||
}
|
}
|
||||||
} else { // If we have the repository we check access
|
} 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)
|
perm, err := access_model.GetUserRepoPermission(ctx, repository, ctx.Doer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err.Error())
|
ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err.Error())
|
||||||
|
|
|
@ -19,7 +19,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
actions_model "code.gitea.io/gitea/models/actions"
|
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"
|
"code.gitea.io/gitea/models/perm"
|
||||||
access_model "code.gitea.io/gitea/models/perm/access"
|
access_model "code.gitea.io/gitea/models/perm/access"
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
|
@ -152,13 +152,18 @@ func httpBase(ctx *context.Context) (h *serviceHandler) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
context.CheckRepoScopedToken(ctx, repo)
|
||||||
|
if ctx.Written() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if ctx.IsBasicAuth && ctx.Data["IsApiToken"] != true && ctx.Data["IsActionsToken"] != true {
|
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 {
|
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
|
// 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")
|
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
|
return
|
||||||
} else if !auth.IsErrTwoFactorNotEnrolled(err) {
|
} else if !auth_model.IsErrTwoFactorNotEnrolled(err) {
|
||||||
ctx.ServerError("IsErrTwoFactorNotEnrolled", err)
|
ctx.ServerError("IsErrTwoFactorNotEnrolled", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -102,6 +102,7 @@ func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore
|
||||||
}
|
}
|
||||||
|
|
||||||
store.GetData()["IsApiToken"] = true
|
store.GetData()["IsApiToken"] = true
|
||||||
|
store.GetData()["ApiTokenScope"] = token.Scope
|
||||||
return u, nil
|
return u, nil
|
||||||
} else if !auth_model.IsErrAccessTokenNotExist(err) && !auth_model.IsErrAccessTokenEmpty(err) {
|
} else if !auth_model.IsErrAccessTokenNotExist(err) && !auth_model.IsErrAccessTokenEmpty(err) {
|
||||||
log.Error("GetAccessTokenBySha: %v", err)
|
log.Error("GetAccessTokenBySha: %v", err)
|
||||||
|
|
|
@ -58,6 +58,11 @@ func GetListLockHandler(ctx *context.Context) {
|
||||||
}
|
}
|
||||||
repository.MustOwner(ctx)
|
repository.MustOwner(ctx)
|
||||||
|
|
||||||
|
context.CheckRepoScopedToken(ctx, repository)
|
||||||
|
if ctx.Written() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
authenticated := authenticate(ctx, repository, rv.Authorization, true, false)
|
authenticated := authenticate(ctx, repository, rv.Authorization, true, false)
|
||||||
if !authenticated {
|
if !authenticated {
|
||||||
ctx.Resp.Header().Set("WWW-Authenticate", "Basic realm=gitea-lfs")
|
ctx.Resp.Header().Set("WWW-Authenticate", "Basic realm=gitea-lfs")
|
||||||
|
@ -145,6 +150,11 @@ func PostLockHandler(ctx *context.Context) {
|
||||||
}
|
}
|
||||||
repository.MustOwner(ctx)
|
repository.MustOwner(ctx)
|
||||||
|
|
||||||
|
context.CheckRepoScopedToken(ctx, repository)
|
||||||
|
if ctx.Written() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
authenticated := authenticate(ctx, repository, authorization, true, true)
|
authenticated := authenticate(ctx, repository, authorization, true, true)
|
||||||
if !authenticated {
|
if !authenticated {
|
||||||
ctx.Resp.Header().Set("WWW-Authenticate", "Basic realm=gitea-lfs")
|
ctx.Resp.Header().Set("WWW-Authenticate", "Basic realm=gitea-lfs")
|
||||||
|
@ -212,6 +222,11 @@ func VerifyLockHandler(ctx *context.Context) {
|
||||||
}
|
}
|
||||||
repository.MustOwner(ctx)
|
repository.MustOwner(ctx)
|
||||||
|
|
||||||
|
context.CheckRepoScopedToken(ctx, repository)
|
||||||
|
if ctx.Written() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
authenticated := authenticate(ctx, repository, authorization, true, true)
|
authenticated := authenticate(ctx, repository, authorization, true, true)
|
||||||
if !authenticated {
|
if !authenticated {
|
||||||
ctx.Resp.Header().Set("WWW-Authenticate", "Basic realm=gitea-lfs")
|
ctx.Resp.Header().Set("WWW-Authenticate", "Basic realm=gitea-lfs")
|
||||||
|
@ -278,6 +293,11 @@ func UnLockHandler(ctx *context.Context) {
|
||||||
}
|
}
|
||||||
repository.MustOwner(ctx)
|
repository.MustOwner(ctx)
|
||||||
|
|
||||||
|
context.CheckRepoScopedToken(ctx, repository)
|
||||||
|
if ctx.Written() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
authenticated := authenticate(ctx, repository, authorization, true, true)
|
authenticated := authenticate(ctx, repository, authorization, true, true)
|
||||||
if !authenticated {
|
if !authenticated {
|
||||||
ctx.Resp.Header().Set("WWW-Authenticate", "Basic realm=gitea-lfs")
|
ctx.Resp.Header().Set("WWW-Authenticate", "Basic realm=gitea-lfs")
|
||||||
|
|
|
@ -86,6 +86,11 @@ func DownloadHandler(ctx *context.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
repository := getAuthenticatedRepository(ctx, rc, true)
|
||||||
|
if repository == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Support resume download using Range header
|
// Support resume download using Range header
|
||||||
var fromByte, toByte int64
|
var fromByte, toByte int64
|
||||||
toByte = meta.Size - 1
|
toByte = meta.Size - 1
|
||||||
|
@ -360,6 +365,11 @@ func VerifyHandler(ctx *context.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
repository := getAuthenticatedRepository(ctx, rc, true)
|
||||||
|
if repository == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
contentStore := lfs_module.NewContentStore()
|
contentStore := lfs_module.NewContentStore()
|
||||||
ok, err := contentStore.Verify(meta.Pointer)
|
ok, err := contentStore.Verify(meta.Pointer)
|
||||||
|
|
||||||
|
@ -423,6 +433,11 @@ func getAuthenticatedRepository(ctx *context.Context, rc *requestContext, requir
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
context.CheckRepoScopedToken(ctx, repository)
|
||||||
|
if ctx.Written() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
return repository
|
return repository
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
auth_model "code.gitea.io/gitea/models/auth"
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
"code.gitea.io/gitea/models/packages"
|
"code.gitea.io/gitea/models/packages"
|
||||||
"code.gitea.io/gitea/models/unittest"
|
"code.gitea.io/gitea/models/unittest"
|
||||||
|
@ -27,7 +28,7 @@ func TestPackageNpm(t *testing.T) {
|
||||||
|
|
||||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
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"
|
packageName := "@scope/test-package"
|
||||||
packageVersion := "1.0.1-pre"
|
packageVersion := "1.0.1-pre"
|
||||||
|
|
|
@ -16,6 +16,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
auth_model "code.gitea.io/gitea/models/auth"
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
"code.gitea.io/gitea/models/packages"
|
"code.gitea.io/gitea/models/packages"
|
||||||
"code.gitea.io/gitea/models/unittest"
|
"code.gitea.io/gitea/models/unittest"
|
||||||
|
@ -74,7 +75,7 @@ func TestPackageNuGet(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
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"
|
packageName := "test.package"
|
||||||
packageVersion := "1.0.3"
|
packageVersion := "1.0.3"
|
||||||
|
|
|
@ -15,6 +15,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
auth_model "code.gitea.io/gitea/models/auth"
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
"code.gitea.io/gitea/models/packages"
|
"code.gitea.io/gitea/models/packages"
|
||||||
"code.gitea.io/gitea/models/unittest"
|
"code.gitea.io/gitea/models/unittest"
|
||||||
|
@ -30,7 +31,7 @@ func TestPackagePub(t *testing.T) {
|
||||||
|
|
||||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
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"
|
packageName := "test_package"
|
||||||
packageVersion := "1.0.1"
|
packageVersion := "1.0.1"
|
||||||
|
|
|
@ -12,6 +12,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
auth_model "code.gitea.io/gitea/models/auth"
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
"code.gitea.io/gitea/models/packages"
|
"code.gitea.io/gitea/models/packages"
|
||||||
"code.gitea.io/gitea/models/unittest"
|
"code.gitea.io/gitea/models/unittest"
|
||||||
|
@ -27,7 +28,7 @@ func TestPackageVagrant(t *testing.T) {
|
||||||
defer tests.PrepareTestEnv(t)()
|
defer tests.PrepareTestEnv(t)()
|
||||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
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"
|
packageName := "test_package"
|
||||||
packageVersion := "1.0.1"
|
packageVersion := "1.0.1"
|
||||||
|
|
Loading…
Reference in New Issue