mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-27 17:44:24 -04:00 
			
		
		
		
	Make restricted users can access public repositories (#35693)
Fix #35690 Change the "restricted user" behavior introduced by #6274. Now restricted user can also access public repositories when sign-in is not required. For required sign-in, the behavior isn't changed.
This commit is contained in:
		| @@ -429,6 +429,10 @@ func HasOrgOrUserVisible(ctx context.Context, orgOrUser, user *user_model.User) | |||||||
| 		return true | 		return true | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	if !setting.Service.RequireSignInViewStrict && orgOrUser.Visibility == structs.VisibleTypePublic { | ||||||
|  | 		return true | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	if (orgOrUser.Visibility == structs.VisibleTypePrivate || user.IsRestricted) && !OrgFromUser(orgOrUser).hasMemberWithUserID(ctx, user.ID) { | 	if (orgOrUser.Visibility == structs.VisibleTypePrivate || user.IsRestricted) && !OrgFromUser(orgOrUser).hasMemberWithUserID(ctx, user.ID) { | ||||||
| 		return false | 		return false | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -13,7 +13,9 @@ import ( | |||||||
| 	repo_model "code.gitea.io/gitea/models/repo" | 	repo_model "code.gitea.io/gitea/models/repo" | ||||||
| 	"code.gitea.io/gitea/models/unittest" | 	"code.gitea.io/gitea/models/unittest" | ||||||
| 	user_model "code.gitea.io/gitea/models/user" | 	user_model "code.gitea.io/gitea/models/user" | ||||||
|  | 	"code.gitea.io/gitea/modules/setting" | ||||||
| 	"code.gitea.io/gitea/modules/structs" | 	"code.gitea.io/gitea/modules/structs" | ||||||
|  | 	"code.gitea.io/gitea/modules/test" | ||||||
|  |  | ||||||
| 	"github.com/stretchr/testify/assert" | 	"github.com/stretchr/testify/assert" | ||||||
| 	"github.com/stretchr/testify/require" | 	"github.com/stretchr/testify/require" | ||||||
| @@ -382,6 +384,12 @@ func TestHasOrgVisibleTypePublic(t *testing.T) { | |||||||
| 	assert.True(t, test1) // owner of org | 	assert.True(t, test1) // owner of org | ||||||
| 	assert.True(t, test2) // user not a part of org | 	assert.True(t, test2) // user not a part of org | ||||||
| 	assert.True(t, test3) // logged out user | 	assert.True(t, test3) // logged out user | ||||||
|  |  | ||||||
|  | 	restrictedUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 29, IsRestricted: true}) | ||||||
|  | 	require.True(t, restrictedUser.IsRestricted) | ||||||
|  | 	assert.True(t, organization.HasOrgOrUserVisible(t.Context(), org.AsUser(), restrictedUser)) | ||||||
|  | 	defer test.MockVariableValue(&setting.Service.RequireSignInViewStrict, true)() | ||||||
|  | 	assert.False(t, organization.HasOrgOrUserVisible(t.Context(), org.AsUser(), restrictedUser)) | ||||||
| } | } | ||||||
|  |  | ||||||
| func TestHasOrgVisibleTypeLimited(t *testing.T) { | func TestHasOrgVisibleTypeLimited(t *testing.T) { | ||||||
|   | |||||||
| @@ -13,6 +13,8 @@ import ( | |||||||
| 	"code.gitea.io/gitea/models/perm" | 	"code.gitea.io/gitea/models/perm" | ||||||
| 	repo_model "code.gitea.io/gitea/models/repo" | 	repo_model "code.gitea.io/gitea/models/repo" | ||||||
| 	user_model "code.gitea.io/gitea/models/user" | 	user_model "code.gitea.io/gitea/models/user" | ||||||
|  | 	"code.gitea.io/gitea/modules/setting" | ||||||
|  | 	"code.gitea.io/gitea/modules/structs" | ||||||
|  |  | ||||||
| 	"xorm.io/builder" | 	"xorm.io/builder" | ||||||
| ) | ) | ||||||
| @@ -41,7 +43,12 @@ func accessLevel(ctx context.Context, user *user_model.User, repo *repo_model.Re | |||||||
| 		restricted = user.IsRestricted | 		restricted = user.IsRestricted | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if !restricted && !repo.IsPrivate { | 	if err := repo.LoadOwner(ctx); err != nil { | ||||||
|  | 		return mode, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	repoIsFullyPublic := !setting.Service.RequireSignInViewStrict && repo.Owner.Visibility == structs.VisibleTypePublic && !repo.IsPrivate | ||||||
|  | 	if (restricted && repoIsFullyPublic) || (!restricted && !repo.IsPrivate) { | ||||||
| 		mode = perm.AccessModeRead | 		mode = perm.AccessModeRead | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|   | |||||||
| @@ -12,6 +12,7 @@ import ( | |||||||
| 	repo_model "code.gitea.io/gitea/models/repo" | 	repo_model "code.gitea.io/gitea/models/repo" | ||||||
| 	"code.gitea.io/gitea/models/unittest" | 	"code.gitea.io/gitea/models/unittest" | ||||||
| 	user_model "code.gitea.io/gitea/models/user" | 	user_model "code.gitea.io/gitea/models/user" | ||||||
|  | 	"code.gitea.io/gitea/modules/setting" | ||||||
|  |  | ||||||
| 	"github.com/stretchr/testify/assert" | 	"github.com/stretchr/testify/assert" | ||||||
| ) | ) | ||||||
| @@ -51,7 +52,14 @@ func TestAccessLevel(t *testing.T) { | |||||||
| 	assert.NoError(t, err) | 	assert.NoError(t, err) | ||||||
| 	assert.Equal(t, perm_model.AccessModeNone, level) | 	assert.Equal(t, perm_model.AccessModeNone, level) | ||||||
|  |  | ||||||
| 	// restricted user has no access to a public repo | 	// restricted user has default access to a public repo if no sign-in is required | ||||||
|  | 	setting.Service.RequireSignInViewStrict = false | ||||||
|  | 	level, err = access_model.AccessLevel(t.Context(), user29, repo1) | ||||||
|  | 	assert.NoError(t, err) | ||||||
|  | 	assert.Equal(t, perm_model.AccessModeRead, level) | ||||||
|  |  | ||||||
|  | 	// restricted user has no access to a public repo if sign-in is required | ||||||
|  | 	setting.Service.RequireSignInViewStrict = true | ||||||
| 	level, err = access_model.AccessLevel(t.Context(), user29, repo1) | 	level, err = access_model.AccessLevel(t.Context(), user29, repo1) | ||||||
| 	assert.NoError(t, err) | 	assert.NoError(t, err) | ||||||
| 	assert.Equal(t, perm_model.AccessModeNone, level) | 	assert.Equal(t, perm_model.AccessModeNone, level) | ||||||
|   | |||||||
| @@ -642,6 +642,17 @@ func SearchRepositoryIDsByCondition(ctx context.Context, cond builder.Cond) ([]i | |||||||
| 		Find(&repoIDs) | 		Find(&repoIDs) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func userAllPublicRepoCond(cond builder.Cond, orgVisibilityLimit []structs.VisibleType) builder.Cond { | ||||||
|  | 	return cond.Or(builder.And( | ||||||
|  | 		builder.Eq{"`repository`.is_private": false}, | ||||||
|  | 		// Aren't in a private organisation or limited organisation if we're not logged in | ||||||
|  | 		builder.NotIn("`repository`.owner_id", builder.Select("id").From("`user`").Where( | ||||||
|  | 			builder.And( | ||||||
|  | 				builder.Eq{"type": user_model.UserTypeOrganization}, | ||||||
|  | 				builder.In("visibility", orgVisibilityLimit)), | ||||||
|  | 		)))) | ||||||
|  | } | ||||||
|  |  | ||||||
| // AccessibleRepositoryCondition takes a user a returns a condition for checking if a repository is accessible | // AccessibleRepositoryCondition takes a user a returns a condition for checking if a repository is accessible | ||||||
| func AccessibleRepositoryCondition(user *user_model.User, unitType unit.Type) builder.Cond { | func AccessibleRepositoryCondition(user *user_model.User, unitType unit.Type) builder.Cond { | ||||||
| 	cond := builder.NewCond() | 	cond := builder.NewCond() | ||||||
| @@ -651,15 +662,8 @@ func AccessibleRepositoryCondition(user *user_model.User, unitType unit.Type) bu | |||||||
| 		if user == nil || user.ID <= 0 { | 		if user == nil || user.ID <= 0 { | ||||||
| 			orgVisibilityLimit = append(orgVisibilityLimit, structs.VisibleTypeLimited) | 			orgVisibilityLimit = append(orgVisibilityLimit, structs.VisibleTypeLimited) | ||||||
| 		} | 		} | ||||||
| 		// 1. Be able to see all non-private repositories that either: | 		// 1. Be able to see all non-private repositories | ||||||
| 		cond = cond.Or(builder.And( | 		cond = userAllPublicRepoCond(cond, orgVisibilityLimit) | ||||||
| 			builder.Eq{"`repository`.is_private": false}, |  | ||||||
| 			// 2. Aren't in an private organisation or limited organisation if we're not logged in |  | ||||||
| 			builder.NotIn("`repository`.owner_id", builder.Select("id").From("`user`").Where( |  | ||||||
| 				builder.And( |  | ||||||
| 					builder.Eq{"type": user_model.UserTypeOrganization}, |  | ||||||
| 					builder.In("visibility", orgVisibilityLimit)), |  | ||||||
| 			)))) |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if user != nil { | 	if user != nil { | ||||||
| @@ -683,6 +687,9 @@ func AccessibleRepositoryCondition(user *user_model.User, unitType unit.Type) bu | |||||||
| 		if !user.IsRestricted { | 		if !user.IsRestricted { | ||||||
| 			// 5. Be able to see all public repos in private organizations that we are an org_user of | 			// 5. Be able to see all public repos in private organizations that we are an org_user of | ||||||
| 			cond = cond.Or(userOrgPublicRepoCond(user.ID)) | 			cond = cond.Or(userOrgPublicRepoCond(user.ID)) | ||||||
|  | 		} else if !setting.Service.RequireSignInViewStrict { | ||||||
|  | 			orgVisibilityLimit := []structs.VisibleType{structs.VisibleTypePrivate, structs.VisibleTypeLimited} | ||||||
|  | 			cond = userAllPublicRepoCond(cond, orgVisibilityLimit) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|   | |||||||
| @@ -10,9 +10,14 @@ import ( | |||||||
| 	"code.gitea.io/gitea/models/db" | 	"code.gitea.io/gitea/models/db" | ||||||
| 	repo_model "code.gitea.io/gitea/models/repo" | 	repo_model "code.gitea.io/gitea/models/repo" | ||||||
| 	"code.gitea.io/gitea/models/unittest" | 	"code.gitea.io/gitea/models/unittest" | ||||||
|  | 	user_model "code.gitea.io/gitea/models/user" | ||||||
| 	"code.gitea.io/gitea/modules/optional" | 	"code.gitea.io/gitea/modules/optional" | ||||||
|  | 	"code.gitea.io/gitea/modules/setting" | ||||||
|  | 	"code.gitea.io/gitea/modules/structs" | ||||||
|  | 	"code.gitea.io/gitea/modules/test" | ||||||
|  |  | ||||||
| 	"github.com/stretchr/testify/assert" | 	"github.com/stretchr/testify/assert" | ||||||
|  | 	"github.com/stretchr/testify/require" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func getTestCases() []struct { | func getTestCases() []struct { | ||||||
| @@ -182,7 +187,16 @@ func getTestCases() []struct { | |||||||
|  |  | ||||||
| func TestSearchRepository(t *testing.T) { | func TestSearchRepository(t *testing.T) { | ||||||
| 	assert.NoError(t, unittest.PrepareTestDatabase()) | 	assert.NoError(t, unittest.PrepareTestDatabase()) | ||||||
|  | 	t.Run("SearchRepositoryPublic", testSearchRepositoryPublic) | ||||||
|  | 	t.Run("SearchRepositoryPublicRestricted", testSearchRepositoryRestricted) | ||||||
|  | 	t.Run("SearchRepositoryPrivate", testSearchRepositoryPrivate) | ||||||
|  | 	t.Run("SearchRepositoryNonExistingOwner", testSearchRepositoryNonExistingOwner) | ||||||
|  | 	t.Run("SearchRepositoryWithInDescription", testSearchRepositoryWithInDescription) | ||||||
|  | 	t.Run("SearchRepositoryNotInDescription", testSearchRepositoryNotInDescription) | ||||||
|  | 	t.Run("SearchRepositoryCases", testSearchRepositoryCases) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func testSearchRepositoryPublic(t *testing.T) { | ||||||
| 	// test search public repository on explore page | 	// test search public repository on explore page | ||||||
| 	repos, count, err := repo_model.SearchRepositoryByName(t.Context(), repo_model.SearchRepoOptions{ | 	repos, count, err := repo_model.SearchRepositoryByName(t.Context(), repo_model.SearchRepoOptions{ | ||||||
| 		ListOptions: db.ListOptions{ | 		ListOptions: db.ListOptions{ | ||||||
| @@ -211,9 +225,54 @@ func TestSearchRepository(t *testing.T) { | |||||||
| 	assert.NoError(t, err) | 	assert.NoError(t, err) | ||||||
| 	assert.Equal(t, int64(2), count) | 	assert.Equal(t, int64(2), count) | ||||||
| 	assert.Len(t, repos, 2) | 	assert.Len(t, repos, 2) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func testSearchRepositoryRestricted(t *testing.T) { | ||||||
|  | 	user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) | ||||||
|  | 	restrictedUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 29, IsRestricted: true}) | ||||||
|  |  | ||||||
|  | 	performSearch := func(t *testing.T, user *user_model.User) (publicRepoIDs []int64) { | ||||||
|  | 		repos, count, err := repo_model.SearchRepositoryByName(t.Context(), repo_model.SearchRepoOptions{ | ||||||
|  | 			ListOptions: db.ListOptions{Page: 1, PageSize: 10000}, | ||||||
|  | 			Actor:       user, | ||||||
|  | 		}) | ||||||
|  | 		require.NoError(t, err) | ||||||
|  | 		assert.Len(t, repos, int(count)) | ||||||
|  | 		for _, repo := range repos { | ||||||
|  | 			require.NoError(t, repo.LoadOwner(t.Context())) | ||||||
|  | 			if repo.Owner.Visibility == structs.VisibleTypePublic && !repo.IsPrivate { | ||||||
|  | 				publicRepoIDs = append(publicRepoIDs, repo.ID) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		return publicRepoIDs | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	normalPublicRepoIDs := performSearch(t, user2) | ||||||
|  | 	require.Greater(t, len(normalPublicRepoIDs), 10) // quite a lot | ||||||
|  |  | ||||||
|  | 	t.Run("RestrictedUser-NoSignInRequirement", func(t *testing.T) { | ||||||
|  | 		// restricted user can also see public repositories if no "required sign-in" | ||||||
|  | 		repoIDs := performSearch(t, restrictedUser) | ||||||
|  | 		assert.ElementsMatch(t, normalPublicRepoIDs, repoIDs) | ||||||
|  | 	}) | ||||||
|  |  | ||||||
|  | 	defer test.MockVariableValue(&setting.Service.RequireSignInViewStrict, true)() | ||||||
|  |  | ||||||
|  | 	t.Run("NormalUser-RequiredSignIn", func(t *testing.T) { | ||||||
|  | 		// normal user can still see all public repos, not affected by "required sign-in" | ||||||
|  | 		repoIDs := performSearch(t, user2) | ||||||
|  | 		assert.ElementsMatch(t, normalPublicRepoIDs, repoIDs) | ||||||
|  | 	}) | ||||||
|  | 	t.Run("RestrictedUser-RequiredSignIn", func(t *testing.T) { | ||||||
|  | 		// restricted user can see only their own repo | ||||||
|  | 		repoIDs := performSearch(t, restrictedUser) | ||||||
|  | 		assert.Equal(t, []int64{4}, repoIDs) | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func testSearchRepositoryPrivate(t *testing.T) { | ||||||
| 	// test search private repository on explore page | 	// test search private repository on explore page | ||||||
| 	repos, count, err = repo_model.SearchRepositoryByName(t.Context(), repo_model.SearchRepoOptions{ | 	repos, count, err := repo_model.SearchRepositoryByName(t.Context(), repo_model.SearchRepoOptions{ | ||||||
| 		ListOptions: db.ListOptions{ | 		ListOptions: db.ListOptions{ | ||||||
| 			Page:     1, | 			Page:     1, | ||||||
| 			PageSize: 10, | 			PageSize: 10, | ||||||
| @@ -242,16 +301,18 @@ func TestSearchRepository(t *testing.T) { | |||||||
| 	assert.NoError(t, err) | 	assert.NoError(t, err) | ||||||
| 	assert.Equal(t, int64(3), count) | 	assert.Equal(t, int64(3), count) | ||||||
| 	assert.Len(t, repos, 3) | 	assert.Len(t, repos, 3) | ||||||
|  | } | ||||||
|  |  | ||||||
| 	// Test non existing owner | func testSearchRepositoryNonExistingOwner(t *testing.T) { | ||||||
| 	repos, count, err = repo_model.SearchRepositoryByName(t.Context(), repo_model.SearchRepoOptions{OwnerID: unittest.NonexistentID}) | 	repos, count, err := repo_model.SearchRepositoryByName(t.Context(), repo_model.SearchRepoOptions{OwnerID: unittest.NonexistentID}) | ||||||
|  |  | ||||||
| 	assert.NoError(t, err) | 	assert.NoError(t, err) | ||||||
| 	assert.Empty(t, repos) | 	assert.Empty(t, repos) | ||||||
| 	assert.Equal(t, int64(0), count) | 	assert.Equal(t, int64(0), count) | ||||||
|  | } | ||||||
|  |  | ||||||
| 	// Test search within description | func testSearchRepositoryWithInDescription(t *testing.T) { | ||||||
| 	repos, count, err = repo_model.SearchRepository(t.Context(), repo_model.SearchRepoOptions{ | 	repos, count, err := repo_model.SearchRepository(t.Context(), repo_model.SearchRepoOptions{ | ||||||
| 		ListOptions: db.ListOptions{ | 		ListOptions: db.ListOptions{ | ||||||
| 			Page:     1, | 			Page:     1, | ||||||
| 			PageSize: 10, | 			PageSize: 10, | ||||||
| @@ -266,9 +327,10 @@ func TestSearchRepository(t *testing.T) { | |||||||
| 		assert.Equal(t, "test_repo_14", repos[0].Name) | 		assert.Equal(t, "test_repo_14", repos[0].Name) | ||||||
| 	} | 	} | ||||||
| 	assert.Equal(t, int64(1), count) | 	assert.Equal(t, int64(1), count) | ||||||
|  | } | ||||||
|  |  | ||||||
| 	// Test NOT search within description | func testSearchRepositoryNotInDescription(t *testing.T) { | ||||||
| 	repos, count, err = repo_model.SearchRepository(t.Context(), repo_model.SearchRepoOptions{ | 	repos, count, err := repo_model.SearchRepository(t.Context(), repo_model.SearchRepoOptions{ | ||||||
| 		ListOptions: db.ListOptions{ | 		ListOptions: db.ListOptions{ | ||||||
| 			Page:     1, | 			Page:     1, | ||||||
| 			PageSize: 10, | 			PageSize: 10, | ||||||
| @@ -281,7 +343,9 @@ func TestSearchRepository(t *testing.T) { | |||||||
| 	assert.NoError(t, err) | 	assert.NoError(t, err) | ||||||
| 	assert.Empty(t, repos) | 	assert.Empty(t, repos) | ||||||
| 	assert.Equal(t, int64(0), count) | 	assert.Equal(t, int64(0), count) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func testSearchRepositoryCases(t *testing.T) { | ||||||
| 	testCases := getTestCases() | 	testCases := getTestCases() | ||||||
|  |  | ||||||
| 	for _, testCase := range testCases { | 	for _, testCase := range testCases { | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user