mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2026-05-12 22:10:25 +00:00
feat: services/authz package for evaluating fine-grained access token
This commit is contained in:
parent
a1eff6f0dc
commit
44c18465b5
12 changed files with 550 additions and 0 deletions
|
|
@ -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
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
-
|
||||
id: 1
|
||||
token_id: 7
|
||||
repo_id: 1
|
||||
created_unix: 1772158384
|
||||
28
services/authz/access_token.go
Normal file
28
services/authz/access_token.go
Normal file
|
|
@ -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
|
||||
}
|
||||
46
services/authz/access_token_test.go
Normal file
46
services/authz/access_token_test.go
Normal file
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
29
services/authz/all_access_reducer.go
Normal file
29
services/authz/all_access_reducer.go
Normal file
|
|
@ -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
|
||||
}
|
||||
49
services/authz/all_access_reducer_test.go
Normal file
49
services/authz/all_access_reducer_test.go
Normal file
|
|
@ -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())
|
||||
})
|
||||
}
|
||||
39
services/authz/authorization_reducer.go
Normal file
39
services/authz/authorization_reducer.go
Normal file
|
|
@ -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
|
||||
}
|
||||
14
services/authz/main_test.go
Normal file
14
services/authz/main_test.go
Normal file
|
|
@ -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)
|
||||
}
|
||||
46
services/authz/public_repos_reducer.go
Normal file
46
services/authz/public_repos_reducer.go
Normal file
|
|
@ -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
|
||||
}
|
||||
73
services/authz/public_repos_reducer_test.go
Normal file
73
services/authz/public_repos_reducer_test.go
Normal file
|
|
@ -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())
|
||||
})
|
||||
}
|
||||
73
services/authz/specific_repos_reducer.go
Normal file
73
services/authz/specific_repos_reducer.go
Normal file
|
|
@ -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
|
||||
}
|
||||
117
services/authz/specific_repos_reducer_test.go
Normal file
117
services/authz/specific_repos_reducer_test.go
Normal file
|
|
@ -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())
|
||||
})
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue