feat: add GetUserRepoPermissionWithReducer

This commit is contained in:
Mathieu Fenniak 2026-02-15 13:35:00 -07:00 committed by Mathieu Fenniak
parent 635f13a07e
commit 2f19237a14
6 changed files with 203 additions and 1 deletions

View file

@ -49,6 +49,7 @@ forgejo.org/models/organization
SearchMembersOptions.ToConds
forgejo.org/models/perm/access
GetUserRepoPermissionWithReducer
GetRepoWriters
forgejo.org/models/user

View file

@ -1124,6 +1124,11 @@
"path": "github.com/ssor/bom/LICENSE",
"licenseText": "MIT License\n\nCopyright (c) 2017 Asher\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
},
{
"name": "github.com/stretchr/objx",
"path": "github.com/stretchr/objx/LICENSE",
"licenseText": "The MIT License\n\nCopyright (c) 2014 Stretchr, Inc.\nCopyright (c) 2017-2018 objx contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
},
{
"name": "github.com/stretchr/testify",
"path": "github.com/stretchr/testify/LICENSE",

1
go.mod
View file

@ -245,6 +245,7 @@ require (
github.com/sorairolake/lzip-go v0.3.8 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/tinylib/msgp v1.6.1 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/zeebo/assert v1.3.0 // indirect

View file

@ -15,6 +15,7 @@ import (
"forgejo.org/models/unit"
user_model "forgejo.org/models/user"
"forgejo.org/modules/log"
"forgejo.org/services/authz"
)
// Permission contains all the permissions related variables to a repository for a user
@ -164,7 +165,28 @@ func GetActionRepoPermission(ctx context.Context, repo *repo_model.Repository, t
return GetUserRepoPermission(ctx, repo, user_model.NewActionsUser())
}
// GetUserRepoPermission returns the user permissions to the repository
// GetUserRepoPermission returns the user permissions to the repository, where the user's permissions may be
// artificially restricted by a an authorization reducer.
func GetUserRepoPermissionWithReducer(ctx context.Context, repo *repo_model.Repository, user *user_model.User, reducer authz.AuthorizationReducer) (Permission, error) {
perm, err := GetUserRepoPermission(ctx, repo, user)
if err != nil {
return perm, err
}
perm.AccessMode, err = reducer.ReduceRepoAccess(ctx, repo, perm.AccessMode)
if err != nil {
return perm, fmt.Errorf("failure in ReduceRepoAccess: %w", err)
}
for unit, currentAccessMode := range perm.UnitsMode {
reduced, err := reducer.ReduceRepoAccess(ctx, repo, currentAccessMode)
if err != nil {
return perm, fmt.Errorf("failure in ReduceRepoAccess: %w", err)
}
perm.UnitsMode[unit] = reduced
}
return perm, nil
}
// GetUserRepoPermission returns the user permissions to the repository.
func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, user *user_model.User) (Permission, error) {
var perm Permission
if log.IsTrace() {

View file

@ -8,9 +8,13 @@ import (
perm_model "forgejo.org/models/perm"
"forgejo.org/models/perm/access"
repo_model "forgejo.org/models/repo"
"forgejo.org/models/unit"
"forgejo.org/models/unittest"
user_model "forgejo.org/models/user"
"forgejo.org/services/authz"
"github.com/stretchr/testify/assert"
mock "github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
@ -76,3 +80,73 @@ func TestActionTaskNoAccessPrivateRepo(t *testing.T) {
require.NoError(t, err)
assertAccess(t, perm_model.AccessModeNone, &perm)
}
func TestGetUserRepoPermissionWithReducer(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
t.Run("no unit-level overrides", func(t *testing.T) {
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
// Baseline check that without a reducer, we get AccessModeOwner...
permWithoutReducer, err := access.GetUserRepoPermission(t.Context(), repo, user)
require.NoError(t, err)
require.NotNil(t, permWithoutReducer)
assert.True(t, permWithoutReducer.IsOwner())
assert.True(t, permWithoutReducer.IsAdmin())
assert.True(t, permWithoutReducer.HasAccess())
assert.True(t, permWithoutReducer.CanWrite(unit.TypeIssues))
reducer := authz.NewMockAuthorizationReducer(t)
reducer.On(
"ReduceRepoAccess",
mock.Anything, // context
mock.MatchedBy(func(repo *repo_model.Repository) bool { // repo
return repo.ID == 1
}),
perm_model.AccessModeOwner, // incoming access mode
).Return(perm_model.AccessModeNone, nil)
permWithReducer, err := access.GetUserRepoPermissionWithReducer(t.Context(), repo, user, reducer)
require.NoError(t, err)
require.NotNil(t, permWithReducer)
assert.False(t, permWithReducer.IsOwner())
assert.False(t, permWithReducer.IsAdmin())
assert.False(t, permWithReducer.HasAccess())
assert.False(t, permWithReducer.CanWrite(unit.TypeIssues))
})
t.Run("team unit-level overrides", func(t *testing.T) {
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 15})
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 32})
// Baseline check that without a reducer, we get mixed access for different units...
permWithoutReducer, err := access.GetUserRepoPermission(t.Context(), repo, user)
require.NoError(t, err)
require.NotNil(t, permWithoutReducer)
require.NotEmpty(t, permWithoutReducer.UnitsMode) // unit-specific access modes loaded
assert.True(t, permWithoutReducer.CanRead(unit.TypeCode))
assert.False(t, permWithoutReducer.CanWrite(unit.TypeCode))
assert.True(t, permWithoutReducer.CanRead(unit.TypeIssues))
assert.True(t, permWithoutReducer.CanWrite(unit.TypeIssues))
reducer := authz.NewMockAuthorizationReducer(t)
reducer.On(
"ReduceRepoAccess",
mock.Anything, // context
mock.MatchedBy(func(repo *repo_model.Repository) bool { // repo
return repo.ID == 32
}),
mock.Anything, // incoming access mode - will vary for each unit
).Return(perm_model.AccessModeRead, nil)
permWithReducer, err := access.GetUserRepoPermissionWithReducer(t.Context(), repo, user, reducer)
require.NoError(t, err)
require.NotNil(t, permWithReducer)
require.NotEmpty(t, permWithReducer.UnitsMode) // unit-specific access modes loaded
assert.True(t, permWithReducer.CanRead(unit.TypeCode))
assert.False(t, permWithReducer.CanWrite(unit.TypeCode))
assert.True(t, permWithReducer.CanRead(unit.TypeIssues))
assert.False(t, permWithReducer.CanWrite(unit.TypeIssues))
})
}

