From 0c8ce71188cae1d59380a213816a22bce48691db Mon Sep 17 00:00:00 2001
From: KN4CK3R <admin@oldschoolhack.me>
Date: Sat, 24 Sep 2022 17:17:08 +0200
Subject: [PATCH] Make NuGet service index publicly accessible (#21242)

Addition to #20734, Fixes #20717

The `/index.json` endpoint needs to be accessible even if the registry
is private. The NuGet client uses this endpoint without
authentification.

The old fix only works if the NuGet cli is used with `--source <name>`
but not with `--source <url>/index.json`.

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 routers/api/packages/api.go                  | 54 +++++++-------
 tests/integration/api_packages_nuget_test.go | 78 ++++++++++++--------
 2 files changed, 77 insertions(+), 55 deletions(-)

diff --git a/routers/api/packages/api.go b/routers/api/packages/api.go
index cb9b3b78ab..3354fe12d4 100644
--- a/routers/api/packages/api.go
+++ b/routers/api/packages/api.go
@@ -69,7 +69,7 @@ func Routes(ctx gocontext.Context) *web.Route {
 			r.Get("/p2/{vendorname}/{projectname}.json", composer.PackageMetadata)
 			r.Get("/files/{package}/{version}/{filename}", composer.DownloadPackageFile)
 			r.Put("", reqPackageAccess(perm.AccessModeWrite), composer.UploadPackage)
-		})
+		}, reqPackageAccess(perm.AccessModeRead))
 		r.Group("/conan", func() {
 			r.Group("/v1", func() {
 				r.Get("/ping", conan.Ping)
@@ -157,7 +157,7 @@ func Routes(ctx gocontext.Context) *web.Route {
 					}, conan.ExtractPathParameters)
 				})
 			})
-		})
+		}, reqPackageAccess(perm.AccessModeRead))
 		r.Group("/generic", func() {
 			r.Group("/{packagename}/{packageversion}", func() {
 				r.Delete("", reqPackageAccess(perm.AccessModeWrite), generic.DeletePackage)
@@ -169,33 +169,35 @@ func Routes(ctx gocontext.Context) *web.Route {
 					}, reqPackageAccess(perm.AccessModeWrite))
 				})
 			})
-		})
+		}, reqPackageAccess(perm.AccessModeRead))
 		r.Group("/helm", func() {
 			r.Get("/index.yaml", helm.Index)
 			r.Get("/{filename}", helm.DownloadPackageFile)
 			r.Post("/api/charts", reqPackageAccess(perm.AccessModeWrite), helm.UploadPackage)
-		})
+		}, reqPackageAccess(perm.AccessModeRead))
 		r.Group("/maven", func() {
 			r.Put("/*", reqPackageAccess(perm.AccessModeWrite), maven.UploadPackageFile)
 			r.Get("/*", maven.DownloadPackageFile)
-		})
+		}, reqPackageAccess(perm.AccessModeRead))
 		r.Group("/nuget", func() {
-			r.Get("/index.json", nuget.ServiceIndex)
-			r.Get("/query", nuget.SearchService)
-			r.Group("/registration/{id}", func() {
-				r.Get("/index.json", nuget.RegistrationIndex)
-				r.Get("/{version}", nuget.RegistrationLeaf)
-			})
-			r.Group("/package/{id}", func() {
-				r.Get("/index.json", nuget.EnumeratePackageVersions)
-				r.Get("/{version}/{filename}", nuget.DownloadPackageFile)
-			})
+			r.Get("/index.json", nuget.ServiceIndex) // Needs to be unauthenticated for the NuGet client.
 			r.Group("", func() {
-				r.Put("/", nuget.UploadPackage)
-				r.Put("/symbolpackage", nuget.UploadSymbolPackage)
-				r.Delete("/{id}/{version}", nuget.DeletePackage)
-			}, reqPackageAccess(perm.AccessModeWrite))
-			r.Get("/symbols/{filename}/{guid:[0-9a-f]{32}}FFFFFFFF/{filename2}", nuget.DownloadSymbolFile)
+				r.Get("/query", nuget.SearchService)
+				r.Group("/registration/{id}", func() {
+					r.Get("/index.json", nuget.RegistrationIndex)
+					r.Get("/{version}", nuget.RegistrationLeaf)
+				})
+				r.Group("/package/{id}", func() {
+					r.Get("/index.json", nuget.EnumeratePackageVersions)
+					r.Get("/{version}/{filename}", nuget.DownloadPackageFile)
+				})
+				r.Group("", func() {
+					r.Put("/", nuget.UploadPackage)
+					r.Put("/symbolpackage", nuget.UploadSymbolPackage)
+					r.Delete("/{id}/{version}", nuget.DeletePackage)
+				}, reqPackageAccess(perm.AccessModeWrite))
+				r.Get("/symbols/{filename}/{guid:[0-9a-f]{32}}FFFFFFFF/{filename2}", nuget.DownloadSymbolFile)
+			}, reqPackageAccess(perm.AccessModeRead))
 		})
 		r.Group("/npm", func() {
 			r.Group("/@{scope}/{id}", func() {
@@ -239,7 +241,7 @@ func Routes(ctx gocontext.Context) *web.Route {
 			r.Group("/-/v1/search", func() {
 				r.Get("", npm.PackageSearch)
 			})
-		})
+		}, reqPackageAccess(perm.AccessModeRead))
 		r.Group("/pub", func() {
 			r.Group("/api/packages", func() {
 				r.Group("/versions/new", func() {
@@ -253,12 +255,12 @@ func Routes(ctx gocontext.Context) *web.Route {
 					r.Get("/{version}", pub.PackageVersionMetadata)
 				})
 			})
-		})
+		}, reqPackageAccess(perm.AccessModeRead))
 		r.Group("/pypi", func() {
 			r.Post("/", reqPackageAccess(perm.AccessModeWrite), pypi.UploadPackageFile)
 			r.Get("/files/{id}/{version}/{filename}", pypi.DownloadPackageFile)
 			r.Get("/simple/{id}", pypi.PackageMetadata)
-		})
+		}, reqPackageAccess(perm.AccessModeRead))
 		r.Group("/rubygems", func() {
 			r.Get("/specs.4.8.gz", rubygems.EnumeratePackages)
 			r.Get("/latest_specs.4.8.gz", rubygems.EnumeratePackagesLatest)
@@ -269,7 +271,7 @@ func Routes(ctx gocontext.Context) *web.Route {
 				r.Post("/", rubygems.UploadPackageFile)
 				r.Delete("/yank", rubygems.DeletePackage)
 			}, reqPackageAccess(perm.AccessModeWrite))
-		})
+		}, reqPackageAccess(perm.AccessModeRead))
 		r.Group("/vagrant", func() {
 			r.Group("/authenticate", func() {
 				r.Get("", vagrant.CheckAuthenticate)
@@ -282,8 +284,8 @@ func Routes(ctx gocontext.Context) *web.Route {
 					r.Put("", reqPackageAccess(perm.AccessModeWrite), vagrant.UploadPackageFile)
 				})
 			})
-		})
-	}, context_service.UserAssignmentWeb(), context.PackageAssignment(), reqPackageAccess(perm.AccessModeRead))
+		}, reqPackageAccess(perm.AccessModeRead))
+	}, context_service.UserAssignmentWeb(), context.PackageAssignment())
 
 	return r
 }
