From 8adba93498ccdde7edcb54e10f6a3d176c3815c4 Mon Sep 17 00:00:00 2001
From: KN4CK3R <admin@oldschoolhack.me>
Date: Sat, 7 May 2022 18:21:15 +0200
Subject: [PATCH] Hide private repositories in packages (#19584)

---
 integrations/api_packages_test.go       | 38 +++++++++++++++++++++++++
 modules/convert/package.go              | 22 ++++++++++----
 modules/notification/webhook/webhook.go | 18 ++++++------
 routers/api/v1/packages/package.go      | 15 ++++++++--
 routers/web/repo/packages.go            |  1 +
 routers/web/user/package.go             | 29 +++++++++++++++++++
 templates/package/shared/list.tmpl      |  4 +++
 templates/package/view.tmpl             |  8 +++---
 8 files changed, 115 insertions(+), 20 deletions(-)

diff --git a/integrations/api_packages_test.go b/integrations/api_packages_test.go
index b3f6e88d9f..1f24807060 100644
--- a/integrations/api_packages_test.go
+++ b/integrations/api_packages_test.go
@@ -71,6 +71,44 @@ func TestPackageAPI(t *testing.T) {
 		assert.Equal(t, packageVersion, p.Version)
 		assert.NotNil(t, p.Creator)
 		assert.Equal(t, user.Name, p.Creator.UserName)
+
+		t.Run("RepositoryLink", func(t *testing.T) {
+			defer PrintCurrentTest(t)()
+
+			p, err := packages_model.GetPackageByName(db.DefaultContext, user.ID, packages_model.TypeGeneric, packageName)
+			assert.NoError(t, err)
+
+			// no repository link
+			req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/packages/%s/generic/%s/%s?token=%s", user.Name, packageName, packageVersion, token))
+			resp := MakeRequest(t, req, http.StatusOK)
+
+			var ap1 *api.Package
+			DecodeJSON(t, resp, &ap1)
+			assert.Nil(t, ap1.Repository)
+
+			// link to public repository
+			assert.NoError(t, packages_model.SetRepositoryLink(db.DefaultContext, p.ID, 1))
+
+			req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/packages/%s/generic/%s/%s?token=%s", user.Name, packageName, packageVersion, token))
+			resp = MakeRequest(t, req, http.StatusOK)
+
+			var ap2 *api.Package
+			DecodeJSON(t, resp, &ap2)
+			assert.NotNil(t, ap2.Repository)
+			assert.EqualValues(t, 1, ap2.Repository.ID)
+
+			// link to private repository
+			assert.NoError(t, packages_model.SetRepositoryLink(db.DefaultContext, p.ID, 2))
+
+			req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/packages/%s/generic/%s/%s?token=%s", user.Name, packageName, packageVersion, token))
+			resp = MakeRequest(t, req, http.StatusOK)
+
+			var ap3 *api.Package
+			DecodeJSON(t, resp, &ap3)
+			assert.Nil(t, ap3.Repository)
+
+			assert.NoError(t, packages_model.UnlinkRepositoryFromAllPackages(db.DefaultContext, 2))
+		})
 	})
 
 	t.Run("ListPackageFiles", func(t *testing.T) {
diff --git a/modules/convert/package.go b/modules/convert/package.go
index 681219ca1a..a4ea41d522 100644
--- a/modules/convert/package.go
+++ b/modules/convert/package.go
@@ -5,28 +5,38 @@
 package convert
 
 import (
+	"context"
+
+	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/models/packages"
-	"code.gitea.io/gitea/models/perm"
+	user_model "code.gitea.io/gitea/models/user"
 	api "code.gitea.io/gitea/modules/structs"
 )
 
 // ToPackage convert a packages.PackageDescriptor to api.Package
-func ToPackage(pd *packages.PackageDescriptor) *api.Package {
+func ToPackage(ctx context.Context, pd *packages.PackageDescriptor, doer *user_model.User) (*api.Package, error) {
 	var repo *api.Repository
 	if pd.Repository != nil {
-		repo = ToRepo(pd.Repository, perm.AccessModeNone)
+		permission, err := models.GetUserRepoPermission(ctx, pd.Repository, doer)
+		if err != nil {
+			return nil, err
+		}
+
+		if permission.HasAccess() {
+			repo = ToRepo(pd.Repository, permission.AccessMode)
+		}
 	}
 
 	return &api.Package{
 		ID:         pd.Version.ID,
-		Owner:      ToUser(pd.Owner, nil),
+		Owner:      ToUser(pd.Owner, doer),
 		Repository: repo,
-		Creator:    ToUser(pd.Creator, nil),
+		Creator:    ToUser(pd.Creator, doer),
 		Type:       string(pd.Package.Type),
 		Name:       pd.Package.Name,
 		Version:    pd.Version.Version,
 		CreatedAt:  pd.Version.CreatedUnix.AsTime(),
-	}
+	}, nil
 }
 
 // ToPackageFile converts packages.PackageFileDescriptor to api.PackageFile
diff --git a/modules/notification/webhook/webhook.go b/modules/notification/webhook/webhook.go
index d24440d585..c59e972ed6 100644
--- a/modules/notification/webhook/webhook.go
+++ b/modules/notification/webhook/webhook.go
@@ -872,17 +872,19 @@ func notifyPackage(sender *user_model.User, pd *packages_model.PackageDescriptor
 		return
 	}
 
-	org := pd.Owner
-	if !org.IsOrganization() {
-		org = nil
+	ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(), fmt.Sprintf("webhook.notifyPackage Package: %s[%d]", pd.Package.Name, pd.Package.ID))
+	defer finished()
+
+	apiPackage, err := convert.ToPackage(ctx, pd, sender)
+	if err != nil {
+		log.Error("Error converting package: %v", err)
+		return
 	}
 
 	if err := webhook_services.PrepareWebhooks(pd.Repository, webhook.HookEventPackage, &api.PackagePayload{
-		Action:       action,
-		Repository:   convert.ToRepo(pd.Repository, perm.AccessModeNone),
-		Package:      convert.ToPackage(pd),
-		Organization: convert.ToUser(org, nil),
-		Sender:       convert.ToUser(sender, nil),
+		Action:  action,
+		Package: apiPackage,
+		Sender:  convert.ToUser(sender, nil),
 	}); err != nil {
 		log.Error("PrepareWebhooks: %v", err)
 	}
diff --git a/routers/api/v1/packages/package.go b/routers/api/v1/packages/package.go
index f3aa19c319..038924737a 100644
--- a/routers/api/v1/packages/package.go
+++ b/routers/api/v1/packages/package.go
@@ -73,7 +73,12 @@ func ListPackages(ctx *context.APIContext) {
 
 	apiPackages := make([]*api.Package, 0, len(pds))
 	for _, pd := range pds {
-		apiPackages = append(apiPackages, convert.ToPackage(pd))
+		apiPackage, err := convert.ToPackage(ctx, pd, ctx.Doer)
+		if err != nil {
+			ctx.Error(http.StatusInternalServerError, "Error converting package for api", err)
+			return
+		}
+		apiPackages = append(apiPackages, apiPackage)
 	}
 
 	ctx.SetLinkHeader(int(count), listOptions.PageSize)
@@ -115,7 +120,13 @@ func GetPackage(ctx *context.APIContext) {
 	//   "404":
 	//     "$ref": "#/responses/notFound"
 
-	ctx.JSON(http.StatusOK, convert.ToPackage(ctx.Package.Descriptor))
+	apiPackage, err := convert.ToPackage(ctx, ctx.Package.Descriptor, ctx.Doer)
+	if err != nil {
+		ctx.Error(http.StatusInternalServerError, "Error converting package for api", err)
+		return
+	}
+
+	ctx.JSON(http.StatusOK, apiPackage)
 }
 
 // DeletePackage deletes a package
diff --git a/routers/web/repo/packages.go b/routers/web/repo/packages.go
index b4db2d5787..03ea4fc5f4 100644
--- a/routers/web/repo/packages.go
+++ b/routers/web/repo/packages.go
@@ -62,6 +62,7 @@ func Packages(ctx *context.Context) {
 	ctx.Data["HasPackages"] = hasPackages
 	ctx.Data["PackageDescriptors"] = pds
 	ctx.Data["Total"] = total
+	ctx.Data["RepositoryAccessMap"] = map[int64]bool{ctx.Repo.Repository.ID: true} // There is only the current repository
 
 	pager := context.NewPagination(int(total), setting.UI.PackagesPagingNum, page, 5)
 	pager.AddParam(ctx, "q", "Query")
diff --git a/routers/web/user/package.go b/routers/web/user/package.go
index 04b4e1e8ec..1c33998db9 100644
--- a/routers/web/user/package.go
+++ b/routers/web/user/package.go
@@ -58,6 +58,23 @@ func ListPackages(ctx *context.Context) {
 		return
 	}
 
+	repositoryAccessMap := make(map[int64]bool)
+	for _, pd := range pds {
+		if pd.Repository == nil {
+			continue
+		}
+		if _, has := repositoryAccessMap[pd.Repository.ID]; has {
+			continue
+		}
+
+		permission, err := models.GetUserRepoPermission(ctx, pd.Repository, ctx.Doer)
+		if err != nil {
+			ctx.ServerError("GetUserRepoPermission", err)
+			return
+		}
+		repositoryAccessMap[pd.Repository.ID] = permission.HasAccess()
+	}
+
 	hasPackages, err := packages_model.HasOwnerPackages(ctx, ctx.ContextUser.ID)
 	if err != nil {
 		ctx.ServerError("HasOwnerPackages", err)
@@ -72,6 +89,7 @@ func ListPackages(ctx *context.Context) {
 	ctx.Data["HasPackages"] = hasPackages
 	ctx.Data["PackageDescriptors"] = pds
 	ctx.Data["Total"] = total
+	ctx.Data["RepositoryAccessMap"] = repositoryAccessMap
 
 	pager := context.NewPagination(int(total), setting.UI.PackagesPagingNum, page, 5)
 	pager.AddParam(ctx, "q", "Query")
@@ -157,6 +175,17 @@ func ViewPackageVersion(ctx *context.Context) {
 
 	ctx.Data["CanWritePackages"] = ctx.Package.AccessMode >= perm.AccessModeWrite || ctx.IsUserSiteAdmin()
 
+	hasRepositoryAccess := false
+	if pd.Repository != nil {
+		permission, err := models.GetUserRepoPermission(ctx, pd.Repository, ctx.Doer)
+		if err != nil {
+			ctx.ServerError("GetUserRepoPermission", err)
+			return
+		}
+		hasRepositoryAccess = permission.HasAccess()
+	}
+	ctx.Data["HasRepositoryAccess"] = hasRepositoryAccess
+
 	ctx.HTML(http.StatusOK, tplPackagesView)
 }
 
diff --git a/templates/package/shared/list.tmpl b/templates/package/shared/list.tmpl
index 0b0f71283b..9e6bf5ce9e 100644
--- a/templates/package/shared/list.tmpl
+++ b/templates/package/shared/list.tmpl
@@ -30,7 +30,11 @@
 					</div>
 					<div class="desc issue-item-bottom-row df ac fw my-1">
 						{{$timeStr := TimeSinceUnix .Version.CreatedUnix $.i18n.Lang}}
+						{{$hasRepositoryAccess := false}}
 						{{if .Repository}}
+							{{$hasRepositoryAccess = index $.RepositoryAccessMap .Repository.ID}}
+						{{end}}
+						{{if $hasRepositoryAccess}}
 							{{$.i18n.Tr "packages.published_by_in" $timeStr .Creator.HomeLink (.Creator.GetDisplayName | Escape) .Repository.HTMLURL (.Repository.FullName | Escape) | Safe}}
 						{{else}}
 							{{$.i18n.Tr "packages.published_by" $timeStr .Creator.HomeLink (.Creator.GetDisplayName | Escape) | Safe}}
diff --git a/templates/package/view.tmpl b/templates/package/view.tmpl
index bb96da3410..efad9f9b8f 100644
--- a/templates/package/view.tmpl
+++ b/templates/package/view.tmpl
@@ -10,7 +10,7 @@
 					</div>
 					<div>
 						{{$timeStr := TimeSinceUnix .PackageDescriptor.Version.CreatedUnix $.i18n.Lang}}
-						{{if .PackageDescriptor.Repository}}
+						{{if .HasRepositoryAccess}}
 							{{.i18n.Tr "packages.published_by_in" $timeStr .PackageDescriptor.Creator.HomeLink (.PackageDescriptor.Creator.GetDisplayName | Escape) .PackageDescriptor.Repository.HTMLURL (.PackageDescriptor.Repository.FullName | Escape) | Safe}}
 						{{else}}
 							{{.i18n.Tr "packages.published_by" $timeStr .PackageDescriptor.Creator.HomeLink (.PackageDescriptor.Creator.GetDisplayName | Escape) | Safe}}
@@ -35,7 +35,7 @@
 						<strong>{{.i18n.Tr "packages.details"}}</strong>
 						<div class="ui relaxed list">
 							<div class="item">{{svg .PackageDescriptor.Package.Type.SVGName 16 "mr-3"}} {{.PackageDescriptor.Package.Type.Name}}</div>
-							{{if .PackageDescriptor.Repository}}
+							{{if .HasRepositoryAccess}}
 							<div class="item">{{svg "octicon-repo" 16 "mr-3"}} <a href="{{.PackageDescriptor.Repository.HTMLURL}}">{{.PackageDescriptor.Repository.FullName}}</a></div>
 							{{end}}
 							<div class="item">{{svg "octicon-calendar" 16 "mr-3"}} {{.PackageDescriptor.Version.CreatedUnix.FormatDate}}</div>
@@ -76,10 +76,10 @@
 							{{end}}
 							</div>
 						{{end}}
-						{{if or .CanWritePackages .PackageDescriptor.Repository}}
+						{{if or .CanWritePackages .HasRepositoryAccess}}
 							<div class="ui divider"></div>
 							<div class="ui relaxed list">
-								{{if .PackageDescriptor.Repository}}
+								{{if .HasRepositoryAccess}}
 								<div class="item">{{svg "octicon-issue-opened" 16 "mr-3"}} <a href="{{.PackageDescriptor.Repository.HTMLURL}}/issues">{{.i18n.Tr "repo.issues"}}</a></div>
 								{{end}}
 								{{if .CanWritePackages}}