From 0bed1232ee3fb28c4c977bc8b3f0640db47f8be5 Mon Sep 17 00:00:00 2001 From: bircni Date: Sun, 23 Aug 2026 09:57:51 +0200 Subject: [PATCH] fix(packages): restrict limited owner package access (#39043) Apply restricted-viewer visibility rules when resolving package access. --------- Co-authored-by: silverwind --- models/packages/package_blob.go | 6 +++++- models/packages/package_blob_test.go | 23 +++++++++++++++++++++++ services/context/package.go | 5 ++--- services/context/package_test.go | 26 ++++++++++++++++++++++++++ 4 files changed, 56 insertions(+), 4 deletions(-) create mode 100644 services/context/package_test.go diff --git a/models/packages/package_blob.go b/models/packages/package_blob.go index db3288a4be3..e3db4527609 100644 --- a/models/packages/package_blob.go +++ b/models/packages/package_blob.go @@ -126,6 +126,10 @@ func IsBlobAccessibleForUser(ctx context.Context, blobID int64, user *user_model if user.IsAdmin { return true, nil } + ownerVisibilities := []structs.VisibleType{structs.VisibleTypePublic} + if !user.IsRestricted { + ownerVisibilities = append(ownerVisibilities, structs.VisibleTypeLimited) + } maxTeamAuthorize := builder. Select("max(team.authorize)"). @@ -144,7 +148,7 @@ func IsBlobAccessibleForUser(ctx context.Context, blobID int64, user *user_model // owner = user builder.Eq{"`user`.id": user.ID}. // user can see owner - Or(builder.Eq{"`user`.visibility": structs.VisibleTypePublic}.Or(builder.Eq{"`user`.visibility": structs.VisibleTypeLimited})). + Or(builder.In("`user`.visibility", ownerVisibilities)). // owner is an organization and user has access to it Or(builder.Eq{"`user`.type": user_model.UserTypeOrganization}. And(builder.Lte{strconv.Itoa(int(perm.AccessModeRead)): maxTeamAuthorize}.Or(builder.Lte{strconv.Itoa(int(perm.AccessModeRead)): maxTeamUnitAccessMode}))), diff --git a/models/packages/package_blob_test.go b/models/packages/package_blob_test.go index b73328d26b2..55ae037c7e4 100644 --- a/models/packages/package_blob_test.go +++ b/models/packages/package_blob_test.go @@ -7,6 +7,7 @@ import ( "testing" "gitea.dev/models/unittest" + user_model "gitea.dev/models/user" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -49,3 +50,25 @@ func TestGetOrInsertBlobConcurrent(t *testing.T) { } assert.Equal(t, numGoroutines-1, existedCount) } + +func TestIsBlobAccessibleForRestrictedUser(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + + owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 33}) + pkg, err := TryInsertPackage(t.Context(), &Package{OwnerID: owner.ID, Type: TypeContainer, Name: "limited", LowerName: "limited"}) + require.NoError(t, err) + version, err := GetOrInsertVersion(t.Context(), &PackageVersion{PackageID: pkg.ID, Version: "1", LowerVersion: "1"}) + require.NoError(t, err) + blob, _, err := GetOrInsertBlob(t.Context(), &PackageBlob{Size: 1, HashMD5: "md5", HashSHA1: "sha1", HashSHA256: "sha256", HashSHA512: "sha512"}) + require.NoError(t, err) + _, err = TryInsertFile(t.Context(), &PackageFile{VersionID: version.ID, BlobID: blob.ID, Name: "blob", LowerName: "blob"}) + require.NoError(t, err) + + accessible, err := IsBlobAccessibleForUser(t.Context(), blob.ID, unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})) + require.NoError(t, err) + assert.True(t, accessible) + + accessible, err = IsBlobAccessibleForUser(t.Context(), blob.ID, unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 29})) + require.NoError(t, err) + assert.False(t, accessible) +} diff --git a/services/context/package.go b/services/context/package.go index 7c1e55c8a34..977953cefb4 100644 --- a/services/context/package.go +++ b/services/context/package.go @@ -14,7 +14,6 @@ import ( "gitea.dev/models/unit" user_model "gitea.dev/models/user" "gitea.dev/modules/setting" - "gitea.dev/modules/structs" "gitea.dev/modules/templates" ) @@ -155,10 +154,10 @@ func determineAccessMode(ctx *Base, pkgOwner, doer *user_model.User) (perm.Acces // 1. Check if user is package owner if doer.ID == pkgOwner.ID { accessMode = perm.AccessModeOwner - } else if pkgOwner.Visibility == structs.VisibleTypePublic || pkgOwner.Visibility == structs.VisibleTypeLimited { // 2. Check if package owner is public or limited + } else if pkgOwner.Visibility.IsPublic() || (pkgOwner.Visibility.IsLimited() && !doer.IsRestricted) { // 2. Check if package owner is visible to the doer accessMode = perm.AccessModeRead } - } else if pkgOwner.Visibility == structs.VisibleTypePublic { // 3. Check if package owner is public + } else if pkgOwner.Visibility.IsPublic() { // 3. Check if package owner is public accessMode = perm.AccessModeRead } } diff --git a/services/context/package_test.go b/services/context/package_test.go new file mode 100644 index 00000000000..2b0dfd923e7 --- /dev/null +++ b/services/context/package_test.go @@ -0,0 +1,26 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package context + +import ( + "testing" + + "gitea.dev/models/perm" + "gitea.dev/models/user" + "gitea.dev/modules/structs" + + "github.com/stretchr/testify/assert" +) + +func TestDeterminePackageAccessModeForLimitedOwner(t *testing.T) { + owner := &user.User{ID: 1, Visibility: structs.VisibleTypeLimited} + + accessMode, err := determineAccessMode(&Base{}, owner, &user.User{ID: 2, IsActive: true}) + assert.NoError(t, err) + assert.Equal(t, perm.AccessModeRead, accessMode) + + accessMode, err = determineAccessMode(&Base{}, owner, &user.User{ID: 3, IsActive: true, IsRestricted: true}) + assert.NoError(t, err) + assert.Equal(t, perm.AccessModeNone, accessMode) +}