diff --git a/tests/integration/api_packages_nuget_test.go b/tests/integration/api_packages_nuget_test.go
index 87275feb3e..9d53311d35 100644
--- a/tests/integration/api_packages_nuget_test.go
+++ b/tests/integration/api_packages_nuget_test.go
@@ -19,6 +19,7 @@ import (
 	user_model "code.gitea.io/gitea/models/user"
 	nuget_module "code.gitea.io/gitea/modules/packages/nuget"
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/routers/api/packages/nuget"
 	"code.gitea.io/gitea/tests"
 
@@ -66,39 +67,58 @@ func TestPackageNuGet(t *testing.T) {
 	t.Run("ServiceIndex", func(t *testing.T) {
 		defer tests.PrintCurrentTest(t)()
 
-		req := NewRequest(t, "GET", fmt.Sprintf("%s/index.json", url))
-		req = AddBasicAuthHeader(req, user.Name)
-		MakeRequest(t, req, http.StatusOK)
+		privateUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{Visibility: structs.VisibleTypePrivate})
 
-		req = NewRequest(t, "GET", fmt.Sprintf("%s/index.json", url))
-		req = addNuGetAPIKeyHeader(req, token)
-		resp := MakeRequest(t, req, http.StatusOK)
+		cases := []struct {
+			Owner        string
+			UseBasicAuth bool
+			UseTokenAuth bool
+		}{
+			{privateUser.Name, false, false},
+			{privateUser.Name, true, false},
+			{privateUser.Name, false, true},
+			{user.Name, false, false},
+			{user.Name, true, false},
+			{user.Name, false, true},
+		}
 
-		var result nuget.ServiceIndexResponse
-		DecodeJSON(t, resp, &result)
+		for _, c := range cases {
+			url := fmt.Sprintf("/api/packages/%s/nuget", c.Owner)
 
-		assert.Equal(t, "3.0.0", result.Version)
-		assert.NotEmpty(t, result.Resources)
+			req := NewRequest(t, "GET", fmt.Sprintf("%s/index.json", url))
+			if c.UseBasicAuth {
+				req = AddBasicAuthHeader(req, user.Name)
+			} else if c.UseTokenAuth {
+				req = addNuGetAPIKeyHeader(req, token)
+			}
+			resp := MakeRequest(t, req, http.StatusOK)
 
-		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.ServiceIndexResponse
+			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)
+				}
 			}
 		}
 	})