View file

@ -0,0 +1,99 @@
// Code generated by mockery v2.53.5. DO NOT EDIT.
package authz
import (
context "context"
"forgejo.org/models/perm"
repo_model "forgejo.org/models/repo"
mock "github.com/stretchr/testify/mock"
"xorm.io/builder"
)
// MockAuthorizationReducer is an autogenerated mock type for the AuthorizationReducer type
type MockAuthorizationReducer struct {
mock.Mock
}
// AllowAdminOverride provides a mock function with no fields
func (_m *MockAuthorizationReducer) AllowAdminOverride() bool {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for AllowAdminOverride")
}
var r0 bool
if rf, ok := ret.Get(0).(func() bool); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(bool)
}
return r0
}
// ReduceRepoAccess provides a mock function with given fields: ctx, repo, accessMode
func (_m *MockAuthorizationReducer) ReduceRepoAccess(ctx context.Context, repo *repo_model.Repository, accessMode perm.AccessMode) (perm.AccessMode, error) {
ret := _m.Called(ctx, repo, accessMode)
if len(ret) == 0 {
panic("no return value specified for ReduceRepoAccess")
}
var r0 perm.AccessMode
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, *repo_model.Repository, perm.AccessMode) (perm.AccessMode, error)); ok {
return rf(ctx, repo, accessMode)
}
if rf, ok := ret.Get(0).(func(context.Context, *repo_model.Repository, perm.AccessMode) perm.AccessMode); ok {
r0 = rf(ctx, repo, accessMode)
} else {
r0 = ret.Get(0).(perm.AccessMode)
}
if rf, ok := ret.Get(1).(func(context.Context, *repo_model.Repository, perm.AccessMode) error); ok {
r1 = rf(ctx, repo, accessMode)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// RepoFilter provides a mock function with given fields: accessMode
func (_m *MockAuthorizationReducer) RepoFilter(accessMode perm.AccessMode) builder.Cond {
ret := _m.Called(accessMode)
if len(ret) == 0 {
panic("no return value specified for RepoFilter")
}
var r0 builder.Cond
if rf, ok := ret.Get(0).(func(perm.AccessMode) builder.Cond); ok {
r0 = rf(accessMode)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(builder.Cond)
}
}
return r0
}
// NewMockAuthorizationReducer creates a new instance of MockAuthorizationReducer. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewMockAuthorizationReducer(t interface {
mock.TestingT
Cleanup(func())
},
) *MockAuthorizationReducer {
mock := &MockAuthorizationReducer{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}