Fix nuget/conan/container packages upload bugs (#31967)

pull/31951/head
Lunny Xiao 3 months ago committed by GitHub
parent 74b1c589c6
commit 5c05dddbed
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 16
      models/auth/access_token_scope.go
  2. 10
      routers/api/packages/conan/auth.go
  3. 35
      routers/api/packages/conan/conan.go
  4. 11
      routers/api/packages/container/auth.go
  5. 17
      routers/api/packages/container/container.go
  6. 3
      routers/api/packages/nuget/auth.go
  7. 27
      services/auth/basic.go
  8. 28
      services/packages/auth.go
  9. 170
      tests/integration/api_packages_conan_test.go
  10. 80
      tests/integration/api_packages_container_test.go
  11. 113
      tests/integration/api_packages_nuget_test.go

@ -309,6 +309,22 @@ func (s AccessTokenScope) HasScope(scopes ...AccessTokenScope) (bool, error) {
return true, nil 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 // hasScope returns true if the string has the given scope
func (bitmap accessTokenScopeBitmap) hasScope(scope AccessTokenScope) (bool, error) { func (bitmap accessTokenScopeBitmap) hasScope(scope AccessTokenScope) (bool, error) {
expectedBits, ok := allAccessTokenScopeBits[scope] expectedBits, ok := allAccessTokenScopeBits[scope]

@ -22,21 +22,25 @@ func (a *Auth) Name() string {
// Verify extracts the user from the Bearer token // 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) { 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 { if err != nil {
log.Trace("ParseAuthorizationToken: %v", err) log.Trace("ParseAuthorizationToken: %v", err)
return nil, err return nil, err
} }
if uid == 0 { if packageMeta == nil || packageMeta.UserID == 0 {
return nil, nil return nil, nil
} }
u, err := user_model.GetUserByID(req.Context(), uid) u, err := user_model.GetUserByID(req.Context(), packageMeta.UserID)
if err != nil { if err != nil {
log.Error("GetUserByID: %v", err) log.Error("GetUserByID: %v", err)
return nil, err return nil, err
} }
if packageMeta.Scope != "" {
store.GetData()["IsApiToken"] = true
store.GetData()["ApiTokenScope"] = packageMeta.Scope
}
return u, nil return u, nil
} }

@ -11,6 +11,7 @@ import (
"strings" "strings"
"time" "time"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
packages_model "code.gitea.io/gitea/models/packages" packages_model "code.gitea.io/gitea/models/packages"
conan_model "code.gitea.io/gitea/models/packages/conan" conan_model "code.gitea.io/gitea/models/packages/conan"
@ -21,6 +22,7 @@ import (
conan_module "code.gitea.io/gitea/modules/packages/conan" conan_module "code.gitea.io/gitea/modules/packages/conan"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/routers/api/packages/helper" "code.gitea.io/gitea/routers/api/packages/helper"
auth_service "code.gitea.io/gitea/services/auth"
"code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context"
notify_service "code.gitea.io/gitea/services/notify" notify_service "code.gitea.io/gitea/services/notify"
packages_service "code.gitea.io/gitea/services/packages" packages_service "code.gitea.io/gitea/services/packages"
@ -117,7 +119,20 @@ func Authenticate(ctx *context.Context) {
return 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 { if err != nil {
apiError(ctx, http.StatusInternalServerError, err) apiError(ctx, http.StatusInternalServerError, err)
return return
@ -130,9 +145,23 @@ func Authenticate(ctx *context.Context) {
func CheckCredentials(ctx *context.Context) { func CheckCredentials(ctx *context.Context) {
if ctx.Doer == nil { if ctx.Doer == nil {
ctx.Status(http.StatusUnauthorized) ctx.Status(http.StatusUnauthorized)
} else { return
ctx.Status(http.StatusOK)
} }
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 // RecipeSnapshot displays the recipe files with their md5 hash

@ -23,21 +23,26 @@ func (a *Auth) Name() string {
// Verify extracts the user from the Bearer token // Verify extracts the user from the Bearer token
// If it's an anonymous session a ghost user is returned // 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) { 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 { if err != nil {
log.Trace("ParseAuthorizationToken: %v", err) log.Trace("ParseAuthorizationToken: %v", err)
return nil, err return nil, err
} }
if uid == 0 { if packageMeta == nil || packageMeta.UserID == 0 {
return nil, nil return nil, nil
} }
u, err := user_model.GetPossibleUserByID(req.Context(), uid) u, err := user_model.GetPossibleUserByID(req.Context(), packageMeta.UserID)
if err != nil { if err != nil {
log.Error("GetPossibleUserByID: %v", err) log.Error("GetPossibleUserByID: %v", err)
return nil, err return nil, err
} }
if packageMeta.Scope != "" {
store.GetData()["IsApiToken"] = true
store.GetData()["ApiTokenScope"] = packageMeta.Scope
}
return u, nil return u, nil
} }

@ -14,6 +14,7 @@ import (
"strconv" "strconv"
"strings" "strings"
auth_model "code.gitea.io/gitea/models/auth"
packages_model "code.gitea.io/gitea/models/packages" packages_model "code.gitea.io/gitea/models/packages"
container_model "code.gitea.io/gitea/models/packages/container" container_model "code.gitea.io/gitea/models/packages/container"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
@ -25,6 +26,7 @@ import (
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/packages/helper" "code.gitea.io/gitea/routers/api/packages/helper"
auth_service "code.gitea.io/gitea/services/auth"
"code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context"
packages_service "code.gitea.io/gitea/services/packages" packages_service "code.gitea.io/gitea/services/packages"
container_service "code.gitea.io/gitea/services/packages/container" 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. // If the current user is anonymous, the ghost user is used unless RequireSignInView is enabled.
func Authenticate(ctx *context.Context) { func Authenticate(ctx *context.Context) {
u := ctx.Doer u := ctx.Doer
packageScope := auth_service.GetAccessScope(ctx.Data)
if u == nil { if u == nil {
if setting.Service.RequireSignInView { if setting.Service.RequireSignInView {
apiUnauthorizedError(ctx) apiUnauthorizedError(ctx)
@ -155,9 +158,21 @@ func Authenticate(ctx *context.Context) {
} }
u = user_model.NewGhostUser() 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 { if err != nil {
apiError(ctx, http.StatusInternalServerError, err) apiError(ctx, http.StatusInternalServerError, err)
return return

@ -43,5 +43,8 @@ func (a *Auth) Verify(req *http.Request, w http.ResponseWriter, store auth.DataS
log.Error("UpdateAccessToken: %v", err) log.Error("UpdateAccessToken: %v", err)
} }
store.GetData()["IsApiToken"] = true
store.GetData()["ApiToken"] = token
return u, nil return u, nil
} }

@ -25,7 +25,12 @@ var (
) )
// BasicMethodName is the constant name of the basic authentication method // 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 // 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" // 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 return nil, err
} }
store.GetData()["LoginMethod"] = OAuth2TokenMethodName
store.GetData()["IsApiToken"] = true store.GetData()["IsApiToken"] = true
return u, nil return u, nil
} }
@ -101,6 +107,7 @@ func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore
log.Error("UpdateAccessToken: %v", err) log.Error("UpdateAccessToken: %v", err)
} }
store.GetData()["LoginMethod"] = AccessTokenMethodName
store.GetData()["IsApiToken"] = true store.GetData()["IsApiToken"] = true
store.GetData()["ApiTokenScope"] = token.Scope store.GetData()["ApiTokenScope"] = token.Scope
return u, nil return u, nil
@ -113,6 +120,7 @@ func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore
if err == nil && task != nil { if err == nil && task != nil {
log.Trace("Basic Authorization: Valid AccessToken for task[%d]", task.ID) log.Trace("Basic Authorization: Valid AccessToken for task[%d]", task.ID)
store.GetData()["LoginMethod"] = ActionTokenMethodName
store.GetData()["IsActionsToken"] = true store.GetData()["IsActionsToken"] = true
store.GetData()["ActionsTaskID"] = task.ID 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) log.Trace("Basic Authorization: Logged in user %-v", u)
return u, nil return u, nil
@ -159,3 +168,19 @@ func validateTOTP(req *http.Request, u *user_model.User) error {
} }
return nil 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 ""
}
}

@ -9,6 +9,7 @@ import (
"strings" "strings"
"time" "time"
auth_model "code.gitea.io/gitea/models/auth"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
@ -18,10 +19,14 @@ import (
type packageClaims struct { type packageClaims struct {
jwt.RegisteredClaims jwt.RegisteredClaims
PackageMeta
}
type PackageMeta struct {
UserID int64 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() now := time.Now()
claims := packageClaims{ claims := packageClaims{
@ -29,7 +34,10 @@ func CreateAuthorizationToken(u *user_model.User) (string, error) {
ExpiresAt: jwt.NewNumericDate(now.Add(24 * time.Hour)), ExpiresAt: jwt.NewNumericDate(now.Add(24 * time.Hour)),
NotBefore: jwt.NewNumericDate(now), NotBefore: jwt.NewNumericDate(now),
}, },
PackageMeta: PackageMeta{
UserID: u.ID, UserID: u.ID,
Scope: packageScope,
},
} }
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
@ -41,32 +49,36 @@ func CreateAuthorizationToken(u *user_model.User) (string, error) {
return tokenString, nil return tokenString, nil
} }
func ParseAuthorizationToken(req *http.Request) (int64, error) { func ParseAuthorizationRequest(req *http.Request) (*PackageMeta, error) {
h := req.Header.Get("Authorization") h := req.Header.Get("Authorization")
if h == "" { if h == "" {
return 0, nil return nil, nil
} }
parts := strings.SplitN(h, " ", 2) parts := strings.SplitN(h, " ", 2)
if len(parts) != 2 { if len(parts) != 2 {
log.Error("split token failed: %s", h) log.Error("split token failed: %s", h)
return 0, fmt.Errorf("split token failed") return nil, fmt.Errorf("split token failed")
}
return ParseAuthorizationToken(parts[1])
} }
token, err := jwt.ParseWithClaims(parts[1], &packageClaims{}, func(t *jwt.Token) (any, error) { 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 { if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"]) return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
} }
return setting.GetGeneralTokenSigningSecret(), nil return setting.GetGeneralTokenSigningSecret(), nil
}) })
if err != nil { if err != nil {
return 0, err return nil, err
} }
c, ok := token.Claims.(*packageClaims) c, ok := token.Claims.(*packageClaims)
if !token.Valid || !ok { 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
} }

@ -11,6 +11,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"
conan_model "code.gitea.io/gitea/models/packages/conan" conan_model "code.gitea.io/gitea/models/packages/conan"
@ -19,6 +20,7 @@ import (
conan_module "code.gitea.io/gitea/modules/packages/conan" conan_module "code.gitea.io/gitea/modules/packages/conan"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
conan_router "code.gitea.io/gitea/routers/api/packages/conan" conan_router "code.gitea.io/gitea/routers/api/packages/conan"
package_service "code.gitea.io/gitea/services/packages"
"code.gitea.io/gitea/tests" "code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -225,7 +227,7 @@ func TestPackageConan(t *testing.T) {
token := "" token := ""
t.Run("Authenticate", func(t *testing.T) { t.Run("UserName/Password Authenticate", func(t *testing.T) {
defer tests.PrintCurrentTest(t)() defer tests.PrintCurrentTest(t)()
req := NewRequest(t, "GET", fmt.Sprintf("%s/v1/users/authenticate", url)). req := NewRequest(t, "GET", fmt.Sprintf("%s/v1/users/authenticate", url)).
@ -234,6 +236,73 @@ func TestPackageConan(t *testing.T) {
token = resp.Body.String() token = resp.Body.String()
assert.NotEmpty(t, token) 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) { 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{ 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, "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) }).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK) MakeRequest(t, req, http.StatusOK)
@ -457,6 +531,10 @@ func TestPackageConan(t *testing.T) {
assert.NotEmpty(t, revisions) assert.NotEmpty(t, revisions)
req := NewRequest(t, "DELETE", fmt.Sprintf("%s/v1/conans/%s/%s/%s/%s", url, name, version1, user1, c.Channel)). 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) AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK) MakeRequest(t, req, http.StatusOK)
@ -480,7 +558,7 @@ func TestPackageConan(t *testing.T) {
token := "" token := ""
t.Run("Authenticate", func(t *testing.T) { t.Run("UserName/Password Authenticate", func(t *testing.T) {
defer tests.PrintCurrentTest(t)() defer tests.PrintCurrentTest(t)()
req := NewRequest(t, "GET", fmt.Sprintf("%s/v2/users/authenticate", url)). req := NewRequest(t, "GET", fmt.Sprintf("%s/v2/users/authenticate", url)).
@ -490,9 +568,75 @@ func TestPackageConan(t *testing.T) {
body := resp.Body.String() body := resp.Body.String()
assert.NotEmpty(t, body) 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) 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) { t.Run("CheckCredentials", func(t *testing.T) {
defer tests.PrintCurrentTest(t)() defer tests.PrintCurrentTest(t)()
@ -511,7 +655,7 @@ func TestPackageConan(t *testing.T) {
pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeConan) pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeConan)
assert.NoError(t, err) 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) 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)). 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) AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK) MakeRequest(t, req, http.StatusOK)
checkPackageRevisionCount(1) 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)). 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) AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK) MakeRequest(t, req, http.StatusOK)
@ -678,6 +830,10 @@ func TestPackageConan(t *testing.T) {
checkPackageReferenceCount(1) 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)). req = NewRequest(t, "DELETE", fmt.Sprintf("%s/v2/conans/%s/%s/%s/%s/revisions/%s/packages", url, name, version1, user1, channel1, revision2)).
AddTokenAuth(token) AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK) MakeRequest(t, req, http.StatusOK)
@ -699,11 +855,19 @@ func TestPackageConan(t *testing.T) {
checkRecipeRevisionCount(2) checkRecipeRevisionCount(2)
req := NewRequest(t, "DELETE", fmt.Sprintf("%s/v2/conans/%s/%s/%s/%s/revisions/%s", url, name, version1, user1, channel1, revision1)). 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) AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK) MakeRequest(t, req, http.StatusOK)
checkRecipeRevisionCount(1) 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)). req = NewRequest(t, "DELETE", fmt.Sprintf("%s/v2/conans/%s/%s/%s/%s", url, name, version1, user1, channel1)).
AddTokenAuth(token) AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK) MakeRequest(t, req, http.StatusOK)

