mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-30 06:38:37 -04:00 
			
		
		
		
	Backport #31967 by @lunny Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
This commit is contained in:
		| @@ -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] | ||||
|   | ||||
| @@ -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 | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
| } | ||||
|   | ||||
| @@ -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 "" | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
| } | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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) | ||||
|  | ||||
|   | ||||
| @@ -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) | ||||
| 				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.UseTokenAuth { | ||||
| 					addNuGetAPIKeyHeader(req, token) | ||||
| 				} | ||||
| 				resp := MakeRequest(t, req, http.StatusOK) | ||||
| 					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) | ||||
| 				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.UseTokenAuth { | ||||
| 					addNuGetAPIKeyHeader(req, token) | ||||
| 				} | ||||
| 				resp := MakeRequest(t, req, http.StatusOK) | ||||
|  | ||||
| 				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) | ||||
| 					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) | ||||
|  | ||||
| 					if c.expectedStatus != http.StatusOK { | ||||
| 						return | ||||
| 					} | ||||
|  | ||||
| 					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) { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user