diff --git a/models/repo/authz.go b/models/repo/authz.go new file mode 100644 index 0000000000..9196370583 --- /dev/null +++ b/models/repo/authz.go @@ -0,0 +1,29 @@ +// Copyright 2026 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package repo + +import ( + "context" + + "forgejo.org/models/perm" + + "xorm.io/builder" +) + +// Defines an API for reducing available permissions to specific repositories. +type RepositoryAuthorizationReducer 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 *Repository, accessMode perm.AccessMode) (perm.AccessMode, error) + + // If querying the repository table, apply the provided condition to query only repositories that the restriction + // will allow AccessModeRead (or higher). For example, when using a fine-grained access token that only grants + // write access to one target repository, `RepoReadAccessFilter()` will return a query condition that provides + // visibility for all the public repos (which have read access) and all the target private repos (which have write + // access, which is greater-than read access). + RepoReadAccessFilter() builder.Cond +} diff --git a/services/authz/all_access_reducer.go b/services/authz/all_access_reducer.go index 8a4590f285..42e3ee2e8e 100644 --- a/services/authz/all_access_reducer.go +++ b/services/authz/all_access_reducer.go @@ -20,7 +20,7 @@ func (*AllAccessAuthorizationReducer) ReduceRepoAccess(ctx context.Context, repo return accessMode, nil } -func (*AllAccessAuthorizationReducer) RepoFilter(accessMode perm.AccessMode) builder.Cond { +func (*AllAccessAuthorizationReducer) RepoReadAccessFilter() builder.Cond { return builder.NewCond() // invalid cond should be excluded and cause no filtering } diff --git a/services/authz/all_access_reducer_test.go b/services/authz/all_access_reducer_test.go index 84c1b1c1ac..ad2d8f72ee 100644 --- a/services/authz/all_access_reducer_test.go +++ b/services/authz/all_access_reducer_test.go @@ -33,14 +33,12 @@ func TestAllAccessAuthorizationReducer(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) + cond := reducer.RepoReadAccessFilter() - 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)) - } + 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) { diff --git a/services/authz/authorization_reducer.go b/services/authz/authorization_reducer.go index 0152daf890..7485f74f40 100644 --- a/services/authz/authorization_reducer.go +++ b/services/authz/authorization_reducer.go @@ -4,30 +4,15 @@ 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 + // Incorporate all the methods of [RepositoryAuthorizationReducer], which allows reducing permissions related to + // repositories specifically. + repo_model.RepositoryAuthorizationReducer // 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 diff --git a/services/authz/authorization_reducer_mock.go b/services/authz/authorization_reducer_mock.go index 27e90b24a4..79562917c4 100644 --- a/services/authz/authorization_reducer_mock.go +++ b/services/authz/authorization_reducer_mock.go @@ -64,20 +64,18 @@ func (_m *MockAuthorizationReducer) ReduceRepoAccess(ctx context.Context, repo * } // RepoFilter provides a mock function with given fields: accessMode -func (_m *MockAuthorizationReducer) RepoFilter(accessMode perm.AccessMode) builder.Cond { - ret := _m.Called(accessMode) +func (_m *MockAuthorizationReducer) RepoReadAccessFilter() builder.Cond { + ret := _m.Called() if len(ret) == 0 { - panic("no return value specified for RepoFilter") + panic("no return value specified for AllowAdminOverride") } var r0 builder.Cond - if rf, ok := ret.Get(0).(func(perm.AccessMode) builder.Cond); ok { - r0 = rf(accessMode) + if rf, ok := ret.Get(0).(func() builder.Cond); ok { + r0 = rf() } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(builder.Cond) - } + r0 = ret.Get(0).(builder.Cond) } return r0 diff --git a/services/authz/public_repos_reducer.go b/services/authz/public_repos_reducer.go index ce603cb569..335e1e60b8 100644 --- a/services/authz/public_repos_reducer.go +++ b/services/authz/public_repos_reducer.go @@ -31,9 +31,8 @@ func (*PublicReposAuthorizationReducer) ReduceRepoAccess(ctx context.Context, re 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. +func (*PublicReposAuthorizationReducer) RepoReadAccessFilter() builder.Cond { + // 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( diff --git a/services/authz/public_repos_reducer_test.go b/services/authz/public_repos_reducer_test.go index 5e72566253..40a0b397fd 100644 --- a/services/authz/public_repos_reducer_test.go +++ b/services/authz/public_repos_reducer_test.go @@ -52,18 +52,16 @@ func TestPublicReposAuthorizationReducer(t *testing.T) { }) 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) + cond := reducer.RepoReadAccessFilter() - 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()) - } + 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()) } }) diff --git a/services/authz/specific_repos_reducer.go b/services/authz/specific_repos_reducer.go index e9c6f6d7c5..dd085189af 100644 --- a/services/authz/specific_repos_reducer.go +++ b/services/authz/specific_repos_reducer.go @@ -44,21 +44,14 @@ func (r *SpecificReposAuthorizationReducer) ReduceRepoAccess(ctx context.Context return min(accessMode, perm.AccessModeRead), nil } -func (r *SpecificReposAuthorizationReducer) RepoFilter(accessMode perm.AccessMode) builder.Cond { +func (r *SpecificReposAuthorizationReducer) RepoReadAccessFilter() 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. + // We should also be able to see all non-private repositories that aren't in a private or limited organization. return builder.Or( targetRepos, builder.And( diff --git a/services/authz/specific_repos_reducer_test.go b/services/authz/specific_repos_reducer_test.go index e99c8515a6..5db65eeca4 100644 --- a/services/authz/specific_repos_reducer_test.go +++ b/services/authz/specific_repos_reducer_test.go @@ -77,22 +77,8 @@ func TestSpecificReposAuthorizationReducer(t *testing.T) { 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) + cond := reducer.RepoReadAccessFilter() var rows []*repo.Repository err := db.GetEngine(t.Context()).Table(&repo.Repository{}).Where(cond).OrderBy("id").Cols("id", "owner_id", "is_private").Find(&rows)