@ -23,6 +23,7 @@ import (
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/modules/test"
package_service "code.gitea.io/gitea/services/packages"
"code.gitea.io/gitea/tests" "code.gitea.io/gitea/tests"
oci "github.com/opencontainers/image-spec/specs-go/v1" oci "github.com/opencontainers/image-spec/specs-go/v1"
@ -78,6 +79,8 @@ func TestPackageContainer(t *testing.T) {
anonymousToken := "" anonymousToken := ""
userToken := "" userToken := ""
readToken := ""
badToken := ""
t.Run("Authenticate", func(t *testing.T) { t.Run("Authenticate", func(t *testing.T) {
type TokenResponse struct { 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")) 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)() defer tests.PrintCurrentTest(t)()
req := NewRequest(t, "GET", fmt.Sprintf("%sv2", setting.AppURL)) req := NewRequest(t, "GET", fmt.Sprintf("%sv2", setting.AppURL))
@ -139,6 +142,10 @@ func TestPackageContainer(t *testing.T) {
DecodeJSON(t, resp, &tokenResponse) DecodeJSON(t, resp, &tokenResponse)
assert.NotEmpty(t, tokenResponse.Token) 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) userToken = fmt.Sprintf("Bearer %s", tokenResponse.Token)
@ -146,6 +153,52 @@ func TestPackageContainer(t *testing.T) {
AddTokenAuth(userToken) AddTokenAuth(userToken)
MakeRequest(t, req, http.StatusOK) 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) { t.Run("DetermineSupport", func(t *testing.T) {
@ -155,6 +208,15 @@ func TestPackageContainer(t *testing.T) {
AddTokenAuth(userToken) AddTokenAuth(userToken)
resp := MakeRequest(t, req, http.StatusOK) resp := MakeRequest(t, req, http.StatusOK)
assert.Equal(t, "registry/2.0", resp.Header().Get("Docker-Distribution-Api-Version")) 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 { for _, image := range images {
@ -168,6 +230,14 @@ func TestPackageContainer(t *testing.T) {
AddTokenAuth(anonymousToken) AddTokenAuth(anonymousToken)
MakeRequest(t, req, http.StatusUnauthorized) 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)). req = NewRequestWithBody(t, "POST", fmt.Sprintf("%s/blobs/uploads?digest=%s", url, unknownDigest), bytes.NewReader(blobContent)).
AddTokenAuth(userToken) AddTokenAuth(userToken)
MakeRequest(t, req, http.StatusBadRequest) MakeRequest(t, req, http.StatusBadRequest)
@ -195,6 +265,14 @@ func TestPackageContainer(t *testing.T) {
defer tests.PrintCurrentTest(t)() defer tests.PrintCurrentTest(t)()
req := NewRequest(t, "POST", fmt.Sprintf("%s/blobs/uploads", url)). 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) AddTokenAuth(userToken)
resp := MakeRequest(t, req, http.StatusAccepted) resp := MakeRequest(t, req, http.StatusAccepted)

@ -26,6 +26,7 @@ import (
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/routers/api/packages/nuget" "code.gitea.io/gitea/routers/api/packages/nuget"
packageService "code.gitea.io/gitea/services/packages"
"code.gitea.io/gitea/tests" "code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -81,7 +82,9 @@ 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, 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" packageName := "test.package"
packageVersion := "1.0.3" packageVersion := "1.0.3"
@ -129,32 +132,42 @@ func TestPackageNuGet(t *testing.T) {
cases := []struct { cases := []struct {
Owner string Owner string
UseBasicAuth bool UseBasicAuth bool
UseTokenAuth bool token string
expectedStatus int
}{ }{
{privateUser.Name, false, false}, {privateUser.Name, false, "", http.StatusOK},
{privateUser.Name, true, false}, {privateUser.Name, true, "", http.StatusOK},
{privateUser.Name, false, true}, {privateUser.Name, false, writeToken, http.StatusOK},
{user.Name, false, false}, {privateUser.Name, false, readToken, http.StatusOK},
{user.Name, true, false}, {privateUser.Name, false, badToken, http.StatusOK},
{user.Name, false, true}, {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 { for _, c := range cases {
t.Run(c.Owner, func(t *testing.T) {
url := fmt.Sprintf("/api/packages/%s/nuget", c.Owner) url := fmt.Sprintf("/api/packages/%s/nuget", c.Owner)
req := NewRequest(t, "GET", url) req := NewRequest(t, "GET", url)
if c.UseBasicAuth { if c.UseBasicAuth {
req.AddBasicAuth(user.Name) req.AddBasicAuth(user.Name)
} else if c.UseTokenAuth { } else if c.token != "" {
addNuGetAPIKeyHeader(req, token) addNuGetAPIKeyHeader(req, c.token)
}
resp := MakeRequest(t, req, c.expectedStatus)
if c.expectedStatus != http.StatusOK {
return
} }
resp := MakeRequest(t, req, http.StatusOK)
var result nuget.ServiceIndexResponseV2 var result nuget.ServiceIndexResponseV2
decodeXML(t, resp, &result) decodeXML(t, resp, &result)
assert.Equal(t, setting.AppURL+url[1:], result.Base) assert.Equal(t, setting.AppURL+url[1:], result.Base)
assert.Equal(t, "Packages", result.Workspace.Collection.Href) assert.Equal(t, "Packages", result.Workspace.Collection.Href)
})
} }
}) })
@ -166,26 +179,36 @@ func TestPackageNuGet(t *testing.T) {
cases := []struct { cases := []struct {
Owner string Owner string
UseBasicAuth bool UseBasicAuth bool
UseTokenAuth bool token string
expectedStatus int
}{ }{
{privateUser.Name, false, false}, {privateUser.Name, false, "", http.StatusOK},
{privateUser.Name, true, false}, {privateUser.Name, true, "", http.StatusOK},
{privateUser.Name, false, true}, {privateUser.Name, false, writeToken, http.StatusOK},
{user.Name, false, false}, {privateUser.Name, false, readToken, http.StatusOK},
{user.Name, true, false}, {privateUser.Name, false, badToken, http.StatusOK},
{user.Name, false, true}, {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 { for _, c := range cases {
t.Run(c.Owner, func(t *testing.T) {
url := fmt.Sprintf("/api/packages/%s/nuget", c.Owner) url := fmt.Sprintf("/api/packages/%s/nuget", c.Owner)
req := NewRequest(t, "GET", fmt.Sprintf("%s/index.json", url)) req := NewRequest(t, "GET", fmt.Sprintf("%s/index.json", url))
if c.UseBasicAuth { if c.UseBasicAuth {
req.AddBasicAuth(user.Name) req.AddBasicAuth(user.Name)
} else if c.UseTokenAuth { } else if c.token != "" {
addNuGetAPIKeyHeader(req, token) addNuGetAPIKeyHeader(req, c.token)
}
resp := MakeRequest(t, req, c.expectedStatus)
if c.expectedStatus != http.StatusOK {
return
} }
resp := MakeRequest(t, req, http.StatusOK)
var result nuget.ServiceIndexResponseV3 var result nuget.ServiceIndexResponseV3
DecodeJSON(t, resp, &result) DecodeJSON(t, resp, &result)
@ -214,6 +237,7 @@ func TestPackageNuGet(t *testing.T) {
assert.Equal(t, root, r.ID) assert.Equal(t, root, r.ID)
} }
} }
})
} }
}) })
}) })
@ -222,6 +246,7 @@ func TestPackageNuGet(t *testing.T) {
t.Run("DependencyPackage", func(t *testing.T) { t.Run("DependencyPackage", func(t *testing.T) {
defer tests.PrintCurrentTest(t)() defer tests.PrintCurrentTest(t)()
// create with username/password
req := NewRequestWithBody(t, "PUT", url, bytes.NewReader(content)). req := NewRequestWithBody(t, "PUT", url, bytes.NewReader(content)).
AddBasicAuth(user.Name) AddBasicAuth(user.Name)
MakeRequest(t, req, http.StatusCreated) MakeRequest(t, req, http.StatusCreated)
@ -258,6 +283,52 @@ func TestPackageNuGet(t *testing.T) {
req = NewRequestWithBody(t, "PUT", url, bytes.NewReader(content)). req = NewRequestWithBody(t, "PUT", url, bytes.NewReader(content)).
AddBasicAuth(user.Name) AddBasicAuth(user.Name)
MakeRequest(t, req, http.StatusConflict) 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) { t.Run("SymbolPackage", func(t *testing.T) {

Loading…
Cancel
Save