aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLunny Xiao <xiaolunwen@gmail.com>2024-09-05 15:05:42 +0800
committerGitHub <noreply@github.com>2024-09-05 07:05:42 +0000
commit5c05dddbed8247a4fb272619f1eb7cf090443b8b (patch)
treef929b204ebe00f0162b87bacd73b884b7d6971f7
parent74b1c589c6c1a4261556e1a1a868bbcb2964a5d3 (diff)
downloadgitea-5c05dddbed8247a4fb272619f1eb7cf090443b8b.tar.gz
gitea-5c05dddbed8247a4fb272619f1eb7cf090443b8b.zip
Fix nuget/conan/container packages upload bugs (#31967)
-rw-r--r--models/auth/access_token_scope.go16
-rw-r--r--routers/api/packages/conan/auth.go10
-rw-r--r--routers/api/packages/conan/conan.go35
-rw-r--r--routers/api/packages/container/auth.go11
-rw-r--r--routers/api/packages/container/container.go17
-rw-r--r--routers/api/packages/nuget/auth.go3
-rw-r--r--services/auth/basic.go27
-rw-r--r--services/packages/auth.go30
-rw-r--r--tests/integration/api_packages_conan_test.go170
-rw-r--r--tests/integration/api_packages_container_test.go80
-rw-r--r--tests/integration/api_packages_nuget_test.go203
11 files changed, 512 insertions, 90 deletions
diff --git a/models/auth/access_token_scope.go b/models/auth/access_token_scope.go
index fe57276700..897ff3fc9e 100644
--- a/models/auth/access_token_scope.go
+++ b/models/auth/access_token_scope.go
@@ -309,6 +309,22 @@ func (s AccessTokenScope) HasScope(scopes ...AccessTokenScope) (bool, error) {
return true, nil
}
+// HasAnyScope returns true if any of the scopes is contained in the string
+func (s AccessTokenScope) HasAnyScope(scopes ...AccessTokenScope) (bool, error) {
+ bitmap, err := s.parse()
+ if err != nil {
+ return false, err
+ }
+
+ for _, s := range scopes {
+ if has, err := bitmap.hasScope(s); has || err != nil {
+ return has, err
+ }
+ }
+
+ return false, nil
+}
+
// hasScope returns true if the string has the given scope
func (bitmap accessTokenScopeBitmap) hasScope(scope AccessTokenScope) (bool, error) {
expectedBits, ok := allAccessTokenScopeBits[scope]
diff --git a/routers/api/packages/conan/auth.go b/routers/api/packages/conan/auth.go
index 521fa12372..9c03d01391 100644
--- a/routers/api/packages/conan/auth.go
+++ b/routers/api/packages/conan/auth.go
@@ -22,21 +22,25 @@ func (a *Auth) Name() string {
// Verify extracts the user from the Bearer token
func (a *Auth) Verify(req *http.Request, w http.ResponseWriter, store auth.DataStore, sess auth.SessionStore) (*user_model.User, error) {
- uid, err := packages.ParseAuthorizationToken(req)
+ packageMeta, err := packages.ParseAuthorizationRequest(req)
if err != nil {
log.Trace("ParseAuthorizationToken: %v", err)
return nil, err
}
- if uid == 0 {
+ if packageMeta == nil || packageMeta.UserID == 0 {
return nil, nil
}
- u, err := user_model.GetUserByID(req.Context(), uid)
+ u, err := user_model.GetUserByID(req.Context(), packageMeta.UserID)
if err != nil {
log.Error("GetUserByID: %v", err)
return nil, err
}
+ if packageMeta.Scope != "" {
+ store.GetData()["IsApiToken"] = true
+ store.GetData()["ApiTokenScope"] = packageMeta.Scope
+ }
return u, nil
}
diff --git a/routers/api/packages/conan/conan.go b/routers/api/packages/conan/conan.go
index 7afca2fab1..4a9f0a3ffc 100644
--- a/routers/api/packages/conan/conan.go
+++ b/routers/api/packages/conan/conan.go
@@ -11,6 +11,7 @@ import (
"strings"
"time"
+ auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
packages_model "code.gitea.io/gitea/models/packages"
conan_model "code.gitea.io/gitea/models/packages/conan"
@@ -21,6 +22,7 @@ import (
conan_module "code.gitea.io/gitea/modules/packages/conan"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/routers/api/packages/helper"
+ auth_service "code.gitea.io/gitea/services/auth"
"code.gitea.io/gitea/services/context"
notify_service "code.gitea.io/gitea/services/notify"
packages_service "code.gitea.io/gitea/services/packages"
@@ -117,7 +119,20 @@ func Authenticate(ctx *context.Context) {
return
}
- token, err := packages_service.CreateAuthorizationToken(ctx.Doer)
+ packageScope := auth_service.GetAccessScope(ctx.Data)
+ if has, err := packageScope.HasAnyScope(
+ auth_model.AccessTokenScopeReadPackage,
+ auth_model.AccessTokenScopeWritePackage,
+ auth_model.AccessTokenScopeAll,
+ ); !has {
+ if err != nil {
+ log.Error("Error checking access scope: %v", err)
+ }
+ apiError(ctx, http.StatusForbidden, nil)
+ return
+ }
+
+ token, err := packages_service.CreateAuthorizationToken(ctx.Doer, packageScope)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
@@ -130,9 +145,23 @@ func Authenticate(ctx *context.Context) {
func CheckCredentials(ctx *context.Context) {
if ctx.Doer == nil {
ctx.Status(http.StatusUnauthorized)
- } else {
- ctx.Status(http.StatusOK)
+ return
}
+
+ packageScope := auth_service.GetAccessScope(ctx.Data)
+ if has, err := packageScope.HasAnyScope(
+ auth_model.AccessTokenScopeReadPackage,
+ auth_model.AccessTokenScopeWritePackage,
+ auth_model.AccessTokenScopeAll,
+ ); !has {
+ if err != nil {
+ log.Error("Error checking access scope: %v", err)
+ }
+ ctx.Status(http.StatusForbidden)
+ return
+ }
+
+ ctx.Status(http.StatusOK)
}
// RecipeSnapshot displays the recipe files with their md5 hash
diff --git a/routers/api/packages/container/auth.go b/routers/api/packages/container/auth.go
index 1c7afa95ff..1d8ae6af7d 100644
--- a/routers/api/packages/container/auth.go
+++ b/routers/api/packages/container/auth.go
@@ -23,21 +23,26 @@ func (a *Auth) Name() string {
// Verify extracts the user from the Bearer token
// If it's an anonymous session a ghost user is returned
func (a *Auth) Verify(req *http.Request, w http.ResponseWriter, store auth.DataStore, sess auth.SessionStore) (*user_model.User, error) {
- uid, err := packages.ParseAuthorizationToken(req)
+ packageMeta, err := packages.ParseAuthorizationRequest(req)
if err != nil {
log.Trace("ParseAuthorizationToken: %v", err)
return nil, err
}
- if uid == 0 {
+ if packageMeta == nil || packageMeta.UserID == 0 {
return nil, nil
}
- u, err := user_model.GetPossibleUserByID(req.Context(), uid)
+ u, err := user_model.GetPossibleUserByID(req.Context(), packageMeta.UserID)
if err != nil {
log.Error("GetPossibleUserByID: %v", err)
return nil, err
}
+ if packageMeta.Scope != "" {
+ store.GetData()["IsApiToken"] = true
+ store.GetData()["ApiTokenScope"] = packageMeta.Scope
+ }
+
return u, nil
}
diff --git a/routers/api/packages/container/container.go b/routers/api/packages/container/container.go
index 74a3295f09..d495d199d9 100644
--- a/routers/api/packages/container/container.go
+++ b/routers/api/packages/container/container.go
@@ -14,6 +14,7 @@ import (
"strconv"
"strings"
+ auth_model "code.gitea.io/gitea/models/auth"
packages_model "code.gitea.io/gitea/models/packages"
container_model "code.gitea.io/gitea/models/packages/container"
user_model "code.gitea.io/gitea/models/user"
@@ -25,6 +26,7 @@ import (
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/packages/helper"
+ auth_service "code.gitea.io/gitea/services/auth"
"code.gitea.io/gitea/services/context"
packages_service "code.gitea.io/gitea/services/packages"
container_service "code.gitea.io/gitea/services/packages/container"
@@ -148,6 +150,7 @@ func DetermineSupport(ctx *context.Context) {
// If the current user is anonymous, the ghost user is used unless RequireSignInView is enabled.
func Authenticate(ctx *context.Context) {
u := ctx.Doer
+ packageScope := auth_service.GetAccessScope(ctx.Data)
if u == nil {
if setting.Service.RequireSignInView {
apiUnauthorizedError(ctx)
@@ -155,9 +158,21 @@ func Authenticate(ctx *context.Context) {
}
u = user_model.NewGhostUser()
+ } else {
+ if has, err := packageScope.HasAnyScope(
+ auth_model.AccessTokenScopeReadPackage,
+ auth_model.AccessTokenScopeWritePackage,
+ auth_model.AccessTokenScopeAll,
+ ); !has {
+ if err != nil {
+ log.Error("Error checking access scope: %v", err)
+ }
+ apiUnauthorizedError(ctx)
+ return
+ }
}
- token, err := packages_service.CreateAuthorizationToken(u)
+ token, err := packages_service.CreateAuthorizationToken(u, packageScope)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
diff --git a/routers/api/packages/nuget/auth.go b/routers/api/packages/nuget/auth.go
index 1bb68d059b..e81ad01b2b 100644
--- a/routers/api/packages/nuget/auth.go
+++ b/routers/api/packages/nuget/auth.go
@@ -43,5 +43,8 @@ func (a *Auth) Verify(req *http.Request, w http.ResponseWriter, store auth.DataS
log.Error("UpdateAccessToken: %v", err)
}
+ store.GetData()["IsApiToken"] = true
+ store.GetData()["ApiToken"] = token
+
return u, nil
}
diff --git a/services/auth/basic.go b/services/auth/basic.go
index 1184d12d1c..90bd642370 100644
--- a/services/auth/basic.go
+++ b/services/auth/basic.go
@@ -25,7 +25,12 @@ var (
)
// BasicMethodName is the constant name of the basic authentication method
-const BasicMethodName = "basic"
+const (
+ BasicMethodName = "basic"
+ AccessTokenMethodName = "access_token"
+ OAuth2TokenMethodName = "oauth2_token"
+ ActionTokenMethodName = "action_token"
+)
// Basic implements the Auth interface and authenticates requests (API requests
// only) by looking for Basic authentication data or "x-oauth-basic" token in the "Authorization"
@@ -82,6 +87,7 @@ func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore
return nil, err
}
+ store.GetData()["LoginMethod"] = OAuth2TokenMethodName
store.GetData()["IsApiToken"] = true
return u, nil
}
@@ -101,6 +107,7 @@ func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore
log.Error("UpdateAccessToken: %v", err)
}
+ store.GetData()["LoginMethod"] = AccessTokenMethodName
store.GetData()["IsApiToken"] = true
store.GetData()["ApiTokenScope"] = token.Scope
return u, nil
@@ -113,6 +120,7 @@ func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore
if err == nil && task != nil {
log.Trace("Basic Authorization: Valid AccessToken for task[%d]", task.ID)
+ store.GetData()["LoginMethod"] = ActionTokenMethodName
store.GetData()["IsActionsToken"] = true
store.GetData()["ActionsTaskID"] = task.ID
@@ -138,6 +146,7 @@ func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore
}
}
+ store.GetData()["LoginMethod"] = BasicMethodName
log.Trace("Basic Authorization: Logged in user %-v", u)
return u, nil
@@ -159,3 +168,19 @@ func validateTOTP(req *http.Request, u *user_model.User) error {
}
return nil
}
+
+func GetAccessScope(store DataStore) auth_model.AccessTokenScope {
+ if v, ok := store.GetData()["ApiTokenScope"]; ok {
+ return v.(auth_model.AccessTokenScope)
+ }
+ switch store.GetData()["LoginMethod"] {
+ case OAuth2TokenMethodName:
+ fallthrough
+ case BasicMethodName, AccessTokenMethodName:
+ return auth_model.AccessTokenScopeAll
+ case ActionTokenMethodName:
+ fallthrough
+ default:
+ return ""
+ }
+}
diff --git a/services/packages/auth.go b/services/packages/auth.go
index 8263c28bed..4526a8e303 100644
--- a/services/packages/auth.go
+++ b/services/packages/auth.go
@@ -9,6 +9,7 @@ import (
"strings"
"time"
+ auth_model "code.gitea.io/gitea/models/auth"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
@@ -18,10 +19,14 @@ import (
type packageClaims struct {
jwt.RegisteredClaims
+ PackageMeta
+}
+type PackageMeta struct {
UserID int64
+ Scope auth_model.AccessTokenScope
}
-func CreateAuthorizationToken(u *user_model.User) (string, error) {
+func CreateAuthorizationToken(u *user_model.User, packageScope auth_model.AccessTokenScope) (string, error) {
now := time.Now()
claims := packageClaims{
@@ -29,7 +34,10 @@ func CreateAuthorizationToken(u *user_model.User) (string, error) {
ExpiresAt: jwt.NewNumericDate(now.Add(24 * time.Hour)),
NotBefore: jwt.NewNumericDate(now),
},
- UserID: u.ID,
+ PackageMeta: PackageMeta{
+ UserID: u.ID,
+ Scope: packageScope,
+ },
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
@@ -41,32 +49,36 @@ func CreateAuthorizationToken(u *user_model.User) (string, error) {
return tokenString, nil
}
-func ParseAuthorizationToken(req *http.Request) (int64, error) {
+func ParseAuthorizationRequest(req *http.Request) (*PackageMeta, error) {
h := req.Header.Get("Authorization")
if h == "" {
- return 0, nil
+ return nil, nil
}
parts := strings.SplitN(h, " ", 2)
if len(parts) != 2 {
log.Error("split token failed: %s", h)
- return 0, fmt.Errorf("split token failed")
+ return nil, fmt.Errorf("split token failed")
}
- token, err := jwt.ParseWithClaims(parts[1], &packageClaims{}, func(t *jwt.Token) (any, error) {
+ return ParseAuthorizationToken(parts[1])
+}
+
+func ParseAuthorizationToken(tokenStr string) (*PackageMeta, error) {
+ token, err := jwt.ParseWithClaims(tokenStr, &packageClaims{}, func(t *jwt.Token) (any, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
}
return setting.GetGeneralTokenSigningSecret(), nil
})
if err != nil {
- return 0, err
+ return nil, err
}
c, ok := token.Claims.(*packageClaims)
if !token.Valid || !ok {
- return 0, fmt.Errorf("invalid token claim")
+ return nil, fmt.Errorf("invalid token claim")
}
- return c.UserID, nil
+ return &c.PackageMeta, nil
}
diff --git a/tests/integration/api_packages_conan_test.go b/tests/integration/api_packages_conan_test.go
index a25713f039..3055e57a2e 100644
--- a/tests/integration/api_packages_conan_test.go
+++ b/tests/integration/api_packages_conan_test.go
@@ -11,6 +11,7 @@ import (
"testing"
"time"
+ auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/packages"
conan_model "code.gitea.io/gitea/models/packages/conan"
@@ -19,6 +20,7 @@ import (
conan_module "code.gitea.io/gitea/modules/packages/conan"
"code.gitea.io/gitea/modules/setting"
conan_router "code.gitea.io/gitea/routers/api/packages/conan"
+ package_service "code.gitea.io/gitea/services/packages"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
@@ -225,7 +227,7 @@ func TestPackageConan(t *testing.T) {
token := ""
- t.Run("Authenticate", func(t *testing.T) {
+ t.Run("UserName/Password Authenticate", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequest(t, "GET", fmt.Sprintf("%s/v1/users/authenticate", url)).
@@ -234,6 +236,73 @@ func TestPackageConan(t *testing.T) {
token = resp.Body.String()
assert.NotEmpty(t, token)
+
+ pkgMeta, err := package_service.ParseAuthorizationToken(token)
+ assert.NoError(t, err)
+ assert.Equal(t, user.ID, pkgMeta.UserID)
+ assert.Equal(t, auth_model.AccessTokenScopeAll, pkgMeta.Scope)
+ })
+
+ badToken := ""
+ t.Run("Token Scope Authentication", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ session := loginUser(t, user.Name)
+
+ badToken = getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadNotification)
+
+ testCase := func(t *testing.T, scope auth_model.AccessTokenScope, expectedAuthStatusCode, expectedStatusCode int) {
+ t.Helper()
+
+ token := getTokenForLoggedInUser(t, session, scope)
+
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/v1/users/authenticate", url)).
+ AddTokenAuth(token)
+ resp := MakeRequest(t, req, expectedAuthStatusCode)
+ if expectedAuthStatusCode != http.StatusOK {
+ return
+ }
+
+ body := resp.Body.String()
+ assert.NotEmpty(t, body)
+
+ pkgMeta, err := package_service.ParseAuthorizationToken(body)
+ assert.NoError(t, err)
+ assert.Equal(t, user.ID, pkgMeta.UserID)
+ assert.Equal(t, scope, pkgMeta.Scope)
+
+ recipeURL := fmt.Sprintf("%s/v1/conans/%s/%s/%s/%s", url, "TestScope", version1, "testing", channel1)
+
+ req = NewRequestWithJSON(t, "POST", fmt.Sprintf("%s/upload_urls", recipeURL), map[string]int64{
+ conanfileName: 64,
+ "removed.txt": 0,
+ }).AddTokenAuth(token)
+ MakeRequest(t, req, expectedStatusCode)
+ }
+
+ t.Run("No Package permission", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ testCase(t, auth_model.AccessTokenScopeReadNotification, http.StatusUnauthorized, http.StatusForbidden)
+ })
+
+ t.Run("Package Read permission", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ testCase(t, auth_model.AccessTokenScopeReadPackage, http.StatusOK, http.StatusUnauthorized)
+ })
+
+ t.Run("Package Write permission", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ testCase(t, auth_model.AccessTokenScopeWritePackage, http.StatusOK, http.StatusOK)
+ })
+
+ t.Run("All permission", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ testCase(t, auth_model.AccessTokenScopeAll, http.StatusOK, http.StatusOK)
+ })
})
t.Run("CheckCredentials", func(t *testing.T) {
@@ -431,6 +500,11 @@ func TestPackageConan(t *testing.T) {
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("%s/v1/conans/%s/%s/%s/%s/packages/delete", url, name, version1, user1, c.Channel), map[string][]string{
"package_ids": c.References,
+ }).AddTokenAuth(badToken)
+ MakeRequest(t, req, http.StatusUnauthorized)
+
+ req = NewRequestWithJSON(t, "POST", fmt.Sprintf("%s/v1/conans/%s/%s/%s/%s/packages/delete", url, name, version1, user1, c.Channel), map[string][]string{
+ "package_ids": c.References,
}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
@@ -457,6 +531,10 @@ func TestPackageConan(t *testing.T) {
assert.NotEmpty(t, revisions)
req := NewRequest(t, "DELETE", fmt.Sprintf("%s/v1/conans/%s/%s/%s/%s", url, name, version1, user1, c.Channel)).
+ AddTokenAuth(badToken)
+ MakeRequest(t, req, http.StatusUnauthorized)
+
+ req = NewRequest(t, "DELETE", fmt.Sprintf("%s/v1/conans/%s/%s/%s/%s", url, name, version1, user1, c.Channel)).
AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
@@ -480,7 +558,7 @@ func TestPackageConan(t *testing.T) {
token := ""
- t.Run("Authenticate", func(t *testing.T) {
+ t.Run("UserName/Password Authenticate", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequest(t, "GET", fmt.Sprintf("%s/v2/users/authenticate", url)).
@@ -490,9 +568,75 @@ func TestPackageConan(t *testing.T) {
body := resp.Body.String()
assert.NotEmpty(t, body)
+ pkgMeta, err := package_service.ParseAuthorizationToken(body)
+ assert.NoError(t, err)
+ assert.Equal(t, user.ID, pkgMeta.UserID)
+ assert.Equal(t, auth_model.AccessTokenScopeAll, pkgMeta.Scope)
+
token = fmt.Sprintf("Bearer %s", body)
})
+ badToken := ""
+
+ t.Run("Token Scope Authentication", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ session := loginUser(t, user.Name)
+
+ badToken = getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadNotification)
+
+ testCase := func(t *testing.T, scope auth_model.AccessTokenScope, expectedAuthStatusCode, expectedStatusCode int) {
+ t.Helper()
+
+ token := getTokenForLoggedInUser(t, session, scope)
+
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/v2/users/authenticate", url)).
+ AddTokenAuth(token)
+ resp := MakeRequest(t, req, expectedAuthStatusCode)
+ if expectedAuthStatusCode != http.StatusOK {
+ return
+ }
+
+ body := resp.Body.String()
+ assert.NotEmpty(t, body)
+
+ pkgMeta, err := package_service.ParseAuthorizationToken(body)
+ assert.NoError(t, err)
+ assert.Equal(t, user.ID, pkgMeta.UserID)
+ assert.Equal(t, scope, pkgMeta.Scope)
+
+ recipeURL := fmt.Sprintf("%s/v2/conans/%s/%s/%s/%s/revisions/%s", url, "TestScope", version1, "testing", channel1, revision1)
+
+ req = NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/files/%s", recipeURL, conanfileName), strings.NewReader("Demo Conan file")).
+ AddTokenAuth(token)
+ MakeRequest(t, req, expectedStatusCode)
+ }
+
+ t.Run("No Package permission", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ testCase(t, auth_model.AccessTokenScopeReadNotification, http.StatusUnauthorized, http.StatusUnauthorized)
+ })
+
+ t.Run("Package Read permission", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ testCase(t, auth_model.AccessTokenScopeReadPackage, http.StatusOK, http.StatusUnauthorized)
+ })
+
+ t.Run("Package Write permission", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ testCase(t, auth_model.AccessTokenScopeWritePackage, http.StatusOK, http.StatusCreated)
+ })
+
+ t.Run("All permission", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ testCase(t, auth_model.AccessTokenScopeAll, http.StatusOK, http.StatusCreated)
+ })
+ })
+
t.Run("CheckCredentials", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
@@ -511,7 +655,7 @@ func TestPackageConan(t *testing.T) {
pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeConan)
assert.NoError(t, err)
- assert.Len(t, pvs, 2)
+ assert.Len(t, pvs, 3)
})
})
@@ -663,12 +807,20 @@ func TestPackageConan(t *testing.T) {
checkPackageRevisionCount(2)
req := NewRequest(t, "DELETE", fmt.Sprintf("%s/v2/conans/%s/%s/%s/%s/revisions/%s/packages/%s/revisions/%s", url, name, version1, user1, channel1, revision1, conanPackageReference, revision1)).
+ AddTokenAuth(badToken)
+ MakeRequest(t, req, http.StatusUnauthorized)
+
+ req = NewRequest(t, "DELETE", fmt.Sprintf("%s/v2/conans/%s/%s/%s/%s/revisions/%s/packages/%s/revisions/%s", url, name, version1, user1, channel1, revision1, conanPackageReference, revision1)).
AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
checkPackageRevisionCount(1)
req = NewRequest(t, "DELETE", fmt.Sprintf("%s/v2/conans/%s/%s/%s/%s/revisions/%s/packages/%s", url, name, version1, user1, channel1, revision1, conanPackageReference)).
+ AddTokenAuth(badToken)
+ MakeRequest(t, req, http.StatusUnauthorized)
+
+ req = NewRequest(t, "DELETE", fmt.Sprintf("%s/v2/conans/%s/%s/%s/%s/revisions/%s/packages/%s", url, name, version1, user1, channel1, revision1, conanPackageReference)).
AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
@@ -679,6 +831,10 @@ func TestPackageConan(t *testing.T) {
checkPackageReferenceCount(1)
req = NewRequest(t, "DELETE", fmt.Sprintf("%s/v2/conans/%s/%s/%s/%s/revisions/%s/packages", url, name, version1, user1, channel1, revision2)).
+ AddTokenAuth(badToken)
+ MakeRequest(t, req, http.StatusUnauthorized)
+
+ req = NewRequest(t, "DELETE", fmt.Sprintf("%s/v2/conans/%s/%s/%s/%s/revisions/%s/packages", url, name, version1, user1, channel1, revision2)).
AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
@@ -699,12 +855,20 @@ func TestPackageConan(t *testing.T) {
checkRecipeRevisionCount(2)
req := NewRequest(t, "DELETE", fmt.Sprintf("%s/v2/conans/%s/%s/%s/%s/revisions/%s", url, name, version1, user1, channel1, revision1)).
+ AddTokenAuth(badToken)
+ MakeRequest(t, req, http.StatusUnauthorized)
+
+ req = NewRequest(t, "DELETE", fmt.Sprintf("%s/v2/conans/%s/%s/%s/%s/revisions/%s", url, name, version1, user1, channel1, revision1)).
AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
checkRecipeRevisionCount(1)
req = NewRequest(t, "DELETE", fmt.Sprintf("%s/v2/conans/%s/%s/%s/%s", url, name, version1, user1, channel1)).
+ AddTokenAuth(badToken)
+ MakeRequest(t, req, http.StatusUnauthorized)
+
+ req = NewRequest(t, "DELETE", fmt.Sprintf("%s/v2/conans/%s/%s/%s/%s", url, name, version1, user1, channel1)).
AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
diff --git a/tests/integration/api_packages_container_test.go b/tests/integration/api_packages_container_test.go
index fcd1cc529f..409e7513a6 100644
--- a/tests/integration/api_packages_container_test.go
+++ b/tests/integration/api_packages_container_test.go
@@ -23,6 +23,7 @@ import (
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/test"
+ package_service "code.gitea.io/gitea/services/packages"
"code.gitea.io/gitea/tests"
oci "github.com/opencontainers/image-spec/specs-go/v1"
@@ -78,6 +79,8 @@ func TestPackageContainer(t *testing.T) {
anonymousToken := ""
userToken := ""
+ readToken := ""
+ badToken := ""
t.Run("Authenticate", func(t *testing.T) {
type TokenResponse struct {
@@ -123,7 +126,7 @@ func TestPackageContainer(t *testing.T) {
assert.Equal(t, `Bearer realm="https://domain:8443/v2/token",service="container_registry",scope="*"`, resp.Header().Get("WWW-Authenticate"))
})
- t.Run("User", func(t *testing.T) {
+ t.Run("UserName/Password", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequest(t, "GET", fmt.Sprintf("%sv2", setting.AppURL))
@@ -139,6 +142,10 @@ func TestPackageContainer(t *testing.T) {
DecodeJSON(t, resp, &tokenResponse)
assert.NotEmpty(t, tokenResponse.Token)
+ pkgMeta, err := package_service.ParseAuthorizationToken(tokenResponse.Token)
+ assert.NoError(t, err)
+ assert.Equal(t, user.ID, pkgMeta.UserID)
+ assert.Equal(t, auth_model.AccessTokenScopeAll, pkgMeta.Scope)
userToken = fmt.Sprintf("Bearer %s", tokenResponse.Token)
@@ -146,6 +153,52 @@ func TestPackageContainer(t *testing.T) {
AddTokenAuth(userToken)
MakeRequest(t, req, http.StatusOK)
})
+
+ // Token that should enforce the read scope.
+ t.Run("AccessToken", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ session := loginUser(t, user.Name)
+
+ readToken = getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadPackage)
+ req := NewRequest(t, "GET", fmt.Sprintf("%sv2/token", setting.AppURL))
+ req.Request.SetBasicAuth(user.Name, readToken)
+ resp := MakeRequest(t, req, http.StatusOK)
+ tokenResponse := &TokenResponse{}
+ DecodeJSON(t, resp, &tokenResponse)
+
+ readToken = fmt.Sprintf("Bearer %s", tokenResponse.Token)
+
+ badToken = getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadNotification)
+ req = NewRequest(t, "GET", fmt.Sprintf("%sv2/token", setting.AppURL))
+ req.Request.SetBasicAuth(user.Name, badToken)
+ MakeRequest(t, req, http.StatusUnauthorized)
+
+ testCase := func(scope auth_model.AccessTokenScope, expectedAuthStatus, expectedStatus int) {
+ token := getTokenForLoggedInUser(t, session, scope)
+
+ req := NewRequest(t, "GET", fmt.Sprintf("%sv2/token", setting.AppURL))
+ req.SetBasicAuth(user.Name, token)
+
+ resp := MakeRequest(t, req, expectedAuthStatus)
+ if expectedAuthStatus != http.StatusOK {
+ return
+ }
+
+ tokenResponse := &TokenResponse{}
+ DecodeJSON(t, resp, &tokenResponse)
+
+ assert.NotEmpty(t, tokenResponse.Token)
+
+ req = NewRequest(t, "GET", fmt.Sprintf("%sv2", setting.AppURL)).
+ AddTokenAuth(fmt.Sprintf("Bearer %s", tokenResponse.Token))
+ MakeRequest(t, req, expectedStatus)
+ }
+ testCase(auth_model.AccessTokenScopeReadPackage, http.StatusOK, http.StatusOK)
+ testCase(auth_model.AccessTokenScopeAll, http.StatusOK, http.StatusOK)
+ testCase(auth_model.AccessTokenScopeReadNotification, http.StatusUnauthorized, http.StatusUnauthorized)
+ testCase(auth_model.AccessTokenScopeWritePackage, http.StatusOK, http.StatusOK)
+ })
})
t.Run("DetermineSupport", func(t *testing.T) {
@@ -155,6 +208,15 @@ func TestPackageContainer(t *testing.T) {
AddTokenAuth(userToken)
resp := MakeRequest(t, req, http.StatusOK)
assert.Equal(t, "registry/2.0", resp.Header().Get("Docker-Distribution-Api-Version"))
+
+ req = NewRequest(t, "GET", fmt.Sprintf("%sv2", setting.AppURL)).
+ AddTokenAuth(readToken)
+ resp = MakeRequest(t, req, http.StatusOK)
+ assert.Equal(t, "registry/2.0", resp.Header().Get("Docker-Distribution-Api-Version"))
+
+ req = NewRequest(t, "GET", fmt.Sprintf("%sv2", setting.AppURL)).
+ AddTokenAuth(badToken)
+ MakeRequest(t, req, http.StatusUnauthorized)
})
for _, image := range images {
@@ -168,6 +230,14 @@ func TestPackageContainer(t *testing.T) {
AddTokenAuth(anonymousToken)
MakeRequest(t, req, http.StatusUnauthorized)
+ req = NewRequest(t, "POST", fmt.Sprintf("%s/blobs/uploads", url)).
+ AddTokenAuth(readToken)
+ MakeRequest(t, req, http.StatusUnauthorized)
+
+ req = NewRequest(t, "POST", fmt.Sprintf("%s/blobs/uploads", url)).
+ AddTokenAuth(badToken)
+ MakeRequest(t, req, http.StatusUnauthorized)
+
req = NewRequestWithBody(t, "POST", fmt.Sprintf("%s/blobs/uploads?digest=%s", url, unknownDigest), bytes.NewReader(blobContent)).
AddTokenAuth(userToken)
MakeRequest(t, req, http.StatusBadRequest)
@@ -195,6 +265,14 @@ func TestPackageContainer(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequest(t, "POST", fmt.Sprintf("%s/blobs/uploads", url)).
+ AddTokenAuth(readToken)
+ MakeRequest(t, req, http.StatusUnauthorized)
+
+ req = NewRequest(t, "POST", fmt.Sprintf("%s/blobs/uploads", url)).
+ AddTokenAuth(badToken)
+ MakeRequest(t, req, http.StatusUnauthorized)
+
+ req = NewRequest(t, "POST", fmt.Sprintf("%s/blobs/uploads", url)).
AddTokenAuth(userToken)
resp := MakeRequest(t, req, http.StatusAccepted)
diff --git a/tests/integration/api_packages_nuget_test.go b/tests/integration/api_packages_nuget_test.go
index 630b4de3f9..622c2c4394 100644
--- a/tests/integration/api_packages_nuget_test.go
+++ b/tests/integration/api_packages_nuget_test.go
@@ -26,6 +26,7 @@ import (
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/routers/api/packages/nuget"
+ packageService "code.gitea.io/gitea/services/packages"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
@@ -81,7 +82,9 @@ func TestPackageNuGet(t *testing.T) {
}
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
- token := getUserToken(t, user.Name, auth_model.AccessTokenScopeWritePackage)
+ writeToken := getUserToken(t, user.Name, auth_model.AccessTokenScopeWritePackage)
+ readToken := getUserToken(t, user.Name, auth_model.AccessTokenScopeReadPackage)
+ badToken := getUserToken(t, user.Name, auth_model.AccessTokenScopeReadNotification)
packageName := "test.package"
packageVersion := "1.0.3"
@@ -127,34 +130,44 @@ func TestPackageNuGet(t *testing.T) {
privateUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{Visibility: structs.VisibleTypePrivate})
cases := []struct {
- Owner string
- UseBasicAuth bool
- UseTokenAuth bool
+ Owner string
+ UseBasicAuth bool
+ token string
+ expectedStatus int
}{
- {privateUser.Name, false, false},
- {privateUser.Name, true, false},
- {privateUser.Name, false, true},
- {user.Name, false, false},
- {user.Name, true, false},
- {user.Name, false, true},
+ {privateUser.Name, false, "", http.StatusOK},
+ {privateUser.Name, true, "", http.StatusOK},
+ {privateUser.Name, false, writeToken, http.StatusOK},
+ {privateUser.Name, false, readToken, http.StatusOK},
+ {privateUser.Name, false, badToken, http.StatusOK},
+ {user.Name, false, "", http.StatusOK},
+ {user.Name, true, "", http.StatusOK},
+ {user.Name, false, writeToken, http.StatusOK},
+ {user.Name, false, readToken, http.StatusOK},
+ {user.Name, false, badToken, http.StatusOK},
}
for _, c := range cases {
- url := fmt.Sprintf("/api/packages/%s/nuget", c.Owner)
-
- req := NewRequest(t, "GET", url)
- if c.UseBasicAuth {
- req.AddBasicAuth(user.Name)
- } else if c.UseTokenAuth {
- addNuGetAPIKeyHeader(req, token)
- }
- resp := MakeRequest(t, req, http.StatusOK)
+ t.Run(c.Owner, func(t *testing.T) {
+ url := fmt.Sprintf("/api/packages/%s/nuget", c.Owner)
+
+ req := NewRequest(t, "GET", url)
+ if c.UseBasicAuth {
+ req.AddBasicAuth(user.Name)
+ } else if c.token != "" {
+ addNuGetAPIKeyHeader(req, c.token)
+ }
+ resp := MakeRequest(t, req, c.expectedStatus)
+ if c.expectedStatus != http.StatusOK {
+ return
+ }
- var result nuget.ServiceIndexResponseV2
- decodeXML(t, resp, &result)
+ var result nuget.ServiceIndexResponseV2
+ decodeXML(t, resp, &result)
- assert.Equal(t, setting.AppURL+url[1:], result.Base)
- assert.Equal(t, "Packages", result.Workspace.Collection.Href)
+ assert.Equal(t, setting.AppURL+url[1:], result.Base)
+ assert.Equal(t, "Packages", result.Workspace.Collection.Href)
+ })
}
})
@@ -164,56 +177,67 @@ func TestPackageNuGet(t *testing.T) {
privateUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{Visibility: structs.VisibleTypePrivate})
cases := []struct {
- Owner string
- UseBasicAuth bool
- UseTokenAuth bool
+ Owner string
+ UseBasicAuth bool
+ token string
+ expectedStatus int
}{
- {privateUser.Name, false, false},
- {privateUser.Name, true, false},
- {privateUser.Name, false, true},
- {user.Name, false, false},
- {user.Name, true, false},
- {user.Name, false, true},
+ {privateUser.Name, false, "", http.StatusOK},
+ {privateUser.Name, true, "", http.StatusOK},
+ {privateUser.Name, false, writeToken, http.StatusOK},
+ {privateUser.Name, false, readToken, http.StatusOK},
+ {privateUser.Name, false, badToken, http.StatusOK},
+ {user.Name, false, "", http.StatusOK},
+ {user.Name, true, "", http.StatusOK},
+ {user.Name, false, writeToken, http.StatusOK},
+ {user.Name, false, readToken, http.StatusOK},
+ {user.Name, false, badToken, http.StatusOK},
}
for _, c := range cases {
- url := fmt.Sprintf("/api/packages/%s/nuget", c.Owner)
-
- req := NewRequest(t, "GET", fmt.Sprintf("%s/index.json", url))
- if c.UseBasicAuth {
- req.AddBasicAuth(user.Name)
- } else if c.UseTokenAuth {
- addNuGetAPIKeyHeader(req, token)
- }
- resp := MakeRequest(t, req, http.StatusOK)
+ t.Run(c.Owner, func(t *testing.T) {
+ url := fmt.Sprintf("/api/packages/%s/nuget", c.Owner)
+
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/index.json", url))
+ if c.UseBasicAuth {
+ req.AddBasicAuth(user.Name)
+ } else if c.token != "" {
+ addNuGetAPIKeyHeader(req, c.token)
+ }
+ resp := MakeRequest(t, req, c.expectedStatus)
- var result nuget.ServiceIndexResponseV3
- DecodeJSON(t, resp, &result)
+ if c.expectedStatus != http.StatusOK {
+ return
+ }
- assert.Equal(t, "3.0.0", result.Version)
- assert.NotEmpty(t, result.Resources)
-
- root := setting.AppURL + url[1:]
- for _, r := range result.Resources {
- switch r.Type {
- case "SearchQueryService":
- fallthrough
- case "SearchQueryService/3.0.0-beta":
- fallthrough
- case "SearchQueryService/3.0.0-rc":
- assert.Equal(t, root+"/query", r.ID)
- case "RegistrationsBaseUrl":
- fallthrough
- case "RegistrationsBaseUrl/3.0.0-beta":
- fallthrough
- case "RegistrationsBaseUrl/3.0.0-rc":
- assert.Equal(t, root+"/registration", r.ID)
- case "PackageBaseAddress/3.0.0":
- assert.Equal(t, root+"/package", r.ID)
- case "PackagePublish/2.0.0":
- assert.Equal(t, root, r.ID)
+ var result nuget.ServiceIndexResponseV3
+ DecodeJSON(t, resp, &result)
+
+ assert.Equal(t, "3.0.0", result.Version)
+ assert.NotEmpty(t, result.Resources)
+
+ root := setting.AppURL + url[1:]
+ for _, r := range result.Resources {
+ switch r.Type {
+ case "SearchQueryService":
+ fallthrough
+ case "SearchQueryService/3.0.0-beta":
+ fallthrough
+ case "SearchQueryService/3.0.0-rc":
+ assert.Equal(t, root+"/query", r.ID)
+ case "RegistrationsBaseUrl":
+ fallthrough
+ case "RegistrationsBaseUrl/3.0.0-beta":
+ fallthrough
+ case "RegistrationsBaseUrl/3.0.0-rc":
+ assert.Equal(t, root+"/registration", r.ID)
+ case "PackageBaseAddress/3.0.0":
+ assert.Equal(t, root+"/package", r.ID)
+ case "PackagePublish/2.0.0":
+ assert.Equal(t, root, r.ID)
+ }
}
- }
+ })
}
})
})
@@ -222,6 +246,7 @@ func TestPackageNuGet(t *testing.T) {
t.Run("DependencyPackage", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
+ // create with username/password
req := NewRequestWithBody(t, "PUT", url, bytes.NewReader(content)).
AddBasicAuth(user.Name)
MakeRequest(t, req, http.StatusCreated)
@@ -258,6 +283,52 @@ func TestPackageNuGet(t *testing.T) {
req = NewRequestWithBody(t, "PUT", url, bytes.NewReader(content)).
AddBasicAuth(user.Name)
MakeRequest(t, req, http.StatusConflict)
+
+ // delete the package
+ assert.NoError(t, packageService.DeletePackageVersionAndReferences(db.DefaultContext, pvs[0]))
+
+ // create failure with token without write access
+ req = NewRequestWithBody(t, "PUT", url, bytes.NewReader(content)).
+ AddTokenAuth(readToken)
+ MakeRequest(t, req, http.StatusUnauthorized)
+
+ // create with token
+ req = NewRequestWithBody(t, "PUT", url, bytes.NewReader(content)).
+ AddTokenAuth(writeToken)
+ MakeRequest(t, req, http.StatusCreated)
+
+ pvs, err = packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeNuGet)
+ assert.NoError(t, err)
+ assert.Len(t, pvs, 1, "Should have one version")
+
+ pd, err = packages.GetPackageDescriptor(db.DefaultContext, pvs[0])
+ assert.NoError(t, err)
+ assert.NotNil(t, pd.SemVer)
+ assert.IsType(t, &nuget_module.Metadata{}, pd.Metadata)
+ assert.Equal(t, packageName, pd.Package.Name)
+ assert.Equal(t, packageVersion, pd.Version.Version)
+
+ pfs, err = packages.GetFilesByVersionID(db.DefaultContext, pvs[0].ID)
+ assert.NoError(t, err)
+ assert.Len(t, pfs, 2, "Should have 2 files: nuget and nuspec")
+ for _, pf := range pfs {
+ switch pf.Name {
+ case fmt.Sprintf("%s.%s.nupkg", packageName, packageVersion):
+ assert.True(t, pf.IsLead)
+
+ pb, err := packages.GetBlobByID(db.DefaultContext, pf.BlobID)
+ assert.NoError(t, err)
+ assert.Equal(t, int64(len(content)), pb.Size)
+ case fmt.Sprintf("%s.nuspec", packageName):
+ assert.False(t, pf.IsLead)
+ default:
+ assert.Fail(t, "unexpected filename: %v", pf.Name)
+ }
+ }
+
+ req = NewRequestWithBody(t, "PUT", url, bytes.NewReader(content)).
+ AddBasicAuth(user.Name)
+ MakeRequest(t, req, http.StatusConflict)
})
t.Run("SymbolPackage", func(t *testing.T) {