From 44c18465b560dd426cb03630803fc8bd1bd9c520 Mon Sep 17 00:00:00 2001 From: Mathieu Fenniak Date: Sun, 15 Feb 2026 10:58:52 -0700 Subject: [PATCH] feat: services/authz package for evaluating fine-grained access token --- .../access_token.yml | 31 +++++ .../access_token_resource_repo.yml | 5 + services/authz/access_token.go | 28 +++++ services/authz/access_token_test.go | 46 +++++++ services/authz/all_access_reducer.go | 29 +++++ services/authz/all_access_reducer_test.go | 49 ++++++++ services/authz/authorization_reducer.go | 39 ++++++ services/authz/main_test.go | 14 +++ services/authz/public_repos_reducer.go | 46 +++++++ services/authz/public_repos_reducer_test.go | 73 +++++++++++ services/authz/specific_repos_reducer.go | 73 +++++++++++ services/authz/specific_repos_reducer_test.go | 117 ++++++++++++++++++ 12 files changed, 550 insertions(+) create mode 100644 services/authz/TestGetAuthorizationReducerForAccessToken/access_token.yml create mode 100644 services/authz/TestGetAuthorizationReducerForAccessToken/access_token_resource_repo.yml create mode 100644 services/authz/access_token.go create mode 100644 services/authz/access_token_test.go create mode 100644 services/authz/all_access_reducer.go create mode 100644 services/authz/all_access_reducer_test.go create mode 100644 services/authz/authorization_reducer.go create mode 100644 services/authz/main_test.go create mode 100644 services/authz/public_repos_reducer.go create mode 100644 services/authz/public_repos_reducer_test.go create mode 100644 services/authz/specific_repos_reducer.go create mode 100644 services/authz/specific_repos_reducer_test.go diff --git a/services/authz/TestGetAuthorizationReducerForAccessToken/access_token.yml b/services/authz/TestGetAuthorizationReducerForAccessToken/access_token.yml new file mode 100644 index 0000000000..2def7e648e --- /dev/null +++ b/services/authz/TestGetAuthorizationReducerForAccessToken/access_token.yml @@ -0,0 +1,31 @@ +- + id: 5 + uid: 2 + name: Unrestricted Token + token_hash: a6d404048048812d9e911d93aefbe94fc768d4876fdf75e3bef0bdc67828e0af422846d3056f2f25ec35c51dc92075685ec5 + token_salt: 99ArgXKlQQ + token_last_eight: 69d28c91 + created_unix: 946687980 + updated_unix: 946687980 + resource_all_repos: true +- + id: 6 + uid: 2 + name: Public Resources Only Token + token_hash: b6d404048048812d9e911d93aefbe94fc768d4876fdf75e3bef0bdc67828e0af422846d3056f2f25ec35c51dc92075685ec5 + token_salt: 99ArgXKlQQ + token_last_eight: 69d28c91 + created_unix: 946687980 + updated_unix: 946687980 + scope: public-only,read:activitypub + resource_all_repos: true +- + id: 7 + uid: 2 + name: Specific Repos Only Token + token_hash: c6d404048048812d9e911d93aefbe94fc768d4876fdf75e3bef0bdc67828e0af422846d3056f2f25ec35c51dc92075685ec5 + token_salt: 99ArgXKlQQ + token_last_eight: 69d28c91 + created_unix: 946687980 + updated_unix: 946687980 + resource_all_repos: false diff --git a/services/authz/TestGetAuthorizationReducerForAccessToken/access_token_resource_repo.yml b/services/authz/TestGetAuthorizationReducerForAccessToken/access_token_resource_repo.yml new file mode 100644 index 0000000000..29e6ab2c77 --- /dev/null +++ b/services/authz/TestGetAuthorizationReducerForAccessToken/access_token_resource_repo.yml @@ -0,0 +1,5 @@ +- + id: 1 + token_id: 7 + repo_id: 1 + created_unix: 1772158384 diff --git a/services/authz/access_token.go b/services/authz/access_token.go new file mode 100644 index 0000000000..1d7b09e91e --- /dev/null +++ b/services/authz/access_token.go @@ -0,0 +1,28 @@ +// Copyright 2026 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package authz + +import ( + "context" + "fmt" + + auth_model "forgejo.org/models/auth" +) + +func GetAuthorizationReducerForAccessToken(ctx context.Context, token *auth_model.AccessToken) (AuthorizationReducer, error) { + if token.ResourceAllRepos { + if publicOnly, err := token.Scope.PublicOnly(); err != nil { + return nil, fmt.Errorf("PublicOnly: %w", err) + } else if publicOnly { + return &PublicReposAuthorizationReducer{}, nil + } + return &AllAccessAuthorizationReducer{}, nil + } + + repos, err := auth_model.GetRepositoriesAccessibleWithToken(ctx, token.ID) + if err != nil { + return nil, fmt.Errorf("GetRepositoriesAccessibleWithToken: %w", err) + } + return &SpecificReposAuthorizationReducer{resourceRepos: repos}, nil +} diff --git a/services/authz/access_token_test.go b/services/authz/access_token_test.go new file mode 100644 index 0000000000..d037add76f --- /dev/null +++ b/services/authz/access_token_test.go @@ -0,0 +1,46 @@ +// Copyright 2026 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package authz + +import ( + "testing" + + "forgejo.org/models/auth" + "forgejo.org/models/unittest" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetAuthorizationReducerForAccessToken(t *testing.T) { + defer unittest.OverrideFixtures("services/authz/TestGetAuthorizationReducerForAccessToken")() + require.NoError(t, unittest.PrepareTestDatabase()) + + t.Run("all access", func(t *testing.T) { + token := unittest.AssertExistsAndLoadBean(t, &auth.AccessToken{ID: 5}) + reducer, err := GetAuthorizationReducerForAccessToken(t.Context(), token) + require.NoError(t, err) + assert.IsType(t, &AllAccessAuthorizationReducer{}, reducer) + }) + + t.Run("public resources only", func(t *testing.T) { + token := unittest.AssertExistsAndLoadBean(t, &auth.AccessToken{ID: 6}) + reducer, err := GetAuthorizationReducerForAccessToken(t.Context(), token) + require.NoError(t, err) + assert.IsType(t, &PublicReposAuthorizationReducer{}, reducer) + }) + + t.Run("specific repos only", func(t *testing.T) { + token := unittest.AssertExistsAndLoadBean(t, &auth.AccessToken{ID: 7}) + reducer, err := GetAuthorizationReducerForAccessToken(t.Context(), token) + require.NoError(t, err) + + specific, ok := reducer.(*SpecificReposAuthorizationReducer) + require.True(t, ok) + require.NotNil(t, specific) + + require.Len(t, specific.resourceRepos, 1) + assert.EqualValues(t, 1, specific.resourceRepos[0].RepoID) + }) +} diff --git a/services/authz/all_access_reducer.go b/services/authz/all_access_reducer.go new file mode 100644 index 0000000000..8a4590f285 --- /dev/null +++ b/services/authz/all_access_reducer.go @@ -0,0 +1,29 @@ +// Copyright 2026 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package authz + +import ( + "context" + + "forgejo.org/models/perm" + repo_model "forgejo.org/models/repo" + + "xorm.io/builder" +) + +// Implementation of [AuthorizationReducer] that does no authorization reduction, allowing normal access to all +// resources. +type AllAccessAuthorizationReducer struct{} + +func (*AllAccessAuthorizationReducer) ReduceRepoAccess(ctx context.Context, repo *repo_model.Repository, accessMode perm.AccessMode) (perm.AccessMode, error) { + return accessMode, nil +} + +func (*AllAccessAuthorizationReducer) RepoFilter(accessMode perm.AccessMode) builder.Cond { + return builder.NewCond() // invalid cond should be excluded and cause no filtering +} + +func (*AllAccessAuthorizationReducer) AllowAdminOverride() bool { + return true +} diff --git a/services/authz/all_access_reducer_test.go b/services/authz/all_access_reducer_test.go new file mode 100644 index 0000000000..84c1b1c1ac --- /dev/null +++ b/services/authz/all_access_reducer_test.go @@ -0,0 +1,49 @@ +// Copyright 2026 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package authz + +import ( + "testing" + + "forgejo.org/models/db" + "forgejo.org/models/perm" + "forgejo.org/models/repo" + "forgejo.org/models/unittest" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestAllAccessAuthorizationReducer(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + + reducer := &AllAccessAuthorizationReducer{} + + t.Run("ReduceRepoAccess no changes", func(t *testing.T) { + repo1 := unittest.AssertExistsAndLoadBean(t, &repo.Repository{ID: 1}) + for _, am := range []perm.AccessMode{perm.AccessModeOwner, perm.AccessModeAdmin, perm.AccessModeWrite, perm.AccessModeRead} { + p1, err := reducer.ReduceRepoAccess(t.Context(), repo1, am) + require.NoError(t, err) + assert.Equal(t, am, p1) + } + }) + + t.Run("RepoFilter no restrictions", func(t *testing.T) { + numRepos, err := db.GetEngine(t.Context()).Table(&repo.Repository{}).Count() + require.NoError(t, err) + + for _, am := range []perm.AccessMode{perm.AccessModeOwner, perm.AccessModeAdmin, perm.AccessModeWrite, perm.AccessModeRead} { + cond := reducer.RepoFilter(am) + + var rows []*repo.Repository + err := db.GetEngine(t.Context()).Table(&repo.Repository{}).Where(cond).OrderBy("id").Cols("id", "owner_id", "is_private").Find(&rows) + require.NoError(t, err) + assert.Len(t, rows, int(numRepos)) + } + }) + + t.Run("AllowAdminOverride is true", func(t *testing.T) { + assert.True(t, reducer.AllowAdminOverride()) + }) +} diff --git a/services/authz/authorization_reducer.go b/services/authz/authorization_reducer.go new file mode 100644 index 0000000000..0152daf890 --- /dev/null +++ b/services/authz/authorization_reducer.go @@ -0,0 +1,39 @@ +// Copyright 2026 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package authz + +import ( + "context" + + "forgejo.org/models/perm" + repo_model "forgejo.org/models/repo" + + "xorm.io/builder" +) + +// Defines an API for reducing available permissions to specific resources. Typically associated with a fine-grained +// access tokens and provides methods to reduce authorization that the access token provides down to specific resources. +type AuthorizationReducer interface { + // Given a repository and an accessMode, ReduceRepoAccess will return a new, possibly reduced, AccessMode that + // reflects the actual access that is currently permitted. For example, when using a fine-grained access token that + // only grants write access to one target repository, `ReduceRepoAccess(target, AccessModeWrite)` would return + // `AccessModeWrite`, and `ReduceRepoAccess(other-repo, AccessModeWrite)` would return a lesser access mode, + // restricting access to other repositories. + ReduceRepoAccess(ctx context.Context, repo *repo_model.Repository, accessMode perm.AccessMode) (perm.AccessMode, error) + + // If querying the repository table, apply this condition to return only repositories that the restriction will + // allow the target access mode (or higher). For example, when using a fine-grained access token that only grants + // write access to one target repository, `RepoFilter(AccessModeWrite)` would return a filter that only returns that + // single repository, while `RepoFilter(AccessModeRead)` would return a filter that includes all public repositories + // and the target repository. + RepoFilter(accessMode perm.AccessMode) builder.Cond + + // Controls whether the presence of an authorization reducer will prevent administrators from overriding permission + // checks. Typically site administrators and repo administrators are exempted from permission checks, but if an + // authorization reducer is present then it may be intended for its restrictions to apply even to administrators. + // + // `true` allows the typical case where administrators *can* override permissions. `false` disables administrator + // overrides of permission checks. + AllowAdminOverride() bool +} diff --git a/services/authz/main_test.go b/services/authz/main_test.go new file mode 100644 index 0000000000..3412b080cd --- /dev/null +++ b/services/authz/main_test.go @@ -0,0 +1,14 @@ +// Copyright 2026 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package authz + +import ( + "testing" + + "forgejo.org/models/unittest" +) + +func TestMain(m *testing.M) { + unittest.MainTest(m) +} diff --git a/services/authz/public_repos_reducer.go b/services/authz/public_repos_reducer.go new file mode 100644 index 0000000000..ce603cb569 --- /dev/null +++ b/services/authz/public_repos_reducer.go @@ -0,0 +1,46 @@ +// Copyright 2026 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package authz + +import ( + "context" + "fmt" + + "forgejo.org/models/perm" + repo_model "forgejo.org/models/repo" + "forgejo.org/modules/structs" + + "xorm.io/builder" +) + +// Grants access only to public repositories. Does not change the level of access for any public repos. +type PublicReposAuthorizationReducer struct{} + +func (*PublicReposAuthorizationReducer) ReduceRepoAccess(ctx context.Context, repo *repo_model.Repository, accessMode perm.AccessMode) (perm.AccessMode, error) { + if err := repo.LoadOwner(ctx); err != nil { + return 0, fmt.Errorf("failed to LoadOwner during ReduceRepoAccess: %w", err) + } + + // Fine-grained access tokens remove access to any private repositories, or repository owned by non-public users, + // that aren't listed in their resource list. + if !repo.Owner.Visibility.IsPublic() || repo.IsPrivate { + return perm.AccessModeNone, nil + } + + return accessMode, nil +} + +func (*PublicReposAuthorizationReducer) RepoFilter(accessMode perm.AccessMode) builder.Cond { + // Regardless of access mode, allow access only to non-private repositories, that aren't in a private or limited + // organization. + return builder.And( + builder.Eq{"is_private": false}, + builder.NotIn("owner_id", builder.Select("id").From("`user`").Where( + builder.Or(builder.Eq{"visibility": structs.VisibleTypeLimited}, builder.Eq{"visibility": structs.VisibleTypePrivate}), + ))) +} + +func (*PublicReposAuthorizationReducer) AllowAdminOverride() bool { + return false +} diff --git a/services/authz/public_repos_reducer_test.go b/services/authz/public_repos_reducer_test.go new file mode 100644 index 0000000000..5e72566253 --- /dev/null +++ b/services/authz/public_repos_reducer_test.go @@ -0,0 +1,73 @@ +// Copyright 2026 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package authz + +import ( + "testing" + + "forgejo.org/models/db" + "forgejo.org/models/perm" + "forgejo.org/models/repo" + "forgejo.org/models/unittest" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestPublicReposAuthorizationReducer(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + + reducer := &PublicReposAuthorizationReducer{} + + t.Run("ReduceRepoAccess unrestricted on public repos", func(t *testing.T) { + repo1 := unittest.AssertExistsAndLoadBean(t, &repo.Repository{ID: 1}) + repo4 := unittest.AssertExistsAndLoadBean(t, &repo.Repository{ID: 4}) + for _, am := range []perm.AccessMode{perm.AccessModeOwner, perm.AccessModeAdmin, perm.AccessModeWrite, perm.AccessModeRead} { + p1, err := reducer.ReduceRepoAccess(t.Context(), repo1, am) + require.NoError(t, err) + assert.Equal(t, am, p1) + p4, err := reducer.ReduceRepoAccess(t.Context(), repo4, am) + require.NoError(t, err) + assert.Equal(t, am, p4) + } + }) + + t.Run("ReduceRepoAccess restricted to None on private repos", func(t *testing.T) { + // private repo + repo3 := unittest.AssertExistsAndLoadBean(t, &repo.Repository{ID: 3}) + + // public repo on a limited-visibility org + repo38 := unittest.AssertExistsAndLoadBean(t, &repo.Repository{ID: 38}) + + for _, am := range []perm.AccessMode{perm.AccessModeOwner, perm.AccessModeAdmin, perm.AccessModeWrite, perm.AccessModeRead} { + p3, err := reducer.ReduceRepoAccess(t.Context(), repo3, am) + require.NoError(t, err) + assert.Equal(t, perm.AccessModeNone, p3) + + p38, err := reducer.ReduceRepoAccess(t.Context(), repo38, am) + require.NoError(t, err) + assert.Equal(t, perm.AccessModeNone, p38) + } + }) + + t.Run("RepoFilter unrestricted access only permitted to public repos", func(t *testing.T) { + for _, am := range []perm.AccessMode{perm.AccessModeOwner, perm.AccessModeAdmin, perm.AccessModeWrite, perm.AccessModeRead} { + cond := reducer.RepoFilter(am) + + var rows []*repo.Repository + err := db.GetEngine(t.Context()).Table(&repo.Repository{}).Where(cond).OrderBy("id").Cols("id", "owner_id", "is_private").Find(&rows) + require.NoError(t, err) + assert.NotEmpty(t, rows) + for _, repo := range rows { + assert.False(t, repo.IsPrivate) + require.NoError(t, repo.LoadOwner(t.Context())) + assert.True(t, repo.Owner.Visibility.IsPublic()) + } + } + }) + + t.Run("AllowAdminOverride is false", func(t *testing.T) { + assert.False(t, reducer.AllowAdminOverride()) + }) +} diff --git a/services/authz/specific_repos_reducer.go b/services/authz/specific_repos_reducer.go new file mode 100644 index 0000000000..e9c6f6d7c5 --- /dev/null +++ b/services/authz/specific_repos_reducer.go @@ -0,0 +1,73 @@ +// Copyright 2026 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package authz + +import ( + "context" + "fmt" + + auth_model "forgejo.org/models/auth" + "forgejo.org/models/perm" + repo_model "forgejo.org/models/repo" + "forgejo.org/modules/structs" + + "xorm.io/builder" +) + +// For specific repositories listed in [AccessTokenResourceRepo] models, all access is permitted. For public +// repositories that aren't listed among the specific repos, read-only access is permitted. For all other repos, no +// access is permitted. +type SpecificReposAuthorizationReducer struct { + resourceRepos []*auth_model.AccessTokenResourceRepo +} + +func (r *SpecificReposAuthorizationReducer) ReduceRepoAccess(ctx context.Context, repo *repo_model.Repository, accessMode perm.AccessMode) (perm.AccessMode, error) { + for _, tokenRepo := range r.resourceRepos { + if tokenRepo.RepoID == repo.ID { + // No restrictions as this repo is within the scope of the access token. + return accessMode, nil + } + } + + if err := repo.LoadOwner(ctx); err != nil { + return 0, fmt.Errorf("failed to LoadOwner during ReduceRepoAccess: %w", err) + } + + // Fine-grained access tokens remove access to any private repositories, or repository owned by non-public users, + // that aren't listed in their resource list. + if !repo.Owner.Visibility.IsPublic() || repo.IsPrivate { + return perm.AccessModeNone, nil + } + + // Public repos will be reduced to read access. + return min(accessMode, perm.AccessModeRead), nil +} + +func (r *SpecificReposAuthorizationReducer) RepoFilter(accessMode perm.AccessMode) builder.Cond { + repoIDs := make([]int64, len(r.resourceRepos)) + for i, tokenRepo := range r.resourceRepos { + repoIDs[i] = tokenRepo.RepoID + } + targetRepos := builder.In("repository.id", repoIDs) + + // If requesting anything higher than read access, it will only be available for repos within the scope of the + // access token. + if accessMode > perm.AccessModeRead { + return targetRepos + } + + // For read access, we should be able to see all non-private repositories that aren't in a private or limited + // organisation. + return builder.Or( + targetRepos, + builder.And( + builder.Eq{"repository.is_private": false}, + builder.NotIn("repository.owner_id", builder.Select("id").From("`user`").Where( + builder.Or(builder.Eq{"visibility": structs.VisibleTypeLimited}, builder.Eq{"visibility": structs.VisibleTypePrivate}), + )))) +} + +func (*SpecificReposAuthorizationReducer) AllowAdminOverride() bool { + return false +} diff --git a/services/authz/specific_repos_reducer_test.go b/services/authz/specific_repos_reducer_test.go new file mode 100644 index 0000000000..e99c8515a6 --- /dev/null +++ b/services/authz/specific_repos_reducer_test.go @@ -0,0 +1,117 @@ +// Copyright 2026 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package authz + +import ( + "testing" + + "forgejo.org/models/auth" + "forgejo.org/models/db" + "forgejo.org/models/perm" + "forgejo.org/models/repo" + "forgejo.org/models/unittest" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSpecificReposAuthorizationReducer(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + + reducer := &SpecificReposAuthorizationReducer{ + resourceRepos: []*auth.AccessTokenResourceRepo{ + { + RepoID: 1, + }, + { + RepoID: 2, + }, + }, + } + + t.Run("ReduceRepoAccess unrestricted on targeted repos", func(t *testing.T) { + repo1 := unittest.AssertExistsAndLoadBean(t, &repo.Repository{ID: 1}) + repo2 := unittest.AssertExistsAndLoadBean(t, &repo.Repository{ID: 2}) + for _, am := range []perm.AccessMode{perm.AccessModeOwner, perm.AccessModeAdmin, perm.AccessModeWrite, perm.AccessModeRead} { + p1, err := reducer.ReduceRepoAccess(t.Context(), repo1, am) + require.NoError(t, err) + assert.Equal(t, am, p1) + p2, err := reducer.ReduceRepoAccess(t.Context(), repo2, am) + require.NoError(t, err) + assert.Equal(t, am, p2) + } + }) + + t.Run("ReduceRepoAccess restricted to None on private repos", func(t *testing.T) { + // private repo + repo3 := unittest.AssertExistsAndLoadBean(t, &repo.Repository{ID: 3}) + + // public repo on a limited-visibility org + repo38 := unittest.AssertExistsAndLoadBean(t, &repo.Repository{ID: 38}) + + for _, am := range []perm.AccessMode{perm.AccessModeOwner, perm.AccessModeAdmin, perm.AccessModeWrite, perm.AccessModeRead} { + p3, err := reducer.ReduceRepoAccess(t.Context(), repo3, am) + require.NoError(t, err) + assert.Equal(t, perm.AccessModeNone, p3) + + p38, err := reducer.ReduceRepoAccess(t.Context(), repo38, am) + require.NoError(t, err) + assert.Equal(t, perm.AccessModeNone, p38) + } + }) + + t.Run("ReduceRepoAccess restricted to Read on public repos", func(t *testing.T) { + // public repo + repo4 := unittest.AssertExistsAndLoadBean(t, &repo.Repository{ID: 4}) + + for _, am := range []perm.AccessMode{perm.AccessModeOwner, perm.AccessModeAdmin, perm.AccessModeWrite, perm.AccessModeRead} { + p3, err := reducer.ReduceRepoAccess(t.Context(), repo4, am) + require.NoError(t, err) + assert.Equal(t, perm.AccessModeRead, p3) + } + + // don't elevate AccessModeNone to AccessModeRead: + p3, err := reducer.ReduceRepoAccess(t.Context(), repo4, perm.AccessModeNone) + require.NoError(t, err) + assert.Equal(t, perm.AccessModeNone, p3) + }) + + t.Run("RepoFilter >write access only permitted to targeted repos", func(t *testing.T) { + for _, am := range []perm.AccessMode{perm.AccessModeOwner, perm.AccessModeAdmin, perm.AccessModeWrite} { + cond := reducer.RepoFilter(am) + + var repoIDs []int64 + err := db.GetEngine(t.Context()).Table(&repo.Repository{}).Where(cond).OrderBy("id").Cols("id").Find(&repoIDs) + require.NoError(t, err) + + assert.Len(t, repoIDs, 2) + assert.EqualValues(t, 1, repoIDs[0]) + assert.EqualValues(t, 2, repoIDs[1]) + } + }) + + t.Run("RepoFilter read access only permitted to target repos & public repos", func(t *testing.T) { + cond := reducer.RepoFilter(perm.AccessModeRead) + + var rows []*repo.Repository + err := db.GetEngine(t.Context()).Table(&repo.Repository{}).Where(cond).OrderBy("id").Cols("id", "owner_id", "is_private").Find(&rows) + require.NoError(t, err) + + // Both target repos should be returned: + assert.EqualValues(t, 1, rows[0].ID) + assert.EqualValues(t, 2, rows[1].ID) + + // And there should be more return values, all of which appear as public repos: + assert.Greater(t, len(rows), 2) + for _, repo := range rows[2:] { + assert.False(t, repo.IsPrivate) + require.NoError(t, repo.LoadOwner(t.Context())) + assert.True(t, repo.Owner.Visibility.IsPublic()) + } + }) + + t.Run("AllowAdminOverride is false", func(t *testing.T) { + assert.False(t, reducer.AllowAdminOverride()) + }) +}