From 5c05dddbed8247a4fb272619f1eb7cf090443b8b Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Thu, 5 Sep 2024 15:05:42 +0800 Subject: Fix nuget/conan/container packages upload bugs (#31967) --- models/auth/access_token_scope.go | 16 ++ routers/api/packages/conan/auth.go | 10 +- routers/api/packages/conan/conan.go | 35 +++- routers/api/packages/container/auth.go | 11 +- routers/api/packages/container/container.go | 17 +- routers/api/packages/nuget/auth.go | 3 + services/auth/basic.go | 27 ++- services/packages/auth.go | 30 +++- tests/integration/api_packages_conan_test.go | 170 ++++++++++++++++++- tests/integration/api_packages_container_test.go | 80 ++++++++- tests/integration/api_packages_nuget_test.go | 203 +++++++++++++++-------- 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,11 +807,19 @@ 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) @@ -678,6 +830,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,11 +855,19 @@ 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) { -- cgit v1.2.3